@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.
- package/bin/ijfw-memorize +14 -7
- package/fixtures/team/book.json +6 -6
- package/fixtures/team/business.json +146 -20
- package/fixtures/team/content.json +6 -6
- package/fixtures/team/design.json +148 -20
- package/fixtures/team/mixed.json +206 -27
- package/fixtures/team/research.json +146 -20
- package/fixtures/team/software.json +148 -20
- package/fixtures/truncation-corpus/_generate-corpus.js +367 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
- package/package.json +6 -3
- package/src/active-extension-writer.js +144 -64
- package/src/api-client.js +43 -5
- package/src/audit-roster.js +80 -5
- package/src/blackboard.js +298 -6
- package/src/cli-run.js +33 -5
- package/src/codex-agents.js +96 -5
- package/src/cost/aggregator.js +39 -9
- package/src/cost/pricing.js +57 -0
- package/src/cost/readers/gemini.js +1 -1
- package/src/cross-audit-chunker.js +189 -0
- package/src/cross-dispatcher.js +124 -21
- package/src/cross-orchestrator-cli.js +754 -159
- package/src/cross-orchestrator.js +1065 -17
- package/src/cross-project-search.js +195 -9
- package/src/dashboard-client-waves.html +304 -0
- package/src/dashboard-client.html +5 -1
- package/src/dashboard-server.js +73 -0
- package/src/deploy-alerts.js +150 -0
- package/src/design/iframe-bridge.js +242 -0
- package/src/design-companion.js +144 -0
- package/src/dispatch/checkpoint-cli.js +97 -0
- package/src/dispatch/colon-syntax.js +81 -1
- package/src/dispatch/extension.js +26 -2
- package/src/dispatch/registry-cli.js +4 -1
- package/src/dispatch/wave-cli.js +201 -6
- package/src/dispatch/worktree-cli.js +40 -0
- package/src/dispatch-planner.js +97 -2
- package/src/dream/runner.mjs +47 -11
- package/src/dream/stage-runner.js +40 -0
- package/src/dream/state-file.js +102 -0
- package/src/extension-installer.js +70 -24
- package/src/extension-quota-tracker.js +4 -2
- package/src/extension-registry.js +289 -35
- package/src/feedback-detector.js +26 -0
- package/src/fs-lock.js +259 -7
- package/src/gate-result.js +95 -1
- package/src/hardware-signer.js +4 -2
- package/src/hero-line.js +86 -5
- package/src/intent-router.js +35 -0
- package/src/lib/a11y-contract.js +117 -0
- package/src/lib/atomic-io.js +29 -8
- package/src/lib/cache-keepalive.js +150 -0
- package/src/lib/jsonl-rotation.js +104 -0
- package/src/lib/lighthouse-pillar.js +121 -0
- package/src/lib/llm-call.js +121 -0
- package/src/lib/playwright-baseline.js +205 -0
- package/src/lib/rekor-bridge.js +221 -0
- package/src/lib/repo-map.js +392 -0
- package/src/lib/shasum-verify.js +164 -0
- package/src/lib/sketches-gc.js +132 -0
- package/src/lib/tmp-suffix.js +62 -0
- package/src/lib/ui-review-runner.js +595 -0
- package/src/lib/uispec-drift.js +301 -0
- package/src/lib/uispec-intake.js +381 -0
- package/src/lib/worktree-guards.js +118 -0
- package/src/lib/worktree-recovery.js +100 -0
- package/src/memory/auto-linker.js +267 -0
- package/src/memory/benchmark.js +498 -0
- package/src/memory/dedup.js +126 -0
- package/src/memory/embedding-cache.js +136 -0
- package/src/memory/fact-extractor.js +168 -0
- package/src/memory/fts5.js +65 -1
- package/src/memory/migration-runner.js +6 -1
- package/src/memory/migrations/004-bitemporal.js +91 -0
- package/src/memory/migrations/005-vector-cache.js +61 -0
- package/src/memory/migrations/006-obsidian-graph.js +46 -0
- package/src/memory/migrations/007-skill-telemetry.js +24 -0
- package/src/memory/migrations/008-write-provenance.js +41 -0
- package/src/memory/migrations/009-obsidian-backfill.js +50 -0
- package/src/memory/obsidian-parser.js +152 -0
- package/src/memory/query-dataview.js +86 -0
- package/src/memory/search.js +46 -15
- package/src/memory/temporal.js +529 -0
- package/src/memory/tokenize.js +10 -0
- package/src/memory-facts-handler.js +37 -0
- package/src/memory-feedback.js +260 -2
- package/src/model-refresh.js +292 -0
- package/src/observability/cost-anomaly.js +166 -0
- package/src/observability/evaluator-checkpoint-contract.js +117 -0
- package/src/observability/trace-id.js +163 -0
- package/src/orchestrator/agents-md-blackboard.js +152 -0
- package/src/orchestrator/checkpoint-contract.md +140 -0
- package/src/orchestrator/debug-trident-trigger.js +374 -0
- package/src/orchestrator/debug-trident.js +570 -0
- package/src/orchestrator/merge-block-aware.js +350 -0
- package/src/orchestrator/plan-checker.js +475 -0
- package/src/orchestrator/post-done-runner.js +277 -0
- package/src/orchestrator/review.js +38 -3
- package/src/orchestrator/skill-telemetry-sink.js +29 -0
- package/src/orchestrator/skill-telemetry.js +37 -0
- package/src/orchestrator/state-events.js +459 -0
- package/src/orchestrator/state-sdk.js +1932 -0
- package/src/orchestrator/status-protocol.js +84 -17
- package/src/orchestrator/subagent-telemetry.js +471 -0
- package/src/orchestrator/termination.js +160 -0
- package/src/orchestrator/verification-gate.js +200 -16
- package/src/orchestrator/wave-state.js +332 -23
- package/src/orchestrator/worktree-provision.js +77 -0
- package/src/override-resolver.js +5 -3
- package/src/override-use-registry.js +111 -5
- package/src/receipts.js +36 -4
- package/src/recovery/checkpoint.js +56 -3
- package/src/recovery/code-fixer.js +961 -0
- package/src/recovery/truncation.js +317 -0
- package/src/redactor.js +75 -6
- package/src/runtime-mediator.js +15 -1
- package/src/sanitizer.js +10 -0
- package/src/search-hybrid.js +139 -0
- package/src/server.js +795 -112
- package/src/swarm/worktree.js +27 -4
- package/src/swarm-config.js +102 -17
- package/src/team/domain-templates/book.json +51 -0
- package/src/team/domain-templates/business.json +44 -0
- package/src/team/domain-templates/content.json +50 -0
- package/src/team/domain-templates/design.json +44 -0
- package/src/team/domain-templates/research.json +44 -0
- package/src/team/domain-templates/software.json +40 -0
- package/src/team/generator.js +440 -3
- package/src/team/modify.js +203 -0
- package/src/team/schemas.js +48 -0
- package/src/update-apply.js +19 -3
- 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 @@
|
|
|
1
|
+
{"phase":"errT-PARTIAL-3"}
|
|
@@ -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
|
+
}
|
package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl
ADDED
|
@@ -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 @@
|
|
|
1
|
+
{"phase":"errT-PARTIAL-5"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ijfw/memory-server",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Cross-platform persistent memory server for IJFW.
|
|
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
|
|
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
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
//
|
|
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
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
}
|