@sema-agent/core 5.35.0 → 5.37.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 (51) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/dist/agents/subagent.d.ts +10 -0
  3. package/dist/agents/subagent.js +29 -2
  4. package/dist/core/auto-compaction.d.ts +23 -0
  5. package/dist/core/auto-compaction.js +8 -0
  6. package/dist/core/checkpoint-store.d.ts +16 -0
  7. package/dist/core/context-guard.d.ts +41 -0
  8. package/dist/core/context-guard.js +76 -0
  9. package/dist/core/governance-codes.js +4 -0
  10. package/dist/core/memory-engine/engine.d.ts +142 -0
  11. package/dist/core/memory-engine/engine.js +265 -3
  12. package/dist/core/memory-engine/file-backend.d.ts +490 -16
  13. package/dist/core/memory-engine/file-backend.js +1099 -36
  14. package/dist/core/memory-engine/index.d.ts +2 -2
  15. package/dist/core/memory-engine/index.js +1 -1
  16. package/dist/core/memory-engine/layout.d.ts +42 -2
  17. package/dist/core/memory-engine/layout.js +76 -12
  18. package/dist/core/memory-engine/memory-backend-contract.d.ts +13 -0
  19. package/dist/core/memory-engine/memory-backend-contract.js +89 -0
  20. package/dist/core/park-selfcheck.d.ts +5 -0
  21. package/dist/core/protocol-table.d.ts +4 -4
  22. package/dist/core/runner/assemble-result.d.ts +8 -0
  23. package/dist/core/runner/assemble-result.js +4 -1
  24. package/dist/core/runner/git-status-frame.d.ts +219 -0
  25. package/dist/core/runner/git-status-frame.js +212 -0
  26. package/dist/core/runner/prepare-memory.d.ts +11 -1
  27. package/dist/core/runner/prepare-memory.js +48 -2
  28. package/dist/core/runner/prepare-task.d.ts +21 -0
  29. package/dist/core/runner/prepare-task.js +28 -35
  30. package/dist/core/runner/runtask.js +270 -5
  31. package/dist/core/task-registry-agent.d.ts +15 -0
  32. package/dist/core/task-registry-agent.js +9 -0
  33. package/dist/core/task-registry.d.ts +3 -0
  34. package/dist/core/task-registry.js +4 -1
  35. package/dist/core/types.d.ts +122 -7
  36. package/dist/engine/harness/types.d.ts +65 -1
  37. package/dist/engine/harness/types.js +20 -0
  38. package/dist/engine/session/import-validate.js +10 -1
  39. package/dist/engine/session/session.d.ts +37 -1
  40. package/dist/engine/session/session.js +56 -1
  41. package/dist/index.d.ts +2 -2
  42. package/dist/index.js +1 -1
  43. package/dist/internal/harness-types.d.ts +1 -0
  44. package/dist/internal/harness.d.ts +2 -0
  45. package/dist/internal/harness.js +2 -0
  46. package/dist/prompt-assembly/epoch.js +1 -1
  47. package/dist/prompt-assembly/event-registry.js +1 -0
  48. package/dist/prompts/default.d.ts +20 -7
  49. package/dist/prompts/default.js +2 -7
  50. package/package.json +1 -1
  51. package/test/export-surface.snapshot.json +13 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,120 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.37.0 — 2026-08-16
4
+
5
+ No BREAKING changes. Narrowings disclosed below (fail-closed forms replacing silent tolerance).
6
+
7
+ ### Added
8
+
9
+ - design/178 v2-a — the provenance/audit read family (part 2): `MemoryEngine.provenanceOf(id)`
10
+ returns a versioned envelope joining the entry's custody chain, lineage account and committed
11
+ binding; backend faces `committedSnapshotOf` / `committedSnapshotsOfScopes` / `custodyOf` answer
12
+ the committed store state and per-id custody without touching live planes. `TransferEvidence` and
13
+ the custody/snapshot report types are exported; the contract kit gains optional clauses for the
14
+ new faces.
15
+ - design/178 v2-a — the memory-visibility observation seat (part 1): `TaskResult.effectiveMemoryScopes`
16
+ (exported union `EffectiveMemoryScopes`) reports the leg's EFFECTIVE memory posture — mounted
17
+ (visibility rows in effective service order, with per-scope admission origin), memoryless
18
+ (mount-failed with residue, or no-backend), or none (no-spec / disabled). Minted after the
19
+ materialize outcome on every terminal that completed prepare, including the stream-layer backstop
20
+ terminal; delegated child legs each mint their own.
21
+ - design/178 v2-b — the erasure evidence protocol: `MemoryEngine.eraseMemoryEntries` /
22
+ backend `eraseWithEvidence` perform evidence-bearing erasure — store-level anchor plus per-scope
23
+ request projections in one journaled transaction, delete rows causally bound to the physical
24
+ deletes, a chain-level resurrection backstop (an erased id's replanted bytes are refused on BOTH
25
+ the adoption and retrieval faces), a census-backed scope selector (unledgered-live ghost files are
26
+ members), an erase-time MEMORY.md index sweep (residue is swept or disclosed on
27
+ `residuals.indexUncleared`, never silently left — including on §4.6 replays, where the evidence
28
+ row's recorded binding drives the sweep under an occupancy check so a freed slug taken by a live
29
+ entry is never touched), and an unevidenced degradation lane behind an explicit `allowUnevidenced`
30
+ opt-in. `MemoryErasureAttestation` rows are re-checkable against the evidence chain.
31
+ - `EntryCustodyReport.reason` (optional): a custody chain that carries one event id with two
32
+ different payloads answers `state: "damaged"` with the contradiction named, instead of a clean
33
+ "complete" over a spliced chain; byte-equal duplicate rows collapse instead of double-counting.
34
+ - Erasure host-API codes registered: `memory.erasure_evidence_unavailable`,
35
+ `memory.erasure_selector_mismatch`, `memory.erasure_census_incomplete`,
36
+ `memory.erasure_index_residue` (all non-governance caller/state-shape verdicts).
37
+
38
+ ### Fixed
39
+
40
+ - Erasure selectors resolve prototype-member keys as data, never as phantom sets: a
41
+ `sessionId: "constructor"` selector answers the empty resolution instead of treating every
42
+ lineaged entry as a match (class-swept across the lineage/scope/cursor keyed reads).
43
+ - `provenanceOf` names the face that is actually missing: the unknown-binding reason literal is now
44
+ `audit-snapshot-capability-absent` (was minted as `custody-capability-absent` even while the
45
+ custody face was answering).
46
+ - The retrieval face's delete-evidence guard is loud on an account-read fault: the documented
47
+ conservative-withhold direction is real (ids carrying delete evidence are withheld during the
48
+ fault window, entries without delete evidence keep serving), and the fault is announced once per
49
+ fault code per mount instead of a silent vanish.
50
+
51
+ ### Narrowed (disclosed)
52
+
53
+ - Transfer-evidence journal replay: a journal row whose event id matches an existing chain row with
54
+ a DIFFERENT payload is now fail-closed corruption (loud refusal naming the ev), replacing the
55
+ pre-v2b silent skip/overwrite. One ev is one identity.
56
+ - `{scope}` erasure membership is bound rows ∪ the census's unledgered-live inhabitants — a ghost
57
+ file no longer survives a scope erase under a clean attestation.
58
+
59
+
60
+ ## 5.36.0 — 2026-08-15
61
+
62
+ No BREAKING changes. Upgrade note: the system-prompt shape change resets provider prompt caches
63
+ ONCE on upgrade — the point of the feature is that they stop resetting after that.
64
+
65
+ ### Added
66
+
67
+ - Env-tail prefix stability (#254): the turn-dynamic git facts (branch line, working-tree line,
68
+ status snapshot) leave the system prompt for a value-keyed `git_status` turn frame. The system
69
+ prompt stays byte-stable across legs while the working tree moves, so provider prefix caches
70
+ survive leg/resume/fork boundaries (live-verified cache hits past the static-section watermark).
71
+ The frame re-sends only when its `(kind, hash)` changes; kind flips announce a tombstone once
72
+ ("may be stale" / "no longer a git repository"); recovery re-asserts loudly. Compaction re-asserts
73
+ the current frame beside the summary on all three landing paths (boundary / PTL recovery /
74
+ end-of-task, one single-source hook). Announcement mirrors are branch-authoritative with a
75
+ transcript-side newest-frame arbiter: the transcript is the truth, a mirror that cannot be audited
76
+ degrades to one conservative re-announcement.
77
+ - Wire/durable additions for #254: `steering_injected.source` gains `git_status` (preview is a
78
+ CONSTANT phrase per frame kind — repo text never enters the event face); durable session entry
79
+ `git_announcement`, compaction `details.gitAnnouncement`, `CheckpointState.gitAnnouncement` (all
80
+ additive, no version bump — absent fields resume with one conservative re-announcement); `Session`
81
+ gains two OPTIONAL members (`appendGitAnnouncement`/`getGitAnnouncement`); event-registry row
82
+ `git_status`. Existing sessions record one `legacy_migration` pin on first resume (env probe
83
+ sentinel `probe-env@2`).
84
+ - New `TaskResult.errorCode`: `irreducible_core_over_budget` — the request budget cannot fit even
85
+ the compaction summaries plus the DEGRADED git frame; the run fails loudly instead of shipping a
86
+ dishonest request. The frame has a trim-protected replace-by-key slot with exactly one
87
+ deterministic degrade shrink; H4 probe failures degrade to a two-line branch+dirty frame instead
88
+ of silently omitting the section; the probe sentinel mis-split arm is loud.
89
+ - Fleet row generation out-line (#258): `task_progress` ticks and `BackgroundChildEvent` spawn/tick
90
+ frames from background-agent rows carry `seq` — the row's stop-cycle generation (fresh spawn = 1,
91
+ every launched revival bumps it), one axis with `TaskNotificationPayload.seq` — so a fleet
92
+ consumer can tell a LATE first frame from a REVIVED cycle's frame. BCE terminal frames fall back
93
+ to the registry generation when the notify-mirror carrier is absent (one run speaks one number;
94
+ the notification payload's design/144 seq contract is unchanged). Absence stays a fact: sync
95
+ children, workflow agents and top-level runs carry nothing — never read absence as "cycle 1".
96
+ Hardening shipped with it: a parked-born resume speaks ONE generation end to end (the consume
97
+ flip's target), a retained wake realigns forward past a foreign tier-3 cycle's durable advance,
98
+ and BCE ticks always stamp the ENCLOSING row's generation.
99
+
100
+ ### Changed
101
+
102
+ - `buildEnvironmentContext` no longer renders gitBranch/gitDirty/gitSnapshot (fields remain as
103
+ data; the snapshot rides the `git_status` frame). Pre-call budget estimates now include the frame
104
+ bytes (an informed tightening). Frame bodies clamp astral-safe (U+FFFD at a split code point).
105
+ - Merged-code rescan hardening (18 findings across two rounds, all verified then disposed): the
106
+ degraded-shrink replacement inserts via thunk (a branch named with `$&`/`$$` — legal ref bytes —
107
+ no longer expands match context into the frame or silently renames itself; same class swept to
108
+ `buildMemoryInstruction`); the irreducible core charges the frame segment + summaries only, and a
109
+ trim-dropped carrier is never resurrected — the guard re-inserts the frame SEGMENT as its own
110
+ engine-minted request-view message, so the trim's adjudication stands and the request stays
111
+ servable; the listing-replay scan strips positive git frames before parsing (a commit subject
112
+ spelling a roster header can no longer reset the announced set); the degraded frame joins the
113
+ request-lossy disclosure; old-checkpoint resume compat re-pinned honestly (one conservative
114
+ re-announcement); the PTL-recovery landing path gained real coverage.
115
+ - `probeParkRoundTrip` JSDoc names its write side loudly (diagnostic probe, not a health-poll
116
+ body).
117
+
3
118
  ## 5.35.0 — 2026-08-15
4
119
 
5
120
  No BREAKING changes. One deliberate fail-closed tighten and one loosening-direction fix are called
@@ -492,6 +492,16 @@ export interface SubagentToolOptions {
492
492
  /** β 批 A-2 (BREAKING 1.365.0): REQUIRED — `"default"` is the single-tenant spelling (explicit,
493
493
  * matching the engine chain's `principal ?? "default"`), never implied by omission. */
494
494
  scope: string;
495
+ /**
496
+ * Deployment-level completion-push FALLBACK — not an always-on tap. The settle notification is
497
+ * delivered through `ctx.onTaskNotification ?? background.notify`: inside an engine-driven run
498
+ * the tool-execution context carries the run's own notification injector (the completion lands
499
+ * in the PARENT's live injection queue at its next turn boundary), and this sink is then never
500
+ * called — one completion, one channel, no double-send. It fires only when the tool is executed
501
+ * WITHOUT an engine notification channel (direct `execute()` harnesses, minimal mounts). A
502
+ * deployment that wants an unconditional process-level completion tap should use
503
+ * `RunnerDeps.onBackgroundChildEvent` (the BCE terminal frame) instead.
504
+ */
495
505
  notify?: (n: import("../core/task-notification.js").TaskNotificationPayload, opts?: {
496
506
  priority?: "now" | "next" | "later";
497
507
  }) => void;
@@ -544,6 +544,11 @@ export function createSubagentResume(deps) {
544
544
  : "resume unavailable: the agent's registry row no longer exists (terminal GC) — relaunch a new agent instead.", revived.reason === "still_running" ? "steering.still_running" : revived.reason === "recycling" ? "resume.row_recycling" : "resume.row_gone");
545
545
  }
546
546
  reviveCycle = revived.cycle;
547
+ if (deps.registry !== undefined && deps.taskId !== undefined) {
548
+ const settledSeq = deps.registry.backgroundAgentCycleSeq(deps.taskId);
549
+ if (settledSeq !== undefined && settledSeq > entry.cycleSeq)
550
+ entry.cycleSeq = settledSeq;
551
+ }
547
552
  if (deps.bgSink !== undefined) {
548
553
  reviveEmit = (event) => {
549
554
  try {
@@ -560,6 +565,7 @@ export function createSubagentResume(deps) {
560
565
  ...(deps.rowScope !== undefined ? { scope: deps.rowScope } : {}),
561
566
  description: `${deps.rowDescription ?? "sub-agent"} (resumed ${marker})`,
562
567
  ...(deps.rowAgentType !== undefined ? { agentType: deps.rowAgentType } : {}),
568
+ seq: entry.cycleSeq,
563
569
  ...(deps.rowName !== undefined ? { name: deps.rowName } : {}),
564
570
  sessionId: entry.childSessionId,
565
571
  transcriptId: entry.childSessionId,
@@ -585,6 +591,7 @@ export function createSubagentResume(deps) {
585
591
  transcriptId: entry.childSessionId,
586
592
  sessionId: entry.childSessionId,
587
593
  ...(deps.parentToolCallId !== undefined ? { parentToolCallId: deps.parentToolCallId } : {}),
594
+ seq: entry.cycleSeq,
588
595
  progressTaskId: entry.childSessionId,
589
596
  ...(currentAction !== undefined ? { currentAction } : {}),
590
597
  ...(currentTool !== undefined ? { currentTool } : {}),
@@ -593,6 +600,7 @@ export function createSubagentResume(deps) {
593
600
  }, deps.registry !== undefined && deps.taskId !== undefined ? () => deps.registry.noteBackgroundAgentActivity(deps.taskId) : undefined);
594
601
  stream = childRunner.runTaskStream(resumeSpec, undefined, {
595
602
  ...entry.internalsSnapshot,
603
+ cycleSeq: entry.cycleSeq,
596
604
  ...(true
597
605
  ? {
598
606
  onForwardEvent: (e) => {
@@ -612,6 +620,7 @@ export function createSubagentResume(deps) {
612
620
  sessionId: entry.childSessionId,
613
621
  transcriptId: entry.childSessionId,
614
622
  ...(deps.parentToolCallId !== undefined ? { parentToolCallId: deps.parentToolCallId } : {}),
623
+ seq: entry.cycleSeq,
615
624
  progressTaskId: e.taskId,
616
625
  ...(e.parentTaskId !== undefined ? { progressParentTaskId: e.parentTaskId } : {}),
617
626
  ...(e.name !== undefined ? { name: e.name } : {}),
@@ -1955,6 +1964,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1955
1964
  bg.registry.attachAgentTerminalNotifier(taskId, () => {
1956
1965
  const stoppedByReap = bg.registry.getStopAttribution(taskId) ?? "system";
1957
1966
  const completionIdReap = bg.registry.getCompletionId(taskId);
1967
+ const seqReap = bg.registry.backgroundAgentCycleSeq(taskId);
1958
1968
  const summaryReap = ccCompletionText(shortDesc, "killed", "killed", Date.now() - forkBgHangAt);
1959
1969
  sinkEmit({
1960
1970
  kind: "terminal",
@@ -1964,6 +1974,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1964
1974
  transcriptId: forkedId,
1965
1975
  parentToolCallId: ctx.toolCallId,
1966
1976
  status: "killed",
1977
+ ...(seqReap !== undefined ? { seq: seqReap } : {}),
1967
1978
  stoppedBy: stoppedByReap,
1968
1979
  summary: summaryReap,
1969
1980
  resumable: false,
@@ -1992,6 +2003,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1992
2003
  catch {
1993
2004
  }
1994
2005
  };
2006
+ const bgForkCycleSeq = bg.registry.backgroundAgentCycleSeq(taskId);
1995
2007
  sinkEmit({
1996
2008
  kind: "spawn",
1997
2009
  taskId,
@@ -2000,6 +2012,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2000
2012
  ...(bgScope !== undefined ? { scope: bgScope } : {}),
2001
2013
  description: shortDesc,
2002
2014
  agentType: spawnAgentType,
2015
+ ...(bgForkCycleSeq !== undefined ? { seq: bgForkCycleSeq } : {}),
2003
2016
  ...(agentName !== undefined ? { name: agentName } : {}),
2004
2017
  sessionId: forkedId,
2005
2018
  transcriptId: forkedId,
@@ -2028,6 +2041,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2028
2041
  parentToolCallId: ctx.toolCallId,
2029
2042
  progressTaskId: forkedId,
2030
2043
  agentType: spawnAgentType,
2044
+ ...(bgForkCycleSeq !== undefined ? { seq: bgForkCycleSeq } : {}),
2031
2045
  ...(currentAction !== undefined ? { currentAction } : {}),
2032
2046
  ...(currentTool !== undefined ? { currentTool } : {}),
2033
2047
  usage: { toolUses: toolStarts },
@@ -2039,6 +2053,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2039
2053
  ...(forkQuestionStrip !== undefined ? forkQuestionStrip.internalsFlag : {}),
2040
2054
  onNotifyInjectorReady: s2ForkNotifyReady,
2041
2055
  delegationTaskType: "background_agent",
2056
+ ...(bgForkCycleSeq !== undefined ? { cycleSeq: bgForkCycleSeq } : {}),
2042
2057
  onForwardEvent: (e) => {
2043
2058
  try {
2044
2059
  recordingForward(e, { bgAgentId: taskId });
@@ -2056,6 +2071,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2056
2071
  sessionId: forkedId,
2057
2072
  transcriptId: forkedId,
2058
2073
  parentToolCallId: ctx.toolCallId,
2074
+ ...(bgForkCycleSeq !== undefined ? { seq: bgForkCycleSeq } : {}),
2059
2075
  progressTaskId: e.taskId,
2060
2076
  ...(e.parentTaskId !== undefined ? { progressParentTaskId: e.parentTaskId } : {}),
2061
2077
  agentType: spawnAgentType,
@@ -2072,6 +2088,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2072
2088
  ...(forkQuestionStrip !== undefined ? forkQuestionStrip.internalsFlag : {}),
2073
2089
  onNotifyInjectorReady: s2ForkNotifyReady,
2074
2090
  delegationTaskType: "background_agent",
2091
+ ...(bgForkCycleSeq !== undefined ? { cycleSeq: bgForkCycleSeq } : {}),
2075
2092
  onForwardEvent: (e) => {
2076
2093
  try {
2077
2094
  recordingForward(e, { bgAgentId: taskId });
@@ -2147,6 +2164,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2147
2164
  ...(stillbornFork ? {} : { transcriptId: forkTranscriptId }),
2148
2165
  parentToolCallId: ctx.toolCallId,
2149
2166
  status: settledBg,
2167
+ ...(bgForkCycleSeq !== undefined ? { seq: bgForkCycleSeq } : {}),
2150
2168
  ...(stoppedByBg !== undefined ? { stoppedBy: stoppedByBg } : {}),
2151
2169
  summary: forkTerminalSummary,
2152
2170
  ...residualFork,
@@ -2204,6 +2222,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2204
2222
  transcriptId: forkedId,
2205
2223
  parentToolCallId: ctx.toolCallId,
2206
2224
  status: settledBg,
2225
+ ...(bgForkCycleSeq !== undefined ? { seq: bgForkCycleSeq } : {}),
2207
2226
  ...(stoppedByBg !== undefined ? { stoppedBy: stoppedByBg } : {}),
2208
2227
  summary: summaryBg,
2209
2228
  ...(completionIdForkReject !== undefined ? { completionId: completionIdForkReject } : {}),
@@ -2420,9 +2439,11 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2420
2439
  const bgStartedAt = Date.now();
2421
2440
  stepRecorder.lockTo(bgChildSessionId);
2422
2441
  bg.registry.bindBackgroundAgentSession(taskId, bgChildSessionId);
2442
+ const bgCycleSeq = bg.registry.backgroundAgentCycleSeq(taskId);
2423
2443
  bg.registry.attachAgentTerminalNotifier(taskId, () => {
2424
2444
  const stoppedByReap = bg.registry.getStopAttribution(taskId) ?? "system";
2425
2445
  const completionIdReap = bg.registry.getCompletionId(taskId);
2446
+ const seqReap = bg.registry.backgroundAgentCycleSeq(taskId);
2426
2447
  const summaryReap = ccCompletionText(shortDesc, "killed", "killed", Date.now() - bgStartedAt);
2427
2448
  sinkEmit({
2428
2449
  kind: "terminal",
@@ -2432,6 +2453,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2432
2453
  transcriptId: bgChildSessionId,
2433
2454
  parentToolCallId: ctx.toolCallId,
2434
2455
  status: "killed",
2456
+ ...(seqReap !== undefined ? { seq: seqReap } : {}),
2435
2457
  stoppedBy: stoppedByReap,
2436
2458
  summary: summaryReap,
2437
2459
  resumable: false,
@@ -2459,6 +2481,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2459
2481
  ...(bgScope !== undefined ? { scope: bgScope } : {}),
2460
2482
  description: reviveRow !== undefined ? `${shortDesc} (revived)` : shortDesc,
2461
2483
  agentType: spawnAgentType,
2484
+ ...(bgCycleSeq !== undefined ? { seq: bgCycleSeq } : {}),
2462
2485
  ...(agentName !== undefined ? { name: agentName } : {}),
2463
2486
  sessionId: bgChildSessionId,
2464
2487
  transcriptId: bgChildSessionId,
@@ -2531,6 +2554,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2531
2554
  parentToolCallId: ctx.toolCallId,
2532
2555
  progressTaskId: bgChildSessionId,
2533
2556
  agentType: spawnAgentType,
2557
+ ...(bgCycleSeq !== undefined ? { seq: bgCycleSeq } : {}),
2534
2558
  ...(currentAction !== undefined ? { currentAction } : {}),
2535
2559
  ...(currentTool !== undefined ? { currentTool } : {}),
2536
2560
  usage: { toolUses: toolStarts },
@@ -2542,6 +2566,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2542
2566
  ...(bgQuestionStrip !== undefined ? bgQuestionStrip.internalsFlag : {}),
2543
2567
  onNotifyInjectorReady: s2NotifyReady,
2544
2568
  delegationTaskType: "background_agent",
2569
+ ...(bgCycleSeq !== undefined ? { cycleSeq: bgCycleSeq } : {}),
2545
2570
  onForwardEvent: (e) => {
2546
2571
  try {
2547
2572
  recordingForward(e, { bgAgentId: taskId });
@@ -2559,6 +2584,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2559
2584
  sessionId: bgChildSessionId,
2560
2585
  transcriptId: bgChildSessionId,
2561
2586
  parentToolCallId: ctx.toolCallId,
2587
+ ...(bgCycleSeq !== undefined ? { seq: bgCycleSeq } : {}),
2562
2588
  progressTaskId: e.taskId,
2563
2589
  ...(e.parentTaskId !== undefined ? { progressParentTaskId: e.parentTaskId } : {}),
2564
2590
  agentType: spawnAgentType,
@@ -2575,6 +2601,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2575
2601
  ...(bgQuestionStrip !== undefined ? bgQuestionStrip.internalsFlag : {}),
2576
2602
  onNotifyInjectorReady: s2NotifyReady,
2577
2603
  delegationTaskType: "background_agent",
2604
+ ...(bgCycleSeq !== undefined ? { cycleSeq: bgCycleSeq } : {}),
2578
2605
  onForwardEvent: (e) => {
2579
2606
  try {
2580
2607
  recordingForward(e, { bgAgentId: taskId });
@@ -2858,7 +2885,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2858
2885
  ...(child.sessionId ? { sessionId: child.sessionId, transcriptId: child.sessionId } : {}),
2859
2886
  parentToolCallId: ctx.toolCallId,
2860
2887
  status: settled,
2861
- ...(seqAtSettle !== undefined ? { seq: seqAtSettle } : {}),
2888
+ ...(seqAtSettle !== undefined ? { seq: seqAtSettle } : bgCycleSeq !== undefined ? { seq: bgCycleSeq } : {}),
2862
2889
  ...(stoppedBy !== undefined ? { stoppedBy } : {}),
2863
2890
  summary: bgTerminalSummary,
2864
2891
  ...residual,
@@ -2948,7 +2975,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2948
2975
  transcriptId: bgChildSessionId,
2949
2976
  parentToolCallId: ctx.toolCallId,
2950
2977
  status: settled,
2951
- ...(seqAtSettle !== undefined ? { seq: seqAtSettle } : {}),
2978
+ ...(seqAtSettle !== undefined ? { seq: seqAtSettle } : bgCycleSeq !== undefined ? { seq: bgCycleSeq } : {}),
2952
2979
  ...(stoppedBy !== undefined ? { stoppedBy } : {}),
2953
2980
  summary: `${settled === "failed" ? `Agent "${shortDesc}" failed: ${msg}${ccElapsedTag(Date.now() - bgStartedAt)}` : ccCompletionText(shortDesc, settled, settled, Date.now() - bgStartedAt)}${observerNote}`.slice(0, 300) + (settled === "failed" ? errorKindClause(errClassBgReject) : ""),
2954
2981
  ...(completionIdBgReject !== undefined ? { completionId: completionIdBgReject } : {}),
@@ -323,6 +323,29 @@ export interface MaybeCompactOptions {
323
323
  path: string;
324
324
  content: string;
325
325
  }>, preserveReadState?: ReadonlyArray<string>) => void;
326
+ /**
327
+ * env-tail migration — the git-status lane's compaction interaction (two-phase receipt). The git
328
+ * frame lives on the MESSAGE plane, does not enter the summary, and the projection strips it from
329
+ * serialized history — so a landed compaction would silently lose the model's git view while the
330
+ * announced hash keeps suppressing every re-send. When present:
331
+ * - `pending` is restated on the compaction entry's `details.gitAnnouncement` with
332
+ * `pending: true` in the SAME CAS as the new baseline (the promptEpoch/announcedListings
333
+ * restatement posture) — any reader between the landing and the re-assert receipt sees an
334
+ * OWED announcement, never a false "announced";
335
+ * - `land()` is invoked immediately after the landed baseline, BEFORE any context rebuild or
336
+ * retry (all three routes — turn-boundary auto, prompt-too-long recovery, end-of-task finish
337
+ * — funnel through this one append): it re-appends the current frame and writes the
338
+ * announced mirror once the append receipt is in hand. `land` contains its own failures (a
339
+ * failed re-assert leaves the pending restatement authoritative — the next boundary/leg
340
+ * re-announces); a throw here must never fail a LANDED compaction, so the call is belted.
341
+ */
342
+ gitRestate?: {
343
+ pending: {
344
+ kind: "full" | "degraded" | "unavailable" | "non-repo";
345
+ hash: string;
346
+ };
347
+ land: () => Promise<void>;
348
+ };
326
349
  /**
327
350
  * Fixed per-request prompt overhead (system prompt + tool schemas, in tokens), applied ONLY in the
328
351
  * anchor-less regime (design/64 §26.7, search [91] fourth candidate): when no assistant in the
@@ -446,7 +446,15 @@ export async function maybeCompact(opts) {
446
446
  promptEpoch: restatedEpoch,
447
447
  ...(restatedListings !== undefined ? { announcedListings: restatedListings } : {}),
448
448
  ...(opts.activeTools !== undefined && opts.activeTools.length > 0 ? { activeTools: [...opts.activeTools] } : {}),
449
+ ...(opts.gitRestate !== undefined ? { gitAnnouncement: { kind: opts.gitRestate.pending.kind, hash: opts.gitRestate.pending.hash, pending: true } } : {}),
449
450
  }, false);
451
+ if (opts.gitRestate !== undefined) {
452
+ try {
453
+ await opts.gitRestate.land();
454
+ }
455
+ catch {
456
+ }
457
+ }
450
458
  try {
451
459
  opts.onApplied?.(attachedComplete, excludedReadStatePreserveKeys);
452
460
  }
@@ -1008,6 +1008,22 @@ export interface CheckpointState {
1008
1008
  skills?: string[];
1009
1009
  models?: string[];
1010
1010
  };
1011
+ /**
1012
+ * The git-status frame's announced `(kind, hash)` state at suspend (env-tail migration: the
1013
+ * run-loop-maintained mirror, advanced at every frame receipt). The LOWEST rung of the resume
1014
+ * read ladder — the session branch walk is the authority (a nearest PENDING mirror there, or an
1015
+ * announced mirror whose frame entry is off the active branch, forces a re-announce regardless of
1016
+ * this field); the checkpoint seeds only when the branch carries no mirror at all, and its
1017
+ * `entryId` must itself sit on the active branch to count as announced. Schema ADDITION
1018
+ * (additive, old readers ignore it — the Q5 posture): an older checkpoint without the field re-announces
1019
+ * conservatively — the frame is a tail append, so a duplicate costs bytes, never a prefix break.
1020
+ */
1021
+ gitAnnouncement?: {
1022
+ kind: "full" | "degraded" | "unavailable" | "non-repo";
1023
+ hash: string;
1024
+ entryId?: string;
1025
+ pending?: true;
1026
+ };
1011
1027
  /**
1012
1028
  * design/180 R-4 (R-2 cross-process variant) — the armed delegation child's MONOTONIC runtime-provenance
1013
1029
  * aggregate at suspend ({@link import("./memory-engine/delegation-provenance.js").DelegationProvenanceAggregate}:
@@ -53,6 +53,47 @@ export declare function dropOrphanToolResults(messages: AgentMessage[]): {
53
53
  nextRole: string;
54
54
  }>;
55
55
  };
56
+ /** R2-3 (falsification round 2) — does the message's ENGINE-AUTHORED region cover [start, start+len)?
57
+ * The carrier scan must never crown a counterfeit: a tool result or a user paste can reproduce the
58
+ * wrapped frame bytes verbatim, but only ENGINE metadata (engineMinted whole-message, the
59
+ * enginePrefixChars head, or an engineSegments range) proves the engine put them there. */
60
+ export declare function engineRegionCovers(m: AgentMessage, start: number, len: number): boolean;
61
+ /** Outcome of {@link protectGitFrame} — `over_budget` means the irreducible core (summaries + the
62
+ * protected frame carrier) exceeds the budget even after the one-shot degraded shrink: the caller
63
+ * must fail the request LOUDLY (`irreducible_core_over_budget`) instead of pretending the trim
64
+ * succeeded or looping compaction/re-assertion. */
65
+ export type GitFrameGuardOutcome = {
66
+ messages: AgentMessage[];
67
+ action: "kept" | "absent" | "reinserted" | "shrunk";
68
+ } | {
69
+ action: "over_budget";
70
+ };
71
+ /**
72
+ * env-tail migration — the git frame's UNCLIPPABLE-CORE protection over a trimmed request view.
73
+ * The frame's "value unchanged ⇒ never re-sent" contract makes a request-only drop uniquely
74
+ * dangerous for it: the commit receipt has already landed, so a trim that cuts the carrier makes
75
+ * the announced hash suppress every future re-send while the model never saw the frame. Rules:
76
+ * - REPLACE-BY-KEY SLOT: exactly ONE carrier is protected — the newest message whose flat text CONTAINS
77
+ * `protectedText` (the ENGINE-HELD wrapped frame segment; set at delivery/re-assert AND
78
+ * re-seeded on value-unchanged legs, so the protection survives legs that send nothing).
79
+ * Older frames trim like ordinary history.
80
+ * - Re-insert a dropped carrier after the leading summaries/trim-notice (its original relative
81
+ * position is always ≥ there — the carrier predates every kept tail message).
82
+ * - IRREDUCIBLE-CORE arc: when the core (always-kept summaries + the carrier) alone exceeds the
83
+ * budget, substitute the carrier's frame segment with the deterministic DEGRADED rendering —
84
+ * exactly once per run (`alreadyShrunk` latch, request-view only; the run loop re-announces the
85
+ * shrunk view through its own receipt machinery). Still over after the shrink ⇒ `over_budget`.
86
+ * - A total overshoot whose core FITS is left alone (turn-alignment overshoot is a pre-existing,
87
+ * tolerated trade — validity beats budget; this guard must not convert it into a failure).
88
+ */
89
+ export declare function protectGitFrame(before: AgentMessage[], trimmed: AgentMessage[], budgetTokens: number, frame: {
90
+ protectedText: string;
91
+ substitute?: {
92
+ find: string;
93
+ replace: string;
94
+ };
95
+ }, // protectedText = the wrapped frame SEGMENT (see the carrier scan below)
96
+ charsPerToken?: number): GitFrameGuardOutcome;
56
97
  /** Budget the in-task guard targets for a model — CC-aligned headroom numbers with a 0.85
57
98
  * small-window floor, sema-owned drop-oldest action (see {@link GUARD_HEADROOM_TOKENS}); always
58
99
  * above the compaction/clearStale frontier. Dual-window semantics: deliberately the PHYSICAL
@@ -114,6 +114,82 @@ export function dropOrphanToolResults(messages) {
114
114
  }
115
115
  return { messages: out, dropped };
116
116
  }
117
+ export function engineRegionCovers(m, start, len) {
118
+ const meta = m;
119
+ if (meta.engineMinted === true)
120
+ return true;
121
+ if (Array.isArray(meta.engineSegments))
122
+ return meta.engineSegments.some((seg) => seg.start <= start && start + len <= seg.end);
123
+ if (typeof meta.enginePrefixChars === "number")
124
+ return start + len <= meta.enginePrefixChars;
125
+ return false;
126
+ }
127
+ function singleText(m) {
128
+ const c = m.content;
129
+ if (typeof c === "string")
130
+ return c;
131
+ if (Array.isArray(c) && c.length === 1) {
132
+ const b = c[0];
133
+ if (b?.type === "text" && typeof b.text === "string")
134
+ return b.text;
135
+ }
136
+ return undefined;
137
+ }
138
+ export function protectGitFrame(before, trimmed, budgetTokens, frame, charsPerToken) {
139
+ const cpt = charsPerToken ?? DEFAULT_CHARS_PER_TOKEN;
140
+ let carrier;
141
+ for (let i = before.length - 1; i >= 0; i--) {
142
+ const candidate = before[i];
143
+ if (candidate === undefined || messageRole(candidate) !== "user")
144
+ continue;
145
+ const text = singleText(candidate);
146
+ if (text === undefined)
147
+ continue;
148
+ const at = text.indexOf(frame.protectedText);
149
+ if (at === -1 || !engineRegionCovers(candidate, at, frame.protectedText.length))
150
+ continue;
151
+ carrier = candidate;
152
+ break;
153
+ }
154
+ if (carrier === undefined)
155
+ return { messages: trimmed, action: "absent" };
156
+ let out = trimmed;
157
+ let action = "kept";
158
+ if (!out.includes(carrier)) {
159
+ carrier = { role: "user", content: [{ type: "text", text: frame.protectedText }], timestamp: carrier.timestamp ?? Date.now(), engineMinted: true };
160
+ let at = 0;
161
+ for (; at < out.length; at++) {
162
+ const head = out[at];
163
+ if (head === undefined || (!isSummary(head) && singleText(head) !== CONTEXT_TRIM_NOTICE))
164
+ break;
165
+ }
166
+ out = [...out.slice(0, at), carrier, ...out.slice(at)];
167
+ action = "reinserted";
168
+ }
169
+ const segTokens = (text) => estimateTokens({ role: "user", content: [{ type: "text", text }] }, cpt);
170
+ const coreTokens = (view, frameText) => {
171
+ let t = segTokens(frameText);
172
+ for (const m of view)
173
+ if (isSummary(m))
174
+ t += estimateTokens(m, cpt);
175
+ return t;
176
+ };
177
+ if (coreTokens(out, frame.protectedText) <= budgetTokens)
178
+ return { messages: out, action };
179
+ if (frame.substitute !== undefined) {
180
+ const text = singleText(carrier);
181
+ const shrunkText = text.replace(frame.substitute.find, () => frame.substitute.replace);
182
+ if (shrunkText !== text) {
183
+ const shrunkCarrier = { ...carrier, content: [{ type: "text", text: shrunkText }] };
184
+ const idx = out.indexOf(carrier);
185
+ out = [...out.slice(0, idx), shrunkCarrier, ...out.slice(idx + 1)];
186
+ if (coreTokens(out, frame.substitute.replace) <= budgetTokens)
187
+ return { messages: out, action: "shrunk" };
188
+ return { action: "over_budget" };
189
+ }
190
+ }
191
+ return { action: "over_budget" };
192
+ }
117
193
  export function guardBudget(model) {
118
194
  const window = model.contextTokens ?? model.contextWindow;
119
195
  if (!Number.isFinite(window) || window <= 0) {
@@ -20,6 +20,10 @@ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
20
20
  "memory.challenge_ledger_oversize",
21
21
  "memory.control_plane_rebuilt",
22
22
  "memory.control_plane_not_corrupt",
23
+ "memory.erasure_evidence_unavailable",
24
+ "memory.erasure_selector_mismatch",
25
+ "memory.erasure_census_incomplete",
26
+ "memory.erasure_index_residue",
23
27
  ]);
24
28
  export function governanceRetryClass(code) {
25
29
  if (Object.prototype.hasOwnProperty.call(GOVERNANCE_CODES, code)) {