@sema-agent/core 5.34.0 → 5.36.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 (47) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/dist/agents/subagent.js +29 -2
  3. package/dist/core/auto-compaction.d.ts +23 -0
  4. package/dist/core/auto-compaction.js +8 -0
  5. package/dist/core/checkpoint-store.d.ts +49 -4
  6. package/dist/core/context-guard.d.ts +41 -0
  7. package/dist/core/context-guard.js +76 -0
  8. package/dist/core/hooks.d.ts +98 -3
  9. package/dist/core/hooks.js +146 -8
  10. package/dist/core/memory-engine/engine.js +1 -1
  11. package/dist/core/park-selfcheck.d.ts +161 -0
  12. package/dist/core/park-selfcheck.js +251 -0
  13. package/dist/core/runner/assemble-result.d.ts +3 -0
  14. package/dist/core/runner/assemble-result.js +3 -0
  15. package/dist/core/runner/git-status-frame.d.ts +219 -0
  16. package/dist/core/runner/git-status-frame.js +212 -0
  17. package/dist/core/runner/prepare-acquire-reconcile.d.ts +6 -0
  18. package/dist/core/runner/prepare-acquire-reconcile.js +2 -1
  19. package/dist/core/runner/prepare-task.d.ts +28 -4
  20. package/dist/core/runner/prepare-task.js +86 -52
  21. package/dist/core/runner/runtask.d.ts +6 -1
  22. package/dist/core/runner/runtask.js +330 -19
  23. package/dist/core/task-registry-agent.d.ts +15 -0
  24. package/dist/core/task-registry-agent.js +9 -0
  25. package/dist/core/task-registry.d.ts +3 -0
  26. package/dist/core/task-registry.js +4 -1
  27. package/dist/core/tool-errors.d.ts +2 -2
  28. package/dist/core/tool-policy.d.ts +125 -0
  29. package/dist/core/tool-policy.js +35 -2
  30. package/dist/core/types.d.ts +98 -9
  31. package/dist/engine/harness/types.d.ts +65 -1
  32. package/dist/engine/harness/types.js +20 -0
  33. package/dist/engine/session/import-validate.js +10 -1
  34. package/dist/engine/session/session.d.ts +37 -1
  35. package/dist/engine/session/session.js +56 -1
  36. package/dist/index.d.ts +3 -2
  37. package/dist/index.js +3 -2
  38. package/dist/internal/harness-types.d.ts +1 -0
  39. package/dist/internal/harness.d.ts +2 -0
  40. package/dist/internal/harness.js +2 -0
  41. package/dist/orchestration/workflow.d.ts +1 -1
  42. package/dist/prompt-assembly/epoch.js +1 -1
  43. package/dist/prompt-assembly/event-registry.js +1 -0
  44. package/dist/prompts/default.d.ts +20 -7
  45. package/dist/prompts/default.js +2 -7
  46. package/package.json +1 -1
  47. package/test/export-surface.snapshot.json +17 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,109 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.36.0 — 2026-08-15
4
+
5
+ No BREAKING changes. Upgrade note: the system-prompt shape change resets provider prompt caches
6
+ ONCE on upgrade — the point of the feature is that they stop resetting after that.
7
+
8
+ ### Added
9
+
10
+ - Env-tail prefix stability (#254): the turn-dynamic git facts (branch line, working-tree line,
11
+ status snapshot) leave the system prompt for a value-keyed `git_status` turn frame. The system
12
+ prompt stays byte-stable across legs while the working tree moves, so provider prefix caches
13
+ survive leg/resume/fork boundaries (live-verified cache hits past the static-section watermark).
14
+ The frame re-sends only when its `(kind, hash)` changes; kind flips announce a tombstone once
15
+ ("may be stale" / "no longer a git repository"); recovery re-asserts loudly. Compaction re-asserts
16
+ the current frame beside the summary on all three landing paths (boundary / PTL recovery /
17
+ end-of-task, one single-source hook). Announcement mirrors are branch-authoritative with a
18
+ transcript-side newest-frame arbiter: the transcript is the truth, a mirror that cannot be audited
19
+ degrades to one conservative re-announcement.
20
+ - Wire/durable additions for #254: `steering_injected.source` gains `git_status` (preview is a
21
+ CONSTANT phrase per frame kind — repo text never enters the event face); durable session entry
22
+ `git_announcement`, compaction `details.gitAnnouncement`, `CheckpointState.gitAnnouncement` (all
23
+ additive, no version bump — absent fields resume with one conservative re-announcement); `Session`
24
+ gains two OPTIONAL members (`appendGitAnnouncement`/`getGitAnnouncement`); event-registry row
25
+ `git_status`. Existing sessions record one `legacy_migration` pin on first resume (env probe
26
+ sentinel `probe-env@2`).
27
+ - New `TaskResult.errorCode`: `irreducible_core_over_budget` — the request budget cannot fit even
28
+ the compaction summaries plus the DEGRADED git frame; the run fails loudly instead of shipping a
29
+ dishonest request. The frame has a trim-protected replace-by-key slot with exactly one
30
+ deterministic degrade shrink; H4 probe failures degrade to a two-line branch+dirty frame instead
31
+ of silently omitting the section; the probe sentinel mis-split arm is loud.
32
+ - Fleet row generation out-line (#258): `task_progress` ticks and `BackgroundChildEvent` spawn/tick
33
+ frames from background-agent rows carry `seq` — the row's stop-cycle generation (fresh spawn = 1,
34
+ every launched revival bumps it), one axis with `TaskNotificationPayload.seq` — so a fleet
35
+ consumer can tell a LATE first frame from a REVIVED cycle's frame. BCE terminal frames fall back
36
+ to the registry generation when the notify-mirror carrier is absent (one run speaks one number;
37
+ the notification payload's design/144 seq contract is unchanged). Absence stays a fact: sync
38
+ children, workflow agents and top-level runs carry nothing — never read absence as "cycle 1".
39
+ Hardening shipped with it: a parked-born resume speaks ONE generation end to end (the consume
40
+ flip's target), a retained wake realigns forward past a foreign tier-3 cycle's durable advance,
41
+ and BCE ticks always stamp the ENCLOSING row's generation.
42
+
43
+ ### Changed
44
+
45
+ - `buildEnvironmentContext` no longer renders gitBranch/gitDirty/gitSnapshot (fields remain as
46
+ data; the snapshot rides the `git_status` frame). Pre-call budget estimates now include the frame
47
+ bytes (an informed tightening). Frame bodies clamp astral-safe (U+FFFD at a split code point).
48
+ - Merged-code rescan hardening (18 findings across two rounds, all verified then disposed): the
49
+ degraded-shrink replacement inserts via thunk (a branch named with `$&`/`$$` — legal ref bytes —
50
+ no longer expands match context into the frame or silently renames itself; same class swept to
51
+ `buildMemoryInstruction`); the irreducible core charges the frame segment + summaries only, and a
52
+ trim-dropped carrier is never resurrected — the guard re-inserts the frame SEGMENT as its own
53
+ engine-minted request-view message, so the trim's adjudication stands and the request stays
54
+ servable; the listing-replay scan strips positive git frames before parsing (a commit subject
55
+ spelling a roster header can no longer reset the announced set); the degraded frame joins the
56
+ request-lossy disclosure; old-checkpoint resume compat re-pinned honestly (one conservative
57
+ re-announcement); the PTL-recovery landing path gained real coverage.
58
+ - `probeParkRoundTrip` JSDoc names its write side loudly (diagnostic probe, not a health-poll
59
+ body).
60
+
61
+ ## 5.35.0 — 2026-08-15
62
+
63
+ No BREAKING changes. One deliberate fail-closed tighten and one loosening-direction fix are called
64
+ out under Changed; everything else is additive.
65
+
66
+ ### Added
67
+
68
+ - `TaskStream.steer` takes an optional `inputId` dedup key (additive; absent ⇒ byte-identical
69
+ behavior). The live steer leg becomes idempotent the way the parked leg already was: an identical
70
+ payload under a seen key is a no-op (nothing injected, no second ledger frame), a DIFFERENT payload
71
+ under a seen key is refused loudly (`steering.duplicate_input_id`, the parked leg's code), and the
72
+ caller's key rides the `human_input` frame verbatim (previously a fresh uuid was always minted, so
73
+ a replayed injection was indistinguishable from an idempotent hit in the receipt). Validation runs
74
+ before liveness (empty/overlong/reserved-sentinel values are refused, never silently treated as
75
+ "no key"). Dedup scope is the live stream's lifetime — narrower than the parked queue's persisted
76
+ window; the contract states the asymmetry rather than pretending parity.
77
+ - `AskRequest.ruleEvidence` (design/252 G-2): an ask carries the rule-provenance evidence behind it —
78
+ the org snapshot `revision`, the org rule text, and the matched personal rule's add dots — each
79
+ member present as a value or as a NAMED absence (`not_wired` / `not_adjudicated` / `unavailable` /
80
+ `no_match` / `not_reported`), so "nothing governed this", "the source was unreadable" and "the lane
81
+ was skipped" stop sharing one wire shape.
82
+ - Settlement approver attribution (design/252 G-7): both settlement lanes accept an optional
83
+ `approver` identity, screened (length/control-characters/lone-surrogates, refused not truncated)
84
+ and TRANSCRIBED — never adjudicated — onto the settled call's `tool_end` frame beside `settledBy`.
85
+ - `probeParkRoundTrip` + `durableParkGapOf` (design/252 G-6): a park-lane liveness self-check (file a
86
+ synthetic checkpoint, read it back, enumerate, fence) plus the static two-halves reading that turns
87
+ a missing-session resume symptom into a named absent seat. `ParkSelfCheckResult.casLossObserved`
88
+ (additive) marks the one green arm a CAS-mis-reporting backend would pass through.
89
+
90
+ ### Changed
91
+
92
+ - Fail-closed tighten: a `persistedRules.admits()` answer that is neither a string nor an object no
93
+ longer clears an ask. Previously any non-`undefined` answer cleared it — "`null` admits this call"
94
+ was an allow built on an unreadable answer.
95
+ - The org-verdict normalizer reads ANY non-array carrier (class instances and callables included —
96
+ the structural contract admits them) with own-data field reads; refusing the carrier outright
97
+ could fold a structurally valid org DENY into `unavailable`, i.e. from un-approvable into a
98
+ real-approval ask a person could clear. Inherited members still never authorize; revoked proxies
99
+ are contained; foreign array methods (an overridden or species-substituted `.map`/`.filter`) no
100
+ longer run anywhere on the evidence path.
101
+ - The park probe sweeps behind a late-committing timed-out `put` on either settle, and the
102
+ approver screen refuses oversized values before materializing a code-point array.
103
+ - Dev-surface: the live-gate recipe and examples switch the reference live model to
104
+ `deepseek-v4-flash` (operator directive; prior cache/behavior figures quoted against
105
+ `deepseek-v4-pro` re-anchor on next measurement).
106
+
3
107
  ## 5.34.0 — 2026-08-15
4
108
 
5
109
  No BREAKING changes. Two behavior-surface additions (both additive seats), one packaging addition,
@@ -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
  }
@@ -577,6 +577,31 @@ export type ResumeOutcome = {
577
577
  * with the backends and belongs to its own change, not to this one.
578
578
  */
579
579
  settledBy?: import("./tool-policy.js").ApprovalSettledBy;
580
+ /**
581
+ * design/252 G-7 — WHOSE settlement: the identifier the approval channel reports for the party
582
+ * that ended this wait, transcribed onto the resolved call's `tool_end` frame beside
583
+ * {@link settledBy}.
584
+ *
585
+ * Core AUTHENTICATES NOTHING here. On the durable lane the identity work is entirely the
586
+ * deployment's — its approval card, its signature check, its operator console decide who this
587
+ * was; a library holding no identity surface has nothing to check the claim against and does not
588
+ * pretend otherwise. What core owns is that the claim is well-formed: a plain string, bounded,
589
+ * free of control characters, refused pre-CAS (`checkpoint.invalid_outcome`) otherwise, like
590
+ * every other out-of-domain field on this arm. Refused rather than dropped, because a settlement
591
+ * that silently lost its attributed party reads as unattributed — the misreport this seat exists
592
+ * to end.
593
+ *
594
+ * SCOPE — like {@link settledBy}, this rides the SETTLEMENT and not the persisted winner:
595
+ * `winnerFromOutcome` records what an `env_failed` replay must reproduce exactly, and who
596
+ * reported a settlement is not part of that authorization. A replay states its own attribution
597
+ * and is not held to the first one; binding it into the durable row would also make a re-supply
598
+ * that merely omits the field fail closed on the recovery path. Carrying it into the row is a
599
+ * store-shape change owned jointly with the backends and belongs to its own change.
600
+ *
601
+ * OPTIONAL and additive: absent means the caller named nobody, which is not the same as "nobody
602
+ * approved this" and must not be read as one.
603
+ */
604
+ approver?: string;
580
605
  }
581
606
  /** design/74: continue a resource-suspended run with the next slice's allowance. NO decision payload and
582
607
  * NO budget figure — the allowance is computed from {@link Checkpoint.resourceLedger}, so money never
@@ -983,6 +1008,22 @@ export interface CheckpointState {
983
1008
  skills?: string[];
984
1009
  models?: string[];
985
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
+ };
986
1027
  /**
987
1028
  * design/180 R-4 (R-2 cross-process variant) — the armed delegation child's MONOTONIC runtime-provenance
988
1029
  * aggregate at suspend ({@link import("./memory-engine/delegation-provenance.js").DelegationProvenanceAggregate}:
@@ -1631,12 +1672,14 @@ export declare class CheckpointError extends Error {
1631
1672
  * action: re-review), or `"answer"` (the content-ask answer is missing, or was attached to a
1632
1673
  * decision that cannot carry one: re-decide with a well-formed answer — ruled 2026-08-04), or
1633
1674
  * `"settledBy"` (the settlement source is outside the three-word vocabulary: re-decide naming one
1634
- * of them, or omit the field). The throw
1675
+ * of them, or omit the field), or — design/252 G-7 — `"approver"` (the attribution is not a
1676
+ * well-formed identifier: re-decide with a plain, bounded, control-character-free string, or omit
1677
+ * it). The throw
1635
1678
  * sites always knew which arm fired; this carries that fact instead of dropping it (ruled
1636
1679
  * 2026-08-03, cross-repo HITL discussion). Additive and optional — `code` remains the only REQUIRED
1637
1680
  * discriminant. */
1638
1681
  readonly detail?: {
1639
- field?: "boundCallId" | "boundInputHash" | "answer" | "settledBy";
1682
+ field?: "boundCallId" | "boundInputHash" | "answer" | "settledBy" | "approver";
1640
1683
  /** WHICH pre-CAS refusal arm fired, where one `code` covers several (requested 2026-08-10: a
1641
1684
  * deployment retry policy needs to tell "a newer worker can redeem this row" from "this row is
1642
1685
  * damaged/caller-declared and no worker ever will" — blanket-retriable and gate-shape heuristics were
@@ -1752,12 +1795,14 @@ export declare class CheckpointError extends Error {
1752
1795
  * action: re-review), or `"answer"` (the content-ask answer is missing, or was attached to a
1753
1796
  * decision that cannot carry one: re-decide with a well-formed answer — ruled 2026-08-04), or
1754
1797
  * `"settledBy"` (the settlement source is outside the three-word vocabulary: re-decide naming one
1755
- * of them, or omit the field). The throw
1798
+ * of them, or omit the field), or — design/252 G-7 — `"approver"` (the attribution is not a
1799
+ * well-formed identifier: re-decide with a plain, bounded, control-character-free string, or omit
1800
+ * it). The throw
1756
1801
  * sites always knew which arm fired; this carries that fact instead of dropping it (ruled
1757
1802
  * 2026-08-03, cross-repo HITL discussion). Additive and optional — `code` remains the only REQUIRED
1758
1803
  * discriminant. */
1759
1804
  detail?: {
1760
- field?: "boundCallId" | "boundInputHash" | "answer" | "settledBy";
1805
+ field?: "boundCallId" | "boundInputHash" | "answer" | "settledBy" | "approver";
1761
1806
  /** WHICH pre-CAS refusal arm fired, where one `code` covers several (requested 2026-08-10: a
1762
1807
  * deployment retry policy needs to tell "a newer worker can redeem this row" from "this row is
1763
1808
  * damaged/caller-declared and no worker ever will" — blanket-retriable and gate-shape heuristics were
@@ -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) {