@ijfw/memory-server 1.4.4 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/bin/ijfw-memorize +14 -7
  2. package/fixtures/team/book.json +6 -6
  3. package/fixtures/team/business.json +146 -20
  4. package/fixtures/team/content.json +6 -6
  5. package/fixtures/team/design.json +148 -20
  6. package/fixtures/team/mixed.json +206 -27
  7. package/fixtures/team/research.json +146 -20
  8. package/fixtures/team/software.json +148 -20
  9. package/fixtures/truncation-corpus/_generate-corpus.js +367 -0
  10. package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
  11. package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
  12. package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
  13. package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
  14. package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
  15. package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
  16. package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
  17. package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
  18. package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
  19. package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
  20. package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
  21. package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
  22. package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
  23. package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
  24. package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
  25. package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
  26. package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
  27. package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
  28. package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
  29. package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
  30. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
  31. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
  32. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
  33. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
  34. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
  35. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
  36. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
  37. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
  38. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
  39. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
  40. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
  41. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
  42. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
  43. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
  44. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
  45. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
  46. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
  47. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
  48. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
  49. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
  50. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
  51. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
  52. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
  53. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
  54. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
  55. package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
  56. package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
  57. package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
  58. package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
  59. package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
  60. package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
  61. package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
  62. package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
  63. package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
  64. package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
  65. package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
  66. package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
  67. package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
  68. package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
  69. package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
  70. package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
  71. package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
  72. package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
  73. package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
  74. package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
  75. package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
  76. package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
  77. package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
  78. package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
  79. package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
  80. package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
  81. package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
  82. package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
  83. package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
  84. package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
  85. package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
  86. package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
  87. package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
  88. package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
  89. package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
  90. package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
  91. package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
  92. package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
  93. package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
  94. package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
  95. package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
  96. package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
  97. package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
  98. package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
  99. package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
  100. package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
  101. package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
  102. package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
  103. package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
  104. package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
  105. package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
  106. package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
  107. package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
  108. package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
  109. package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
  110. package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
  111. package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
  112. package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
  113. package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
  114. package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
  115. package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
  116. package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
  117. package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
  118. package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
  119. package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
  120. package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
  121. package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
  122. package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
  123. package/package.json +6 -3
  124. package/src/active-extension-writer.js +144 -64
  125. package/src/api-client.js +43 -5
  126. package/src/audit-roster.js +80 -5
  127. package/src/blackboard.js +298 -6
  128. package/src/cli-run.js +33 -5
  129. package/src/codex-agents.js +96 -5
  130. package/src/cost/aggregator.js +39 -9
  131. package/src/cost/pricing.js +57 -0
  132. package/src/cost/readers/gemini.js +1 -1
  133. package/src/cross-audit-chunker.js +189 -0
  134. package/src/cross-dispatcher.js +124 -21
  135. package/src/cross-orchestrator-cli.js +754 -159
  136. package/src/cross-orchestrator.js +1065 -17
  137. package/src/cross-project-search.js +195 -9
  138. package/src/dashboard-client-waves.html +304 -0
  139. package/src/dashboard-client.html +5 -1
  140. package/src/dashboard-server.js +73 -0
  141. package/src/deploy-alerts.js +150 -0
  142. package/src/design/iframe-bridge.js +242 -0
  143. package/src/design-companion.js +144 -0
  144. package/src/dispatch/checkpoint-cli.js +97 -0
  145. package/src/dispatch/colon-syntax.js +81 -1
  146. package/src/dispatch/extension.js +26 -2
  147. package/src/dispatch/registry-cli.js +4 -1
  148. package/src/dispatch/wave-cli.js +201 -6
  149. package/src/dispatch/worktree-cli.js +40 -0
  150. package/src/dispatch-planner.js +97 -2
  151. package/src/dream/runner.mjs +47 -11
  152. package/src/dream/stage-runner.js +40 -0
  153. package/src/dream/state-file.js +102 -0
  154. package/src/extension-installer.js +70 -24
  155. package/src/extension-quota-tracker.js +4 -2
  156. package/src/extension-registry.js +289 -35
  157. package/src/feedback-detector.js +26 -0
  158. package/src/fs-lock.js +259 -7
  159. package/src/gate-result.js +95 -1
  160. package/src/hardware-signer.js +4 -2
  161. package/src/hero-line.js +86 -5
  162. package/src/intent-router.js +35 -0
  163. package/src/lib/a11y-contract.js +117 -0
  164. package/src/lib/atomic-io.js +29 -8
  165. package/src/lib/cache-keepalive.js +150 -0
  166. package/src/lib/jsonl-rotation.js +104 -0
  167. package/src/lib/lighthouse-pillar.js +121 -0
  168. package/src/lib/llm-call.js +121 -0
  169. package/src/lib/playwright-baseline.js +205 -0
  170. package/src/lib/rekor-bridge.js +221 -0
  171. package/src/lib/repo-map.js +392 -0
  172. package/src/lib/shasum-verify.js +164 -0
  173. package/src/lib/sketches-gc.js +132 -0
  174. package/src/lib/tmp-suffix.js +62 -0
  175. package/src/lib/ui-review-runner.js +595 -0
  176. package/src/lib/uispec-drift.js +301 -0
  177. package/src/lib/uispec-intake.js +381 -0
  178. package/src/lib/worktree-guards.js +118 -0
  179. package/src/lib/worktree-recovery.js +100 -0
  180. package/src/memory/auto-linker.js +267 -0
  181. package/src/memory/benchmark.js +498 -0
  182. package/src/memory/dedup.js +126 -0
  183. package/src/memory/embedding-cache.js +136 -0
  184. package/src/memory/fact-extractor.js +168 -0
  185. package/src/memory/fts5.js +65 -1
  186. package/src/memory/migration-runner.js +6 -1
  187. package/src/memory/migrations/004-bitemporal.js +91 -0
  188. package/src/memory/migrations/005-vector-cache.js +61 -0
  189. package/src/memory/migrations/006-obsidian-graph.js +46 -0
  190. package/src/memory/migrations/007-skill-telemetry.js +24 -0
  191. package/src/memory/migrations/008-write-provenance.js +41 -0
  192. package/src/memory/migrations/009-obsidian-backfill.js +50 -0
  193. package/src/memory/obsidian-parser.js +152 -0
  194. package/src/memory/query-dataview.js +86 -0
  195. package/src/memory/search.js +46 -15
  196. package/src/memory/temporal.js +529 -0
  197. package/src/memory/tokenize.js +10 -0
  198. package/src/memory-facts-handler.js +37 -0
  199. package/src/memory-feedback.js +260 -2
  200. package/src/model-refresh.js +292 -0
  201. package/src/observability/cost-anomaly.js +166 -0
  202. package/src/observability/evaluator-checkpoint-contract.js +117 -0
  203. package/src/observability/trace-id.js +163 -0
  204. package/src/orchestrator/agents-md-blackboard.js +152 -0
  205. package/src/orchestrator/checkpoint-contract.md +140 -0
  206. package/src/orchestrator/debug-trident-trigger.js +374 -0
  207. package/src/orchestrator/debug-trident.js +570 -0
  208. package/src/orchestrator/merge-block-aware.js +350 -0
  209. package/src/orchestrator/plan-checker.js +475 -0
  210. package/src/orchestrator/post-done-runner.js +277 -0
  211. package/src/orchestrator/review.js +38 -3
  212. package/src/orchestrator/skill-telemetry-sink.js +29 -0
  213. package/src/orchestrator/skill-telemetry.js +37 -0
  214. package/src/orchestrator/state-events.js +459 -0
  215. package/src/orchestrator/state-sdk.js +1932 -0
  216. package/src/orchestrator/status-protocol.js +84 -17
  217. package/src/orchestrator/subagent-telemetry.js +471 -0
  218. package/src/orchestrator/termination.js +160 -0
  219. package/src/orchestrator/verification-gate.js +200 -16
  220. package/src/orchestrator/wave-state.js +332 -23
  221. package/src/orchestrator/worktree-provision.js +77 -0
  222. package/src/override-resolver.js +5 -3
  223. package/src/override-use-registry.js +111 -5
  224. package/src/receipts.js +36 -4
  225. package/src/recovery/checkpoint.js +56 -3
  226. package/src/recovery/code-fixer.js +961 -0
  227. package/src/recovery/truncation.js +317 -0
  228. package/src/redactor.js +75 -6
  229. package/src/runtime-mediator.js +15 -1
  230. package/src/sanitizer.js +10 -0
  231. package/src/search-hybrid.js +139 -0
  232. package/src/server.js +795 -112
  233. package/src/swarm/worktree.js +27 -4
  234. package/src/swarm-config.js +102 -17
  235. package/src/team/domain-templates/book.json +51 -0
  236. package/src/team/domain-templates/business.json +44 -0
  237. package/src/team/domain-templates/content.json +50 -0
  238. package/src/team/domain-templates/design.json +44 -0
  239. package/src/team/domain-templates/research.json +44 -0
  240. package/src/team/domain-templates/software.json +40 -0
  241. package/src/team/generator.js +440 -3
  242. package/src/team/modify.js +203 -0
  243. package/src/team/schemas.js +48 -0
  244. package/src/update-apply.js +19 -3
  245. package/src/dashboard-charts.js +0 -239
@@ -0,0 +1,18 @@
1
+ {
2
+ "id": "fx-05-error-terminated-03",
3
+ "category": "error-terminated",
4
+ "waveId": "W20-errT-3",
5
+ "subId": "errT3",
6
+ "notes": "error-terminated with overwrite partial — rollback restores baseline",
7
+ "expectedDetection": {
8
+ "truncated": "error-terminated",
9
+ "reasonContains": "outcome='error'"
10
+ },
11
+ "expectedRecovery": {
12
+ "recovered": true
13
+ },
14
+ "expectedFinalState": {
15
+ ".ijfw/state/workflow.json": "{\"phase\":\"errT-pre-3\"}"
16
+ },
17
+ "expectedTerminalVerb": "subagent.post-done"
18
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "verbId": "v-errT-3-partial",
3
+ "targets": [
4
+ {
5
+ "relPath": ".ijfw/state/workflow.json",
6
+ "absPath": "<<ABS>>",
7
+ "existed": true,
8
+ "content": "{\"phase\":\"errT-pre-3\"}"
9
+ }
10
+ ]
11
+ }
@@ -0,0 +1,2 @@
1
+ {"seq":1,"verb":"subagent.checkpoint","subagentId":"errT4","ts":"2026-05-20T12:00:00.000Z","verbId":"v-errT-4-ckpt","outcome":"ok","payloadDigest":"sha256-fixture-v-errT-4-ckpt"}
2
+ {"seq":2,"verb":"decision.add","subagentId":"errT4","ts":"2026-05-20T12:00:00.000Z","verbId":"v-errT-4-partial","outcome":"error","payloadDigest":"sha256-fixture-v-errT-4-partial"}
@@ -0,0 +1,3 @@
1
+ {"verb":"subagent.checkpoint","verbId":"v-errT-4-ckpt","phase":"begin","ts":"2026-05-20T12:00:00.000Z","targets":[".ijfw/wave-W20-errT-4/subagent-errT4.checkpoint.json"],"payloadDigest":"sha256-fixture-v-errT-4-ckpt","kind":"append","dedupKey":"dk-ckpt-errT-4"}
2
+ {"verb":"subagent.checkpoint","verbId":"v-errT-4-ckpt","phase":"commit","ts":"2026-05-20T12:00:00.000Z","payloadDigest":"sha256-fixture-v-errT-4-ckpt","kind":"append","dedupKey":"dk-ckpt-errT-4"}
3
+ {"verb":"decision.add","verbId":"v-errT-4-partial","phase":"begin","ts":"2026-05-20T12:00:00.000Z","targets":[".ijfw/blackboard/decisions.jsonl"],"payloadDigest":"sha256-fixture-v-errT-4-partial","kind":"append","dedupKey":"dk-errT-4"}
@@ -0,0 +1,18 @@
1
+ {
2
+ "id": "fx-05-error-terminated-04",
3
+ "category": "error-terminated",
4
+ "waveId": "W20-errT-4",
5
+ "subId": "errT4",
6
+ "notes": "error-terminated with append partial — seal-in-place preserves record",
7
+ "expectedDetection": {
8
+ "truncated": "error-terminated",
9
+ "reasonContains": "outcome='error'"
10
+ },
11
+ "expectedRecovery": {
12
+ "recovered": true
13
+ },
14
+ "expectedFinalState": {
15
+ ".ijfw/blackboard/decisions.jsonl": "{\"id\":\"d-errT-4\",\"text\":\"errT 4\",\"dedupKey\":\"dk-errT-4\"}\n"
16
+ },
17
+ "expectedTerminalVerb": "subagent.post-done"
18
+ }
@@ -0,0 +1 @@
1
+ {"id":"d-errT-4","text":"errT 4","dedupKey":"dk-errT-4"}
@@ -0,0 +1,2 @@
1
+ {"seq":1,"verb":"workflow.set-phase","subagentId":"errT5","ts":"2026-05-20T12:00:00.000Z","verbId":"v-errT-5-ok","outcome":"ok","payloadDigest":"sha256-fixture-v-errT-5-ok"}
2
+ {"seq":2,"verb":"wave.advance","subagentId":"errT5","ts":"2026-05-20T12:00:00.000Z","verbId":"v-errT-5-partial","outcome":"error","payloadDigest":"sha256-fixture-v-errT-5-partial"}
@@ -0,0 +1,3 @@
1
+ {"verb":"workflow.set-phase","verbId":"v-errT-5-ok","phase":"begin","ts":"2026-05-20T12:00:00.000Z","targets":[".ijfw/state/workflow.json"],"payloadDigest":"sha256-fixture-v-errT-5-ok","kind":"overwrite"}
2
+ {"verb":"workflow.set-phase","verbId":"v-errT-5-ok","phase":"commit","ts":"2026-05-20T12:00:00.000Z","payloadDigest":"sha256-fixture-v-errT-5-ok","kind":"overwrite"}
3
+ {"verb":"wave.advance","verbId":"v-errT-5-partial","phase":"begin","ts":"2026-05-20T12:00:00.000Z","targets":[".ijfw/state/workflow.json"],"payloadDigest":"sha256-fixture-v-errT-5-partial","kind":"overwrite"}
@@ -0,0 +1,18 @@
1
+ {
2
+ "id": "fx-05-error-terminated-05",
3
+ "category": "error-terminated",
4
+ "waveId": "W20-errT-5",
5
+ "subId": "errT5",
6
+ "notes": "error-terminated with overwrite partial — rollback restores baseline",
7
+ "expectedDetection": {
8
+ "truncated": "error-terminated",
9
+ "reasonContains": "outcome='error'"
10
+ },
11
+ "expectedRecovery": {
12
+ "recovered": true
13
+ },
14
+ "expectedFinalState": {
15
+ ".ijfw/state/workflow.json": "{\"phase\":\"errT-pre-5\"}"
16
+ },
17
+ "expectedTerminalVerb": "subagent.post-done"
18
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "verbId": "v-errT-5-partial",
3
+ "targets": [
4
+ {
5
+ "relPath": ".ijfw/state/workflow.json",
6
+ "absPath": "<<ABS>>",
7
+ "existed": true,
8
+ "content": "{\"phase\":\"errT-pre-5\"}"
9
+ }
10
+ ]
11
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ijfw/memory-server",
3
- "version": "1.4.4",
4
- "description": "Cross-platform persistent memory server for IJFW. 10 MCP tools (memory + admin/update). Works with 13 MCP-using platforms (Claude Code, Codex, Gemini CLI, Cursor, Windsurf, Copilot, Hermes, Wayland, OpenCode, QwenCode, Cline, KimiCode, OpenClaw) plus Aider via rules-only tier.",
3
+ "version": "1.5.1",
4
+ "description": "Cross-platform persistent memory server for IJFW. 13 MCP tools (memory + admin/update). Works with 15 platforms: 14 via MCP (Claude Code, Codex, Gemini CLI, Cursor, Windsurf, Copilot, Hermes, Wayland, OpenCode, QwenCode, Cline, KimiCode, OpenClaw, Antigravity) plus Aider via the rules-only tier.",
5
5
  "author": "Sean Donahoe",
6
6
  "license": "MIT",
7
7
  "type": "module",
@@ -14,7 +14,10 @@
14
14
  "scripts": {
15
15
  "start": "node src/server.js",
16
16
  "dev": "node --watch src/server.js",
17
- "test": "node test.js"
17
+ "test": "node test.js && node --experimental-sqlite --test --test-force-exit test-*.js",
18
+ "test:smoke": "node test.js",
19
+ "test:full": "node --experimental-sqlite --test --test-force-exit test-*.js",
20
+ "test:graders": "node test/grade-symbol-graph-spec.js && node test/grade-symbol-graph-consistency.js && node test/grade-cascading-staleness.js && node test/grade-project-types.js"
18
21
  },
19
22
  "engines": {
20
23
  "node": ">=18.0.0"
@@ -5,14 +5,38 @@
5
5
  * clears it on deactivate. Used by:
6
6
  * - `ijfw_run extension:activate <name>` CLI command
7
7
  * - installExtension when opts.activate is set
8
+ *
9
+ * v1.5.0 T10 — Migrated to the state-SDK. The legacy
10
+ * `writeFile`/`rename`/`unlink` direct-write path is replaced by a call to
11
+ * `query('extension.set-active', ...)`. The state-SDK is the single mutation
12
+ * surface for `~/.ijfw/state/active-extension.json`; this writer now adapts
13
+ * the SDK's verb-returns to the writer's `{ok, path, error}` /
14
+ * `{ok, removed}` external API so every existing caller keeps working without
15
+ * change (`extension-installer.js`, `dispatch/active-cli.js`,
16
+ * `dispatch/extension.js`, the test suite).
17
+ *
18
+ * Side-effects retained verbatim from the v1.4.0/W7.1 contract:
19
+ * - quota counters are reset on activate (B16/SEC-M-02) and clear (B16) via
20
+ * `resetExtensionQuotas` — best-effort, never blocks the verb result.
21
+ * - B18 cross-IDE last-seen / divergence detection (`detectCrossIdeDivergence`)
22
+ * stays as-is — it is a read-only consumer of the homedir state file and
23
+ * does not need the SDK.
24
+ * - `findInstalledManifest` stays as-is — it reads project/org/user-scope
25
+ * manifest.json files; it is a read of installed extensions, not a write
26
+ * to the homedir state file, so it is outside the state-SDK surface.
8
27
  */
9
28
 
10
29
  import { readFile, writeFile, unlink, mkdir, readdir, stat } from 'node:fs/promises';
30
+ import { existsSync } from 'node:fs';
11
31
  import { join, dirname } from 'node:path';
12
32
  import { homedir } from 'node:os';
13
- import { randomBytes } from 'node:crypto';
14
33
 
15
34
  import { resetExtensionQuotas } from './extension-quota-tracker.js';
35
+ // v1.5.0 audit-LOW-update-#13: shared tmp-suffix helper.
36
+ import { tmpSuffix } from './lib/tmp-suffix.js';
37
+ // v1.5.0 T10: route the canonical write/clear of ~/.ijfw/state/active-extension.json
38
+ // through the state-SDK verb.
39
+ import { query } from './orchestrator/state-sdk.js';
16
40
 
17
41
  const STATE_PATH_REL = ['.ijfw', 'state', 'active-extension.json'];
18
42
 
@@ -34,16 +58,37 @@ function stateDir(home) {
34
58
  return join(home || homedir(), '.ijfw', 'state');
35
59
  }
36
60
 
61
+ /**
62
+ * Pick a state-SDK projectRoot to satisfy the verb's `ctx.projectRoot`
63
+ * requirement. The writer's external callers don't carry a project root
64
+ * (extension activation is a homedir-scoped operation), so we fall back to
65
+ * `process.cwd()`. The SDK uses this only for the intent-journal lock path
66
+ * (`<projectRoot>/.ijfw/state/intent-journal.jsonl`) — it never mutates any
67
+ * project file under it. opts.projectRoot, when supplied, takes precedence.
68
+ */
69
+ function pickProjectRoot(opts) {
70
+ if (opts && typeof opts.projectRoot === 'string' && opts.projectRoot.length > 0) {
71
+ return opts.projectRoot;
72
+ }
73
+ return process.cwd();
74
+ }
75
+
37
76
  /**
38
77
  * Write the active-extension state file from a manifest + scope.
39
- * Validates required fields before write. Atomic write via tmp+rename.
78
+ * Validates required fields before write. Atomic write via the state-SDK
79
+ * `extension.set-active` verb (tmp+rename via atomic-io).
40
80
  *
41
- * @param {{ name: string, permissions: { reads: string[], writes: string[] } }} manifest
81
+ * @param {{ name: string, permissions: { reads: string[], writes: string[] }, quotas?: object }} manifest
42
82
  * @param {'project'|'org'|'user'} scope
43
- * @param {{ homeDir?: string, ideId?: string|null }} [opts]
83
+ * @param {{ homeDir?: string, ideId?: string|null, projectRoot?: string }} [opts]
44
84
  * @returns {Promise<{ ok: boolean, path?: string, error?: string }>}
45
85
  */
46
86
  export async function writeActiveExtension(manifest, scope, opts = {}) {
87
+ // Validation — external API contract (returns ok:false, never throws).
88
+ // Mirrors the pre-v1.5.0 behavior so every existing caller keeps the same
89
+ // error-handling shape. The SDK verb would throw on the same inputs; the
90
+ // adapter catches that downstream too, but explicit pre-checks give the
91
+ // historical error messages.
47
92
  if (!manifest || typeof manifest !== 'object') {
48
93
  return { ok: false, error: 'manifest must be an object' };
49
94
  }
@@ -56,57 +101,73 @@ export async function writeActiveExtension(manifest, scope, opts = {}) {
56
101
  if (!manifest.permissions || typeof manifest.permissions !== 'object') {
57
102
  return { ok: false, error: 'manifest.permissions required' };
58
103
  }
59
- const reads = Array.isArray(manifest.permissions.reads) ? manifest.permissions.reads : [];
60
- const writes = Array.isArray(manifest.permissions.writes) ? manifest.permissions.writes : [];
61
- const activatedAt = new Date().toISOString();
104
+
105
+ const home = opts && opts.homeDir ? opts.homeDir : (process.env.HOME || homedir());
106
+
62
107
  // B18: stamp activated_by_ide + activated_by_pid when ideId is provided.
63
108
  // Caller (CLI) is responsible for calling detectIde() and threading the
64
- // value in. When opts.ideId is null/undefined, fields are omitted (so the
65
- // file stays back-compatible with v1.4.1 readers).
109
+ // value in. When opts.ideId is null/undefined or invalid, fields are
110
+ // omitted (so the file stays back-compatible with v1.4.1 readers).
66
111
  const ideId = (typeof opts.ideId === 'string' && IDE_ID_PATTERN.test(opts.ideId))
67
112
  ? opts.ideId
68
113
  : null;
69
- const out = {
114
+
115
+ // Normalize permissions + quotas in the manifest payload so the SDK verb
116
+ // sees the exact shape it expects. The verb itself filters quotas to
117
+ // positive integers — we leave that filtering to it (single writer rule).
118
+ const reads = Array.isArray(manifest.permissions.reads) ? manifest.permissions.reads : [];
119
+ const writes = Array.isArray(manifest.permissions.writes) ? manifest.permissions.writes : [];
120
+ const verbManifest = {
70
121
  name: manifest.name,
71
- scope,
72
122
  permissions: { reads, writes },
73
- activated_at: activatedAt,
74
123
  };
75
- if (ideId) {
76
- out.activated_by_ide = ideId;
77
- out.activated_by_pid = process.pid;
78
- }
79
- // R12-H-01: persist manifest.quotas so the tier-2 hook
80
- // (extension-permission-check.mjs) can enforce quotas on Edit/Write/Bash
81
- // dispatch. Without this the tier-2 hook reads `active.quotas` as undefined
82
- // and silently bypasses the v1.4.3 quota gate that the server-side
83
- // gatePermissionAndQuota path enforces. Schema (extension-manifest-schema.js):
84
- // optional object whose values are positive integers — currently
85
- // max_files_written / max_bytes_written / max_wall_clock_ms (forward-compat:
86
- // unknown dimensions are kept as-is — schema rejects unknowns at install).
87
124
  if (
88
125
  manifest.quotas !== undefined &&
89
126
  manifest.quotas !== null &&
90
127
  typeof manifest.quotas === 'object' &&
91
128
  !Array.isArray(manifest.quotas)
92
129
  ) {
93
- const cleanQuotas = {};
94
- let copied = 0;
95
- for (const [k, v] of Object.entries(manifest.quotas)) {
96
- if (typeof v === 'number' && Number.isFinite(v) && Number.isInteger(v) && v > 0) {
97
- cleanQuotas[k] = v;
98
- copied++;
99
- }
100
- }
101
- if (copied > 0) out.quotas = cleanQuotas;
130
+ verbManifest.quotas = manifest.quotas;
102
131
  }
103
- const home = opts && opts.homeDir ? opts.homeDir : (process.env.HOME || homedir());
104
- const path = statePath(home);
105
- await mkdir(dirname(path), { recursive: true });
106
- const tmp = `${path}.tmp.${randomBytes(4).toString('hex')}`;
107
- await writeFile(tmp, JSON.stringify(out, null, 2) + '\n', 'utf8');
108
- const { rename } = await import('node:fs/promises');
109
- await rename(tmp, path);
132
+
133
+ const payload = {
134
+ manifest: verbManifest,
135
+ scope,
136
+ homeDir: home,
137
+ };
138
+ if (ideId) {
139
+ payload.activated_by_ide = ideId;
140
+ payload.activated_by_pid = process.pid;
141
+ }
142
+
143
+ let result;
144
+ try {
145
+ result = await query('extension.set-active', payload, {
146
+ projectRoot: pickProjectRoot(opts),
147
+ homeDir: home,
148
+ });
149
+ } catch (err) {
150
+ // The SDK verb throws on protocol violations. Surface as the writer's
151
+ // {ok:false, error} contract so callers don't have to learn a new shape.
152
+ return { ok: false, error: err && err.message ? err.message : String(err) };
153
+ }
154
+ if (!result || result.ok !== true) {
155
+ return { ok: false, error: 'state-sdk extension.set-active returned non-ok' };
156
+ }
157
+
158
+ // Recover the activated_at timestamp the SDK stamped so we can pass it to
159
+ // resetExtensionQuotas (parity with the legacy writer's wall_clock_ms window).
160
+ // Best-effort: if the read fails, omit activated_at and the tracker resets
161
+ // counters without the window stamp.
162
+ let activatedAt;
163
+ try {
164
+ const raw = await readFile(result.path, 'utf8');
165
+ const parsed = JSON.parse(raw);
166
+ if (parsed && typeof parsed.activated_at === 'string') {
167
+ activatedAt = parsed.activated_at;
168
+ }
169
+ } catch { /* best-effort */ }
170
+
110
171
  // B16/SEC-M-02: reset quota counters on activate; stamp activated_at so
111
172
  // wall_clock_ms can be computed against this activation window.
112
173
  try {
@@ -115,21 +176,24 @@ export async function writeActiveExtension(manifest, scope, opts = {}) {
115
176
  // Quota reset failure must not block activation. Counters will self-heal
116
177
  // on next deactivate or the next activate of the same name.
117
178
  }
118
- return { ok: true, path };
179
+
180
+ return { ok: true, path: result.path };
119
181
  }
120
182
 
121
183
  /**
122
184
  * Clear the active-extension state file. Idempotent -- succeeds if file is absent.
123
185
  *
124
- * @param {{ homeDir?: string }} [opts]
186
+ * @param {{ homeDir?: string, projectRoot?: string }} [opts]
125
187
  * @returns {Promise<{ ok: boolean, removed: boolean }>}
126
188
  */
127
189
  export async function clearActiveExtension(opts = {}) {
128
190
  const home = opts && opts.homeDir ? opts.homeDir : (process.env.HOME || homedir());
129
- // B16/SEC-M-02: read the active extension name BEFORE unlinking so we can
130
- // clear its quota counters. Best-effort: if the file is missing or
191
+
192
+ // B16/SEC-M-02: read the active extension name BEFORE clearing so we can
193
+ // reset its quota counters. Best-effort: if the file is missing or
131
194
  // malformed, deactivate still succeeds.
132
195
  let extName = null;
196
+ const wasPresent = existsSync(statePath(home));
133
197
  try {
134
198
  const raw = await readFile(statePath(home), 'utf8');
135
199
  const parsed = JSON.parse(raw);
@@ -139,27 +203,42 @@ export async function clearActiveExtension(opts = {}) {
139
203
  } catch {
140
204
  // ignore — extName stays null
141
205
  }
206
+
207
+ // Drive the canonical clear through the SDK verb (manifest:null is the
208
+ // documented clear semantics — see STATE-SDK-CONTRACT.md §7).
209
+ // The verb is idempotent on an absent file (best-effort unlink).
210
+ let verbOk = false;
142
211
  try {
143
- await unlink(statePath(home));
144
- if (extName) {
145
- try {
146
- await resetExtensionQuotas(extName, { homeDir: home });
147
- } catch {
148
- // best-effort
149
- }
150
- }
151
- return { ok: true, removed: true };
152
- } catch (err) {
153
- if (err && err.code === 'ENOENT') {
154
- if (extName) {
155
- try {
156
- await resetExtensionQuotas(extName, { homeDir: home });
157
- } catch { /* best-effort */ }
158
- }
159
- return { ok: true, removed: false };
160
- }
161
- return { ok: false, removed: false };
212
+ const r = await query('extension.set-active', {
213
+ manifest: null,
214
+ // The verb requires a valid scope. 'project' is a safe sentinel — the
215
+ // clear branch does not consult `scope` for any state-file content
216
+ // (clear unlinks the file unconditionally). Any of the three valid
217
+ // values would work.
218
+ scope: 'project',
219
+ homeDir: home,
220
+ }, {
221
+ projectRoot: pickProjectRoot(opts),
222
+ homeDir: home,
223
+ });
224
+ verbOk = !!(r && r.ok);
225
+ } catch {
226
+ verbOk = false;
227
+ }
228
+
229
+ // Always reset quota counters if we know the ext name, regardless of
230
+ // whether the verb succeeded or the file was present — mirrors the
231
+ // legacy writer's ENOENT-tolerant best-effort reset.
232
+ if (extName) {
233
+ try {
234
+ await resetExtensionQuotas(extName, { homeDir: home });
235
+ } catch { /* best-effort */ }
236
+ }
237
+
238
+ if (verbOk) {
239
+ return { ok: true, removed: wasPresent };
162
240
  }
241
+ return { ok: false, removed: false };
163
242
  }
164
243
 
165
244
  /**
@@ -247,7 +326,8 @@ async function writeLastSeen(ideId, opts = {}) {
247
326
  try {
248
327
  await mkdir(dirname(path), { recursive: true });
249
328
  const body = JSON.stringify({ ide: ideId, last_seen_at: new Date().toISOString() }, null, 2) + '\n';
250
- const tmp = `${path}.tmp.${randomBytes(4).toString('hex')}`;
329
+ // v1.5.0 audit-LOW-update-#13: tmpSuffix() replaces inline randomBytes call.
330
+ const tmp = `${path}.tmp.${tmpSuffix({ bytes: 4, includePid: false })}`;
251
331
  await writeFile(tmp, body, 'utf8');
252
332
  const { rename } = await import('node:fs/promises');
253
333
  await rename(tmp, path);
package/src/api-client.js CHANGED
@@ -8,6 +8,19 @@ import { getTemplate } from './cross-dispatcher.js';
8
8
 
9
9
  const DEFAULT_TIMEOUT_MS = 30_000;
10
10
 
11
+ // v1.5.0 audit-DISPUTED-1 — user-message cache_control threshold.
12
+ //
13
+ // Anthropic prompt-caching has a server-side minimum (~1024 tokens). We
14
+ // gate the user-content-block split on a conservative 2KB byte cutoff so
15
+ // short audits (which would never recover the cache-write overhead)
16
+ // remain plain strings, while large diff/transcript targets (which DO
17
+ // amortize the cache) get split into a cacheable prefix + ephemeral tail.
18
+ //
19
+ // Pairs with the H4.2 ordering invariant: cycleSummary (or any per-turn
20
+ // content) MUST land AFTER the cacheable block so it never busts the
21
+ // cache prefix. See ADJUDICATIONS.md DISPUTED-1.
22
+ const CACHE_USER_THRESHOLD_BYTES = 2048;
23
+
11
24
  // ---------------------------------------------------------------------------
12
25
  // Provider request builders
13
26
  // ---------------------------------------------------------------------------
@@ -68,7 +81,7 @@ function buildGemini(system, user, model, key, timeoutMs, endpoint) {
68
81
  // Sonnet 4.5 prompt-caching threshold: 1024 tokens (rough: chars / 4).
69
82
  const CACHE_TOKEN_THRESHOLD = 1024;
70
83
 
71
- function buildAnthropic(system, user, model, key, timeoutMs) {
84
+ function buildAnthropic(system, user, model, key, timeoutMs, cycleSummary = null) {
72
85
  const promptChars = system.length + user.length;
73
86
  const estimatedTokens = Math.floor(promptChars / 4);
74
87
  const cacheEligible = estimatedTokens >= CACHE_TOKEN_THRESHOLD;
@@ -77,6 +90,26 @@ function buildAnthropic(system, user, model, key, timeoutMs) {
77
90
  ? [{ type: 'text', text: system, cache_control: { type: 'ephemeral' } }]
78
91
  : system;
79
92
 
93
+ // v1.5.0 audit-DISPUTED-1 — user-message cache_control.
94
+ //
95
+ // When the user content is large enough to amortize the cache-write
96
+ // overhead, split into a content-blocks array: the stable target gets
97
+ // cache_control:{type:'ephemeral'}, and any per-turn tail (cycleSummary)
98
+ // follows as a plain text block so it never busts the cache prefix.
99
+ //
100
+ // Below the threshold we keep the legacy plain-string form -- no need
101
+ // to spend cache-write tokens we can't recover.
102
+ const userBytes = Buffer.byteLength(user, 'utf8');
103
+ let userContent;
104
+ if (userBytes >= CACHE_USER_THRESHOLD_BYTES) {
105
+ const cacheableBlock = { type: 'text', text: user, cache_control: { type: 'ephemeral' } };
106
+ userContent = cycleSummary
107
+ ? [cacheableBlock, { type: 'text', text: cycleSummary }]
108
+ : [cacheableBlock];
109
+ } else {
110
+ userContent = user;
111
+ }
112
+
80
113
  return {
81
114
  url: 'https://api.anthropic.com/v1/messages',
82
115
  options: {
@@ -91,7 +124,7 @@ function buildAnthropic(system, user, model, key, timeoutMs) {
91
124
  model,
92
125
  max_tokens: 4096,
93
126
  system: systemBlock,
94
- messages: [{ role: 'user', content: user }],
127
+ messages: [{ role: 'user', content: userContent }],
95
128
  }),
96
129
  signal: AbortSignal.timeout(timeoutMs),
97
130
  },
@@ -138,9 +171,13 @@ function extractCacheStats(json, cacheEligible) {
138
171
  // Main export
139
172
  // ---------------------------------------------------------------------------
140
173
 
141
- // runViaApi(pick, mode, angle, target, env, timeoutMs?, abortSignal?)
174
+ // runViaApi(pick, mode, angle, target, env, timeoutMs?, abortSignal?, opts?)
142
175
  // Returns { status: 'ok', raw, model } or { status: 'failed', error, model }.
143
- export async function runViaApi(pick, mode, angle, target, env = process.env, timeoutMs = DEFAULT_TIMEOUT_MS, abortSignal = null) {
176
+ //
177
+ // opts.cycleSummary (string|null): when set on Anthropic calls, lands in a
178
+ // trailing ephemeral content block AFTER the cacheable user block so it
179
+ // never busts the cache prefix. Ignored for non-Anthropic providers.
180
+ export async function runViaApi(pick, mode, angle, target, env = process.env, timeoutMs = DEFAULT_TIMEOUT_MS, abortSignal = null, opts = {}) {
144
181
  const fb = pick.apiFallback;
145
182
  if (!fb) return { status: 'failed', error: 'no API fallback configured', model: '' };
146
183
 
@@ -149,6 +186,7 @@ export async function runViaApi(pick, mode, angle, target, env = process.env, ti
149
186
 
150
187
  const { system, format } = getTemplate(mode, angle);
151
188
  const user = `${format}\n\n## Target\n\n${target}`;
189
+ const cycleSummary = opts.cycleSummary ?? null;
152
190
 
153
191
  // Combine caller abort signal with our per-call timeout signal.
154
192
  const timeoutSig = AbortSignal.timeout(timeoutMs);
@@ -164,7 +202,7 @@ export async function runViaApi(pick, mode, angle, target, env = process.env, ti
164
202
  } else if (fb.provider === 'google') {
165
203
  req = buildGemini(system, user, fb.model, key, timeoutMs, fb.endpoint);
166
204
  } else if (fb.provider === 'anthropic') {
167
- req = buildAnthropic(system, user, fb.model, key, timeoutMs);
205
+ req = buildAnthropic(system, user, fb.model, key, timeoutMs, cycleSummary);
168
206
  } else {
169
207
  return { status: 'failed', error: `unknown provider: ${fb.provider}`, model: fb.model };
170
208
  }