@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +150 -11
  2. package/dist/core/auto-mode-prompt.js +1 -1
  3. package/dist/core/checkpoint-store.d.ts +12 -0
  4. package/dist/core/governance-codes.js +2 -0
  5. package/dist/core/hooks.js +9 -1
  6. package/dist/core/memory-engine/engine.d.ts +27 -0
  7. package/dist/core/memory-engine/engine.js +103 -1
  8. package/dist/core/memory-engine/export-bundle.d.ts +192 -0
  9. package/dist/core/memory-engine/export-bundle.js +306 -0
  10. package/dist/core/memory-engine/file-backend.d.ts +178 -1
  11. package/dist/core/memory-engine/file-backend.js +648 -6
  12. package/dist/core/memory-engine/index.d.ts +2 -1
  13. package/dist/core/memory-engine/index.js +1 -0
  14. package/dist/core/memory-engine/layout.d.ts +99 -1
  15. package/dist/core/memory-engine/layout.js +143 -7
  16. package/dist/core/memory-engine/memory-backend-contract.d.ts +1 -1
  17. package/dist/core/memory-engine/memory-backend-contract.js +52 -0
  18. package/dist/core/memory-engine/tools.js +8 -1
  19. package/dist/core/permission-rule-consent.js +14 -2
  20. package/dist/core/runner/prepare-config-doors.js +20 -0
  21. package/dist/core/runner/prepare-task.js +13 -0
  22. package/dist/core/runner/runtask.js +14 -1
  23. package/dist/core/runner/synthetic-tools.js +3 -1
  24. package/dist/core/runner/tool-disclosure.js +2 -1
  25. package/dist/core/types.d.ts +7 -0
  26. package/dist/core/write-protect.d.ts +0 -20
  27. package/dist/core/write-protect.js +4 -3
  28. package/dist/engine/harness/agent-harness.d.ts +17 -0
  29. package/dist/engine/harness/agent-harness.js +19 -1
  30. package/dist/engine/harness/types.d.ts +5 -0
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/tools/fs/safety.d.ts +1 -1
  34. package/dist/tools/fs/safety.js +1 -1
  35. package/dist/tools/fs/search.d.ts +33 -0
  36. package/dist/tools/fs/search.js +72 -0
  37. package/package.json +1 -1
  38. 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 planting vector
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/forgery guards).
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
- hostile text shapes get typed refusals instead of raising inside the refusal path.
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: hostile 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.
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 hostile headers).
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-attack 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.
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 hostile 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.
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 attack 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%.
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 / hostile note / fresh-task-never), resume-leg e2e gating, cwd-alignment guard both ways (honoring env silent; dropping env observed, task proceeds).
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 disarms exfil-lure links) 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).
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 (hostile-metadata sanitization, alias-invoked preview loss, durable-summary projection all folded). Suite 6181 green, tsc 0, deepseek live 9/9.
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)) {
@@ -398,7 +398,15 @@ export async function runToolGate(input) {
398
398
  denySource = input.shellGated === true ? "shellGate" : "safety";
399
399
  }
400
400
  }
401
- if (input.peerMessage === true && decision.action === "allow" && input.autoMode !== undefined && !input.autoMode.decider.breakerOpen()) {
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(`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)`);
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);