Skip to content

fix: stop marking unpersisted extractions as complete, harden uninstall#249

Merged
edheltzel merged 9 commits into
mainfrom
fm/recall-pr248-review-fixes-r10
Jul 23, 2026
Merged

fix: stop marking unpersisted extractions as complete, harden uninstall#249
edheltzel merged 9 commits into
mainfrom
fm/recall-pr248-review-fixes-r10

Conversation

@edheltzel

Copy link
Copy Markdown
Owner

Intent

Fix the four concrete post-merge review defects discovered after Recall PR #248 merged: classify lowercase OpenCode markdown extraction failures as failed and retryable; make SQLite WAL writer failures visible and prevent batch extraction from recording success without persisted records; make uninstall resilient to malformed OpenCode JSONC while preserving user config and continuing remaining integrations without a node_modules/jsonc-parser runtime dependency; and verify the real OpenCode event plugin boundary and hook-name contract. Add focused regressions for classification, persistence and lock visibility and retry/success, resilient full uninstall and JSONC preservation, and the real OpenCode boundary. Preserve Phase 4 scope; do not expand into semantic issues #240, #241, or #226, Codex lifecycle capture, broad installer cleanup, release/version work, or local installation reconciliation. Work from updated main on fm/recall-pr248-review-fixes-r10, keep coherent fixes separately committed, and validate with focused and full tests, build, lint, shell checks, and the complete no-mistakes pipeline; never merge the PR.

What Changed

  • Extraction now fails loudly when records don't land. runExtractCore returns a new persistence_failed outcome when the SQLite dual-write reports any per-table failure; both the Stop and markdown paths in RecallExtract.ts log the failure details and call markAsFailed (24h retry) instead of markAsExtracted. RecallBatchExtract.ts routes both classification sites through a shared isExtractionFailureOutput matcher that catches lowercase [FabricExtract] (markdown) extraction failed, sqlite write failed, and persistence failed while leaving the non-fatal provider-cascade messages alone, and a successful extraction whose tracker write fails is now counted as failed and retried rather than as extracted. Because that seam is now replayed, the four plain-INSERT writers in hooks/lib/sqlite-writers.ts take an opt-in skipDuplicates keyed on (session_id, content) — content-scoped so in-session windows and the correction writer are unaffected.
  • Uninstall survives a malformed OpenCode config, with no jsonc-parser runtime dependency. The inline bun -e blocks in lib/install-lib.sh are replaced by a new dependency-free lib/jsonc-mcp.ts (merge / remove subcommands) that preserves comments, trailing commas, sibling MCP entries, and custom Recall entry fields, and refuses to write malformed or unwritable configs; jsonc-parser is dropped from package.json and bun.lock. remove_opencode now records the config failure but still removes plugins, agent, and guide — returning 0 on the clean path — and main() continues the remaining integrations with a warning instead of aborting.
  • Regressions and docs. scripts/e2e-opencode.ts asserts the OpenCode plugin factory exposes a real event function and no obsolete session.idle key before invoking the payload. New tests cover batch success accounting, DB-lock persistence visibility, dual-write dedupe on replay, a full resilient uninstall, and JSONC preservation. docs/architecture.md, docs/installation.md, docs/OPENCODE_INTEGRATION.md, AGENTS.md, and hooks/AGENTS.md are updated for the persistence check, the retry window's new failure class, the bundled parser, and the skipDuplicates contract.

Risk Assessment

⚠️ Medium: This round's fixes are small, well-targeted, and empirically verified correct with no new defects introduced, but the branch as a whole still replaces jsonc-parser with a hand-rolled JSONC editor that mutates user config files and carries the previously-raised, human-accepted residual corruption edge case on the uninstall removal path.

Testing

Baseline first: the full bun test suite is green (1277 pass, 0 fail) on the target commit, so the ~10 worktree lifecycle failures tracked in issue #243 did not reproduce here and nothing red pre-existed. On top of that I exercised each of the four defects the way an operator would experience it, running every scenario twice — once against a git archive export of the base commit's hooks/lib/uninstall tree and once against the target — so the transcripts show the actual behavior change rather than just a passing assertion. The classification fix turns a crashed markdown extraction from "SUCCESS: Extracted and tracked, never revisited, session lost" into a FAILED row with retry_after that later recovers the archive; the persistence fix makes a SQLite writer failure print the per-table failures and stops the batch from claiming success with zero persisted records; the uninstall fix leaves a malformed opencode.json byte-identical while continuing to clean up Pi and Claude and reaching "Uninstall Complete" with exit 0, and the bundled JSONC helper merges/removes entries with no node_modules on disk; the OpenCode boundary was verified against the live OpenCode 1.18.4 CLI via the repo e2e (which also ran bun run build clean) plus a direct factory-surface check. I added the one regression the intent enumerates but the PR lacked — batch retry/success accounting — and confirmed it fails against the base implementation. Per the task rules I did not run linters/tsc/bash -n, and did not re-run the no-mistakes pipeline (this run is that validation step). These are CLI, lifecycle-hook, and installer surfaces with no rendered UI, so no screenshot, GIF, or rendered-HTML artifact applies; the reviewer-visible evidence is the command transcripts, persisted SQLite tracker/memory state, and the preserved config files. Everything passed; the only open item is the author-decision warning about duplicate rows on the newly enabled retry path. All temp sandboxes were removed and the worktree contains only the intentional test-file addition.

Evidence: Defect 1 — lowercase markdown extraction failure: base marks it extracted forever and loses the session; fixed records a retryable failure and recovers

[BASE] SUCCESS: Extracted and tracked | tracker extracted_at set | next run: (file was never revisited) | DISTILLED.md: NO — session lost [FIXED] QUALITY GATE FAILED or extraction failed ... FAILED: Will retry after 24h cooldown tracker: {"extracted_at":null,"failed_at":"...","retry_after":"2026-07-24 ..."} next run after fault cleared: SUCCESS: Extracted and tracked | DISTILLED.md: YES — session archived memory records: crashed attempt {loa_entries:1,decisions:1} -> after retry {loa_entries:2,decisions:2}


==============================================================================
[BASE] OpenCode drop file whose markdown extraction crashes
==============================================================================
--- RecallExtract stderr seen by the batch extractor ---
(none)
--- RecallBatchExtract decision + summary ---
[2026-07-23T14:12:56.899Z]   SUCCESS: Extracted and tracked
--- extraction_tracker row ---
{"conversation_path":"/var/folders/88/3h9cyc4979d2l6p7xkn79yqr0000gn/T/recall-ev-classify-base-jA0c2I/recall-home/MEMORY/opencode-sessions/ses_classify_demo.md","extracted_at":"2026-07-23 14:12:56","failed_at":null,"retry_after":null,"error":null}
--- memory records persisted by the crashed attempt ---
{"loa_entries":1,"extraction_sessions":1,"decisions":1,"learnings":0}
--- next batch run, after the fault is cleared ---
(file was never revisited)
--- DISTILLED.md archive now contains the session? ---
NO — session lost
--- memory records after the retry ---
{"loa_entries":1,"extraction_sessions":1,"decisions":1,"learnings":0}
--- extraction_tracker row after retry window ---
{"conversation_path":"/var/folders/88/3h9cyc4979d2l6p7xkn79yqr0000gn/T/recall-ev-classify-base-jA0c2I/recall-home/MEMORY/opencode-sessions/ses_classify_demo.md","extracted_at":"2026-07-23 14:12:56","failed_at":null,"retry_after":null,"error":null}

==============================================================================
[FIXED] OpenCode drop file whose markdown extraction crashes
==============================================================================
--- RecallExtract stderr seen by the batch extractor ---
(none)
--- RecallBatchExtract decision + summary ---
[2026-07-23T14:12:57.272Z]   QUALITY GATE FAILED or extraction failed for /var/folders/88/3h9cyc4979d2l6p7xkn79yqr0000gn/T/recall-ev-classify-fixed-zdWNjF/recall-home/MEMORY/opencode-sessions/ses_classify_demo.md
[2026-07-23T14:12:57.273Z]   FAILED: Will retry after 24h cooldown
--- extraction_tracker row ---
{"conversation_path":"/var/folders/88/3h9cyc4979d2l6p7xkn79yqr0000gn/T/recall-ev-classify-fixed-zdWNjF/recall-home/MEMORY/opencode-sessions/ses_classify_demo.md","extracted_at":null,"failed_at":"2026-07-23 14:12:57","retry_after":"2026-07-24 14:12:57","error":"RecallBatchExtract: quality gate or runtime failure"}
--- memory records persisted by the crashed attempt ---
{"loa_entries":1,"extraction_sessions":1,"decisions":1,"learnings":0}
--- next batch run, after the fault is cleared ---
[2026-07-23T14:12:57.401Z]   SUCCESS: Extracted and tracked
--- DISTILLED.md archive now contains the session? ---
YES — session archived
--- memory records after the retry ---
{"loa_entries":2,"extraction_sessions":1,"decisions":2,"learnings":0}
--- extraction_tracker row after retry window ---
{"conversation_path":"/var/folders/88/3h9cyc4979d2l6p7xkn79yqr0000gn/T/recall-ev-classify-fixed-zdWNjF/recall-home/MEMORY/opencode-sessions/ses_classify_demo.md","extracted_at":"2026-07-23 14:12:57","failed_at":null,"retry_after":null,"error":null}
Evidence: Defect 2 — SQLite write failures are visible and never reported as success

A. read-only Recall DB [BASE] (silent) [FabricExtract] Appended markdown extraction to DISTILLED.md [FIXED] [FabricExtract] SQLite write failed (markdown): {"sessions":"attempt to write a readonly database","decisions":...,"loa":...} archive preserved in both; 0 memory records persisted in both B. every memory write fails, tracker writable [BASE] SUCCESS: Extracted and tracked | records {loa_entries:0,...} | tracker extracted_at set | never revisited [FIXED] FAILED: Will retry after 24h cooldown | retry_after set | after fault cleared: SUCCESS, records {loa_entries:1,extraction_sessions:1,decisions:1}


==============================================================================
[BASE] A — extraction against a read-only Recall database
==============================================================================
--- what the RecallExtract hook tells the operator ---
[FabricExtract] Appended markdown extraction to DISTILLED.md
--- memory records persisted ---
{"loa_entries":0,"extraction_sessions":0,"decisions":0,"learnings":0}
--- DISTILLED.md archive kept the session anyway? ---
YES — archive preserved

==============================================================================
[FIXED] A — extraction against a read-only Recall database
==============================================================================
--- what the RecallExtract hook tells the operator ---
[FabricExtract] SQLite write failed (markdown): {"sessions":"attempt to write a readonly database","decisions":"attempt to write a readonly database","breadcrumbs":"attempt to write a readonly database","loa":"attempt to write a readonly database"}
[FabricExtract] Appended markdown extraction to DISTILLED.md
--- memory records persisted ---
{"loa_entries":0,"extraction_sessions":0,"decisions":0,"learnings":0}
--- DISTILLED.md archive kept the session anyway? ---
YES — archive preserved

==============================================================================
[BASE] B — batch extraction when every memory write fails
==============================================================================
--- RecallBatchExtract verdict ---
[2026-07-23T14:01:04.574Z]   SUCCESS: Extracted and tracked
--- memory records persisted ---
{"loa_entries":0,"extraction_sessions":0,"decisions":0,"learnings":0}
--- extraction_tracker row ---
{"conversation_path":"/var/folders/88/3h9cyc4979d2l6p7xkn79yqr0000gn/T/recall-ev-nowrite-base-QFfzi7/recall-home/MEMORY/opencode-sessions/ses_nowrite_demo.md","extracted_at":"2026-07-23 14:01:04","failed_at":null,"retry_after":null,"error":null}
--- next batch run once writes work again ---
(file was never revisited)
--- memory records after recovery ---
{"loa_entries":0,"extraction_sessions":0,"decisions":0,"learnings":0}

==============================================================================
[FIXED] B — batch extraction when every memory write fails
==============================================================================
--- RecallBatchExtract verdict ---
[2026-07-23T14:01:05.279Z]   QUALITY GATE FAILED or extraction failed for /var/folders/88/3h9cyc4979d2l6p7xkn79yqr0000gn/T/recall-ev-nowrite-fixed-WWEYCC/recall-home/MEMORY/opencode-sessions/ses_nowrite_demo.md
[2026-07-23T14:01:05.280Z]   FAILED: Will retry after 24h cooldown
--- memory records persisted ---
{"loa_entries":0,"extraction_sessions":0,"decisions":0,"learnings":0}
--- extraction_tracker row ---
{"conversation_path":"/var/folders/88/3h9cyc4979d2l6p7xkn79yqr0000gn/T/recall-ev-nowrite-fixed-WWEYCC/recall-home/MEMORY/opencode-sessions/ses_nowrite_demo.md","extracted_at":null,"failed_at":"2026-07-23 14:01:05","retry_after":"2026-07-24 14:01:05","error":"RecallBatchExtract: quality gate or runtime failure"}
--- next batch run once writes work again ---
[2026-07-23T14:01:05.523Z]   SUCCESS: Extracted and tracked
--- memory records after recovery ---
{"loa_entries":1,"extraction_sessions":1,"decisions":1,"learnings":0}
Evidence: Defect 2 (retry/success accounting) — tracker write failure must not count as extracted

[BASE] WARN: failed to mark extracted in SQLite ... no such table: extraction_tracker SUCCESS: Extracted and tracked <- false success [FIXED] WARN: failed to mark extracted ... / WARN: failed to mark failed ... FAILED: Extraction completed but tracking failed; will retry after 24h cooldown New regression added (tests/hooks/RecallBatchExtract.test.ts): 17 pass / 0 fail; base tree prints '1 extracted, 0 failed' for the same input, so the test is non-vacuous.


==============================================================================
[BASE] batch extraction when the tracker table cannot be written
==============================================================================
--- RecallBatchExtract verdict ---
[2026-07-23T14:03:43.393Z] WARN: failed to mark extracted in SQLite for /var/folders/88/3h9cyc4979d2l6p7xkn79yqr0000gn/T/recall-ev-tracker-base-YjXtEU/recall-home/MEMORY/opencode-sessions/ses_tracker_demo.md: no such table: extraction_tracker
[2026-07-23T14:03:43.393Z]   SUCCESS: Extracted and tracked
--- memory records persisted ---
{"loa_entries":1,"extraction_sessions":1,"decisions":1,"learnings":0}

==============================================================================
[FIXED] batch extraction when the tracker table cannot be written
==============================================================================
--- RecallBatchExtract verdict ---
[2026-07-23T14:03:43.725Z] WARN: failed to mark extracted in SQLite for /var/folders/88/3h9cyc4979d2l6p7xkn79yqr0000gn/T/recall-ev-tracker-fixed-6urFXd/recall-home/MEMORY/opencode-sessions/ses_tracker_demo.md: no such table: extraction_tracker
[2026-07-23T14:03:43.725Z] WARN: failed to mark failed in SQLite for /var/folders/88/3h9cyc4979d2l6p7xkn79yqr0000gn/T/recall-ev-tracker-fixed-6urFXd/recall-home/MEMORY/opencode-sessions/ses_tracker_demo.md: no such table: extraction_tracker
[2026-07-23T14:03:43.725Z]   FAILED: Extraction completed but tracking failed; will retry after 24h cooldown
--- memory records persisted ---
{"loa_entries":1,"extraction_sessions":1,"decisions":1,"learnings":0}

==============================================================================
New regression added: tests/hooks/RecallBatchExtract.test.ts
  'batch extraction success accounting' — drives the real script end to end
==============================================================================
$ bun test tests/hooks/RecallBatchExtract.test.ts   (target commit)
 17 pass  0 fail

Non-vacuity check — same staged sandbox against the BASE hooks tree:
  [..]   SUCCESS: Extracted and tracked
  [..] === RecallBatchExtract complete: 1 extracted, 0 failed, 0 remaining ===
  -> the new test's assertions ('FAILED: Extraction completed but tracking failed',
     '0 extracted, 1 failed') fail on base and pass on the fix.
Evidence: Defect 3 — malformed OpenCode JSONC preserved, uninstall continues, dependency-free JSONC helper

[BASE] ✗ Failed to remove recall-memory ... — exit status 1 pi/mcp.json still has recall-memory: 1 | reached 'Uninstall Complete': NO [FIXED] ⚠ OpenCode config was left unchanged; continuing uninstall ✓ Removed recall-memory from .../pi/mcp.json → ║ Uninstall Complete ║ — exit status 0 malformed opencode.json byte-for-byte identical in BOTH runs; user's github entry preserved Packaged layout (only lib/jsonc-mcp.ts, node_modules present: NO): merge exit 0 / remove exit 0 — // comments and JSON5 trailing comma survive


==============================================================================
[BASE] uninstall with a malformed ~/.config/opencode/opencode.json
==============================================================================
--- uninstall transcript (OpenCode step onward) ---
→ Removing OpenCode integration...
recall: refusing to modify /var/folders/88/3h9cyc4979d2l6p7xkn79yqr0000gn/T//recall-uninstall-base-Wv6kFk/opencode/opencode.json — existing file is not valid JSON/JSONC
✗ Failed to remove recall-memory from /var/folders/88/3h9cyc4979d2l6p7xkn79yqr0000gn/T//recall-uninstall-base-Wv6kFk/opencode/opencode.json (existing config is invalid or unsupported — left unchanged)
--- exit status: 1 ---
--- user's malformed opencode.json unchanged? ---
YES — byte-for-byte identical (github + recall-memory entries and the comment intact)
--- remaining integrations cleaned up? ---
pi/mcp.json still has recall-memory:      1  (expected 0)
pi/mcp.json still has the user's github:  1  (expected 1)
claude/settings.json has recall-memory:   0  (expected 0)
claude/hooks/RecallExtract.ts removed:    YES
Recall_GUIDE.md removed:                  YES
reached 'Uninstall Complete':             NO

==============================================================================
[FIXED] uninstall with a malformed ~/.config/opencode/opencode.json
==============================================================================
--- uninstall transcript (OpenCode step onward) ---
→ Removing OpenCode integration...
recall: JSONC operation failed — expected comma at 216
✗ Failed to remove recall-memory from /var/folders/88/3h9cyc4979d2l6p7xkn79yqr0000gn/T//recall-uninstall-fixed-uhpJEv/opencode/opencode.json (existing config is invalid or unsupported — left unchanged)
⚠ OpenCode config was left unchanged; continuing uninstall

→ Removing Pi integration...
✓ Removed Recall's Pi package registration
✓ Removed recall-memory from /var/folders/88/3h9cyc4979d2l6p7xkn79yqr0000gn/T//recall-uninstall-fixed-uhpJEv/pi/mcp.json

→ Unlinking global binaries...
→ Skipping bun unlink (RECALL_SKIP_BUN_UNLINK=true)

╔══════════════════════════════════════════════════════════╗
║                    Uninstall Complete                    ║
╚══════════════════════════════════════════════════════════╝

✓ Recall uninstalled successfully.

Preserved (remove manually if desired):
  • ~/.agents/Recall/ (DB, canonical files, backups)
  • ~/.claude/memory.db (legacy DB if not migrated)
  • ~/.claude/MEMORY/ (non-Recall artifacts)
  • Source directory at /Users/ed/.no-mistakes/worktrees/eead88ef695d/01KY7J0N2K6QR3FZBD6XFQDWW3 (remove with: rm -rf /Users/ed/.no-mistakes/worktrees/eead88ef695d/01KY7J0N2K6QR3FZBD6XFQDWW3)
--- exit status: 0 ---
--- user's malformed opencode.json unchanged? ---
YES — byte-for-byte identical (github + recall-memory entries and the comment intact)
--- remaining integrations cleaned up? ---
pi/mcp.json still has recall-memory:      0  (expected 0)
pi/mcp.json still has the user's github:  1  (expected 1)
claude/settings.json has recall-memory:   0  (expected 0)
claude/hooks/RecallExtract.ts removed:    YES
Recall_GUIDE.md removed:                  YES
reached 'Uninstall Complete':             YES

==============================================================================
Dependency-free JSONC helper — packaged layout with no node_modules
==============================================================================
packaged files: ./lib/jsonc-mcp.ts 
node_modules present: NO
--- before ---
{
  // user comment
  "theme": "dark",
  "mcp": {
    "github": { "type": "local", "command": ["gh-mcp"] },
  },
}
--- after merge (exit 0) ---
{
  // user comment
  "theme": "dark",
  "mcp": {
    "github": { "type": "local", "command": ["gh-mcp"] },
    "recall-memory": {
      "type": "local",
      "command": [
        "recall-mcp"
      ],
      "environment": {
        "RECALL_DB_PATH": "/tmp/recall.db"
      }
    }
  },
}
--- after remove (exit 0) ---
{
  // user comment
  "theme": "dark",
  "mcp": {
    "github": { "type": "local", "command": ["gh-mcp"] }
  },
}
Evidence: Defect 4 — OpenCode plugin exposes the real `event` hook boundary

hooks exposed by the plugin factory : ["event"] typeof plugin.event : function obsolete 'session.idle' hook key : absent (correct) session.idle payload -> drop file created: .../MEMORY/opencode-sessions/ses_boundary_demo.md non session.idle event -> no drop created, existing drop untouched


==============================================================================
OpenCode plugin hook surface
==============================================================================
hooks exposed by the plugin factory : ["event"]
typeof plugin.event                 : function
obsolete 'session.idle' hook key    : absent (correct)

==============================================================================
OpenCode 1.18.4 emits session.idle through the event hook
==============================================================================
drop file created                   : /var/folders/88/3h9cyc4979d2l6p7xkn79yqr0000gn/T/recall-boundary-owbpNP/MEMORY/opencode-sessions/ses_boundary_demo.md
drop file head:
  # OpenCode Session: Recall boundary check
  
  Session ID: ses_boundary_demo
  
  ## user
  
  MARKER:BOUNDARYDEMO
  

==============================================================================
Non session.idle events are ignored at the same boundary
==============================================================================
other-event drop created            : no (correct)
existing drop untouched             : yes
Evidence: Real OpenCode 1.18.4 e2e (includes build) — boundary asserts, export retry, concurrent WAL writers, installer JSONC rollback

ESM ⚡️ Build success in 79ms / DTS ⚡️ Build success in 2052ms opencode.version=1.18.4 opencode.search_markers=OPENCODE_PHASE4_SEARCHABLE_MARKER,... opencode.export_failure_retry=verified opencode.concurrent_wal_writers=verified opencode.installer_jsonc_rollback=verified isolation.production_db_opened=false EXIT=0

$ bun run build && bun run scripts/e2e-opencode.ts
$ tsup src/index.ts src/mcp-server.ts --format esm --dts --clean --external bun:sqlite && node -e "const fs=require('fs');['dist/index.js','dist/mcp-server.js'].forEach(f=>{const c=fs.readFileSync(f,'utf8');if(!c.startsWith('#!/usr/bin/env node'))throw new Error(f+' missing expected shebang');fs.writeFileSync(f,c.replace('#!/usr/bin/env node','#!/usr/bin/env bun'))})"
CLI Building entry: src/index.ts, src/mcp-server.ts
CLI Using tsconfig: tsconfig.json
CLI tsup v8.5.1
CLI Target: es2022
CLI Cleaning output folder
ESM Build start
ESM dist/dump-IMJCGEP7.js  225.00 B
ESM dist/chunk-76L3MJB6.js 14.08 KB
ESM dist/mcp-server.js     30.68 KB
ESM dist/chunk-6QFGMCES.js 94.06 KB
ESM dist/chunk-YHD4BVUY.js 6.50 KB
ESM dist/index.js          236.11 KB
ESM ⚡️ Build success in 79ms
DTS Build start
DTS ⚡️ Build success in 2052ms
DTS dist/index.d.ts      20.00 B
DTS dist/mcp-server.d.ts 1.42 KB
isolation.recall_home=/var/folders/88/3h9cyc4979d2l6p7xkn79yqr0000gn/T/recall-opencode-e2e-VT5zod/recall-home
isolation.opencode_config=/var/folders/88/3h9cyc4979d2l6p7xkn79yqr0000gn/T/recall-opencode-e2e-VT5zod/xdg-config/opencode
isolation.test_db=/var/folders/88/3h9cyc4979d2l6p7xkn79yqr0000gn/T/recall-opencode-e2e-VT5zod/recall.db
isolation.production_db=/Users/ed/.agents/Recall/recall.db
isolation.production_before={"exists":true,"size":178925568,"mtimeMs":1784772395790.1538,"ino":571168746}
isolation.production_db_opened=false
[recall] OpenCode session export failed for ses_recall_phase4_retry; will retry on a later idle event: synthetic export failure
opencode.version=1.18.4
opencode.search_markers=OPENCODE_PHASE4_SEARCHABLE_MARKER,OPENCODE_PHASE4_SEARCHABLE_MARKER_RETRY,OPENCODEPHASE4CONCURRENTA,OPENCODEPHASE4CONCURRENTB
opencode.export_failure_retry=verified
opencode.concurrent_wal_writers=verified
opencode.installer_jsonc_rollback=verified
isolation.production_db_opened=false
EXIT=0
- Outcome: ⚠️ 1 warning across 1 run (25m10s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 11 issues found → auto-fixed ✅
  • 🚨 hooks/RecallBatchExtract.ts:78 - The broadened case-insensitive alternative /extraction failed/i also matches non-fatal provider-cascade messages that RecallExtract's child prints on a SUCCESSFUL run: [FabricExtract] Claude CLI extraction failed: ... (hooks/lib/hosts/claude/extraction-provider.ts:94). The cascade order is [claude, ollama], so on any machine where the Claude CLI is broken and Ollama works, every extraction succeeds, the child calls markAsExtracted, and then extractFile() returns false -> the batch calls recordFailure(). markAsFailed is INSERT OR REPLACE (hooks/lib/extraction-tracker.ts:53) with no extracted_at column, so it wipes the success record and sets a 24h retry -> the same conversation is re-extracted forever, appending duplicate rows to decisions/learnings/breadcrumbs (writeDecisionsBatch is a plain INSERT with no dedup) and duplicate blocks to DISTILLED.md / DECISIONS.log. This is systematic, not an edge case, on ollama-fallback setups. The old case-sensitive output.includes(&#39;Extraction failed&#39;) did not match these lowercase provider messages. Fix constraint: do NOT simply revert — the lowercase markdown case is exactly what the PR set out to fix. Drop only the bare extraction failed alternative and replace it with an anchored form such as /\[FabricExtract\] (?:markdown )?extraction failed/i, keeping quality gate failed|all extraction methods failed|sqlite write failed|persistence failed intact so Markdown extraction failed (RecallExtract.ts:839) and Extraction failed: (RecallExtract.ts:697) still classify as failures.
  • 🚨 lib/jsonc-mcp.ts:205 - The single-property removal branch splices out only current.keyStart..current.value.end and leaves a preceding/following trailing comma behind. Verified: input {\n &#34;mcp&#34;: {\n &#34;recall-memory&#34;: { &#34;type&#34;: &#34;local&#34; },\n }\n} produces {\n &#34;mcp&#34;: {\n ,\n }\n} and the process exits 0, so uninstall.sh logs Removed recall-memory from ... while having written a config that no JSON or JSONC parser accepts. The parser deliberately accepts trailing commas (object() continues past , before }), and the docs updated in this same diff claim the helper 'preserves ... trailing commas' and 'leaves a malformed OpenCode config unchanged' — so this directly inverts the intent's requirement to make uninstall 'preserv[e] user config'. jsonc-parser's modify() handled the separator; the replacement does not. Needs a separator-aware removal range (extend the deleted span through a following comma, or through a preceding one when it is the last property).
  • ⚠️ uninstall.sh:483 - [[ &#34;$config_failed&#34; == &#34;true&#34; ]] &amp;&amp; return 1 is the last statement in remove_opencode(), so on the SUCCESS path the [[ ]] test fails, the &amp;&amp; short-circuits, and the function's exit status is 1. main() then takes the if ! remove_opencode branch and prints OpenCode config was left unchanged; continuing uninstall on every uninstall, including ones where the config was removed cleanly. Verified with bash -euo pipefail. Neither new test catches it (both only assert overall status 0 / 'Uninstall Complete'). Fix: if [[ &#34;$config_failed&#34; == &#34;true&#34; ]]; then return 1; fi followed by an implicit success, or append return 0.
  • ⚠️ lib/jsonc-mcp.ts:209 - When recall-memory is not the last property, removal splices current.keyStart .. properties[currentIndex+1].keyStart, which swallows every comment sitting between the removed entry and the NEXT key — comments that belong to the surviving sibling. Verified: input containing &#34;recall-memory&#34;: {...},\n // GitHub MCP - hand tuned, do not touch\n &#34;github&#34;: {...} comes back as { &#34;mcp&#34;: { &#34;github&#34;: { &#34;type&#34;: &#34;local&#34; } } } with the comment gone. The mirror branch at line 212 (recall-memory last) deletes from the previous entry's value end, eating any comment attached after it too. This contradicts the contract asserted in tests/opencode-integration.test.ts and the docs updated here ('preserves comments'), on the uninstall path the intent requires to preserve user config.
  • ⚠️ lib/jsonc-mcp.ts:158 - insertProperty() applies the separator comma at the closing brace rather than immediately after the previous property's value, so the comma lands on its own line. Verified: {\n &#34;$schema&#34;: &#34;...&#34;,\n &#34;theme&#34;: &#34;dark&#34;\n} becomes ...\n &#34;theme&#34;: &#34;dark&#34;\n,\n &#34;mcp&#34;: {...}\n}. This is the common first-install path (an opencode.json with $schema/theme but no mcp key), so most users' configs get visibly mangled formatting that jsonc-parser's modify() did not produce. Fix: emit the separator as a separate splice at lastProperty.value.end and keep the new property insertion at close.
  • ⚠️ hooks/RecallExtract.ts:689 - On persistence_failed the code still runs every markdown archive side-effect (DISTILLED.md append, HOT_RECALL, SESSION_INDEX, LoA transcript, DECISIONS/REJECTIONS/ERROR_PATTERNS) and only then calls markAsFailed and returns — deliberate per commit 4d4a4fa. But dualWriteToSqlite is not transactional: each table has its own try/catch (hooks/lib/extraction-parsers.ts:143-215), so a partial failure (e.g. sessions+decisions written, loa failed) still yields persistence_failed. The 24h retry then re-runs the whole thing: writeDecisionsBatch / writeLearningsBatch / writeBreadcrumbsBatch are plain INSERTs with no dedup (only writeExtractionSession is INSERT OR REPLACE), and the markdown files are append-only. Result: duplicated memory rows and duplicated archive blocks. The pre-change behavior was silent loss; the new behavior trades it for silent duplication. Worth deciding explicitly — e.g. treat only a total write failure (failures._db) as retryable, or skip the archive writes when the entry will be re-extracted. Same pattern at line 831 for the markdown path.
  • ⚠️ hooks/lib/insession.ts:470 - The in-session path now returns dualWrite alongside outcome: &#39;persistence_failed&#39;, but hooks/lib/insession.ts contains no logging at all and its only production caller (hooks/RecallInSession.ts:194) discards the return value entirely. So a WAL/lock write failure during in-session extraction is still completely invisible — no console.error, no log line — while the Stop and markdown paths in RecallExtract.ts do surface it. The intent requires 'make SQLite WAL writer failures visible'; that requirement is met for the Stop/markdown paths but not for this one. Either log the failure in RecallInSession or state that the in-session path is out of scope.
  • ⚠️ tests/install/uninstall.test.ts:582 - The new full-uninstall test writes a fake bun shim that hardcodes exec /opt/homebrew/bin/bun &#34;$@&#34;. That path only exists on Apple-Silicon Homebrew installs — on Linux CI, Intel Homebrew (/usr/local), or a ~/.bun/bin install the shim exec fails and the test breaks for reasons unrelated to what it asserts. Resolve the real bun once (e.g. process.execPath or which bun) and interpolate it into the shim.
  • ℹ️ tests/hooks/extract-core.test.ts:155 - The new lock test creates /tmp/recall-busy-&lt;pid&gt;.db (plus its journal) and never removes it, and it hardcodes /tmp instead of the mkdtempSync pattern used elsewhere in this suite. The test is also named 'surfaces a WAL database lock' but sets PRAGMA journal_mode = DELETE, so it exercises the rollback-journal lock rather than WAL — worth renaming or switching the pragma so it matches the defect it is pinning.
  • ℹ️ AGENTS.md:31 - lib/ is documented as 'shared bash for the install / update / uninstall lifecycle scripts', but this change adds lib/jsonc-mcp.ts — a shipped TypeScript runtime helper the lifecycle scripts shell out to. Line 180 also scopes root ownership to 'lifecycle scripts ... + their shared lib/install-lib.sh'. The repo's DOX contract requires the owning doc to be updated when durable structure changes; docs/OPENCODE_INTEGRATION.md was updated but AGENTS.md was not.
  • ℹ️ package.json:54 - jsonc-parser is now referenced nowhere in src/, hooks/, lib/, or scripts/ — the only remaining mentions are a comment in lib/jsonc-mcp.ts and a negative assertion in tests/opencode-integration.test.ts. It is still declared under dependencies, so every install pulls it. Either drop it or note why it is retained.

🔧 Fix: fix batch failure classification, JSONC insertion, uninstall status
✅ Re-checked - no issues remain.

⚠️ **Test** - 1 warning
  • ⚠️ hooks/RecallExtract.ts:839 - Retrying an archive-crash extraction duplicates SQLite memory rows. The new lowercase-failure classification correctly makes [FabricExtract] Markdown extraction failed retryable, but that is the only failure class where runExtractCore already returned &#39;extracted&#39; (dual-write persisted) before the archive append threw. The retry re-runs the whole core, so the same session is written twice: in the end-to-end run loa_entries went 1 -> 2 and decisions 1 -> 2 after the retry (extraction_sessions stayed 1, PK-idempotent). Base lost the archive silently instead; whether duplicate loa/decision rows are the better trade is an author call. Evidence: defect1-classification.txt, FIXED branch, 'memory records persisted by the crashed attempt' vs 'memory records after the retry'.
  • bun test (full suite, 1277 pass / 0 fail; issue #243's worktree lifecycle failures did not reproduce)
  • bun test tests/hooks/RecallBatchExtract.test.ts tests/hooks/extract-core.test.ts tests/hooks/extraction-parsers.test.ts
  • bun test tests/install/uninstall.test.ts tests/opencode-integration.test.ts tests/pi-integration.test.ts
  • bun run test:e2e:opencode (includes bun run build; real OpenCode 1.18.4 CLI, asserts typeof plugin.event === &#39;function&#39; and no session.idle key)
  • Added regression tests/hooks/RecallBatchExtract.test.ts -> batch extraction success accounting (drives the real script; asserts a tracker-write failure is not counted as extracted)
  • Non-vacuity check: same staged sandbox run against the base-commit hooks/RecallBatchExtract.ts prints SUCCESS: Extracted and tracked / 1 extracted, 0 failed
  • Manual base-vs-fixed harness evidence-classification.ts — OpenCode drop file whose markdown extraction crashes (read-only DISTILLED.md)
  • Manual base-vs-fixed harness evidence-persistence.ts — read-only Recall DB, and batch extraction with every memory-table write aborting
  • Manual base-vs-fixed harness evidence-tracker-write.ts — extraction succeeds but the extraction_tracker write fails
  • Manual base-vs-fixed harness evidence-uninstall.shbash uninstall.sh --no-confirm --skip-omp against a malformed opencode.json plus a real Pi/Claude install, and bun run lib/jsonc-mcp.ts merge|remove in a packaged layout with no node_modules
  • Manual harness evidence-opencode-boundary.ts — plugin factory hook surface plus a real session.idle event payload producing the markdown drop
✅ **Document** - passed

✅ No issues found.

⚠️ **Lint** - 1 info
  • ⚠️ package.json:54 - jsonc-parser is now an orphaned runtime dependency. This change replaced its only consumer (the require(process.env.REPO_DIR + &#34;/node_modules/jsonc-parser&#34;) calls in lib/install-lib.sh) with the dependency-free lib/jsonc-mcp.ts; a repo-wide grep finds zero remaining imports or requires. It still ships in dependencies, so every npm install pulls dead weight — and dropping the runtime dependency was an explicit goal of this change. Not fixed here: removing a dependency touches package.json plus bun.lock and is a functional change, outside a documentation-and-lint pass.

🔧 Fix: remove orphaned jsonc-parser dep, dedupe extraction dual-write retries
1 info still open:

  • ℹ️ hooks/lib/sqlite-writers.ts:10 - Residual limitation of the idempotency fix, surfaced for visibility rather than left silent. The archive-crash retry path re-runs the extraction model (extractAndAppend -> runExtractCore -> runExtractionCascade), and the cascade sets no temperature (no temperature/max_tokens anywhere in hooks/lib/extract-model.ts, extraction-provider.ts, or hosts/claude/extraction-provider.ts). The new guard keys on (session_id, content), so it eliminates duplicates when the rerun yields identical text -- the case the finding describes -- but a materially different model rerun would still insert new rows. A model-independent guarantee needs a persistence marker in extraction_tracker so dualWrite is skipped entirely for an already-persisted conversation; that is larger than the requested smallest correct fix, so it is noted as follow-up, not done.
✅ **Push** - passed

✅ No issues found.

@edheltzel
edheltzel merged commit eebe92d into main Jul 23, 2026
2 checks passed
@edheltzel
edheltzel deleted the fm/recall-pr248-review-fixes-r10 branch July 23, 2026 15:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant