diff --git a/AGENTS.md b/AGENTS.md index e7366d6..c89d4bb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,9 +40,6 @@ * **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. - -* **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. - * **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. @@ -55,7 +52,7 @@ * **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. -* **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). +* **Lore release process: craft + issue-label publish**: Lore/Craft release pipeline: (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. 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. * **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 \/\, (2) commit, (3) git push -u origin HEAD, (4) gh pr create --title "..." --body "..." --base main, (5) gh pr merge --auto --squash \. Branch name conventions follow merged PR history: fix/\, feat/\, chore/\. Auto-merge with squash is required (merge commits disallowed). Never push directly to main even for trivial changes. @@ -63,5 +60,5 @@ ### Preference -* **Code style**: User prefers no backwards-compat shims — fix callers directly. Prefer explicit error handling over silent failures. Derive thresholds from existing constants rather than hardcoding magic numbers (e.g., use \`raw.length <= COL\_COUNT\` instead of \`n < 10\_000\`). In CI, define shared env vars at workflow level, not per-job. Always dry-run before bulk destructive operations (SELECT before DELETE to verify row count). +* **Code style**: No backwards-compat shims — fix callers directly. Prefer explicit error handling over silent failures. Derive thresholds from existing constants rather than hardcoding magic numbers. In CI, define shared env vars at workflow level, not per-job. Dry-run before bulk destructive operations (SELECT before DELETE). Prefer \`jq\`/\`sed\`/\`awk\` over \`node -e\` for JSON manipulation in CI scripts. diff --git a/src/curator.ts b/src/curator.ts index 9ca4603..c92d14e 100644 --- a/src/curator.ts +++ b/src/curator.ts @@ -113,6 +113,11 @@ export async function run(input: { path: { id: workerID }, query: { limit: 2 }, }); + // Rotate worker session so the next call starts fresh — prevents + // accumulating multiple assistant messages with reasoning/thinking parts, + // which providers reject ("Multiple reasoning_opaque values"). + workerSessions.delete(input.sessionID); + const last = msgs.data?.at(-1); if (!last || last.info.role !== "assistant") return { created: 0, updated: 0, deleted: 0 }; @@ -222,6 +227,9 @@ export async function consolidate(input: { path: { id: workerID }, query: { limit: 2 }, }); + // Rotate worker session — see run() comment. + workerSessions.delete(input.sessionID); + const last = msgs.data?.at(-1); if (!last || last.info.role !== "assistant") return { updated: 0, deleted: 0 }; diff --git a/src/distillation.ts b/src/distillation.ts index 453e5bf..ad4d924 100644 --- a/src/distillation.ts +++ b/src/distillation.ts @@ -388,6 +388,11 @@ async function distillSegment(input: { path: { id: workerID }, query: { limit: 2 }, }); + // Rotate worker session so the next call starts fresh — prevents + // accumulating multiple assistant messages with reasoning/thinking parts, + // which providers reject ("Multiple reasoning_opaque values"). + workerSessions.delete(input.sessionID); + const last = msgs.data?.at(-1); if (!last || last.info.role !== "assistant") return null; @@ -438,6 +443,9 @@ async function metaDistill(input: { path: { id: workerID }, query: { limit: 2 }, }); + // Rotate worker session — see distillSegment() comment. + workerSessions.delete(input.sessionID); + const last = msgs.data?.at(-1); if (!last || last.info.role !== "assistant") return null; diff --git a/src/reflect.ts b/src/reflect.ts index c37fd2c..5addfac 100644 --- a/src/reflect.ts +++ b/src/reflect.ts @@ -1,6 +1,7 @@ import { tool } from "@opencode-ai/plugin/tool"; import * as temporal from "./temporal"; import * as ltm from "./ltm"; +import * as log from "./log"; import { db, ensureProject } from "./db"; import { serialize, inline, h, p, ul, lip, liph, t, root } from "./markdown"; @@ -114,34 +115,46 @@ export function createRecallTool(projectPath: string, knowledgeEnabled = true): const scope = args.scope ?? "all"; const sid = context.sessionID; - const temporalResults = - scope === "knowledge" - ? [] - : temporal.search({ - projectPath, - query: args.query, - sessionID: scope === "session" ? sid : undefined, - limit: 10, - }); + let temporalResults: temporal.TemporalMessage[] = []; + if (scope !== "knowledge") { + try { + temporalResults = temporal.search({ + projectPath, + query: args.query, + sessionID: scope === "session" ? sid : undefined, + limit: 10, + }); + } catch (err) { + log.error("recall: temporal search failed:", err); + } + } - const distillationResults = - scope === "knowledge" - ? [] - : searchDistillations({ - projectPath, - query: args.query, - sessionID: scope === "session" ? sid : undefined, - limit: 5, - }); + let distillationResults: Distillation[] = []; + if (scope !== "knowledge") { + try { + distillationResults = searchDistillations({ + projectPath, + query: args.query, + sessionID: scope === "session" ? sid : undefined, + limit: 5, + }); + } catch (err) { + log.error("recall: distillation search failed:", err); + } + } - const knowledgeResults = - !knowledgeEnabled || scope === "session" - ? [] - : ltm.search({ - query: args.query, - projectPath, - limit: 10, - }); + let knowledgeResults: ltm.KnowledgeEntry[] = []; + if (knowledgeEnabled && scope !== "session") { + try { + knowledgeResults = ltm.search({ + query: args.query, + projectPath, + limit: 10, + }); + } catch (err) { + log.error("recall: knowledge search failed:", err); + } + } return formatResults({ temporalResults,