|
4 | 4 | ### Architecture |
5 | 5 |
|
6 | 6 | <!-- lore:019c904b-791e-772a-ab2b-93ac892a960c --> |
7 | | -* **buildSection() renders AGENTS.md directly without formatKnowledge**: AGENTS.md export/import: buildSection() iterates DB entries grouped by category, emitting \`\<!-- lore:UUID -->\` markers, serialized via remark. splitFile() scans ALL\_START\_MARKERS (current + historical) — self-healing: N duplicate sections collapse to 1 on next export. Import dedup handled by curator LLM at startup when file changed. Missing marker = hand-written; duplicate UUID = first wins. ltm.create() has title-based dedup guard (case-insensitive, skipped for explicit IDs from cross-machine import). Marker text says 'maintained by the coding agent' (not 'auto-maintained') so LLM agents include it in commits. |
| 7 | +* **buildSection() renders AGENTS.md directly without formatKnowledge**: AGENTS.md export/import: buildSection() iterates DB entries grouped by category, emitting \`\<!-- lore:UUID -->\` markers via remark. splitFile() scans ALL\_START\_MARKERS (current + historical) — self-healing: N duplicate sections collapse to 1 on next export. Import dedup handled by curator LLM at startup when file changed. Missing marker = hand-written; duplicate UUID = first wins. ltm.create() has title-based dedup guard (case-insensitive, skipped for explicit IDs from cross-machine import). |
8 | 8 |
|
9 | 9 | <!-- lore:019c94bd-042d-73c0-b850-192d1d62fa68 --> |
10 | 10 | * **Knowledge entry distribution across projects — worktree sessions create separate project IDs**: Knowledge entries are scoped by project\_id from ensureProject(projectPath). OpenCode worktree sessions (paths like ~/.local/share/opencode/worktree/\<hash>/\<slug>/) each get their own project\_id. A single repo can have multiple project\_ids: one for the real path, separate ones per worktree session. Project-specific entries (cross\_project=0) are invisible across different project\_ids. Cross-project entries (cross\_project=1) are shared globally. |
|
16 | 16 | * **Lore temporal pruning runs after distillation and curation on session.idle**: In src/index.ts, session.idle awaits backgroundDistill and backgroundCurate sequentially before running temporal.prune(). Ordering is critical: pruning must not delete unprocessed messages. Pruning defaults: 120-day retention, 1GB max storage (in .lore.json under pruning.retention and pruning.maxStorage). These generous defaults were chosen because the system was new — earlier proposals of 7d/200MB were based on insufficient data. |
17 | 17 |
|
18 | 18 | <!-- lore:019c94bd-042b-7215-b0a0-05719fcd39b2 --> |
19 | | -* **LTM injection pipeline: system transform → forSession → formatKnowledge → gradient deduction**: LTM is injected via experimental.chat.system.transform hook. Flow: getLtmBudget() computes ceiling as (contextLimit - outputReserved - overhead) \* ltmFraction (default 10%, configurable 2-30%). forSession() loads project-specific entries unconditionally + cross-project entries scored by term overlap, greedy-packs into budget. formatKnowledge() renders as markdown. setLtmTokens() records consumption so gradient deducts it. Key: LTM goes into output.system (system prompt), not the message array — invisible to tryFit(), counts against overhead budget. |
| 19 | +* **LTM injection pipeline: system transform → forSession → formatKnowledge → gradient deduction**: LTM injected via experimental.chat.system.transform hook. getLtmBudget() computes ceiling as (contextLimit - outputReserved - overhead) \* ltmFraction (default 10%, configurable 2-30%). forSession() loads project-specific entries unconditionally + cross-project entries scored by term overlap, greedy-packs into budget. formatKnowledge() renders as markdown. setLtmTokens() records consumption so gradient deducts it. Key: LTM goes into output.system (system prompt) — invisible to tryFit(), counts against overhead budget. |
20 | 20 |
|
21 | 21 | ### Decision |
22 | 22 |
|
|
34 | 34 | <!-- lore:019cc40e-e56e-71e9-bc5d-545f97df732b --> |
35 | 35 | * **Consola prompt cancel returns truthy Symbol, not false**: When a user cancels a \`consola\` / \`@clack/prompts\` confirmation prompt (Ctrl+C), the return value is \`Symbol(clack:cancel)\`, not \`false\`. Since Symbols are truthy in JavaScript, checking \`!confirmed\` will be \`false\` and the code falls through as if the user confirmed. Fix: use \`confirmed !== true\` (strict equality) instead of \`!confirmed\` to correctly handle cancel, false, and any other non-true values. |
36 | 36 |
|
37 | | -<!-- lore:019cc484-f0e7-7a64-bea1-f3f98e9c56c1 --> |
38 | | -* **Craft v2 GitHub App must be installed per-repo**: The Craft v2 release/publish workflows use \`actions/create-github-app-token@v1\` which requires the GitHub App to be installed on the specific repository. If the app is configured for "Only select repositories", adding a new repo to the Craft pipeline requires manually adding it at GitHub Settings → Installations → \[App] → Configure. The \`APP\_ID\` variable and \`APP\_PRIVATE\_KEY\` secret are set in the \`production\` environment, not at repo level. Symptom: 404 on \`GET /repos/{owner}/{repo}/installation\`. |
39 | | - |
40 | 37 | <!-- lore:019cb615-0b10-7bbc-a7db-50111118c200 --> |
41 | | -* **Lore auto-recovery can infinite-loop without re-entrancy guard**: Three bugs in v0.5.2 caused excessive background LLM requests: (1) Auto-recovery infinite loop — session.error overflow handler injected recovery prompt via session.prompt, which could overflow again → another session.error → loop of 2+ LLM calls/cycle. Fix: recoveringSessions Set as re-entrancy guard. (2) Curator ran every idle — \`onIdle || afterTurns\` short-circuited because onIdle=true. Fix: change \`||\` to \`&&\`. Lesson: boolean flag gating numeric threshold needs AND not OR. (3) shouldSkip() fell back to session.list() on every unknown session (short IDs fail session.get). Fix: remove list fallback, cache in activeSessions after first check. |
| 38 | +* **Lore auto-recovery can infinite-loop without re-entrancy guard**: Three v0.5.2 bugs causing excessive background LLM requests: (1) Auto-recovery loop — session.error handler injected recovery prompt → could overflow again → loop. Fix: recoveringSessions Set as re-entrancy guard. (2) Curator ran every idle — \`onIdle || afterTurns\` short-circuited (onIdle=true). Fix: \`||\` → \`&&\`. Lesson: boolean flag gating numeric threshold needs AND not OR. (3) shouldSkip() fell back to session.list() on unknown sessions. Fix: remove list fallback, cache in activeSessions. |
42 | 39 |
|
43 | 40 | <!-- lore:019cb3e6-da66-7534-a573-30d2ecadfd53 --> |
44 | 41 | * **Returning bare promises loses async function from error stack traces**: When an \`async\` function returns another promise without \`await\`, the calling function disappears from error stack traces if the inner promise rejects. A function that drops \`async\` and does \`return someAsyncCall()\` loses its frame entirely. Fix: keep the function \`async\` and use \`return await someAsyncCall()\`. This matters for debugging — the intermediate function name in the stack trace helps locate which code path triggered the failure. ESLint rule \`no-return-await\` is outdated; modern engines optimize \`return await\` in async functions. |
45 | 42 |
|
46 | 43 | <!-- lore:019cd20d-f42c-71bf-9da5-b2dd52c5014d --> |
47 | | -* **sgdisk reserves 33 sectors for backup GPT, shrinking partition vs original layout**: When recreating a GPT partition entry with \`sgdisk\`, it sets \`LastUsableLBA\` conservatively — 33 sectors short of disk end to reserve space for the backup GPT table. If the original partition extended to the very last sector (common for factory-formatted exFAT SD cards), the recreated partition will be 33 sectors too small. Windows strictly validates that the exFAT VolumeLength in the VBR matches the GPT partition size and refuses to mount on mismatch ("drive not formatted" error). Fix: patch the exFAT VBR's VolumeLength to match the GPT partition size (PartitionLastLBA - PartitionFirstLBA + 1), then recalculate the exFAT boot region checksum (sector 11). Do NOT extend LastUsableLBA to the disk's last sector — that's where the backup GPT header lives, and Windows will reject the GPT as corrupt if usable range overlaps it. |
| 44 | +* **sgdisk reserves 33 sectors for backup GPT, shrinking partition vs original layout**: When recreating a GPT partition with \`sgdisk\`, it sets LastUsableLBA 33 sectors short of disk end for backup GPT. If the original partition extended to the last sector (common for factory-formatted exFAT SD cards), the recreated partition is too small. Windows validates exFAT VolumeLength matches GPT partition size — mismatch causes 'drive not formatted' error. Fix: patch the exFAT VBR's VolumeLength to match GPT partition size (LastLBA - FirstLBA + 1), then recalculate boot region checksum (sector 11). Do NOT extend LastUsableLBA past backup GPT header location. |
48 | 45 |
|
49 | 46 | <!-- lore:019c8f4f-67ca-7212-a8c4-8a75b230ceea --> |
50 | 47 | * **Test DB isolation via LORE\_DB\_PATH and Bun test preload**: Lore test suite uses isolated temp DB via test/setup.ts preload (bunfig.toml). Preload sets LORE\_DB\_PATH to mkdtempSync path before any imports of src/db.ts; afterAll cleans up. src/db.ts checks LORE\_DB\_PATH first. agents-file.test.ts needs beforeEach cleanup for intra-file isolation and TEST\_UUIDS cleanup in afterAll (shared with ltm.test.ts). Individual test files don't need close() calls — preload handles DB lifecycle. |
|
58 | 55 | * **Lore logging: LORE\_DEBUG gating for info/warn, always-on for errors**: src/log.ts provides three levels: log.info() and log.warn() are suppressed unless LORE\_DEBUG=1 or LORE\_DEBUG=true; log.error() always emits. All write to stderr with \[lore] prefix. This exists because OpenCode TUI renders all stderr as red error text — routine status messages (distillation counts, pruning stats, consolidation) were alarming users. Rule: use log.info() for successful operations and status, log.warn() for non-actionable oddities (e.g. dropping trailing messages), log.error() only in catch blocks for real failures. Never use console.error directly in plugin source files. |
59 | 56 |
|
60 | 57 | <!-- lore:019cb12a-c957-7e24-b3f5-6869f3429d13 --> |
61 | | -* **Lore release process: craft + issue-label publish**: Release flow: (1) Trigger release.yml via workflow\_dispatch with version='auto' — uses getsentry/craft to determine version from commits and create a GitHub issue titled 'publish: BYK/opencode-lore@X.Y.Z'. (2) Label that issue 'accepted' — triggers publish.yml which runs craft publish with npm OIDC trusted publishing, then closes the issue. Do NOT create a release/X.Y.Z branch or bump package.json manually — craft handles versioning with 'auto'. The repo uses a GitHub App token (APP\_ID + APP\_PRIVATE\_KEY) for checkout in both workflows. |
| 58 | +* **Lore release process: craft + issue-label publish**: Lore/Craft release pipeline and gotchas: (1) Trigger release.yml via workflow\_dispatch with version='auto' — craft determines version and creates GitHub issue. Label 'accepted' → publish.yml runs craft publish with npm OIDC. Don't create release branches or bump package.json manually. (2) GitHub App must be installed per-repo ('Only select repositories' → add at Settings → Installations). APP\_ID/APP\_PRIVATE\_KEY in \`production\` environment. Symptom: 404 on GET /repos/.../installation. (3) npm OIDC only works for publish — \`npm info\` needs NPM\_TOKEN for private packages (public works without auth). |
62 | 59 |
|
63 | 60 | <!-- lore:019cb200-0001-7000-8000-000000000001 --> |
64 | 61 | * **PR workflow for opencode-lore: branch → PR → auto-merge**: All changes (including minor fixes and test-only changes) must go through a branch + PR + auto-merge, never pushed directly to main. Workflow: (1) git checkout -b \<type>/\<slug>, (2) commit, (3) git push -u origin HEAD, (4) gh pr create --title "..." --body "..." --base main, (5) gh pr merge --auto --squash \<PR#>. Branch name conventions follow merged PR history: fix/\<slug>, feat/\<slug>, chore/\<slug>. Auto-merge with squash is required (merge commits disallowed). Never push directly to main even for trivial changes. |
|
0 commit comments