@sema-agent/core 7.28.1 → 7.30.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 (162) hide show
  1. package/CHANGELOG.md +473 -1
  2. package/dist/agents/child-model-seat.d.ts +59 -7
  3. package/dist/agents/child-model-seat.js +33 -7
  4. package/dist/agents/cross-session-judge.d.ts +27 -5
  5. package/dist/agents/cross-session-judge.js +23 -12
  6. package/dist/agents/list-agents-tool.d.ts +31 -6
  7. package/dist/agents/list-agents-tool.js +66 -14
  8. package/dist/agents/peer-admission.d.ts +20 -8
  9. package/dist/agents/peer-admission.js +21 -12
  10. package/dist/agents/peer-directory.d.ts +29 -0
  11. package/dist/agents/peer-directory.js +20 -0
  12. package/dist/agents/peer-held-queue.d.ts +31 -4
  13. package/dist/agents/peer-held-queue.js +32 -1
  14. package/dist/agents/peer-session-drain.d.ts +91 -13
  15. package/dist/agents/peer-session-drain.js +409 -102
  16. package/dist/agents/read-inbox-tool.d.ts +22 -0
  17. package/dist/agents/read-inbox-tool.js +39 -0
  18. package/dist/agents/roster-store.d.ts +2 -4
  19. package/dist/agents/roster-store.js +1 -1
  20. package/dist/agents/send-message-tool.d.ts +4 -4
  21. package/dist/agents/send-message-tool.js +22 -2
  22. package/dist/agents/subagent.js +62 -44
  23. package/dist/agents/team-role.d.ts +34 -0
  24. package/dist/agents/team-role.js +17 -0
  25. package/dist/agents/verify.js +2 -2
  26. package/dist/brain/anthropic.js +4 -2
  27. package/dist/brain/open-responses.js +4 -3
  28. package/dist/brain/openai.js +2 -1
  29. package/dist/brain/stream-engine.js +6 -0
  30. package/dist/brain/stream-shared.d.ts +8 -0
  31. package/dist/brain/stream-shared.js +8 -0
  32. package/dist/core/auto-compaction.d.ts +42 -0
  33. package/dist/core/auto-compaction.js +35 -2
  34. package/dist/core/child-clamp-seats.d.ts +11 -0
  35. package/dist/core/child-clamp-seats.js +3 -0
  36. package/dist/core/context-edit.d.ts +3 -2
  37. package/dist/core/context-edit.js +6 -5
  38. package/dist/core/context-guard.d.ts +2 -1
  39. package/dist/core/context-guard.js +3 -2
  40. package/dist/core/context-settings.d.ts +103 -0
  41. package/dist/core/context-settings.js +60 -0
  42. package/dist/core/engine-notice.d.ts +18 -1
  43. package/dist/core/frame-settlement.d.ts +97 -0
  44. package/dist/core/frame-settlement.js +36 -0
  45. package/dist/core/governance-codes.d.ts +1 -1
  46. package/dist/core/governance-codes.js +4 -0
  47. package/dist/core/hooks.d.ts +10 -10
  48. package/dist/core/mailbox-store.d.ts +41 -7
  49. package/dist/core/mailbox-store.js +9 -3
  50. package/dist/core/message-utils.d.ts +13 -0
  51. package/dist/core/message-utils.js +6 -0
  52. package/dist/core/parent-spec-seat.d.ts +14 -1
  53. package/dist/core/parent-spec-seat.js +6 -0
  54. package/dist/core/permission-rule-model.js +1 -1
  55. package/dist/core/roles.d.ts +20 -0
  56. package/dist/core/roles.js +35 -16
  57. package/dist/core/runner/assemble-result.d.ts +2 -0
  58. package/dist/core/runner/assemble-result.js +3 -1
  59. package/dist/core/runner/compaction-call-options.d.ts +26 -2
  60. package/dist/core/runner/compaction-call-options.js +9 -0
  61. package/dist/core/runner/compaction-seams.d.ts +4 -3
  62. package/dist/core/runner/compaction-seams.js +13 -6
  63. package/dist/core/runner/contracts.d.ts +61 -7
  64. package/dist/core/runner/decide-continuation.d.ts +2 -1
  65. package/dist/core/runner/decide-continuation.js +11 -10
  66. package/dist/core/runner/initial-run-state.js +2 -2
  67. package/dist/core/runner/prepare-caps-and-workflow.js +9 -1
  68. package/dist/core/runner/prepare-config-doors.js +4 -1
  69. package/dist/core/runner/prepare-context-lane.d.ts +9 -2
  70. package/dist/core/runner/prepare-context-lane.js +15 -10
  71. package/dist/core/runner/prepare-delegation-surface.d.ts +3 -2
  72. package/dist/core/runner/prepare-delegation-surface.js +9 -3
  73. package/dist/core/runner/prepare-file-history.js +10 -8
  74. package/dist/core/runner/prepare-gate-stations.d.ts +4 -0
  75. package/dist/core/runner/prepare-gate-stations.js +3 -1
  76. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  77. package/dist/core/runner/prepare-prompt-inputs.d.ts +6 -0
  78. package/dist/core/runner/prepare-prompt-inputs.js +2 -1
  79. package/dist/core/runner/prepare-task.js +22 -9
  80. package/dist/core/runner/prepare-wiring-manifest.d.ts +6 -1
  81. package/dist/core/runner/prepare-wiring-manifest.js +6 -1
  82. package/dist/core/runner/response-model-seat.d.ts +16 -0
  83. package/dist/core/runner/response-model-seat.js +27 -0
  84. package/dist/core/runner/retired-runner-deps.d.ts +3 -1
  85. package/dist/core/runner/retired-runner-deps.js +1 -0
  86. package/dist/core/runner/run-compaction-machinery.d.ts +4 -2
  87. package/dist/core/runner/run-compaction-machinery.js +5 -3
  88. package/dist/core/runner/run-harness-handlers.js +9 -2
  89. package/dist/core/runner/run-identity-wiring.js +19 -1
  90. package/dist/core/runner/run-leg.js +1 -1
  91. package/dist/core/runner/run-notification-lane.js +8 -2
  92. package/dist/core/runner/run-recovery-lanes.js +9 -5
  93. package/dist/core/runner/run-settle-and-teardown.d.ts +4 -22
  94. package/dist/core/runner/run-settle-and-teardown.js +6 -10
  95. package/dist/core/runner/run-stop-and-final-verify.js +1 -0
  96. package/dist/core/runner/run-terminal-adoption.js +5 -5
  97. package/dist/core/runner/run-turn-boundary.js +32 -5
  98. package/dist/core/runner/runtask.d.ts +8 -1
  99. package/dist/core/runner/runtask.js +33 -2
  100. package/dist/core/runner-deps.d.ts +44 -13
  101. package/dist/core/side-query.d.ts +6 -5
  102. package/dist/core/side-query.js +3 -1
  103. package/dist/core/swappable-deps.d.ts +20 -4
  104. package/dist/core/swappable-deps.js +6 -1
  105. package/dist/core/task-event.d.ts +13 -0
  106. package/dist/core/task-notification.d.ts +144 -2
  107. package/dist/core/task-notification.js +154 -5
  108. package/dist/core/task-result.d.ts +27 -3
  109. package/dist/core/task-spec.d.ts +7 -0
  110. package/dist/core/tool-catalog-entries.js +2 -1
  111. package/dist/core/tool-conformance.js +1 -0
  112. package/dist/core/tool-policy.d.ts +9 -4
  113. package/dist/core/tool-spec.d.ts +14 -0
  114. package/dist/core/wiring-manifest.d.ts +12 -0
  115. package/dist/core/wiring-manifest.js +5 -1
  116. package/dist/engine/harness/agent-harness.d.ts +14 -1
  117. package/dist/engine/harness/agent-harness.js +28 -3
  118. package/dist/engine/harness/types.d.ts +3 -1
  119. package/dist/engine/llm/types.d.ts +8 -7
  120. package/dist/engine/llm/types.js +1 -1
  121. package/dist/engine/session/import-validate.js +4 -0
  122. package/dist/index.d.ts +10 -7
  123. package/dist/index.js +7 -5
  124. package/dist/orchestration/run-workflow-tool.d.ts +4 -4
  125. package/dist/orchestration/workflow.d.ts +1 -1
  126. package/dist/orchestration/workflow.js +8 -5
  127. package/dist/shell/lex.d.ts +30 -51
  128. package/dist/shell/lex.js +237 -284
  129. package/dist/stores/cc/mailbox-store.js +3 -2
  130. package/dist/stores/file/corrupt-read-sink.d.ts +28 -2
  131. package/dist/stores/file/file-history-store.d.ts +2 -4
  132. package/dist/stores/file/file-history-store.js +1 -1
  133. package/dist/stores/file/fs-atomic.d.ts +16 -11
  134. package/dist/stores/file/fs-atomic.js +17 -6
  135. package/dist/stores/file/index.d.ts +5 -6
  136. package/dist/stores/file/index.js +1 -1
  137. package/dist/stores/file/mailbox-store.d.ts +4 -9
  138. package/dist/stores/file/mailbox-store.js +29 -30
  139. package/dist/stores/file/session-policy-store.d.ts +2 -0
  140. package/dist/stores/file/session-policy-store.js +1 -1
  141. package/dist/stores/file/session-store.d.ts +11 -7
  142. package/dist/stores/file/session-store.js +23 -16
  143. package/dist/stores/file/shared-ledger.js +3 -14
  144. package/dist/stores/file/tool-result-store.d.ts +2 -0
  145. package/dist/stores/file/tool-result-store.js +5 -2
  146. package/dist/stores/file/workflow-journal-store.js +14 -9
  147. package/dist/tools/fs/bash-program-position.d.ts +15 -24
  148. package/dist/tools/fs/bash-program-position.js +11 -29
  149. package/dist/tools/fs/bash-readonly-classifier.d.ts +125 -117
  150. package/dist/tools/fs/bash-readonly-classifier.js +400 -272
  151. package/dist/tools/fs/shell-read-check.js +3 -3
  152. package/dist/tools/fs/shell-read-establish.js +1 -1
  153. package/dist/tools/fs/shell-read-plan.d.ts +11 -6
  154. package/dist/tools/fs/shell-read-plan.js +167 -183
  155. package/dist/tools/fs/shell-read-word.d.ts +50 -0
  156. package/dist/tools/fs/shell-read-word.js +59 -0
  157. package/dist/tools/fs/shell-reentry-text.d.ts +10 -0
  158. package/dist/tools/fs/shell-reentry-text.js +73 -0
  159. package/package.json +1 -1
  160. package/test/export-surface.snapshot.json +58 -8
  161. package/wire-changes/core@7.29.0.json +185 -0
  162. package/wire-changes/core@7.30.0.json +1821 -0
@@ -119,7 +119,7 @@ export type NoticeAudience = "user" | "operator";
119
119
  * the union's catalog ({@link EngineNotice.code}). The groups below say only which family a row joins,
120
120
  * so the catalog is not written a second time here and the two cannot drift.
121
121
  */
122
- export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.write_protection_unresolved", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "memory.project_scope_absent", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed"];
122
+ export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.context_settings_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.write_protection_unresolved", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "memory.project_scope_absent", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "route.response_model_mismatch", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed"];
123
123
  /** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
124
124
  * `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
125
125
  * sink is a supported shape, and narrowing that field would break it. */
@@ -105,6 +105,7 @@ export const ENGINE_NOTICE_CODES = [
105
105
  "config.env_timeout_discarded",
106
106
  "config.materialize_env_discarded",
107
107
  "config.models_swapped",
108
+ "config.context_settings_swapped",
108
109
  "config.read_face_swapped",
109
110
  "config.read_face_deployment_clamped",
110
111
  "config.write_face_swapped",
@@ -157,6 +158,7 @@ export const ENGINE_NOTICE_CODES = [
157
158
  "route.fallback_to_primary",
158
159
  "route.classifier_unroutable",
159
160
  "route.base_url_changed_key_unchanged",
161
+ "route.response_model_mismatch",
160
162
  "task.user_steer_undrained",
161
163
  "task.user_followup_undrained",
162
164
  "steering.parked_input_blocked",
@@ -197,6 +199,7 @@ const NOTICE_AUDIENCE_TABLE = {
197
199
  "config.env_timeout_discarded": "operator",
198
200
  "config.materialize_env_discarded": "operator",
199
201
  "config.models_swapped": "operator",
202
+ "config.context_settings_swapped": "operator",
200
203
  "config.read_face_swapped": "operator",
201
204
  "config.read_face_deployment_clamped": "operator",
202
205
  "config.write_face_swapped": "operator",
@@ -235,6 +238,7 @@ const NOTICE_AUDIENCE_TABLE = {
235
238
  "route.fallback_to_primary": "operator",
236
239
  "route.classifier_unroutable": "operator",
237
240
  "route.base_url_changed_key_unchanged": "operator",
241
+ "route.response_model_mismatch": "operator",
238
242
  "skills.listing_truncated": "operator",
239
243
  "agents.listing_truncated": "operator",
240
244
  "tool_result.offload_put_failed": "operator",
@@ -540,16 +540,8 @@ export interface StopHookContext {
540
540
  /** Consecutive blocks so far in this run (resets when a stop() call lets the run proceed). */
541
541
  consecutiveBlocks: number;
542
542
  /**
543
- * A view of the conversation this hook is being asked to judge.
544
- *
545
- * Until now a Stop hook received two counters and nothing else. That is enough for a hook that asks
546
- * "have I blocked too often?", and structurally insufficient for the entire class of hooks that ask
547
- * anything ABOUT THE RUN — which is what the prompt-shaped hooks a settings file can declare are for.
548
- * Measured end to end downstream: the carrier was constructed, invoked, and returned without error, and
549
- * the model answered "I don't have direct access to your file system…" — because nobody had given the
550
- * evaluator the session. Every layer reported success and the feature could not work. A hook cannot
551
- * judge a run it cannot see, so the gap belonged here rather than in any of the layers that were
552
- * dutifully passing an empty view along.
543
+ * A view of the conversation this hook is being asked to judge — the counters alone cannot serve a
544
+ * hook that asks anything ABOUT THE RUN (the prompt-shaped hooks a settings file declares).
553
545
  *
554
546
  * LAZY on purpose: a hook that only reads the counters pays nothing, and a hook that needs the
555
547
  * conversation gets it in full rather than through a truncation policy the engine would have to invent
@@ -559,6 +551,14 @@ export interface StopHookContext {
559
551
  * Absent when the runner cannot supply one (a stop gate wired without a session).
560
552
  */
561
553
  getBranch?: () => Promise<readonly SessionTreeEntry[]>;
554
+ /** The run's background tasks still in flight (pending / running / parked under this run's owner triple — the
555
+ * post-compaction reminder's projection; CC `SubagentStop.background_tasks`). LAZY like {@link getBranch};
556
+ * `[]` = none in that projection, ABSENT = no view. A parked child here waits on an approval out of band. */
557
+ backgroundTasks?: () => ReadonlyArray<{
558
+ id: string;
559
+ description?: string;
560
+ status: string;
561
+ }>;
562
562
  /** {@link HookSeatSignal} — this invocation's own abort signal. */
563
563
  signal?: AbortSignal;
564
564
  }
@@ -44,6 +44,10 @@ export interface MailboxPeerMeta {
44
44
  * — a delivery receipt's state (`kind: "delivery_notice"`) or an idle notice's kind
45
45
  * (`kind: "idle_notice"`). The record's `content` is a human-readable mirror; THIS is the authority. */
46
46
  notice?: MailboxPeerNoticeMeta;
47
+ /** `peer_message` only — the SENDER marked it urgent. The recipient's drain presents an urgent record
48
+ * ahead of the non-urgent ones of the same pass and counts it on the inbox notice; it never changes
49
+ * WHEN the recipient reads (the recipient's setting decides that, never the sender). */
50
+ urgent?: boolean;
47
51
  }
48
52
  /** design/385 — the typed body of a notice-kind record. `state` is drawn from the receipt states on a
49
53
  * `delivery_notice` (`held|denied|expired|delivered|refused|dropped`) and from the idle kinds on an
@@ -67,10 +71,23 @@ export interface MailboxPeerNoticeMeta {
67
71
  }
68
72
  export declare const MAILBOX_PEER_FROM_MODES: readonly ["bypass", "prompting"];
69
73
  export type MailboxPeerFromMode = (typeof MAILBOX_PEER_FROM_MODES)[number];
70
- /** design/385: `peer_message` runs the parity judgment; `idle_subscription` registers the sender as an
71
- * idle subscriber of the box owner (§5.2, the store form of CC's `notify_when_idle` frame);
72
- * `idle_notice` / `delivery_notice` land on the notice face (rendered, never enveloped as a peer's words). */
73
- export declare const MAILBOX_PEER_RECORD_KINDS: readonly ["peer_message", "idle_notice", "delivery_notice", "idle_subscription"];
74
+ /**
75
+ * design/385: `peer_message` runs the parity judgment; `idle_subscription` registers the sender as an
76
+ * idle subscriber of the box owner (§5.2, the store form of CC's `notify_when_idle` frame);
77
+ * `idle_notice` / `delivery_notice` land on the notice face (rendered, never enveloped as a peer's words).
78
+ *
79
+ * @contract mailbox.engine_frame_is_not_a_peers_words — an `engine_frame` record is the ENGINE's own
80
+ * task-notification frame parked durably (a child's terminal, a monitor batch, a workflow settle),
81
+ * written by the park seat of the session that owns the box and only over a deployment that vouches
82
+ * every drainer of the box reads the codec (`RunnerDeps.sessionBoxEngineFrames`). Its `content` is the
83
+ * versioned codec text (`encodeEngineFrame`), never a message: the drain decodes it and re-injects the
84
+ * frame through the engine's own notification lane — no parity judgment, no admission gate, no peer
85
+ * envelope — and a codec version this engine does not read STOPS the drain at that record (the box
86
+ * waits for an engine that reads it) rather than rendering engine speech as a peer's words. Only the
87
+ * engine's own lanes (`background_agent` / `monitor` / `workflow`) may be parked this way; an
88
+ * `external` event never is.
89
+ */
90
+ export declare const MAILBOX_PEER_RECORD_KINDS: readonly ["peer_message", "idle_notice", "delivery_notice", "idle_subscription", "engine_frame"];
74
91
  export type MailboxPeerRecordKind = (typeof MAILBOX_PEER_RECORD_KINDS)[number];
75
92
  /** The `notice.state` vocabulary the validator admits (the union of both notice families; the drain
76
93
  * checks the kind↔state pairing). Spelled here, beside the record kinds, so the store contract is
@@ -183,7 +200,7 @@ export interface MailboxAppendMessage {
183
200
  * persists it and hands it back on `claimLease` byte-for-byte (a detached copy), absent stays absent,
184
201
  * and a malformed value is REFUSED with `"mailbox.invalid_peer_meta"` before any side effect
185
202
  * (`readMailboxPeerMeta` is the one validator; bundled backends call it first). T1 cases in
186
- * `mailboxStoreContract` cover the round trip, the refusal and the aliasing.
203
+ * `mailboxStoreContract` cover the pass trip, the refusal and the aliasing.
187
204
  * - CROSS-PROCESS SAFETY (design/385, optional capability declared on the store): the session-box
188
205
  * drain shares ONE box between several OS processes. A backend that is correct under that sharing —
189
206
  * seq minted once across processes, lease/ack never crossing, one process's housekeeping never
@@ -192,6 +209,12 @@ export interface MailboxAppendMessage {
192
209
  * that does not declare it is refused by the lane at mount time with a named reason
193
210
  * ({@link mailboxCrossProcessMountVerdict}) — never silently mounted on luck.
194
211
  */
212
+ /** #1036 — what {@link MailboxStore.ack} answers: did this call actually consume the prefix?
213
+ * `false` = the lease fence refused it (this caller does not hold the box), so nothing was deleted
214
+ * and the records are still parked for whoever does. */
215
+ export interface MailboxAckResult {
216
+ readonly acked: boolean;
217
+ }
195
218
  export interface MailboxStore {
196
219
  /** design/385 — cross-process safety declaration (see the interface notes). `true` = this backend
197
220
  * passed `mailboxCrossProcessContract` over real OS processes; `false` = deliberately process-local
@@ -202,7 +225,18 @@ export interface MailboxStore {
202
225
  * that its recipient is in the deployment's pre-delete state (see the interface notes above). */
203
226
  append(scope: string, handle: string, msg: MailboxAppendMessage): Promise<number>;
204
227
  claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
205
- ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<void>;
228
+ /**
229
+ * Consume the leased prefix up to `upToSeq` (the records are DELETED). Fenced by the current lease:
230
+ * a caller that does not hold it consumes nothing.
231
+ *
232
+ * #1036 — the fence's verdict is REPORTED (`{acked:false}`), not silent. The fence itself is
233
+ * older (MB1): it exists so a stale owner whose lease a live rival already took over cannot delete
234
+ * the rival's messages. What was missing is that a no-op and a success were the same `void` to the
235
+ * caller, so a drain that had lost its lease would clear its own watermark as if the store had
236
+ * obeyed it — the one shape where "the records stay parked" turns into "the records are forgotten".
237
+ * Implementations report `{acked:true}` only when they actually consumed the prefix.
238
+ */
239
+ ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<MailboxAckResult>;
206
240
  releaseLease(scope: string, handle: string, owner: string): Promise<void>;
207
241
  /** Observation face (the "N pending" receipt figure) — count of currently-visible messages. */
208
242
  peekCount(scope: string, handle: string): Promise<number>;
@@ -253,7 +287,7 @@ export declare class InMemoryMailboxStore implements MailboxStore {
253
287
  private box;
254
288
  append(scope: string, handle: string, msg: MailboxAppendMessage): Promise<number>;
255
289
  claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
256
- ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<void>;
290
+ ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<MailboxAckResult>;
257
291
  releaseLease(scope: string, handle: string, owner: string): Promise<void>;
258
292
  peekCount(scope: string, handle: string): Promise<number>;
259
293
  drop(scope: string, handle: string): Promise<void>;
@@ -1,6 +1,6 @@
1
1
  import { assertRetentionPolicy } from "./retention-policy.js";
2
2
  export const MAILBOX_PEER_FROM_MODES = ["bypass", "prompting"];
3
- export const MAILBOX_PEER_RECORD_KINDS = ["peer_message", "idle_notice", "delivery_notice", "idle_subscription"];
3
+ export const MAILBOX_PEER_RECORD_KINDS = ["peer_message", "idle_notice", "delivery_notice", "idle_subscription", "engine_frame"];
4
4
  export const MAILBOX_PEER_NOTICE_STATES = ["held", "denied", "expired", "delivered", "refused", "dropped", "idle", "exited", "unavailable"];
5
5
  export const MAILBOX_INVALID_PEER_META_CODE = "mailbox.invalid_peer_meta";
6
6
  export const MAILBOX_CROSS_PROCESS_UNSAFE_CODE = "mailbox.cross_process_unsafe";
@@ -42,6 +42,11 @@ export function readMailboxPeerMeta(raw) {
42
42
  else if (k === "notice") {
43
43
  out.notice = readMailboxPeerNoticeMeta(v, refuse);
44
44
  }
45
+ else if (k === "urgent") {
46
+ if (typeof v !== "boolean")
47
+ return refuse("urgent must be a boolean");
48
+ out.urgent = v;
49
+ }
45
50
  else {
46
51
  return refuse(`carries an unknown key ${JSON.stringify(k)}`);
47
52
  }
@@ -171,12 +176,13 @@ export class InMemoryMailboxStore {
171
176
  async ack(scope, handle, owner, upToSeq) {
172
177
  const b = this.boxes.get(this.key(scope, handle));
173
178
  if (!b)
174
- return;
179
+ return { acked: false };
175
180
  if (b.lease === undefined || b.lease.owner !== owner)
176
- return;
181
+ return { acked: false };
177
182
  b.messages = b.messages.filter((m) => m.seq > upToSeq);
178
183
  if (b.lease.maxSeq <= upToSeq)
179
184
  b.lease = undefined;
185
+ return { acked: true };
180
186
  }
181
187
  async releaseLease(scope, handle, owner) {
182
188
  const b = this.boxes.get(this.key(scope, handle));
@@ -3,6 +3,19 @@ import type { ToolResultMessage } from "../internal/llm.js";
3
3
  /** Read the (possibly absent) `role` off an AgentMessage defensively — the union doesn't expose it
4
4
  * uniformly, so context passes narrow by role with a single cast in one place. */
5
5
  export declare function messageRole(m: AgentMessage): string | undefined;
6
+ /**
7
+ * The PROVIDER's own name for the model that answered this assistant message, when it differs from the
8
+ * ENGINE-SELECTED id the message is stamped with — the ONE read of `AssistantMessage.responseModel`, so
9
+ * the per-turn frame, the result face, the side-query projection and the mismatch notice admit it on the
10
+ * same terms rather than on four. The READ twin of `noteResponseModel` (`brain/stream-shared.ts`), which
11
+ * states the same law on the write side; this side re-tests rather than trusting, because a HOST's own
12
+ * Brain mints these messages too. `undefined` ⇒ the provider named the stamped id, named nothing, or
13
+ * named something unreadable — ONE reading, never three.
14
+ */
15
+ export declare function providerReportedModel(m: {
16
+ readonly model?: unknown;
17
+ readonly responseModel?: unknown;
18
+ }): string | undefined;
6
19
  /** True if the message is a tool-result message (shared by the context-edit and context-guard passes).
7
20
  * A TYPE PREDICATE, not a bare boolean: `role: "toolResult"` belongs to exactly one member of the
8
21
  * union, so callers that check it can read `.content`/`.toolName` and rebuild the message directly
@@ -1,6 +1,12 @@
1
1
  export function messageRole(m) {
2
2
  return m.role;
3
3
  }
4
+ export function providerReportedModel(m) {
5
+ const reported = m.responseModel;
6
+ if (typeof reported !== "string" || reported === "")
7
+ return undefined;
8
+ return reported === m.model ? undefined : reported;
9
+ }
4
10
  export function isToolResult(m) {
5
11
  return messageRole(m) === "toolResult";
6
12
  }
@@ -13,7 +13,7 @@ import type { Hooks } from "./hooks.js";
13
13
  import type { ToolExecuteContext } from "./tool-spec.js";
14
14
  import type { TaskSpec } from "./types.js";
15
15
  /** The fields of a spec the seat carries — one `Pick`, shared by the mint's input and the apply's target. */
16
- export type ParentSpecSeatFields = Pick<TaskSpec, "principal" | "oneShot" | "clientContext" | "onQuestion" | "hooks" | "additionalDirectories" | "additionalReadDirectories" | "envFacts" | "memoryPersistenceCapable" | "memory">;
16
+ export type ParentSpecSeatFields = Pick<TaskSpec, "principal" | "oneShot" | "clientContext" | "onQuestion" | "hooks" | "additionalDirectories" | "additionalReadDirectories" | "envFacts" | "memoryPersistenceCapable" | "memory" | "compaction" | "skillListingBudgetFraction" | "skillListingMaxDescChars">;
17
17
  /**
18
18
  * The seat a run hands its children. Every member is the parent's own value, frozen at mint (arrays copied and
19
19
  * frozen; the facts object copied). Members present iff the parent carries them — an absent member is "nothing to
@@ -33,6 +33,16 @@ export type ParentSpecSeatFields = Pick<TaskSpec, "principal" | "oneShot" | "cli
33
33
  * `true` a default a child's own declaration may narrow.
34
34
  * · `memoryOff` — the request-level "memory is off" FACT (`spec.memory.enabled === false`): a REMOVAL
35
35
  * (floor) that REWRITES the child's own memory declaration instead of travelling as a value.
36
+ * · `compaction` — the request's CONTEXT-MANAGEMENT settings (enabled / withinTask / reserve / keepRecent /
37
+ * instructions / attachWorkingFiles / staleToolResultOffload / clampTolerance). One set of settings for the
38
+ * whole delegation tree, measured per leg against the leg's OWN window: the numbers are repaired against the
39
+ * child's model by the same sanitizer every leg runs (`sanitizeCompactionSettings`), so a parent's 128k-tuned
40
+ * reserve does not have to be re-tuned to be legal on a 6k child. `enabled: false` is NOT a removal — the
41
+ * reference implementation shares one settings object down the tree rather than flooring children, and a
42
+ * child that authors its own settings wins the whole object (an ORDINARY seat member).
43
+ * · `skillListingBudgetFraction` / `skillListingMaxDescChars` — the listing-budget knobs, the same shape for the
44
+ * same reason: a fraction the parent tuned is a fraction OF EACH LEG'S OWN window, so it travels as a number
45
+ * and every leg multiplies it by its own.
36
46
  */
37
47
  export interface ParentSpecSeat {
38
48
  readonly principal?: string;
@@ -45,6 +55,9 @@ export interface ParentSpecSeat {
45
55
  readonly envFacts?: TaskSpec["envFacts"];
46
56
  readonly memoryPersistenceCapable?: boolean;
47
57
  readonly memoryOff?: true;
58
+ readonly compaction?: TaskSpec["compaction"];
59
+ readonly skillListingBudgetFraction?: number;
60
+ readonly skillListingMaxDescChars?: number;
48
61
  }
49
62
  /** Mint the seat off the parent's REBOUND spec and its frozen question face (spec over deps — the caller resolves it once,
50
63
  * the same value the parent's own question routing reads). Absent fields stay absent; `oneShot` only when `true`. */
@@ -9,6 +9,9 @@ export function parentSpecSeatOf(spec, frozenOnQuestion) {
9
9
  ...(spec.additionalReadDirectories !== undefined ? { additionalReadDirectories: Object.freeze([...spec.additionalReadDirectories]) } : {}),
10
10
  ...(spec.envFacts !== undefined ? { envFacts: Object.freeze({ ...spec.envFacts }) } : {}),
11
11
  ...(spec.memoryPersistenceCapable !== undefined ? { memoryPersistenceCapable: spec.memoryPersistenceCapable } : {}),
12
+ ...(spec.compaction !== undefined ? { compaction: Object.freeze({ ...spec.compaction }) } : {}),
13
+ ...(spec.skillListingBudgetFraction !== undefined ? { skillListingBudgetFraction: spec.skillListingBudgetFraction } : {}),
14
+ ...(spec.skillListingMaxDescChars !== undefined ? { skillListingMaxDescChars: spec.skillListingMaxDescChars } : {}),
12
15
  ...(spec.memory?.enabled === false ? { memoryOff: true } : {}),
13
16
  });
14
17
  }
@@ -25,6 +28,9 @@ export function applyParentSpecSeat(child, seat) {
25
28
  ...fill("additionalDirectories", (seat.additionalDirectories !== undefined ? [...seat.additionalDirectories] : undefined)),
26
29
  ...fill("additionalReadDirectories", (seat.additionalReadDirectories !== undefined ? [...seat.additionalReadDirectories] : undefined)),
27
30
  ...fill("envFacts", (seat.envFacts !== undefined ? { ...seat.envFacts } : undefined)),
31
+ ...fill("compaction", (seat.compaction !== undefined ? { ...seat.compaction } : undefined)),
32
+ ...fill("skillListingBudgetFraction", seat.skillListingBudgetFraction),
33
+ ...fill("skillListingMaxDescChars", seat.skillListingMaxDescChars),
28
34
  ...(seat.oneShot === true ? { oneShot: true } : {}),
29
35
  ...(seat.memoryPersistenceCapable === false ? { memoryPersistenceCapable: false } : fill("memoryPersistenceCapable", seat.memoryPersistenceCapable)),
30
36
  ...(seat.memoryOff === true ? { memory: memoryTurnedOff(child.memory) } : {}),
@@ -263,7 +263,7 @@ export function parseRuleText(text, behavior) {
263
263
  }
264
264
  const shape = ruleLaneShapeOf(body, MATCH_READING);
265
265
  if ("reject" in shape) {
266
- if (body.trim() === "")
266
+ if (body.trim() === "" || shape.reject === "empty command")
267
267
  return reject("invalid.empty_command", `rule "${text}" names no command`);
268
268
  if (foldSpacing(body) === undefined)
269
269
  return reject("invalid.unbalanced_quotes", `rule "${text}" has unbalanced quoting, so the command it names is not determinable`);
@@ -37,6 +37,13 @@ export declare function resolveModelDisplayLabel(ref: string): string;
37
37
  * `resolveModel("best")` refuses like any unknown ref.
38
38
  */
39
39
  export declare function expandTiers(models: Record<string, Model> | undefined, tiers: Record<string, ModelRef> | undefined): Record<string, Model> | undefined;
40
+ /**
41
+ * The words a catalog word stands in for under the tier layer, nearest first — what {@link expandTiers}
42
+ * recorded for a key IT wrote: the tier words its resolution passed through and the binding's own catalog
43
+ * word, each naming the model the key resolves to. A deployment's own key (a shadowing one included), and any
44
+ * catalog that was not tier-expanded, stands in for nothing.
45
+ */
46
+ export declare function tierAliasVia(word: string, models: Record<string, Model> | undefined): readonly string[];
40
47
  export interface ModelMention {
41
48
  /** The matched model name — always one of `allowedNames` — or undefined if none was found. */
42
49
  model?: string;
@@ -98,6 +105,19 @@ export declare const ROLE_TIER_DEFAULTS: Readonly<Partial<Record<ModelRole, stri
98
105
  export declare function roleSpecNamesModel(spec: RoleSpec | undefined): boolean;
99
106
  export interface ResolvedRole {
100
107
  model: Model;
108
+ /**
109
+ * WHICH REF CHOSE {@link model} — the word, when a word chose it (the explicit `spec.model` string, the
110
+ * role's own `model` ref, or the role→tier default's catalog key), else the `Model` object itself (an
111
+ * explicit object, or one a `select` criteria picked: neither has a catalog word).
112
+ *
113
+ * The distinction is load-bearing wherever a LATER tick must ask the catalog the same question again: a
114
+ * word re-resolves against the catalog generation in force at that tick (a hot swap redirects it, a
115
+ * retirement refuses it loudly), while an object is pinned and never re-resolved. The engine-spawned
116
+ * child that inherits its parent's model inherits THIS, not the resolved object, so it lands on the
117
+ * generation its own prepare sees rather than on the one its parent started under; the same word keys
118
+ * the deployment's per-model context-window overlay.
119
+ */
120
+ modelRef: ModelRef;
101
121
  /** The effective thinking level: explicit `spec.thinking` wins, else the role's default. */
102
122
  thinking?: ThinkingLevel;
103
123
  /** The requested role's preset system prompt, if it set one (independent of model fallback). */
@@ -22,59 +22,75 @@ export function resolveModelDisplayLabel(ref) {
22
22
  return Object.hasOwn(CC_MODEL_TIER_ALIASES, ref) ? CC_MODEL_TIER_ALIASES[ref] : ref;
23
23
  }
24
24
  const TIERS_ACTIVE = Symbol.for("sema.tiersActive");
25
+ const TIER_ALIAS_VIA = Symbol.for("sema.tierAliasVia");
25
26
  export function expandTiers(models, tiers) {
26
27
  if (!tiers || Object.keys(tiers).length === 0)
27
28
  return models;
28
29
  const out = { ...models };
29
30
  out[TIERS_ACTIVE] = true;
31
+ const aliasVia = new Map();
32
+ out[TIER_ALIAS_VIA] = aliasVia;
30
33
  const bound = (name) => {
31
34
  const b = Object.hasOwn(tiers, name) ? tiers[name] : undefined;
32
35
  return b === undefined ? undefined : resolveModel(b, models);
33
36
  };
34
- const resolveTier = (name) => {
35
- const direct = bound(name);
37
+ const ownKey = (k) => models !== undefined && Object.hasOwn(models, k) && Boolean(models[k]);
38
+ const landing = (t, self) => {
39
+ const m = bound(t);
40
+ if (!m)
41
+ return undefined;
42
+ const b = Object.hasOwn(tiers, t) ? tiers[t] : undefined;
43
+ return { model: m, via: [...(t !== self && !ownKey(t) ? [t] : []), ...(typeof b === "string" ? [b] : [])] };
44
+ };
45
+ const resolveTier = (name, self = name) => {
46
+ const direct = landing(name, self);
36
47
  if (direct)
37
48
  return direct;
38
49
  const at = DEFAULT_TIER_ORDER.indexOf(name);
39
50
  if (at === -1)
40
51
  return undefined;
41
52
  for (let i = at + 1; i < DEFAULT_TIER_ORDER.length; i++) {
42
- const next = bound(DEFAULT_TIER_ORDER[i]);
53
+ const next = landing(DEFAULT_TIER_ORDER[i], self);
43
54
  if (next)
44
- return next;
55
+ return { model: next.model, via: [...(name !== self && !ownKey(name) ? [name] : []), ...next.via] };
45
56
  }
46
57
  return undefined;
47
58
  };
48
59
  const shadowed = (key) => Object.hasOwn(out, key) && Boolean(out[key]);
49
- const put = (key, m) => {
60
+ const put = (key, m, via) => {
50
61
  Object.defineProperty(out, key, { value: m, writable: true, enumerable: true, configurable: true });
62
+ aliasVia.set(key, via);
51
63
  };
52
64
  const tierNames = new Set([...DEFAULT_TIER_ORDER, ...Object.keys(tiers)]);
53
65
  for (const name of tierNames) {
54
66
  if (shadowed(name))
55
67
  continue;
56
- const m = resolveTier(name);
57
- if (m)
58
- put(name, m);
68
+ const r = resolveTier(name);
69
+ if (r)
70
+ put(name, r.model, r.via);
59
71
  }
60
72
  for (const [alias, tier] of Object.entries(CC_MODEL_TIER_ALIASES)) {
61
73
  if (shadowed(alias))
62
74
  continue;
63
- const m = resolveTier(tier);
64
- if (m)
65
- put(alias, m);
75
+ const r = resolveTier(tier, alias);
76
+ if (r)
77
+ put(alias, r.model, r.via);
66
78
  }
67
79
  if (!shadowed("best")) {
68
80
  for (const name of DEFAULT_TIER_ORDER) {
69
- const m = bound(name);
70
- if (m) {
71
- put("best", m);
81
+ const r = landing(name, "best");
82
+ if (r) {
83
+ put("best", r.model, r.via);
72
84
  break;
73
85
  }
74
86
  }
75
87
  }
76
88
  return out;
77
89
  }
90
+ export function tierAliasVia(word, models) {
91
+ const via = models?.[TIER_ALIAS_VIA];
92
+ return via?.get(word) ?? [];
93
+ }
78
94
  export function parseModelMention(text, allowedNames) {
79
95
  const allowed = new Map();
80
96
  for (const n of allowedNames)
@@ -181,6 +197,7 @@ export function resolveTaskModel(spec, deps) {
181
197
  const startParts = specParts(mergeRoles(deps.roles, spec.roles)[start]);
182
198
  return {
183
199
  model: resolveModel(spec.model, deps.models),
200
+ modelRef: spec.model,
184
201
  thinking: spec.thinking ?? startParts.thinking,
185
202
  systemPrompt: startParts.systemPrompt,
186
203
  };
@@ -190,9 +207,11 @@ export function resolveTaskModel(spec, deps) {
190
207
  const startSystemPrompt = specParts(merged[start]).systemPrompt;
191
208
  for (const role of [start, ...FALLBACK[start]]) {
192
209
  const parts = specParts(merged[role]);
193
- const model = partsModel(parts, deps.models) ?? roleTierModel(role, deps.models);
210
+ const fromParts = partsModel(parts, deps.models);
211
+ const model = fromParts ?? roleTierModel(role, deps.models);
194
212
  if (model) {
195
- return { model, thinking: spec.thinking ?? parts.thinking, systemPrompt: startSystemPrompt };
213
+ const modelRef = fromParts !== undefined ? (parts.model ?? model) : (ROLE_TIER_DEFAULTS[role] ?? model);
214
+ return { model, modelRef, thinking: spec.thinking ?? parts.thinking, systemPrompt: startSystemPrompt };
196
215
  }
197
216
  }
198
217
  throw new Error(`No model for role "${start}": set TaskSpec.model, or configure RunnerDeps.roles / TaskSpec.roles["${start}"] (or a "default" role). A "select" RoleSpec also fails here if no catalog model matches.`);
@@ -165,6 +165,8 @@ export interface ResultFlags {
165
165
  * `reasoning.resolved` trace frame carried (runtask resolves once per leg — two faces, one mint);
166
166
  * absent when thinking was off/unset for the leg, so the key is omitted. */
167
167
  effectiveReasoning?: TaskResult["effectiveReasoning"];
168
+ /** The run's compaction record (see `TaskResult.compaction`); assembly omits the key when absent. */
169
+ compaction?: TaskResult["compaction"];
168
170
  /** ruled 2026-08-04 — the usage-governance wait hint carried by the platform terminal the run adopted
169
171
  * (`undefined` for every other cause: an expiring environment has no return time to give, and a store
170
172
  * failure is not a window). Passed as data rather than read back off `threw` so the seat has a typed
@@ -2,6 +2,7 @@ import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
2
2
  import { extractErrorCode, stripErrorCodePrefix } from "../../brain/errors.js";
3
3
  import { terminalProjection } from "./terminal-projection.js";
4
4
  import { haltConsumed } from "./halt-attribution.js";
5
+ import { providerReportedModel } from "../message-utils.js";
5
6
  import { isErrorLike, readMember } from "../thrown-value.js";
6
7
  import { describeRejectedValue } from "../tool-search-setting.js";
7
8
  const SALVAGE_ELIGIBLE_TERMINALS = new Set([
@@ -153,6 +154,7 @@ export function terminalCauseOf(final, stats, flags) {
153
154
  export function assembleResult(spec, sessionId, final, stats, flags) {
154
155
  const taskId = spec.taskId ?? sessionId;
155
156
  const text = final ? assistantText(final) : "";
157
+ const responseModel = final === undefined ? undefined : providerReportedModel(final);
156
158
  if (!flags.unpricedSpend) {
157
159
  const compactionMicroUsd = stats.compactionMicroUsd ?? 0;
158
160
  const nestedSubagentMicroUsd = stats.nested?.costMicroUsd ?? 0;
@@ -176,5 +178,5 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
176
178
  if (flags.unpricedSpend)
177
179
  delete publicStats.costMicroUsd;
178
180
  const stampHaltedByUser = haltConsumed(flags.haltConsumption);
179
- return { taskId, ...(flags.runId !== undefined ? { runId: flags.runId } : {}), sessionId, terminal, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, ...(apiFailure !== undefined ? { apiFailure } : {}), ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.editedFiles !== undefined && flags.editedFiles.length > 0 ? { editedFiles: flags.editedFiles } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(stampHaltedByUser ? { haltedByUser: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: [...flags.remoteEnvFailures] } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveWriteFace !== undefined ? { effectiveWriteFace: flags.effectiveWriteFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
181
+ return { taskId, ...(flags.runId !== undefined ? { runId: flags.runId } : {}), sessionId, terminal, ...(flags.model !== undefined ? { model: flags.model } : {}), ...(responseModel !== undefined ? { responseModel } : {}), result: result.trim(), salvagedOutput, ...(apiFailure !== undefined ? { apiFailure } : {}), ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.editedFiles !== undefined && flags.editedFiles.length > 0 ? { editedFiles: flags.editedFiles } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(stampHaltedByUser ? { haltedByUser: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: [...flags.remoteEnvFailures] } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveWriteFace !== undefined ? { effectiveWriteFace: flags.effectiveWriteFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), ...(flags.compaction !== undefined ? { compaction: flags.compaction } : {}), stats: publicStats };
180
182
  }
@@ -3,7 +3,9 @@ import type { MaybeCompactOptions } from "../auto-compaction.js";
3
3
  import type { StaleToolResultOffloadOptions, TaskSpec } from "../types.js";
4
4
  import type { Context } from "../../internal/llm.js";
5
5
  import { type ToolResultStore } from "../tool-result-store.js";
6
- import type { Prepared } from "./contracts.js";
6
+ import type { Prepared, RunnerDepsSeat } from "./contracts.js";
7
+ import { type ContextWindowOverlay } from "../context-settings.js";
8
+ import { type CompactionSettings } from "../../internal/harness.js";
7
9
  /**
8
10
  * The maybeCompact call arguments that MUST be assembled identically on all three
9
11
  * of runtask's compaction lanes (turn boundary / prompt-too-long recovery / end-of-task).
@@ -17,7 +19,8 @@ import type { Prepared } from "./contracts.js";
17
19
  *
18
20
  * Every export is a free function: the turn-boundary lane assembles its call inside `createTurnBoundary`
19
21
  * (a module-level function with no `this`), so a method would not be reachable from all three sites.
20
- * Nothing here reads or writes runner state — pure assembly over `spec` + `Prepared`.
22
+ * Nothing here WRITES runner state; one export ({@link legContextOverlayOption}) READS the live deps seat,
23
+ * because reading it at the moment of the pass — not at prepare — is the whole of what it states.
21
24
  */
22
25
  /**
23
26
  * Opt-in working-file attachments (LONGRUN-2 / CC 198 post-compact restore parity).
@@ -142,3 +145,24 @@ writtenRefs: Set<string>): Promise<Context>;
142
145
  * scope inside `finish()`.
143
146
  */
144
147
  export declare function emitInputTruncated(tracer: TracerHook | undefined, taskId: string): NonNullable<MaybeCompactOptions["onInputTruncated"]>;
148
+ /**
149
+ * The deployment's per-model window overlay in force for THIS pass — the one option of the three lanes that
150
+ * is deliberately a LIVE read rather than a prepared value.
151
+ *
152
+ * Two reads, both at the moment of the pass: the leg's own catalog word (the harness's current ref, so a
153
+ * mid-task degrade moves the key along with the model) and the Runner's current deps seat (so a `swapDeps`
154
+ * reaches a leg already running). An object-pinned leg has no word and therefore no overlay — the key is
155
+ * simply absent, which `maybeCompact` reads as "the model card's own declarations", the pre-seat behavior.
156
+ */
157
+ export declare function legContextOverlayOption(prepared: Prepared, runner: RunnerDepsSeat): Pick<MaybeCompactOptions, "contextOverlay">;
158
+ /**
159
+ * The compaction SETTINGS a pass runs under — the assembly all three lanes share.
160
+ *
161
+ * `{ ...DEFAULT_COMPACTION_SETTINGS, ...spec.compaction }` was hand-copied at each lane, and it is where the
162
+ * deployment's live `enabled` declaration belongs: `maybeCompact` has exactly one enablement gate (it reads
163
+ * `settings.enabled`), so folding the overlay in HERE makes the deployment's switch reach the boundary pass,
164
+ * the recovery pass and the end-of-task summary by one edit instead of three, and keeps "who decides whether
165
+ * compaction runs" a single expression ({@link compactionEnabled}: the request authored it > the deployment
166
+ * declared it > on).
167
+ */
168
+ export declare function legCompactionSettings(spec: TaskSpec, overlay: ContextWindowOverlay | undefined): CompactionSettings;
@@ -1,5 +1,7 @@
1
1
  import { emitTrace } from "../trace.js";
2
2
  import { buildToolResultRef, OFFLOAD_TOOL_NAME, PERSISTED_OUTPUT_PREFIX, toolResultContentSegment, toolResultProvenanceOf, } from "../tool-result-store.js";
3
+ import { compactionEnabled, contextOverlayFor } from "../context-settings.js";
4
+ import { DEFAULT_COMPACTION_SETTINGS } from "../../internal/harness.js";
3
5
  export function buildWorkingFileAttachments(spec, prepared) {
4
6
  if (spec.compaction?.attachWorkingFiles === false || !prepared.readTaskFile)
5
7
  return undefined;
@@ -115,3 +117,10 @@ export function emitInputTruncated(tracer, taskId) {
115
117
  ts: Date.now(),
116
118
  }));
117
119
  }
120
+ export function legContextOverlayOption(prepared, runner) {
121
+ const overlay = contextOverlayFor(runner.deps.contextSettings, prepared.harness.getModelRef(), runner.deps);
122
+ return overlay !== undefined ? { contextOverlay: overlay } : {};
123
+ }
124
+ export function legCompactionSettings(spec, overlay) {
125
+ return { ...DEFAULT_COMPACTION_SETTINGS, ...spec.compaction, enabled: compactionEnabled(spec.compaction?.enabled, overlay) };
126
+ }
@@ -8,10 +8,11 @@ import type { Prepared, RunnerDepsSeat } from "./contracts.js";
8
8
  * Runner OWNS the consecutive-reuse counter (`prepared.compactionReuseRef`) so the
9
9
  * `maxConsecutiveProviderReuse` drift guard spans the whole task across both sites: it FEEDS the current
10
10
  * count in as `consecutiveProviderReuse`, and {@link recordCompactionReuse} updates it from the result.
11
- * Returns `undefined` when no provider is wired (so the call site spreads nothing → byte-identical to
12
- * the prior behavior).
11
+ * The provider half is absent when no provider is wired (byte-identical to the prior behavior at every
12
+ * call site); the #1036 `onCommitted` effect sink rides unconditionally, which is why the
13
+ * function itself always returns an object.
13
14
  */
14
- export declare function seamCCompactionOptions(runner: RunnerDepsSeat, prepared: Prepared): Pick<MaybeCompactOptions, "summaryProvider" | "onCompaction" | "maxConsecutiveProviderReuse" | "consecutiveProviderReuse"> | undefined;
15
+ export declare function seamCCompactionOptions(runner: RunnerDepsSeat, prepared: Prepared): Pick<MaybeCompactOptions, "summaryProvider" | "onCompaction" | "maxConsecutiveProviderReuse" | "consecutiveProviderReuse" | "onCommitted">;
15
16
  /** Resolve the pre/postCompact lifecycle callbacks (whole-slot `spec.hooks ??
16
17
  * deps.hooks`, same resolution as the stop hook) and wrap each in a SWALLOW+TRACE shell before
17
18
  * threading them into maybeCompact. The wrapper owns the observability half of the R3 MED contract
@@ -1,13 +1,20 @@
1
1
  import { hookSeatExpiredError, resolveHookTimeoutMs, runHookSeat } from "../hooks.js";
2
2
  import { asThrownError } from "../thrown-value.js";
3
3
  export function seamCCompactionOptions(runner, prepared) {
4
- if (!runner.deps.summaryProvider)
5
- return undefined;
6
4
  return {
7
- summaryProvider: runner.deps.summaryProvider,
8
- onCompaction: runner.deps.onCompaction,
9
- maxConsecutiveProviderReuse: runner.deps.maxConsecutiveProviderReuse,
10
- consecutiveProviderReuse: prepared.compactionReuseRef.consecutive,
5
+ ...(runner.deps.summaryProvider
6
+ ? {
7
+ summaryProvider: runner.deps.summaryProvider,
8
+ onCompaction: runner.deps.onCompaction,
9
+ maxConsecutiveProviderReuse: runner.deps.maxConsecutiveProviderReuse,
10
+ consecutiveProviderReuse: prepared.compactionReuseRef.consecutive,
11
+ }
12
+ : {}),
13
+ onCommitted: (effect) => {
14
+ if (prepared.frameSettlement.wantsEffects?.() !== true)
15
+ return;
16
+ prepared.frameSettlement.compactions.push(effect);
17
+ },
11
18
  };
12
19
  }
13
20
  export function compactionHookOptions(runner, spec, sessionId, trigger, identity, seatBound) {