@sema-agent/core 5.39.0 → 5.41.0
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/CHANGELOG.md +150 -11
- package/dist/core/auto-mode-prompt.js +1 -1
- package/dist/core/checkpoint-store.d.ts +12 -0
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.js +9 -1
- package/dist/core/memory-engine/engine.d.ts +27 -0
- package/dist/core/memory-engine/engine.js +103 -1
- package/dist/core/memory-engine/export-bundle.d.ts +192 -0
- package/dist/core/memory-engine/export-bundle.js +306 -0
- package/dist/core/memory-engine/file-backend.d.ts +178 -1
- package/dist/core/memory-engine/file-backend.js +648 -6
- package/dist/core/memory-engine/index.d.ts +2 -1
- package/dist/core/memory-engine/index.js +1 -0
- package/dist/core/memory-engine/layout.d.ts +99 -1
- package/dist/core/memory-engine/layout.js +143 -7
- package/dist/core/memory-engine/memory-backend-contract.d.ts +1 -1
- package/dist/core/memory-engine/memory-backend-contract.js +52 -0
- package/dist/core/memory-engine/tools.js +8 -1
- package/dist/core/permission-rule-consent.js +14 -2
- package/dist/core/runner/prepare-config-doors.js +20 -0
- package/dist/core/runner/prepare-task.js +13 -0
- package/dist/core/runner/runtask.js +14 -1
- package/dist/core/runner/synthetic-tools.js +3 -1
- package/dist/core/runner/tool-disclosure.js +2 -1
- package/dist/core/types.d.ts +7 -0
- package/dist/core/write-protect.d.ts +0 -20
- package/dist/core/write-protect.js +4 -3
- package/dist/engine/harness/agent-harness.d.ts +17 -0
- package/dist/engine/harness/agent-harness.js +19 -1
- package/dist/engine/harness/types.d.ts +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/tools/fs/safety.d.ts +1 -1
- package/dist/tools/fs/safety.js +1 -1
- package/dist/tools/fs/search.d.ts +33 -0
- package/dist/tools/fs/search.js +72 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +9 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,144 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.41.0 — 2026-08-18
|
|
4
|
+
|
|
5
|
+
No BREAKING changes. Three behavioral narrowings disclosed below (garbage values on the three
|
|
6
|
+
tool-face seats now refuse loudly; the strict sidecar family's journal read and lock fence both
|
|
7
|
+
join the fail-closed law).
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- backlog #259 — user steers stranded at agent_end are ANNOUNCED, never silently dropped behind
|
|
12
|
+
their "queued" receipts: the terminal sweep hands ENGINE notes back for per-session redelivery,
|
|
13
|
+
but USER inputs have no redelivery semantics (a steer aimed at a finished run must not fire at
|
|
14
|
+
the next one) — their remnant counts now ride a dedicated harness sink, the `settled` frame
|
|
15
|
+
(additive `undrainedSteerCount`/`undrainedFollowUpCount`), and a new `EngineNotice` family
|
|
16
|
+
`task.user_steer_undrained`. The stranding window is a narrow race past the loop's final queue
|
|
17
|
+
check, which is exactly why it gets a loud terminal account.
|
|
18
|
+
- backlog #251 — the mixed-version window of the design/186 ledger v2 is machine-detected:
|
|
19
|
+
recovering a v1-format journal over a v2 disk ledger (a shape only a pre-v2 process mints)
|
|
20
|
+
announces the old-writer-alive warning on the external-findings channel; recovery is unchanged.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- backlog #297 (ruled: narrow) — the three tool-face seats (`excludeTools` / `deferTools` /
|
|
25
|
+
`alwaysLoadTools`) get the fourth seat's value door: a garbage value (a bare string, a truthy
|
|
26
|
+
non-iterable, a non-string entry) refuses `config.tool_face_control` instead of stringifying
|
|
27
|
+
into a never-matching list, throwing an uncoded TypeError, or a falsy value silently reading as
|
|
28
|
+
absent. **Narrowing**: a deployment whose misspelled seat previously "worked" now hears it.
|
|
29
|
+
- backlog #299 (ruled: fix alone) — the auto-mode classifier's action block bounds tool args at
|
|
30
|
+
48,000 chars (its own subject-level cap, deliberately above the delegation-review lanes'
|
|
31
|
+
12,000-per-field sampled payloads so their coverage notes ride through uncut): it was the one
|
|
32
|
+
unbounded string in the classifier's prompt, so a single long tool argument (a SendMessage body
|
|
33
|
+
needs nothing crafted, just length) could blow the classifier's context and trip the session
|
|
34
|
+
breaker out of auto mode.
|
|
35
|
+
- backlog #307 (also filed as #256) — the journal-aware strict sidecar read joins the ENOENT-only
|
|
36
|
+
absence law: an EACCES/EIO journal read refused `ControlPlaneCorruptError` instead of silently
|
|
37
|
+
serving the pre-transaction state while a committed next state sat unreadable beside it — the
|
|
38
|
+
one fail-open left in the strict-read family. **Narrowing**: a deployment whose journal reads
|
|
39
|
+
were failing now hears it instead of silently reading stale.
|
|
40
|
+
- backlog #255 — the sidecar lock's commit fence splits by family: the fail-closed LEDGER family
|
|
41
|
+
(challenge/lineage locked updates and the rebuild path) aborts on an UNPROVABLE owner
|
|
42
|
+
(absent/unreadable owner file) — "cannot disprove a steal" is not a fence; the calibration
|
|
43
|
+
family keeps its documented pre-token leniency, and a FOREIGN token aborts both as before.
|
|
44
|
+
**Narrowing**: within the ledger family only.
|
|
45
|
+
- CHANGELOG erratum for 5.40.0 (recorded in that entry): the header counts THREE narrowings
|
|
46
|
+
(the defer alignment was the missing third) and the release date corrects to 08-17.
|
|
47
|
+
|
|
48
|
+
## 5.40.0 — 2026-08-17
|
|
49
|
+
|
|
50
|
+
No BREAKING changes. Three behavioral narrowings disclosed below (the requestedCwd × workspace-restore
|
|
51
|
+
combination now refuses loudly; Grep's ripgrep leg withholds alias-spelled deny-listed paths it
|
|
52
|
+
previously returned; `deferMode: "auto"` never sweeps engine built-ins — a small-window deployment
|
|
53
|
+
whose built-ins were previously deferred now carries their schemas inline, trading context bytes for
|
|
54
|
+
the first-use activation error the sweep caused).
|
|
55
|
+
|
|
56
|
+
<!-- Post-release erratum (2026-08-17, server review [4351]): the header originally said "Two
|
|
57
|
+
behavioral narrowings" and dated the entry 2026-08-18 (pre-written); the defer narrowing was listed
|
|
58
|
+
under Fixed but missing from this header, and the release actually shipped late on 08-17. -->
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
|
|
62
|
+
- design/178 v2-c — governed memory export/import: `MemoryEngine.exportMemoryScopes` /
|
|
63
|
+
`importMemoryBundle` over a self-describing bundle (`MemoryExportBundle`: entries + governance
|
|
64
|
+
rows — unresolved challenges, durable pollution markers, committed lineage, custody evidence —
|
|
65
|
+
+ residuals + a recomputable integrity section). Import is two-phase: entries land under a
|
|
66
|
+
synthetic lineage LATCH (withheld from every model-visible read face) and only a durable,
|
|
67
|
+
fully-validated completion receipt releases them; a crash mid-governance converges by re-running
|
|
68
|
+
the SAME bundle import (idempotent). Tenant slicing is per-artifact three-way (refusal /
|
|
69
|
+
field redaction with an import-visible provenance stamp / global-residual); a `complete:false`
|
|
70
|
+
export refuses to mint a package at all. Integrity detects accidents, never forgery (self-attested
|
|
71
|
+
hashes; authenticity is the transport's job — the multi-tenant service form gates imports behind
|
|
72
|
+
its authenticated endpoint). New exports: `computeMemoryBundleHash` + `MemoryExportBundle` /
|
|
73
|
+
`MemoryImportReport` / `MemoryExportSnapshot` / `MemoryBundleImportPlan` / `BundleChallengeRow` /
|
|
74
|
+
`BundleLineageRow` / `BundlePollutedSession` types. New coded errors: `memory.export_incomplete`,
|
|
75
|
+
`memory.import_rejected` (input-shape errors use the existing `config.memory_export_request` /
|
|
76
|
+
`config.memory_import_request` family). Free-string fields (pollution/challenge reasons, custody
|
|
77
|
+
`req`, session ids) export VERBATIM; the bundle's `doc` lines disclose that sub-scope tenant
|
|
78
|
+
isolation does not extend into those literals.
|
|
79
|
+
- backlog #243 — new coded refusal `checkpoint.cwd_conflicts_restore` (see Fixed below).
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
|
|
83
|
+
- backlog #243 — `RunInternals.requestedCwd` combined with a checkpoint workspace restore is now
|
|
84
|
+
REFUSED loudly instead of half-working: the restored workspace's mountPath is authoritative for
|
|
85
|
+
the task root, so the cwd was semantically ignored at best — and verifying it was prepare's FIRST
|
|
86
|
+
env RPC, which a lazy-connect remote adapter answers by connecting a fresh EMPTY sandbox before
|
|
87
|
+
`resumeVM` (silently running the resumed task on an empty workspace), or the check itself
|
|
88
|
+
deadlocked the approval on a phantom mismatch (`env_failed` reopen retrying deterministically).
|
|
89
|
+
Two gates: the resume ladder refuses PRE-CAS (`checkpoint.cwd_conflicts_restore` — the checkpoint
|
|
90
|
+
stays pending; re-resume without the cwd succeeds), and prepare carries a depth twin for direct
|
|
91
|
+
`runTaskStream(spec, resume, internals)` callers (`config.cwd_conflicts_restore`, refused before
|
|
92
|
+
the env factory mints anything). **Narrowing**: on eager adapters this combination previously
|
|
93
|
+
"worked"; it now refuses on every adapter. Core's own delegation drivers never mint the pair.
|
|
94
|
+
- backlog #303 — Grep's ripgrep leg gains a deny TRIPWIRE: rg's exclusion globs are spelled from
|
|
95
|
+
the pattern text and cannot express the win32 component-alias family (`.aws.` — trailing
|
|
96
|
+
dots/spaces the win32 namespace strips), so a directory planted under an alias spelling slipped
|
|
97
|
+
past the globs and its contents reached Grep results whenever ripgrep was installed, while the JS
|
|
98
|
+
leg correctly pruned it. Every rg output line is now judged with the same two-view matchPath the
|
|
99
|
+
JS walker prunes with; ANY deny hit — or any line the record format cannot account for (e.g. a
|
|
100
|
+
newline inside a path component splitting one rg record across physical lines) — abandons the rg
|
|
101
|
+
pass entirely and the JS scanner answers, pruning with the authoritative judge and declaring the
|
|
102
|
+
abandonment on both the text and structured surfaces. **Narrowing**: results no longer differ by
|
|
103
|
+
whether ripgrep is installed; rg remains the fast path when no guarded entry is involved
|
|
104
|
+
(`#306` tracks the structured `--json` terminal form).
|
|
105
|
+
- backlog #298 — a THROWING deployment-supplied `breakerOpen` on the auto-mode decider reads as
|
|
106
|
+
not-open: the peer-referral ask is still minted (one extra question), never a silent allow.
|
|
107
|
+
- backlog #300 — `WRITE_PROTECTED_DEFAULT_TABLE` and every resolved table are deep-frozen: an
|
|
108
|
+
admin-face in-place mutate (splice / `length = 0`) throws at the mutation site instead of
|
|
109
|
+
silently weakening the process-wide protection floor.
|
|
110
|
+
- backlog #301 — the CC settings-import preview guards the settings ROOT like it already guarded
|
|
111
|
+
the `permissions` slot: a valid-JSON document that is a string/number/array reports the layer as
|
|
112
|
+
UNREAD (`skipped`) instead of two clean zeros; a `null` document remains the readable
|
|
113
|
+
"nothing written".
|
|
114
|
+
- 5.40 pre-release rescan (memory-engine import/export hardening, all pinned):
|
|
115
|
+
- the import's retroactive pollution sweep mints its challenge events under a TOP-LEVEL
|
|
116
|
+
`imp-sweep:` prefix, structurally disjoint from the carried-challenge leg's
|
|
117
|
+
`imp:<hash>:<source eventId>` — a shared key with a stale frozen `challengedRev` (a
|
|
118
|
+
crash-window promote on the source drifting lineage `lastRev`) aborted the read-back verifier
|
|
119
|
+
AFTER the latch staged, and the documented re-import convergence reproduced the abort forever;
|
|
120
|
+
- BOTH export faces refuse over the durable chain-degradation marker
|
|
121
|
+
(`transfers.chain-degraded.json`): a healed (quarantined + truncated) evidence chain parses
|
|
122
|
+
clean, but the store has declared the lost rows unprovable, and a bundle minted over it
|
|
123
|
+
testified to a deletion history the source knew was incomplete (an erased id could resurrect on
|
|
124
|
+
the destination); the composite throws `memory.export_incomplete`, `governanceExport` answers
|
|
125
|
+
`complete:false` with the reason;
|
|
126
|
+
- the bundle-import completion receipt validates EVERY `MemoryImportReport` member — an 8-of-15
|
|
127
|
+
husk could release a standing latch and hand back a report whose missing arrays throw on first
|
|
128
|
+
touch.
|
|
129
|
+
- `deferMode: "auto"` never sweeps engine BUILT-INS (align-with-CC ruling): the pressure valve's
|
|
130
|
+
candidates narrow to caller-supplied specs — CC's own `isDeferredTool` defers MCP as a family
|
|
131
|
+
plus individually self-declared tools and has no pressure valve at all, so built-ins like
|
|
132
|
+
Grep/Glob/Read stay inline unconditionally; sweeping them made every small-window model pay a
|
|
133
|
+
first-use activation error on tools its training treats as always present. An explicit
|
|
134
|
+
`deferTools` naming a built-in still honors the deployment's stated intent.
|
|
135
|
+
- Out-of-root refusals no longer print a duplicated root: a directory declared through two doors
|
|
136
|
+
(`additionalDirectories` + the advertised scratchpad) deduplicates by canonical value in the
|
|
137
|
+
disclosure list (the judgment was already idempotent).
|
|
138
|
+
- backlog #309 — the out-of-root escape hint names the `readFace: "open"` knob beside the
|
|
139
|
+
per-directory seats; the unknown-skill refusal states that foreign skill directories are not
|
|
140
|
+
loaded and points at importing the content instead (signpost ruling).
|
|
141
|
+
|
|
3
142
|
## 5.39.0 — 2026-08-17
|
|
4
143
|
|
|
5
144
|
No BREAKING changes. Two behavioral narrowings disclosed below (write-protection default table;
|
|
@@ -63,7 +202,7 @@ baseline slots refuse at mount instead of silently mounting ungoverned).
|
|
|
63
202
|
spelling was already collapsed by the filesystem itself — a genuinely-resolved POSIX target keeps
|
|
64
203
|
its own reading and a real `.aws.` directory stays readable and writable. The arms that resolve
|
|
65
204
|
NOTHING do not report it and keep both views: UNC keys (minted with zero probes for liveness) and
|
|
66
|
-
not-yet-existing targets (whose tail is rejoined verbatim) — which is where the
|
|
205
|
+
not-yet-existing targets (whose tail is rejoined verbatim) — which is where the pre-creation path
|
|
67
206
|
lives. Unreported is the fail-closed default, so a caller that forgets to pass it loses precision,
|
|
68
207
|
never protection.
|
|
69
208
|
**Residual cost**: a target the filesystem has NOT resolved still over-matches — writing a
|
|
@@ -902,7 +1041,7 @@ breaking change (rows without the new gate bit keep their historic stamps).
|
|
|
902
1041
|
governed row (org origin) is refused PRE-CAS on a worker with no `permissionRuleOrg` wiring (the
|
|
903
1042
|
human decision stays unspent, redeemable on an org-wired worker; a post-CAS belt remains as
|
|
904
1043
|
defense in depth); a v8 row whose bit was stripped in storage, and a sub-v8 row carrying a bit no
|
|
905
|
-
release minted, are both refused pre-CAS (corruption/
|
|
1044
|
+
release minted, are both refused pre-CAS (corruption/fabrication guards).
|
|
906
1045
|
- **`run_in_background: true` is judged by the backgrounding doctrine (#125, P0).** On a
|
|
907
1046
|
`shellGate:"classify"` deployment, `bashReversibilityProbe` used to read only the command text, so
|
|
908
1047
|
`ls` + the parameter form auto-admitted exactly what `ls &` asks for. The parameter spelling now
|
|
@@ -1636,7 +1775,7 @@ mysterious runtime.
|
|
|
1636
1775
|
claiming the row is still pending. Terminal-state refusals name the observed status without
|
|
1637
1776
|
coercing store-supplied values.
|
|
1638
1777
|
- The approval lane's `reason` is validated as plain text at capture; malformed decisions and
|
|
1639
|
-
|
|
1778
|
+
unexpected text shapes get typed refusals instead of raising inside the refusal path.
|
|
1640
1779
|
- Orchestration guidance, the TaskOutput tool card, and the selective-recall affordance stop
|
|
1641
1780
|
teaching retired tool names: the workflow tool is named by its wire name, and
|
|
1642
1781
|
`composeSelectiveBody` accepts an optional caller-supplied `recallToolName` (additive).
|
|
@@ -2178,7 +2317,7 @@ live `onAsk` is wired — drop the approver (or arm `forceDurableGate`) to test
|
|
|
2178
2317
|
|
|
2179
2318
|
- **Memory recall: an unknown write time is incomparable, not 1970.** A note whose `ts` failed to parse used to sort as epoch 0 — it lost every `[[name]]` collision (permanently unreachable through links) and rendered a fabricated age ("written 678 months ago"). `MemoryNoteHeader.timestampMissing` (new, optional) is minted at the single parse point; the one recency comparison treats missing as a tie (the incumbent keeps the name — an unknown-time note neither sinks nor poses as newest), and the render face says "write time unknown — verify it's still current". A literal 1970 timestamp string stays a real time (the numeric value is never the signal; negative pins lock this). Bonus: a wire row with NO `ts` key used to throw and kill the whole scope's manifest build. **Consumer flip**: a probe pinning the fabricated-age wording or the sink-to-bottom collision outcome reds.
|
|
2180
2319
|
- **Three awaited rescue timers are REF'd + cleared-on-settle** (subagent observer drain, teacher abortFallback, WebFetch deadline): unref'd, the process could exit before the rescue fired when the wedged arm held no live handles — the settle never completed. The bg notify drain window keeps its unref by design (a background lane must not pin the process).
|
|
2181
|
-
- **A harness-side throw around ONE tool call becomes that call's disclosed error result — the batch survives.** Worker exceptions that are not cancellation (a rejected `tool_execution_update` delivery, a sink failure) used to reject the whole parallel batch, orphan the sibling calls, and on the worst path rewrite a COMMITTED side effect into `[tool execution harness failed: …]` — inviting the model to re-run an already-effective write. Progress deliveries now settle at one point; a delivery failure on an executed tool keeps `content`/`isError` untouched and rides `details.toolProgressDeliveryFailed` (total by construction:
|
|
2320
|
+
- **A harness-side throw around ONE tool call becomes that call's disclosed error result — the batch survives.** Worker exceptions that are not cancellation (a rejected `tool_execution_update` delivery, a sink failure) used to reject the whole parallel batch, orphan the sibling calls, and on the worst path rewrite a COMMITTED side effect into `[tool execution harness failed: …]` — inviting the model to re-run an already-effective write. Progress deliveries now settle at one point; a delivery failure on an executed tool keeps `content`/`isError` untouched and rides `details.toolProgressDeliveryFailed` (total by construction: throwing accessors or exotic `details` decline the annotation, never the outcome; `afterToolCall` cannot erase it). Cancellation — including one wrapped in the harness's own `AgentHarnessError` (`cause`-chain walk) — stays a hard batch failure on every lane, and the in-stream lane closes admission immediately. The sequential lane shares the same disclosure band. **Consumer flips**: probes pinning the old `[tool execution harness failed]` rewrite or `isError:true` on a delivery-failed success red — re-pin on the new shape plus a retired-dialect tripwire.
|
|
2182
2321
|
- **openai lane: an evidence-free `tool_calls` fragment no longer fabricates an unnamed-call disclosure.** The evidence gate sits at the finalize disclosure point (slot assignment also routes id-less continuations, so gating there corrupted two real calls); the stream's own verdict wins — `finish_reason:"tool_calls"` with zero survivors, or an empty slot beside real calls, discloses loudly; an isolated terminal fragment with `stop`/`length` stays silent.
|
|
2183
2322
|
- **A throwing tool keeps its machine-readable marks on BOTH legs.** Two strip points, one class: the `defineTool` adapter rewrapped a ToolSpec throw as a bare `Error` (marks died even LIVE — a ToolSpec throw carrying `details.code` got no `errorCode` lift while a native tool's identical error did), and the durable-resume batch's catch stringified the message alone (breaking the live/resume frame-parity contract). Both carry `details`/`errorKind` through now; message text unchanged.
|
|
2184
2323
|
- **Cache-break attribution: an aborted / usage-missing row is "unknown", not a cold cache.** Feeding it to the design/31 detector minted a false `server-or-ttl` finding and poisoned the warm baseline for the next real turn. Such rows are skipped; a genuine cacheRead collapse still fires.
|
|
@@ -2708,7 +2847,7 @@ _Emergency direct-publish path (CI publishing resumes 8/1); pre-tag `gate:blackb
|
|
|
2708
2847
|
**API retry chain (user-reported, rate-limited third-party providers)**
|
|
2709
2848
|
|
|
2710
2849
|
- `maxRetries` defaults to 10 (was 2), configurable via explicit brain options (highest priority, uncapped) or `SEMA_MAX_RETRIES` (clamped to [0,15]); `retryDelayMs` base is now 500.
|
|
2711
|
-
- Backoff is increasing-with-light-jitter — `min(500·2^(n-1), 32000) + rand·0.25·backoff` — replacing full-jitter (attempt N could previously wait less than attempt 1). `Retry-After` keeps its existing max-composition semantics; `anthropic-ratelimit-unified-reset` is now honored the same way (header values clamped to 60s against
|
|
2850
|
+
- Backoff is increasing-with-light-jitter — `min(500·2^(n-1), 32000) + rand·0.25·backoff` — replacing full-jitter (attempt N could previously wait less than attempt 1). `Retry-After` keeps its existing max-composition semantics; `anthropic-ratelimit-unified-reset` is now honored the same way (header values clamped to 60s against out-of-range headers).
|
|
2712
2851
|
- Retry liveness reaches the task event stream: `status` frames carry `attempt`/`maxRetries`/`retryInMs`, waits longer than 30s re-announce per slice, recovery emits a `recovered` terminal and exhaustion a `gave_up` terminal (phases added to `BrainStatusPhase`), and `circuit_open` carries the remaining cooldown. Clients can finally clear a stale error row the moment the stream recovers.
|
|
2713
2852
|
|
|
2714
2853
|
**Security (read-only bash boundary — bypass closure, disclosure correction for 2.8.0)**
|
|
@@ -3486,7 +3625,7 @@ Carried over from 1.410.0's open list: **RB-95** (the compaction cut-point's bac
|
|
|
3486
3625
|
|
|
3487
3626
|
## 1.410.0 (2026-07-25)
|
|
3488
3627
|
|
|
3489
|
-
The divergent-
|
|
3628
|
+
The divergent-probe round: four more probe agents, again forbidden to touch source, produced 44 reproducible defects across the approval chain, the session tree, the tool surface and the agent-team stores. 22 are fixed here, 2 are recorded as open with their reds resident. One of them was mine — 1.409.0's own fix introduced it, and it is the most serious thing in this release.
|
|
3490
3629
|
|
|
3491
3630
|
**Note on 1.409.0**: its tag was pushed but CI went red on `flaky-guard` and it never reached npm. The cause was not the release content — the guard's stale-allowlist check flagged a declaration for an untracked `*.local.test.ts` probe, a file that exists on a developer's machine and, by design, not in CI. The check now skips entries whose file is absent from the scan. Everything in 1.409.0 ships here.
|
|
3492
3631
|
|
|
@@ -3513,7 +3652,7 @@ A multi-dimensional red hunt (five independent probe agents + a first-party pass
|
|
|
3513
3652
|
- **fix (RB-64): the anti-re-vote guard was nested entirely inside the `policy_ask` arm**, so a `plan_review` checkpoint reopened after an `env_failed` retry accepted a decision contradicting the one the human actually made — and `winnerFromOutcome` recorded no winner for that gate, leaving nothing to replay against. Winners are now recorded for the review gates (with a synthetic `gate:plan_review` sentinel that can never collide with a real toolCallId) and the guard is gate-agnostic.
|
|
3514
3653
|
- **fix (RB-68): `durableApproval.ttlMs` reached `Date.now() + ttlMs` unvalidated** — `Number(unset_env_var)` produced `deadline = NaN`, and `NaN <= cutoff` is always false, so a checkpoint that carries a TTL and looks bounded could never be reaped by any cutoff. Fixed in both layers: mints normalize a non-finite/non-positive TTL to the documented default (a deliberate `undefined` still means unbounded), and `reap` treats an uncomparable deadline as reapable, since checkpoint rows are also written by deployment-side stores and older versions.
|
|
3515
3654
|
- **fix (RB-70): `resumeStream` never set `requireExistingSession`**, so the headline durable scenario — a restarted process resolving a checkpoint while only the CheckpointStore was wired durably — fabricated a brand-new empty session under the checkpoint's id and pointed a dangling leaf at it. Every other resume-shaped call site already set it. The resulting `resume.session_not_found` now reopens the checkpoint as `env_failed`: the action never ran and the human's decision is still valid, so the approval is retryable rather than burned.
|
|
3516
|
-
- **fix (RB-71): one throwing getter collapsed an entire object to a single `<unserializable>` sentinel**, so any two
|
|
3655
|
+
- **fix (RB-71): one throwing getter collapsed an entire object to a single `<unserializable>` sentinel**, so any two unserializable payloads hashed identically — and `boundInputHashOf` exists precisely to prove the args being executed are the args a human saw. The sentinel is now scoped to the offending key; totality is unchanged, resolution costs one key instead of the whole payload.
|
|
3517
3656
|
- **fix (RB-57): `findOrphanToolCalls` resolved tool calls against a global id set**, so a provider that reuses a toolCallId across turns (OpenAI-compatible gateways that number per request) hid a genuine orphan — and an unclosed `tool_use` makes every subsequent request on that session fail at a strict provider, not just one reconcile. Resolution is now positional.
|
|
3518
3657
|
- **fix (RB-65): `tightenTaskSpec` left `excludeTools`/`deferTools` outside its tighten-only guard** — an override replaced the base list instead of unioning with it, and `[]` cleared every base exclusion outright.
|
|
3519
3658
|
- **fix (RB-69): the CC task-list store's addressable id set and its enumerable id set were different** — a `007.json` counted toward the allocation high-water yet was unreachable through get/set/delete, and `list()` reported it as the id `"7"`. Checked against CC 2.1.219 itself (`edr`/`Uid`/`ste`): CC constrains a character class, not a numeric shape, and its list returns filenames verbatim, so `007` is a first-class id there. Both sides now use CC's charset; sema keeps the stricter half of the divergence (an off-charset id throws rather than being silently rewritten into a neighbouring file).
|
|
@@ -3737,7 +3876,7 @@ design/154 (shellGate classify granularity — server [1558]④): compound read-
|
|
|
3737
3876
|
- **feat: `classifyCompoundReadonly(command, allow)`** (exported) — textual scan-split on `;`/`&&`/`||`/`|`, every segment re-vetted by the single shared `parseLeadingCommandName` + bare-name allowlist; all-green ⇒ read-only. Whole-string hard rejects stay for `< > $ ( )` backtick newline CR backslash; a LONE `&` is hard-rejected too (backgrounding outlives the shell and escapes TaskStop lifecycle governance; `|&` rejects via the same rule). Soundness: with escapes/expansion/redirection hard-rejected, quotes can only HIDE connectors (real boundaries ⊆ textual boundaries), so every executed argv[0] is a vetted segment head — a quote-carrying head is parser-rejected, a clean head equals the executed program name. Error direction is strictly false-negative.
|
|
3738
3877
|
- **feat: `bashReversibilityProbe` upgraded** to the compound classifier (signature unchanged). The `bash_readonly` tool's `effect:"read"` declaration face deliberately KEEPS the strict single-command check (documented fork). The Monitor face intentionally shares the upgraded probe (design/135 G2: same env seam, classification is about the command text; a polled read-only compound stays read-only) — pinned by two wiring tests.
|
|
3739
3878
|
- **NOT included by design:** no git/`find`/subcommand allowlisting — "zero-flag" vetting operates on pre-expansion text while the shell expands quotes/braces/globs into argv, and the candidate git subcommands are not provably read-only under repo config (`core.fsmonitor`, `diff.external`/textconv, promisor fetches, index refresh). `git status` still asks — recorded false-negative; widening needs a dedicated review.
|
|
3740
|
-
- Verification: adversarial review (SOUND across five
|
|
3879
|
+
- Verification: adversarial review (SOUND across five probe directions) + a black-box harness in the server-[1558] methodology — red-first detector self-check, then 5000 seeded fuzz commands with EVERY green actually executed under real bash in a snapshot sandbox: zero filesystem mutations, zero surviving process groups, zero hangs; curated adversarial family all red or verified-harmless. Ask-rate on a realistic coding-task corpus: 69% → 43%.
|
|
3741
3880
|
- Backlog: RB-41 (availability residue — stdin-blocking greens like bare `cat` hang to tool timeout; not a reversibility issue, documented).
|
|
3742
3881
|
|
|
3743
3882
|
## 1.386.0 (2026-07-23)
|
|
@@ -3985,7 +4124,7 @@ CWD case core B-half ([1451]/[1452]/[1461]④): resume workspace-root fidelity +
|
|
|
3985
4124
|
- **hardening round 5 (1 high folded): a divergent restore under a PENDING APPROVED ACTION fails closed.** The approved args were authored + human-approved against the checkpointed root — internal path-state rebase cannot rewrite tool args (`rm /app/…` would run against a root the workspace no longer occupies, or burn the approval on a fence refusal). Divergence + pending approval → `resume.env_failed` → the existing reopen leg (resolved→pending) keeps the approval retryable until the adapter honors `priorHandle` or the knob rolls back (pinned end-to-end: fail → reopen → knob-back retry executes exactly once at the right root). The guard keys on the RESUME OUTCOME (`tool_approval` × an `allow` winner — `PrepareResume.executesApprovedAction`), not on the checkpoint kind: a DENY winner executes nothing and resolves under the diverged root without wedging (round 6); non-approval suspends (resource_limit etc.) likewise proceed under the rebase.
|
|
3986
4125
|
- **hardening round 4 (2 high folded):** the rebase contract is POSIX-ONLY — every remote lane's `mountPath` is a Linux container path; a backslash anywhere marks the value outside the domain and the path passes through UNCHANGED (un-rebased is observable via the divergence observation; wrongly-guessed Windows drive/UNC recomposition would silently corrupt cwd/read-state). And the divergence/misalignment `onError` observations are try/catch-isolated — a throwing observer sink must never turn an already-consumed (resolved) checkpoint into a terminal failure (observe-only means observe-only).
|
|
3987
4126
|
- **residuals ledgered (RB-35, adversarial round 7, LOW):** two documented tails of the narrow "rotation window × non-honoring adapter × durable suspend" pathology — canonical-vs-logical root prefixes can miss the rebase on symlinked mounts (observable degradation: cd failure / fence refusal / read-state miss, never a silent wrong write), and the fail-closed guard keys on the allow winner before zero-execution branches (walltime exhaustion, tightened rules) are known (a permanent rotation can loop fail→reopen with an explicit remedy in every error). Both wait on a real deployment shape.
|
|
3988
|
-
- **test: 13 new pins** — priorHandle rides resumeVM, mount-rotation divergence observation (non-fatal), POSITIVE honor leg (prompt cwd = checkpointed root across a rotation, zero divergence noise), resumeFacts render trio (honest sentences /
|
|
4127
|
+
- **test: 13 new pins** — priorHandle rides resumeVM, mount-rotation divergence observation (non-fatal), POSITIVE honor leg (prompt cwd = checkpointed root across a rotation, zero divergence noise), resumeFacts render trio (honest sentences / degraded note / fresh-task-never), resume-leg e2e gating, cwd-alignment guard both ways (honoring env silent; dropping env observed, task proceeds).
|
|
3989
4128
|
|
|
3990
4129
|
## 1.359.0 (2026-07-20)
|
|
3991
4130
|
|
|
@@ -4123,7 +4262,7 @@ Redaction tiering ([1295]② — the assertion-killer) + workflow schema teachin
|
|
|
4123
4262
|
|
|
4124
4263
|
- **fix: same-principal faces stop redacting paths/URLs.** `boundedRedactedSummary` (workflow agent outputs/results/errors flowing back to the launching model+script; run rows behind the scope-gated observe API) now applies `redactSecrets` only — the old blanket `[redacted-path]`/`[redacted-url]` strip killed the cross-agent assertion pattern ("return the path of the file you wrote") while protecting nothing on faces whose isolation is ACCESS CONTROL. CC redacts nothing here; the secret arm stays as a declared superset. Three host-error sites in the Workflow tool (script-path read failures) join the tier — their paths are the HOST'S own error text.
|
|
4125
4264
|
- **`redactSecrets` rides the centralized `scrubSecrets` scanner** (codex HIGH: a second hand-rolled vocabulary WAS the bypass — SAS `sig=`, JWTs, sk-prefixed path tokens all slipped it): prefixed tokens, JWT, PEM, Bearer, `secretKey=value`, plus two URL arms that matter once URLs survive — userinfo credentials stripped to the LAST `@` of the authority (a first-`@` regex left `p@ssword` half-visible and let an empty-username form escape entirely, codex HIGH), and signed-URL params (`sig`/`signature`/`sas`/`code`) redacted by key.
|
|
4126
|
-
- **External faces keep the FULL strip** (codex HIGH): `untrustedEgressForHuman` (human display; URL stripping also
|
|
4265
|
+
- **External faces keep the FULL strip** (codex HIGH): `untrustedEgressForHuman` (human display; URL stripping also removes outbound-link bait) and the cross-replica `WorkflowCompletionNotifier` payload (its contract promises host paths never escape) still apply `redactHostLeaks`. `subscribeWorkflow` remains in-process/by-runId — a deployment exposing it over HTTP scope-gates first (as `getWorkflowRun` does).
|
|
4127
4266
|
- **teaching ([1295]①④)**: the Workflow tool card now states standard-JSON-Schema union support (`{"type":["string","null"]}` / `anyOf` — typebox handles both, verified) and that cross-agent assertions belong in the SCRIPT body (plain JS between stages). Suite green, tsc 0, deepseek live 9/9.
|
|
4128
4267
|
|
|
4129
4268
|
## 1.346.0 (2026-07-19)
|
|
@@ -4204,7 +4343,7 @@ Approval display projection ([1245] — the "Run a dynamic workflow?" confirmati
|
|
|
4204
4343
|
- **feat: `approvalPreview` seam** — a tool may declare `approvalPreview(args)` (AgentTool/ToolSpec, mechanism-neutral): a pure display projection of its args for human approval surfaces. The gate mints it through ONE helper — alias-aware tool lookup, throw-swallowed, 16KiB serialized clamp, and **control-character sanitization on every string leaf** (terminal escapes/CR can never reach an approval renderer raw). Delivered on BOTH lanes: `AskRequest.preview` (live onAsk dialogs) and the durable checkpoint's `pendingAction.preview` + **`CheckpointSummary.preview`** (the one-call `listByScope` inbox — no N+1 get).
|
|
4205
4344
|
- **Trust contract (explicit)**: the preview is UNTRUSTED, ADVISORY display metadata — renderers must contextually escape it and approval surfaces should show the bound raw args alongside (args + boundInputHash remain the sole execution contract; adjudication and resume never read the preview).
|
|
4206
4345
|
- **run_workflow implements it**: script form → statically parsed meta (name/description/phases[{title,detail}], scriptChars, hasArgs); named/scriptPath forms → identity stubs; malformed meta → bounded parse-error head. Runtime agent prompts are statically unknowable — phase detail is the honest ceiling. Also pinned: run_workflow rides the tool gate with NO exemption (an ask-policy naming it parks/asks like any tool — the [1230] "manual mode ran without confirmation" was a policy name-list gap, not an engine bypass).
|
|
4207
|
-
- codex adversarial 2 rounds (
|
|
4346
|
+
- codex adversarial 2 rounds (malformed-metadata sanitization, alias-invoked preview loss, durable-summary projection all folded). Suite 6181 green, tsc 0, deepseek live 9/9.
|
|
4208
4347
|
|
|
4209
4348
|
## 1.337.0 (2026-07-19)
|
|
4210
4349
|
|
|
@@ -102,5 +102,5 @@ export function renderAutoModeWindow(messages, options) {
|
|
|
102
102
|
export function renderAutoModeAction(input) {
|
|
103
103
|
const ask = input.askMessage ? `\npermission gate: ${input.askMessage}` : "";
|
|
104
104
|
return (`\n## New action to classify (the agent's most recent action — evaluate THIS)\n\n` +
|
|
105
|
-
`[tool_call] ${input.req.toolName} ${JSON.stringify(input.req.args ?? {})}${ask}\n`);
|
|
105
|
+
`[tool_call] ${input.req.toolName} ${excerpt(JSON.stringify(input.req.args ?? {}), 48_000)}${ask}\n`);
|
|
106
106
|
}
|
|
@@ -1610,6 +1610,12 @@ export declare class CheckpointError extends Error {
|
|
|
1610
1610
|
* Rejected pre-CAS so the checkpoint stays `pending` and a capable worker can still resume it
|
|
1611
1611
|
* (design/49 §2/§3, code-ready council round-2). */
|
|
1612
1612
|
| "checkpoint.unsupported_version"
|
|
1613
|
+
/** backlog #243: `resume`/`resumeStream` was handed `internals.requestedCwd` for a checkpoint that
|
|
1614
|
+
* carries a remote `workspaceHandle`. The restored workspace's mountPath is authoritative for the
|
|
1615
|
+
* task root, so the cwd would be ignored at best — and verifying it would touch the env BEFORE the
|
|
1616
|
+
* VM restore (a lazy-connect adapter then connects a fresh empty sandbox, the design/49 disease).
|
|
1617
|
+
* Rejected pre-CAS so the checkpoint stays `pending`; re-resume without `requestedCwd`. */
|
|
1618
|
+
| "checkpoint.cwd_conflicts_restore"
|
|
1613
1619
|
/** design/164: the checkpoint was written by a pre-164 worker AND its ledger carries the RETIRED
|
|
1614
1620
|
* cross-slice wall-clock allocation (`resourceLedger.totalWalltimeSec`). The axis no longer exists,
|
|
1615
1621
|
* so resuming would run the leg with the operator's time ceiling silently unenforced. Refused
|
|
@@ -1733,6 +1739,12 @@ export declare class CheckpointError extends Error {
|
|
|
1733
1739
|
* Rejected pre-CAS so the checkpoint stays `pending` and a capable worker can still resume it
|
|
1734
1740
|
* (design/49 §2/§3, code-ready council round-2). */
|
|
1735
1741
|
| "checkpoint.unsupported_version"
|
|
1742
|
+
/** backlog #243: `resume`/`resumeStream` was handed `internals.requestedCwd` for a checkpoint that
|
|
1743
|
+
* carries a remote `workspaceHandle`. The restored workspace's mountPath is authoritative for the
|
|
1744
|
+
* task root, so the cwd would be ignored at best — and verifying it would touch the env BEFORE the
|
|
1745
|
+
* VM restore (a lazy-connect adapter then connects a fresh empty sandbox, the design/49 disease).
|
|
1746
|
+
* Rejected pre-CAS so the checkpoint stays `pending`; re-resume without `requestedCwd`. */
|
|
1747
|
+
| "checkpoint.cwd_conflicts_restore"
|
|
1736
1748
|
/** design/164: the checkpoint was written by a pre-164 worker AND its ledger carries the RETIRED
|
|
1737
1749
|
* cross-slice wall-clock allocation (`resourceLedger.totalWalltimeSec`). The axis no longer exists,
|
|
1738
1750
|
* so resuming would run the leg with the operator's time ceiling silently unenforced. Refused
|
|
@@ -24,6 +24,8 @@ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
|
|
|
24
24
|
"memory.erasure_selector_mismatch",
|
|
25
25
|
"memory.erasure_census_incomplete",
|
|
26
26
|
"memory.erasure_index_residue",
|
|
27
|
+
"memory.export_incomplete",
|
|
28
|
+
"memory.import_rejected",
|
|
27
29
|
]);
|
|
28
30
|
export function governanceRetryClass(code) {
|
|
29
31
|
if (Object.prototype.hasOwnProperty.call(GOVERNANCE_CODES, code)) {
|
package/dist/core/hooks.js
CHANGED
|
@@ -398,7 +398,15 @@ export async function runToolGate(input) {
|
|
|
398
398
|
denySource = input.shellGated === true ? "shellGate" : "safety";
|
|
399
399
|
}
|
|
400
400
|
}
|
|
401
|
-
|
|
401
|
+
const breakerKnownOpen = (d) => {
|
|
402
|
+
try {
|
|
403
|
+
return d.breakerOpen();
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
if (input.peerMessage === true && decision.action === "allow" && input.autoMode !== undefined && !breakerKnownOpen(input.autoMode.decider)) {
|
|
402
410
|
decision = {
|
|
403
411
|
action: "ask",
|
|
404
412
|
message: `tool "${toolName}" sends a message to another agent — routed for classifier review in auto mode`,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type CommittedBinding, type EraseMemoryEntriesInput, type MemoryErasureAttestation, type TransferEvidence } from "./file-backend.js";
|
|
2
|
+
import { type MemoryExportBundle, type MemoryImportReport } from "./export-bundle.js";
|
|
2
3
|
import { type ChallengeAssignment, type ChallengeEvent, type ControlPlaneRebuildReceipt, type StrictControlPlaneLedger, type ChallengedHistoryRow, type LineagePendingTxn, type LineagePromotion, type MemoryPartitionIncidentSink, type RetrievedAccountRow, type SessionPollutionRecord } from "./layout.js";
|
|
3
4
|
import type { HarvestReport, MemoryAnnouncement, MemoryBackend, MemorySessionHandle, ScanFinding } from "./types.js";
|
|
4
5
|
/**
|
|
@@ -438,6 +439,32 @@ export declare class MemoryEngine {
|
|
|
438
439
|
* reported honestly, never fabricated and never a throw (#196 absence-reports-not-silent-green).
|
|
439
440
|
*/
|
|
440
441
|
provenanceOf(entryId: string): Promise<EntryProvenanceAccount>;
|
|
442
|
+
/**
|
|
443
|
+
* Export the requested scopes as a GOVERNANCE-COMPLETE bundle (design/178 v2 §4.3 + v2-c):
|
|
444
|
+
* committed entries (audit-snapshot semantics — shadow-backed rows a disk scan would miss are
|
|
445
|
+
* in; unavailable content is enumerated, never dressed), the scope-sliced custody chain, the
|
|
446
|
+
* exported entries' unresolved challenge events and lineage rows, and the named sessions'
|
|
447
|
+
* pollution markers — all read inside the backend's single-lock export composite, so a bundle is
|
|
448
|
+
* never a mixed-epoch scene. Refusals are loud and total (`memory.export_incomplete`): there is
|
|
449
|
+
* no degraded/partial bundle shape — "a truncated package that looks complete" is the one
|
|
450
|
+
* deliverable this API is forbidden to produce.
|
|
451
|
+
*/
|
|
452
|
+
exportMemoryScopes(scopes: readonly string[]): Promise<MemoryExportBundle>;
|
|
453
|
+
/**
|
|
454
|
+
* Import a bundle (design/178 v2-c §1/§7). Validation is TOTAL and lands nothing on failure
|
|
455
|
+
* (`memory.import_rejected`): integrity (fixed section-hash key set, per-entry content-rev
|
|
456
|
+
* recomputation, batch-unique ids/event ids), structure (the governance section is REQUIRED —
|
|
457
|
+
* there is no entries-only escape hatch), and — when `opts.expectedScopes` is present (a service
|
|
458
|
+
* endpoint pins its principal's grant set here) — the scope authorization envelope. Everything
|
|
459
|
+
* after validation runs inside the backend's import composite: the synthetic latch (imported ids
|
|
460
|
+
* stay withheld from every model-visible read face until governance fully lands), the five
|
|
461
|
+
* import judgments IN-LOCK, entry landing through the one transaction skeleton, the governance
|
|
462
|
+
* legs, and the receipt-gated latch release. A crash anywhere converges by re-importing the SAME
|
|
463
|
+
* bundle; a completed import answers its recorded report idempotently.
|
|
464
|
+
*/
|
|
465
|
+
importMemoryBundle(bundle: MemoryExportBundle, opts?: {
|
|
466
|
+
expectedScopes?: readonly string[];
|
|
467
|
+
}): Promise<MemoryImportReport>;
|
|
441
468
|
/** Host API: challenge every entry the lineage ledger attributes to `sessionId` (post-hoc source
|
|
442
469
|
* falsification — trustedTools misconfigured, a tool re-classified, late delegation evidence).
|
|
443
470
|
* Same requestId contract as {@link challengeEntries}. */
|
|
@@ -6,6 +6,7 @@ import { inlineUntrusted } from "../untrusted-text.js";
|
|
|
6
6
|
import { formatMemoryAge } from "../memory-recall.js";
|
|
7
7
|
import { computeEntryRev, parseEntryFile, serializeEntryFile } from "./frontmatter.js";
|
|
8
8
|
import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, canonicalJsonStringify, captureErasureInput, erasureRequestInvalid, erasureSelectHash, scanEntryFiles, } from "./file-backend.js";
|
|
9
|
+
import { assembleMemoryExportBundle, computeMemoryBundleHash, memoryBundleInvalid, } from "./export-bundle.js";
|
|
9
10
|
import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, readIndexRevs, writeIndexRevs, bumpScanFuse, canonicalize, claimRootScope, clearScanFuse, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, isContainedIn, markSessionPolluted, readSessionPollution, recordRetrievedAccount, writeFileNoFollow, readRetrievedAccount, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, appendChallengeEvents, appendLineageAudit, rebuildStrictControlPlaneLedger, isStrictControlPlaneLedgerCorrupt, CHALLENGE_LEDGER_MAX_EVENTS, adjudicateLineagePending, challengedEntryIds, clearLineageForEntries, discardLineagePending, lineageAccountOfEntry, lineageContributionsOfSession, lineageLatchedIds, promoteLineagePending, readChallengeEvents, readChallengedHistory, readLineageRecord, recordChallengedHistory, recordLineageCredential, reconcileLineage, resolveChallengeEvent, stageLineagePending, } from "./layout.js";
|
|
10
11
|
import { scanMemoryFileName, scanMemoryWrite, scanRemediation } from "./scan.js";
|
|
11
12
|
export const MEMORY_INSTRUCTION_TEMPLATE = `# Memory
|
|
@@ -521,6 +522,105 @@ export class MemoryEngine {
|
|
|
521
522
|
return { v: 1, id: entryId, binding, contributors: plane.contributors, ...(plane.exclusion !== undefined ? { exclusion: plane.exclusion } : {}), contentState, ...(ingest !== undefined ? { ingest } : {}), custody };
|
|
522
523
|
}
|
|
523
524
|
}
|
|
525
|
+
async exportMemoryScopes(scopes) {
|
|
526
|
+
if (!Array.isArray(scopes) || scopes.length === 0 || scopes.some((s) => typeof s !== "string" || s.length === 0) || new Set(scopes).size !== scopes.length) {
|
|
527
|
+
const e = new Error("exportMemoryScopes: scopes must be a non-empty array of unique non-empty scope names");
|
|
528
|
+
e.code = "config.memory_export_request";
|
|
529
|
+
throw e;
|
|
530
|
+
}
|
|
531
|
+
const requested = [...scopes];
|
|
532
|
+
const face = this.backend.exportSnapshotOf;
|
|
533
|
+
if (typeof face !== "function") {
|
|
534
|
+
const e = new Error("exportMemoryScopes: this backend has no export-snapshot capability (exportSnapshotOf) — a governance-complete bundle cannot be assembled from the generic read faces (they cannot fence the five store faces into one epoch), and a governance-less export is the laundering shape this API refuses by design.");
|
|
535
|
+
e.code = "memory.export_incomplete";
|
|
536
|
+
throw e;
|
|
537
|
+
}
|
|
538
|
+
const snap = await face.call(this.backend, requested);
|
|
539
|
+
const exportedIds = new Set([...snap.entries.map((e) => e.id), ...snap.contentUnavailable.map((r) => r.id)]);
|
|
540
|
+
const resolvedGenerations = new Set();
|
|
541
|
+
for (const e of snap.challenges)
|
|
542
|
+
if (e.kind === "resolve")
|
|
543
|
+
resolvedGenerations.add(`${e.entryId}#${e.generation}`);
|
|
544
|
+
const challenges = [];
|
|
545
|
+
for (const e of snap.challenges) {
|
|
546
|
+
if (e.kind !== "challenge" || !exportedIds.has(e.entryId) || resolvedGenerations.has(`${e.entryId}#${e.generation}`))
|
|
547
|
+
continue;
|
|
548
|
+
challenges.push({ eventId: e.eventId, entryId: e.entryId, reason: e.reason, at: e.at, ...(e.challengedRev !== undefined ? { challengedRev: e.challengedRev } : {}) });
|
|
549
|
+
}
|
|
550
|
+
const lineage = snap.lineage.filter((r) => exportedIds.has(r.entryId));
|
|
551
|
+
const namedSessions = new Set(lineage.map((r) => r.sessionId));
|
|
552
|
+
for (const row of snap.custody) {
|
|
553
|
+
const sessions = row.sessions;
|
|
554
|
+
if (Array.isArray(sessions))
|
|
555
|
+
for (const s of sessions)
|
|
556
|
+
if (typeof s === "string")
|
|
557
|
+
namedSessions.add(s);
|
|
558
|
+
}
|
|
559
|
+
const pollutedSessions = snap.pollutedSessions.filter((p) => namedSessions.has(p.sessionId));
|
|
560
|
+
return assembleMemoryExportBundle({
|
|
561
|
+
at: this.now(),
|
|
562
|
+
scopes: requested,
|
|
563
|
+
storeId: snap.storeId,
|
|
564
|
+
entries: snap.entries,
|
|
565
|
+
challenges,
|
|
566
|
+
pollutedSessions,
|
|
567
|
+
lineage,
|
|
568
|
+
custody: snap.custody,
|
|
569
|
+
residuals: {
|
|
570
|
+
quarantined: snap.quarantined,
|
|
571
|
+
unbound: snap.fullStore ? snap.unbound : [],
|
|
572
|
+
pendingLatch: snap.fullStore ? snap.pendingLatch : [],
|
|
573
|
+
contentUnavailable: snap.contentUnavailable,
|
|
574
|
+
quarantineOpaque: snap.quarantineOpaque,
|
|
575
|
+
unsliceableCustody: snap.unsliceableCustody,
|
|
576
|
+
unboundCount: snap.unbound.length,
|
|
577
|
+
pendingLatchCount: snap.pendingLatch.length,
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
async importMemoryBundle(bundle, opts) {
|
|
582
|
+
let inert;
|
|
583
|
+
try {
|
|
584
|
+
inert = JSON.parse(JSON.stringify(bundle));
|
|
585
|
+
}
|
|
586
|
+
catch (err) {
|
|
587
|
+
const e = new Error(`importMemoryBundle: the bundle is not JSON-serializable (${err instanceof Error ? err.message : String(err)})`);
|
|
588
|
+
e.code = "config.memory_import_request";
|
|
589
|
+
throw e;
|
|
590
|
+
}
|
|
591
|
+
const bad = memoryBundleInvalid(inert, { ...(opts?.expectedScopes !== undefined ? { expectedScopes: opts.expectedScopes } : {}) });
|
|
592
|
+
if (bad !== undefined) {
|
|
593
|
+
const e = new Error(`importMemoryBundle: ${bad} — the whole package is refused, nothing landed`);
|
|
594
|
+
e.code = "memory.import_rejected";
|
|
595
|
+
throw e;
|
|
596
|
+
}
|
|
597
|
+
const b = inert;
|
|
598
|
+
const face = this.backend.importBundleCommit;
|
|
599
|
+
if (typeof face !== "function") {
|
|
600
|
+
const e = new Error("importMemoryBundle: this backend has no bundle-import capability (importBundleCommit) — the import latch/receipt transaction requires it, and an unlatched import would serve entries before their governance lands; nothing was imported.");
|
|
601
|
+
e.code = "memory.import_rejected";
|
|
602
|
+
throw e;
|
|
603
|
+
}
|
|
604
|
+
const bundleHash = computeMemoryBundleHash(b.integrity.sectionHashes);
|
|
605
|
+
const custody = [];
|
|
606
|
+
const seenEv = new Set();
|
|
607
|
+
for (const row of b.governance.custody) {
|
|
608
|
+
if (seenEv.has(row.ev))
|
|
609
|
+
continue;
|
|
610
|
+
seenEv.add(row.ev);
|
|
611
|
+
custody.push(row);
|
|
612
|
+
}
|
|
613
|
+
const plan = {
|
|
614
|
+
bundleHash,
|
|
615
|
+
sourceStoreId: b.storeId,
|
|
616
|
+
entries: b.entries,
|
|
617
|
+
challenges: b.governance.challenges,
|
|
618
|
+
pollutedSessions: b.governance.pollutedSessions,
|
|
619
|
+
lineage: b.governance.lineage,
|
|
620
|
+
custody,
|
|
621
|
+
};
|
|
622
|
+
return face.call(this.backend, plan);
|
|
623
|
+
}
|
|
524
624
|
challengeSession(sessionId, reason, requestId) {
|
|
525
625
|
if (typeof requestId !== "string" || requestId === "") {
|
|
526
626
|
const e = new Error("challengeSession: requestId is required (idempotency identity — retries must reuse it; the engine does not mint one)");
|
|
@@ -822,7 +922,9 @@ export class MemoryEngine {
|
|
|
822
922
|
const rec = reconcileLineage(this.controlDir, this.now);
|
|
823
923
|
this.settlePromotions(rec.promoted);
|
|
824
924
|
for (const u of rec.undecidable) {
|
|
825
|
-
report.warnings.push(
|
|
925
|
+
report.warnings.push(u.kind === "latch-only"
|
|
926
|
+
? `memory bundle-import latch ${u.txnId} is unsettled (an import did not run to completion) — ${u.entryIds.length} entr${u.entryIds.length === 1 ? "y is" : "ies are"} latched (memory reads refuse them) until the SAME bundle is imported again to completion (importMemoryBundle; adjudication is refused on import latches)`
|
|
927
|
+
: `memory lineage transaction ${u.txnId} is unsettled (a crash landed between commit and its durable credential) — ${u.entryIds.length} entr${u.entryIds.length === 1 ? "y is" : "ies are"} latched (memory reads refuse them) until the host adjudicates it (adjudicatePendingLineage)`);
|
|
826
928
|
}
|
|
827
929
|
if (pollutedReason !== undefined && lineageSessionId !== undefined) {
|
|
828
930
|
const rec2 = this.sessionPollution(lineageSessionId);
|