@sema-agent/core 7.29.0 → 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 (149) hide show
  1. package/CHANGELOG.md +361 -0
  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/core/auto-compaction.d.ts +42 -0
  27. package/dist/core/auto-compaction.js +35 -2
  28. package/dist/core/child-clamp-seats.d.ts +11 -0
  29. package/dist/core/child-clamp-seats.js +3 -0
  30. package/dist/core/context-edit.d.ts +3 -2
  31. package/dist/core/context-edit.js +6 -5
  32. package/dist/core/context-guard.d.ts +2 -1
  33. package/dist/core/context-guard.js +3 -2
  34. package/dist/core/context-settings.d.ts +103 -0
  35. package/dist/core/context-settings.js +60 -0
  36. package/dist/core/engine-notice.d.ts +11 -1
  37. package/dist/core/frame-settlement.d.ts +97 -0
  38. package/dist/core/frame-settlement.js +36 -0
  39. package/dist/core/governance-codes.d.ts +1 -1
  40. package/dist/core/governance-codes.js +2 -0
  41. package/dist/core/hooks.d.ts +10 -10
  42. package/dist/core/mailbox-store.d.ts +41 -7
  43. package/dist/core/mailbox-store.js +9 -3
  44. package/dist/core/parent-spec-seat.d.ts +14 -1
  45. package/dist/core/parent-spec-seat.js +6 -0
  46. package/dist/core/permission-rule-model.js +1 -1
  47. package/dist/core/roles.d.ts +20 -0
  48. package/dist/core/roles.js +35 -16
  49. package/dist/core/runner/assemble-result.d.ts +2 -0
  50. package/dist/core/runner/assemble-result.js +1 -1
  51. package/dist/core/runner/compaction-call-options.d.ts +26 -2
  52. package/dist/core/runner/compaction-call-options.js +9 -0
  53. package/dist/core/runner/compaction-seams.d.ts +4 -3
  54. package/dist/core/runner/compaction-seams.js +13 -6
  55. package/dist/core/runner/contracts.d.ts +56 -7
  56. package/dist/core/runner/decide-continuation.d.ts +2 -1
  57. package/dist/core/runner/decide-continuation.js +11 -10
  58. package/dist/core/runner/initial-run-state.js +1 -1
  59. package/dist/core/runner/prepare-caps-and-workflow.js +9 -1
  60. package/dist/core/runner/prepare-config-doors.js +4 -1
  61. package/dist/core/runner/prepare-context-lane.d.ts +9 -2
  62. package/dist/core/runner/prepare-context-lane.js +15 -10
  63. package/dist/core/runner/prepare-delegation-surface.d.ts +3 -2
  64. package/dist/core/runner/prepare-delegation-surface.js +9 -3
  65. package/dist/core/runner/prepare-file-history.js +10 -8
  66. package/dist/core/runner/prepare-gate-stations.d.ts +4 -0
  67. package/dist/core/runner/prepare-gate-stations.js +3 -1
  68. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  69. package/dist/core/runner/prepare-prompt-inputs.d.ts +6 -0
  70. package/dist/core/runner/prepare-prompt-inputs.js +2 -1
  71. package/dist/core/runner/prepare-task.js +22 -9
  72. package/dist/core/runner/prepare-wiring-manifest.d.ts +6 -1
  73. package/dist/core/runner/prepare-wiring-manifest.js +6 -1
  74. package/dist/core/runner/retired-runner-deps.d.ts +3 -1
  75. package/dist/core/runner/retired-runner-deps.js +1 -0
  76. package/dist/core/runner/run-compaction-machinery.d.ts +4 -2
  77. package/dist/core/runner/run-compaction-machinery.js +5 -3
  78. package/dist/core/runner/run-harness-handlers.js +1 -1
  79. package/dist/core/runner/run-identity-wiring.js +19 -1
  80. package/dist/core/runner/run-leg.js +1 -1
  81. package/dist/core/runner/run-notification-lane.js +8 -2
  82. package/dist/core/runner/run-recovery-lanes.js +9 -5
  83. package/dist/core/runner/run-settle-and-teardown.d.ts +4 -22
  84. package/dist/core/runner/run-settle-and-teardown.js +6 -10
  85. package/dist/core/runner/run-stop-and-final-verify.js +1 -0
  86. package/dist/core/runner/run-terminal-adoption.js +5 -5
  87. package/dist/core/runner/run-turn-boundary.js +32 -5
  88. package/dist/core/runner/runtask.d.ts +8 -1
  89. package/dist/core/runner/runtask.js +32 -1
  90. package/dist/core/runner-deps.d.ts +44 -13
  91. package/dist/core/swappable-deps.d.ts +20 -4
  92. package/dist/core/swappable-deps.js +6 -1
  93. package/dist/core/task-event.d.ts +5 -0
  94. package/dist/core/task-notification.d.ts +144 -2
  95. package/dist/core/task-notification.js +154 -5
  96. package/dist/core/task-result.d.ts +14 -0
  97. package/dist/core/task-spec.d.ts +7 -0
  98. package/dist/core/tool-catalog-entries.js +2 -1
  99. package/dist/core/tool-conformance.js +1 -0
  100. package/dist/core/tool-policy.d.ts +9 -4
  101. package/dist/core/tool-spec.d.ts +14 -0
  102. package/dist/core/wiring-manifest.d.ts +12 -0
  103. package/dist/core/wiring-manifest.js +5 -1
  104. package/dist/engine/harness/agent-harness.d.ts +14 -1
  105. package/dist/engine/harness/agent-harness.js +28 -3
  106. package/dist/engine/harness/types.d.ts +3 -1
  107. package/dist/engine/llm/types.d.ts +8 -7
  108. package/dist/engine/llm/types.js +1 -1
  109. package/dist/engine/session/import-validate.js +4 -0
  110. package/dist/index.d.ts +10 -7
  111. package/dist/index.js +7 -5
  112. package/dist/orchestration/run-workflow-tool.d.ts +4 -4
  113. package/dist/orchestration/workflow.d.ts +1 -1
  114. package/dist/orchestration/workflow.js +8 -5
  115. package/dist/shell/lex.d.ts +30 -51
  116. package/dist/shell/lex.js +237 -284
  117. package/dist/stores/cc/mailbox-store.js +3 -2
  118. package/dist/stores/file/corrupt-read-sink.d.ts +28 -2
  119. package/dist/stores/file/file-history-store.d.ts +2 -4
  120. package/dist/stores/file/file-history-store.js +1 -1
  121. package/dist/stores/file/fs-atomic.d.ts +16 -11
  122. package/dist/stores/file/fs-atomic.js +17 -6
  123. package/dist/stores/file/index.d.ts +5 -6
  124. package/dist/stores/file/index.js +1 -1
  125. package/dist/stores/file/mailbox-store.d.ts +4 -9
  126. package/dist/stores/file/mailbox-store.js +29 -30
  127. package/dist/stores/file/session-policy-store.d.ts +2 -0
  128. package/dist/stores/file/session-policy-store.js +1 -1
  129. package/dist/stores/file/session-store.d.ts +11 -7
  130. package/dist/stores/file/session-store.js +23 -16
  131. package/dist/stores/file/shared-ledger.js +3 -14
  132. package/dist/stores/file/tool-result-store.d.ts +2 -0
  133. package/dist/stores/file/tool-result-store.js +5 -2
  134. package/dist/stores/file/workflow-journal-store.js +14 -9
  135. package/dist/tools/fs/bash-program-position.d.ts +15 -24
  136. package/dist/tools/fs/bash-program-position.js +11 -29
  137. package/dist/tools/fs/bash-readonly-classifier.d.ts +125 -117
  138. package/dist/tools/fs/bash-readonly-classifier.js +400 -272
  139. package/dist/tools/fs/shell-read-check.js +3 -3
  140. package/dist/tools/fs/shell-read-establish.js +1 -1
  141. package/dist/tools/fs/shell-read-plan.d.ts +11 -6
  142. package/dist/tools/fs/shell-read-plan.js +167 -183
  143. package/dist/tools/fs/shell-read-word.d.ts +50 -0
  144. package/dist/tools/fs/shell-read-word.js +59 -0
  145. package/dist/tools/fs/shell-reentry-text.d.ts +10 -0
  146. package/dist/tools/fs/shell-reentry-text.js +73 -0
  147. package/package.json +1 -1
  148. package/test/export-surface.snapshot.json +58 -8
  149. package/wire-changes/core@7.30.0.json +1821 -0
@@ -1,3 +1,4 @@
1
+ import { CROSS_SESSION_MESSAGE_TAG } from "../agents/cross-session-envelope.js";
1
2
  import { DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, CompactionError, compact, computeFileLists, dryRunSummarizationClamp, estimateContextTokens, estimateTokens, formatFileOperations, formatPersistedOutputRefs, prepareCompaction, shouldCompact, } from "../internal/harness.js";
2
3
  import { fileArgPath } from "../tools/fs/safety.js";
3
4
  import { contextEditFrontier, resolveTriggerWindow } from "./context-edit.js";
@@ -81,9 +82,13 @@ export async function maybeCompact(opts) {
81
82
  const structuralTokens = ctx.messages.reduce((sum, m) => sum + estimateTokens(m, cpt), 0);
82
83
  const anchorStale = est.usageTokens > 0 && est.tokens > structuralTokens * STALE_ANCHOR_STRUCTURAL_MARGIN + overhead;
83
84
  const tokens = anchorStale || est.usageTokens === 0 ? structuralTokens + overhead : est.tokens;
84
- const window = resolveTriggerWindow(opts.model).window;
85
+ const window = resolveTriggerWindow(opts.model, opts.contextOverlay).window;
85
86
  const windowKnown = Number.isFinite(window) && window > 0;
86
- const windowField = opts.model.autoCompactTokens != null ? "autoCompactTokens" : opts.model.contextTokens != null ? "contextTokens" : "contextWindow";
87
+ const windowField = (opts.contextOverlay?.autoCompactTokens ?? opts.model.autoCompactTokens) != null
88
+ ? "autoCompactTokens"
89
+ : (opts.contextOverlay?.contextTokens ?? opts.model.contextTokens) != null
90
+ ? "contextTokens"
91
+ : "contextWindow";
87
92
  const settings = sanitizeCompactionSettings(rawSettings, window);
88
93
  const contextUsage = windowKnown
89
94
  ? { usedTokens: tokens, windowTokens: window, compactAtTokens: window - settings.reserveTokens }
@@ -459,6 +464,17 @@ export async function maybeCompact(opts) {
459
464
  ...(opts.activeTools !== undefined && opts.activeTools.length > 0 ? { activeTools: [...opts.activeTools] } : {}),
460
465
  ...(opts.gitRestate !== undefined ? { gitAnnouncement: { kind: opts.gitRestate.pending.kind, hash: opts.gitRestate.pending.hash, pending: true } } : {}),
461
466
  }, false);
467
+ if (opts.onCommitted !== undefined) {
468
+ const firstKeptIdx = branch.findIndex((e) => e.id === firstKeptEntryId);
469
+ const effect = firstKeptIdx >= 0
470
+ ? { summarizedEntryIds: new Set(branch.slice(0, firstKeptIdx).map((e) => e.id)) }
471
+ : { summarizedEntryIds: "unknown" };
472
+ try {
473
+ opts.onCommitted(effect);
474
+ }
475
+ catch {
476
+ }
477
+ }
462
478
  if (opts.gitRestate !== undefined) {
463
479
  try {
464
480
  await opts.gitRestate.land();
@@ -514,6 +530,7 @@ export async function maybeCompact(opts) {
514
530
  }), "compaction.postCompact");
515
531
  }
516
532
  const endAt = Date.now();
533
+ const summarizedCrossSessionMessages = countSummarizedCrossSessionMessages(branch, firstKeptEntryId);
517
534
  return {
518
535
  contextUsage,
519
536
  compacted: true,
@@ -522,6 +539,7 @@ export async function maybeCompact(opts) {
522
539
  triggerTokens: tokens,
523
540
  postTriggerTokens,
524
541
  firstKeptEntryId,
542
+ ...(summarizedCrossSessionMessages > 0 ? { summarizedCrossSessionMessages } : {}),
525
543
  reused,
526
544
  naturalTrigger,
527
545
  durationMs: endAt - compactStartAt,
@@ -558,9 +576,24 @@ export function mintCompactedEvent(comp, trigger, identity) {
558
576
  ...(comp.fallbackReason !== undefined ? { fallbackReason: comp.fallbackReason } : {}),
559
577
  ...(comp.clampedRatio !== undefined ? { clampedRatio: comp.clampedRatio } : {}),
560
578
  ...(comp.clampReason !== undefined ? { clampReason: comp.clampReason } : {}),
579
+ ...(comp.summarizedCrossSessionMessages !== undefined ? { summarizedCrossSessionMessages: comp.summarizedCrossSessionMessages } : {}),
561
580
  ...identity,
562
581
  };
563
582
  }
583
+ export function countSummarizedCrossSessionMessages(branch, firstKeptEntryId) {
584
+ const firstKeptIdx = branch.findIndex((e) => e.id === firstKeptEntryId);
585
+ const summarized = firstKeptIdx >= 0 ? branch.slice(0, firstKeptIdx) : branch;
586
+ let n = 0;
587
+ for (const e of summarized) {
588
+ if (e.type !== "message" || e.message === undefined || e.message.role !== "user" || e.message.provenance !== "engine-note")
589
+ continue;
590
+ const c = e.message.content;
591
+ const text = typeof c === "string" ? c : Array.isArray(c) ? c.map((b) => (b !== null && typeof b === "object" && b.type === "text" ? String(b.text ?? "") : "")).join("") : "";
592
+ if (text.startsWith(`<${CROSS_SESSION_MESSAGE_TAG} `) || text.startsWith(`<${CROSS_SESSION_MESSAGE_TAG}>`))
593
+ n += 1;
594
+ }
595
+ return n;
596
+ }
564
597
  export function nextTrimForceBackoff(prev, comp, trimForced = false) {
565
598
  if (trimForced && comp.naturalTrigger !== true && comp.compacted && comp.freedTokens !== undefined && comp.freedTokens <= 0) {
566
599
  return true;
@@ -77,3 +77,14 @@ export interface ChildClampSource {
77
77
  * — so a clamp can never end one level deep, and a new seat cannot reach some constructors and not others.
78
78
  */
79
79
  export declare function childClampSeats(source: ChildClampSource): Partial<TaskSpec>;
80
+ /**
81
+ * The OWN posture of an engine-built leg that never asks the person (the observer sidecar, the verifier): no
82
+ * human-facing tool is mounted AND the leg declares itself headless. Both, always together — the spec's posture
83
+ * outranks the one a leg inherits from its parent or its deployment, and the prepare door refuses an
84
+ * "interactive" posture paired with `interactiveTools: false`, so a leg that stated only the clamp was refused
85
+ * under every interactive parent. Spread ABOVE {@link childClampSeats}: a caller clamp can only tighten it.
86
+ */
87
+ export declare function engineHeadlessLegSeats(): {
88
+ interactiveTools: false;
89
+ interactionPosture: "headless";
90
+ };
@@ -27,3 +27,6 @@ export function childClampSeats(source) {
27
27
  : {}),
28
28
  };
29
29
  }
30
+ export function engineHeadlessLegSeats() {
31
+ return { interactiveTools: false, interactionPosture: "headless" };
32
+ }
@@ -1,5 +1,6 @@
1
1
  import type { AgentMessage } from "../internal/harness.js";
2
2
  import type { Model } from "../internal/llm.js";
3
+ import { type ContextWindowOverlay } from "./context-settings.js";
3
4
  /**
4
5
  * Small-window FLOOR fraction of the clearStale/compaction frontier (see {@link contextEditFrontier},
5
6
  * which is the single source for both the clearStale budget and the compaction-threshold clamp).
@@ -224,7 +225,7 @@ export interface TriggerWindowResolution {
224
225
  * chain is the drift this function exists to end. Non-finite/≤0 declared values keep their historic
225
226
  * meaning (each consumer's own unknown-window sentinel), deliberately untouched here.
226
227
  */
227
- export declare function resolveTriggerWindow(model: Model): TriggerWindowResolution;
228
+ export declare function resolveTriggerWindow(model: Model, overlay?: ContextWindowOverlay): TriggerWindowResolution;
228
229
  /** The token budget at which context-editing begins for a model (below the harder guard trim).
229
230
  * = {@link contextEditFrontier} of the model's AUTOCOMPACT window (CC-parity fixed deduction with
230
231
  * a 0.7 small-window floor) — the same point the compaction threshold is clamped to. Dual-window
@@ -232,7 +233,7 @@ export declare function resolveTriggerWindow(model: Model): TriggerWindowResolut
232
233
  * trim (`guardBudget`) stays on the physical window — which is why the window is read through
233
234
  * {@link resolveTriggerWindow}: an autocompact window declared ABOVE the physical one would put
234
235
  * this frontier above the guard (inverted geometry), so it is clamped there (slice 1b, X5). */
235
- export declare function editBudget(model: Model): number;
236
+ export declare function editBudget(model: Model, overlay?: ContextWindowOverlay): number;
236
237
  /** Content fingerprint of an occurrence — a sha-256 digest over the FULL canonical content form
237
238
  * (per block: type + complete text bytes, and for non-text blocks the mime type + payload length
238
239
  * + a payload head), replay's mis-paint guard (adversarial rounds 1+2): ordinal mapping alone
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { DEFAULT_CHARS_PER_TOKEN, estimateTokens } from "../internal/harness.js";
3
3
  import { isToolResult } from "./message-utils.js";
4
+ import { physicalWindowOf } from "./context-settings.js";
4
5
  import { PERSISTED_OUTPUT_PREFIX, offloadPagebackHint } from "./tool-result-store.js";
5
6
  import { assertNoRetiredKeys } from "./retired-keys.js";
6
7
  import { compactableToolNames } from "./tool-registry.js";
@@ -179,9 +180,9 @@ export function clearStaleToolResults(messages, opts) {
179
180
  reportCleared(opts.onCleared, clears, tokensSaved);
180
181
  return out;
181
182
  }
182
- export function resolveTriggerWindow(model) {
183
- const physical = model.contextTokens ?? model.contextWindow;
184
- const declared = model.autoCompactTokens;
183
+ export function resolveTriggerWindow(model, overlay) {
184
+ const physical = physicalWindowOf(model, overlay);
185
+ const declared = overlay?.autoCompactTokens ?? model.autoCompactTokens;
185
186
  if (declared !== undefined &&
186
187
  Number.isFinite(declared) &&
187
188
  declared > 0 &&
@@ -192,8 +193,8 @@ export function resolveTriggerWindow(model) {
192
193
  }
193
194
  return { window: declared ?? physical, clamped: false };
194
195
  }
195
- export function editBudget(model) {
196
- const window = resolveTriggerWindow(model).window;
196
+ export function editBudget(model, overlay) {
197
+ const window = resolveTriggerWindow(model, overlay).window;
197
198
  if (!Number.isFinite(window) || window <= 0) {
198
199
  return Number.POSITIVE_INFINITY;
199
200
  }
@@ -1,5 +1,6 @@
1
1
  import type { AgentMessage } from "../internal/harness.js";
2
2
  import type { Model } from "../internal/llm.js";
3
+ import { type ContextWindowOverlay } from "./context-settings.js";
3
4
  /**
4
5
  * Last-resort, in-task context trim. Proper compaction (LLM summary) runs *between* tasks;
5
6
  * this prevents a single task whose internal tool-loop balloons from overflowing the window.
@@ -99,4 +100,4 @@ charsPerToken?: number): GitFrameGuardOutcome;
99
100
  * above the compaction/clearStale frontier. Dual-window semantics: deliberately the PHYSICAL
100
101
  * window (`contextTokens ?? contextWindow`), NOT `autoCompactTokens` — the guard protects the
101
102
  * real request limit; a lowered autocompact window only moves the trigger-side geometry. */
102
- export declare function guardBudget(model: Model): number;
103
+ export declare function guardBudget(model: Model, overlay?: ContextWindowOverlay): number;
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_CHARS_PER_TOKEN, estimateContextTokens, estimateTokens } from "../internal/harness.js";
2
2
  import { isToolResult, messageRole } from "./message-utils.js";
3
+ import { physicalWindowOf } from "./context-settings.js";
3
4
  const GUARD_FRACTION = 0.85;
4
5
  const GUARD_HEADROOM_TOKENS = 23000;
5
6
  function isSummary(m) {
@@ -190,8 +191,8 @@ export function protectGitFrame(before, trimmed, budgetTokens, frame, charsPerTo
190
191
  }
191
192
  return { action: "over_budget" };
192
193
  }
193
- export function guardBudget(model) {
194
- const window = model.contextTokens ?? model.contextWindow;
194
+ export function guardBudget(model, overlay) {
195
+ const window = physicalWindowOf(model, overlay);
195
196
  if (!Number.isFinite(window) || window <= 0) {
196
197
  return Number.POSITIVE_INFINITY;
197
198
  }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * The deployment's per-model CONTEXT-WINDOW overlay — the one deployment seat an IN-FLIGHT leg re-reads.
3
+ *
4
+ * WHY IT IS NOT ON THE MODEL. `Model.contextTokens` / `Model.autoCompactTokens` are two things at once: the
5
+ * geometry a leg measures itself against, and part of the immutable routing card a leg was handed at its
6
+ * prepare. The hot-swap law (`swappable-deps.ts`) is right about the card — nothing is re-routed, re-priced or
7
+ * re-fenced mid-run, because a running request must reach the endpoint it was priced and credentialed for.
8
+ * It was wrong about the geometry: an operator who lowers a model's usable window is not re-routing anything,
9
+ * they are changing a SETTING, and a leg that keeps filling the retired window until it ends is the one thing
10
+ * they were trying to prevent. The reference implementation keeps that setting off the model card for exactly
11
+ * this reason and re-reads it on every request, clamping it to the model's physical window.
12
+ *
13
+ * So the overlay lives here, keyed by CATALOG WORD — the same word `ModelRef` already speaks. A leg carries
14
+ * the ref that chose its model (`ResolvedRole.modelRef` → the harness → `ToolExecuteContext.modelRef`), so
15
+ * the lookup is a plain read with no reverse search: an id→word search cannot answer (one model is reachable
16
+ * under several words, two words may route to one id, and an object-pinned model has no word at all), and a
17
+ * leg that knows its own word needs no search.
18
+ *
19
+ * WHAT IT DOES NOT DO. It carries no routing, credential, price or fence field, and never will: those are the
20
+ * card, and the card stays snapshotted. An OBJECT-pinned leg (a spec that named a `Model`, a `select` criteria)
21
+ * has no word and therefore no overlay — the pin is the caller saying "this exact model, as declared".
22
+ */
23
+ import type { Model } from "../internal/llm.js";
24
+ import type { ModelRef } from "./model-seat.js";
25
+ /**
26
+ * One model's overlay. Every member is a DECLARATION the deployment may change between requests; an absent
27
+ * member means "whatever the model card says", never a value.
28
+ * · `contextTokens` — the usable physical window (the model card's `contextTokens ?? contextWindow`).
29
+ * · `autoCompactTokens` — the trigger-side window (dual-window semantics: lowers the compaction trigger and
30
+ * the context-editing frontier without touching the physical request budget). Clamped to the physical
31
+ * window by the one resolver, exactly as a model-card declaration is.
32
+ * · `enabled` — compaction for legs on this model, the deployment's twin of `TaskSpec.compaction.enabled`
33
+ * and with the same reach (the routine boundary pass, the recovery pass and the end-of-task summary —
34
+ * one gate, inside `maybeCompact`, fed by one settings assembly). A request that states its own
35
+ * `compaction.enabled` outranks it: the request is the author, this seat is the deployment's default —
36
+ * the same "the spec's own value wins, the deployment fills an unset one" law every other seat follows.
37
+ */
38
+ export interface ContextWindowOverlay {
39
+ readonly contextTokens?: number;
40
+ readonly autoCompactTokens?: number;
41
+ readonly enabled?: boolean;
42
+ }
43
+ /**
44
+ * The seat: catalog word → overlay. Words absent from the table are "nothing declared".
45
+ *
46
+ * @contract context_settings.geometry_is_read_live — this is the ONE deployment seat a leg re-reads while it
47
+ * runs: every window number a leg is measured against (the compaction trigger, the sanitize clamp, the
48
+ * context-editing frontier, the request guard, the disclosed `contextUsage`) is taken from the overlay in
49
+ * force at the moment of THAT question, keyed by the word that chose the leg's current model, so a swap
50
+ * reaches boundaries of legs already in flight; the leg's ROUTING — endpoint, credentials, price, fence, the
51
+ * `Model` object itself — stays the generation the leg prepared under, and this seat can never carry any of
52
+ * it. A leg whose model was pinned as an object has no word and is never overlaid.
53
+ */
54
+ export type ContextSettings = Readonly<Record<string, ContextWindowOverlay>>;
55
+ /**
56
+ * The overlay in force for a leg, read at the moment of the question. `ref` is the leg's own catalog word
57
+ * (`AgentHarness.getModelRef()` / `ResolvedRole.modelRef`); a `Model` OBJECT answers `undefined` because a
58
+ * pinned model has no word for the table to key on. OWN-property read: a word like `"constructor"` must mean
59
+ * "a key this deployment declared", never `Object.prototype`'s answer.
60
+ *
61
+ * The leg's own word first; when it declares nothing, the words it STANDS IN FOR — a tier word the default
62
+ * role answers with, a CC alias, `best` — as the tier expansion recorded them ({@link tierAliasVia}): only
63
+ * words that name the very model the leg's word resolves to, so a deployment key that a tier word passes by
64
+ * but that names ANOTHER model is never read. Nothing is found by searching for a model.
65
+ */
66
+ export declare function contextOverlayFor(settings: ContextSettings | undefined, ref: ModelRef | undefined, catalog?: {
67
+ models?: Record<string, Model>;
68
+ }): ContextWindowOverlay | undefined;
69
+ /**
70
+ * Every word the overlay declares must be a word of the model catalog (the deployment's keys, the tier words
71
+ * and aliases its `tiers` expanded) — `config.context_settings_unknown_word`. A misspelled word keys a leg
72
+ * nobody can run, so it would govern nothing while reading as in force. Checked at construction and on every
73
+ * swap against the catalog the swap leaves in force, so a catalog swap that would orphan a declared word is
74
+ * refused too. Returns `settings`.
75
+ */
76
+ export declare function assertContextSettingsWords<T extends ContextSettings | undefined>(settings: T, models: Record<string, Model> | undefined, where: string): T;
77
+ /**
78
+ * The seat's VALUE GATE — the same one the hot-swap door and the construction path apply, so a value the door
79
+ * admits is a value every later read admits. A malformed overlay is refused LOUDLY rather than repaired
80
+ * quietly: a window silently dropped back to the model card's is the misconfiguration this seat exists to make
81
+ * visible. `undefined` (the seat cleared) passes through.
82
+ *
83
+ * `where` names the caller in the message (`"swapDeps"` / `"RunnerDeps"`), since the two paths are the two
84
+ * moments an operator can get it wrong.
85
+ */
86
+ export declare function assertContextSettingsValue(v: unknown, where: string): ContextSettings | undefined;
87
+ /** Does a REQUEST's own `compaction.enabled` stand, or does the deployment's overlay decide? One law, stated
88
+ * once: the request is the author, the deployment fills an unset one, and nothing declared means ON. */
89
+ export declare function compactionEnabled(specEnabled: boolean | undefined, overlay: ContextWindowOverlay | undefined): boolean;
90
+ /**
91
+ * The overlay's view of a model's PHYSICAL window — the model card's chain with the deployment's live
92
+ * declaration CLAMPED over it. The single expression `guardBudget` and `resolveTriggerWindow` share.
93
+ *
94
+ * The clamp is the whole difference between a setting and a claim. An overlay says how much of a model a
95
+ * deployment wants used; it cannot say what the endpoint will accept, and the endpoint a leg talks to is the
96
+ * one it prepared under. Letting a declaration RAISE the window would let an operator publishing a larger
97
+ * model and its larger window in one step hand the bigger number to legs still routed at the smaller
98
+ * endpoint — the guard would stop protecting the limit it exists for. So: lower freely, never raise. (Same
99
+ * clamp the reference implementation applies to its configured window: `min(model window, configured)`.)
100
+ * A card with no usable window of its own has nothing to clamp against, and the declaration is then the only
101
+ * number there is.
102
+ */
103
+ export declare function physicalWindowOf(model: Model, overlay: ContextWindowOverlay | undefined): number;
@@ -0,0 +1,60 @@
1
+ import { tierAliasVia } from "./roles.js";
2
+ export function contextOverlayFor(settings, ref, catalog) {
3
+ if (settings === undefined || typeof ref !== "string")
4
+ return undefined;
5
+ for (const word of [ref, ...tierAliasVia(ref, catalog?.models)])
6
+ if (Object.hasOwn(settings, word))
7
+ return settings[word];
8
+ return undefined;
9
+ }
10
+ export function assertContextSettingsWords(settings, models, where) {
11
+ const unknown = Object.keys(settings ?? {}).filter((w) => models === undefined || !Object.hasOwn(models, w));
12
+ if (unknown.length === 0)
13
+ return settings;
14
+ const e = new Error(`${where}: contextSettings declares ${unknown.map((w) => JSON.stringify(w)).join(", ")}, which ${unknown.length === 1 ? "is" : "are"} not a word of the model catalog (its keys, and the tier words and aliases \`tiers\` expanded) — an overlay no leg can be keyed by governs nothing; the current settings stay in force`);
15
+ e.code = "config.context_settings_unknown_word";
16
+ throw e;
17
+ }
18
+ export function assertContextSettingsValue(v, where) {
19
+ if (v === undefined)
20
+ return undefined;
21
+ if (v === null || typeof v !== "object" || Array.isArray(v)) {
22
+ throw new Error(`${where}: contextSettings must be a plain Record<string, {contextTokens?, autoCompactTokens?, enabled?}> (got ${v === null ? "null" : Array.isArray(v) ? "array" : typeof v})`);
23
+ }
24
+ const out = {};
25
+ for (const [word, raw] of Object.entries(v)) {
26
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
27
+ throw new Error(`${where}: contextSettings[${JSON.stringify(word)}] must be a plain object (got ${raw === null ? "null" : Array.isArray(raw) ? "array" : typeof raw})`);
28
+ }
29
+ const o = raw;
30
+ const unknown = Object.keys(o).filter((k) => k !== "contextTokens" && k !== "autoCompactTokens" && k !== "enabled");
31
+ if (unknown.length > 0) {
32
+ throw new Error(`${where}: contextSettings[${JSON.stringify(word)}] carries ${unknown.map((k) => JSON.stringify(k)).join(", ")} — this seat declares window geometry only (contextTokens | autoCompactTokens | enabled); routing, pricing and fencing stay on the model card`);
33
+ }
34
+ for (const k of ["contextTokens", "autoCompactTokens"]) {
35
+ const n = o[k];
36
+ if (n !== undefined && (typeof n !== "number" || !Number.isFinite(n) || n <= 0)) {
37
+ throw new Error(`${where}: contextSettings[${JSON.stringify(word)}].${k} must be a positive finite number (got ${JSON.stringify(n)}) — an unusable window is a configuration defect, not a request to fall back to the model's own`);
38
+ }
39
+ }
40
+ if (o.enabled !== undefined && typeof o.enabled !== "boolean") {
41
+ throw new Error(`${where}: contextSettings[${JSON.stringify(word)}].enabled must be a boolean (got ${JSON.stringify(o.enabled)})`);
42
+ }
43
+ out[word] = Object.freeze({
44
+ ...(o.contextTokens !== undefined ? { contextTokens: o.contextTokens } : {}),
45
+ ...(o.autoCompactTokens !== undefined ? { autoCompactTokens: o.autoCompactTokens } : {}),
46
+ ...(o.enabled !== undefined ? { enabled: o.enabled } : {}),
47
+ });
48
+ }
49
+ return Object.freeze(out);
50
+ }
51
+ export function compactionEnabled(specEnabled, overlay) {
52
+ return specEnabled ?? overlay?.enabled ?? true;
53
+ }
54
+ export function physicalWindowOf(model, overlay) {
55
+ const card = model.contextTokens ?? model.contextWindow;
56
+ const declared = overlay?.contextTokens;
57
+ if (declared === undefined)
58
+ return card;
59
+ return Number.isFinite(card) && card > 0 ? Math.min(declared, card) : declared;
60
+ }
@@ -172,6 +172,16 @@ export interface EngineNotice {
172
172
  * - `"config.write_face_swapped"` — the same line for the WRITE seat (`RunnerDeps.writeFace`),
173
173
  * same payload, same laws. A SEPARATE code rather than a face field on the line above: the audience
174
174
  * table and every consumer table are keyed by code.
175
+ * - `"config.context_settings_swapped"` — `Runner.swapDeps` replaced the deployment's per-model
176
+ * context-window overlay (`RunnerDeps.contextSettings`, the `contextSettings` seat of the swappable
177
+ * set) without a restart. Unlike every other swappable seat this one is read LIVE: a running leg
178
+ * measures its next boundary (the compaction trigger, the sanitize clamp, the request guard, the
179
+ * disclosed `contextUsage`) against the new windows, while its routing stays on the generation it
180
+ * prepared under. `detail: { words, declared, previousDeclared, occurrenceId }` — the declared model
181
+ * WORDS (the first eight) and the two counts, never the numbers: an overlay's values are deployment
182
+ * configuration and this line is an operator breadcrumb, not a config dump. A candidate outside the
183
+ * value gate (`assertContextSettingsValue`) throws atomically and mints nothing. Own `occurrenceId`
184
+ * for the same reason as the catalog line. Dedup key: (code, occurrenceId).
175
185
  * - `"route.fallback_to_primary"` (key↔URL pairing, `src/brain/route-adjudicator.ts`) — a
176
186
  * DERIVED-leg model (role/tier/system-default resolution, never a caller-explicit one) failed
177
187
  * the pairing pre-flight and the seat fell back to the primary model instead of sinking the
@@ -590,7 +600,7 @@ export interface EngineNotice {
590
600
  * disposition table with the error dispositions. The loser MUST NOT retry into the winner's
591
601
  * account. A consumer diffing its own table against the catalog must not add a row for it.
592
602
  *
593
- * - `"config.peer_admission_out_of_range"` (#551) — a `RunnerDeps.peerAdmission` field the
603
+ * - `"config.peer_admission_out_of_range"` (#551) — a `RunnerDeps.peerLane` field the
594
604
  * resolver could not use: outside its legal range, or not a finite number at all (a string off
595
605
  * an untyped host's wiring, `NaN`, `Infinity`). THAT FIELD falls back to its own default (never
596
606
  * a clamp to the nearest edge, never a whole-config reject) and every other field is unaffected.
@@ -0,0 +1,97 @@
1
+ /** What ONE landed compaction did to the transcript, recorded at its commit point. */
2
+ export interface CompactionEffect {
3
+ /** The entry ids that went into the summarized prefix (everything before `firstKeptEntryId`), or
4
+ * `"unknown"` when the kept-tail anchor could not be located in the branch — the conservative
5
+ * reading is that the whole branch was summarized, so a reader treats `"unknown"` as "every
6
+ * receipt this run holds is invalidated" (the same direction
7
+ * {@link import("./auto-compaction.js").countSummarizedCrossSessionMessages} already takes). */
8
+ readonly summarizedEntryIds: ReadonlySet<string> | "unknown";
9
+ }
10
+ /**
11
+ * One receipt ASK: the carrier a frame rides, and — for a frame that rides a TOOL RESULT — the
12
+ * text {@link frameSurvivesInToolResult} must find in that result as committed. An injected frame is its
13
+ * own carrier (the committed message IS the frame), so it carries no text to check.
14
+ */
15
+ export interface FrameReceiptAsk {
16
+ /** The injection queue's dedup key of the frame itself, or {@link toolResultReceiptKey} of the call whose result carries it. */
17
+ readonly carrier: string;
18
+ readonly frameText?: string;
19
+ }
20
+ /**
21
+ * The leg's settlement seam, one cell per run.
22
+ *
23
+ * `awaiting` is an ASK, not a log: only keys a consumer registered are ever given a receipt, an ask
24
+ * leaves when its carrier commits, so a run that delivers thousands of ordinary notification frames
25
+ * does not accumulate one map entry per frame. The key is the consumer's per-frame key.
26
+ */
27
+ export interface FrameSettlement {
28
+ /** Receipt asks a consumer is waiting on (key → ask). The receipt sink records nothing for any other key. */
29
+ readonly awaiting: Map<string, FrameReceiptAsk>;
30
+ /** Receipts collected for awaited keys: key → the transcript entry id the frame landed in. */
31
+ readonly receipts: Map<string, string>;
32
+ /** Awaited keys whose carrier COMMITTED WITHOUT the frame and nothing the HOST did explains it (an engine-side
33
+ * crop): shown nothing, so owed again — never a receipt. */
34
+ readonly withheld: Set<string>;
35
+ /** Carriers whose result a HOST hook rewrote (`postToolUse.updatedOutput`) before the commit — recorded only
36
+ * for a carrier some ask rides ({@link noteHostRewrite}), consumed by the receipt at its commit: a frame the
37
+ * rewrite kept gets an ordinary receipt, a frame it removed is host-withheld. */
38
+ readonly hostRewritten: Set<string>;
39
+ /** Awaited keys whose frame is not in the host's rewrite (removed, truncated, or reshaped beyond whitespace): the
40
+ * host decided what the model reads, so the consumer settles the record as the host's decision (terminal),
41
+ * discloses it loudly, and never hands it over again. */
42
+ readonly hostWithheld: Set<string>;
43
+ /** Compactions that LANDED this run and have not been consumed by a settlement pass yet, in landing
44
+ * order. Recorded only while {@link FrameSettlement.wantsEffects} says a record could still be
45
+ * broken by one, and cleared by the consumer's LOCAL settlement half — which needs no lease, so an
46
+ * unclaimable or empty box cannot leave them piling up for the whole leg. */
47
+ readonly compactions: CompactionEffect[];
48
+ /**
49
+ * #1036 — "is this engine frame's record still parked in a DURABLE box?" Set by the
50
+ * session-box drain when it binds. The terminal sweep hands back engine-note payloads that never
51
+ * drained and the runner pends them PER SESSION (an in-process copy); for a frame whose record the
52
+ * drain no longer acks on delivery, that copy would be a second source of truth — the box already
53
+ * re-serves it to the session's next run. Absent (no drain bound) ⇒ the pre-1036 behavior, which is
54
+ * correct there: nothing else holds those frames.
55
+ */
56
+ boxBacked?: (payload: unknown) => boolean;
57
+ /**
58
+ * #1036 — "is there a record a landed compaction could still break?" Asked at the compaction
59
+ * COMMIT beat, before an effect is recorded at all. Absent (no drain bound) or `false` ⇒ the effect
60
+ * is not recorded: nothing would ever read it, and a leg that compacts many times would otherwise
61
+ * hold every landing's entry-id set until it ended.
62
+ */
63
+ wantsEffects?: () => boolean;
64
+ }
65
+ export declare function createFrameSettlement(): FrameSettlement;
66
+ /** The carrier key of a TOOL RESULT (the `ReadInbox` pull face): its own domain, so it can never
67
+ * collide with an injection queue dedup key. One tool result can carry SEVERAL frames, each asked for
68
+ * under its own key with this carrier. */
69
+ export declare function toolResultReceiptKey(toolCallId: string): string;
70
+ /**
71
+ * THE rule for a frame that rides a TOOL RESULT: the result can be replaced (`postToolUse.updatedOutput`)
72
+ * or cropped (the aggregate result budget) after the frame was handed to it, so handing it over proves
73
+ * nothing — the frame counts as delivered only when its text is present in the result AS COMMITTED
74
+ * (a string, or the text blocks, each read whole), compared with every whitespace run on both sides folded
75
+ * to one space: a rewrite that only re-spaces, re-indents or re-wraps the frame kept it. Any other change
76
+ * (a word, an escape, the case) did not. Before the commit, nothing is proven. The inbox receipt
77
+ * below reads it; the listing ride (`prepare-listings.ts`) does not call it — it never commits announced
78
+ * state at ride time, the conservative answer for a frame whose commit has not happened yet.
79
+ */
80
+ export declare function frameSurvivesInToolResult(committedContent: unknown, frameText: string): boolean;
81
+ /** The station that applies a HOST hook's `updatedOutput` marks the carrier — only when an ask rides it, so an
82
+ * ordinary rewritten result leaves nothing behind. */
83
+ export declare function noteHostRewrite(seam: FrameSettlement, toolCallId: string): void;
84
+ /** The receipt sink's ONE writer: every ask riding `carrier` is answered — a receipt when its frame is in
85
+ * the committed content (or it is its own carrier); when it is not, host-withheld if a host hook rewrote the
86
+ * carrier (the host's decision), withheld otherwise (owed again). */
87
+ export declare function recordFrameReceipt(seam: FrameSettlement, carrier: string, committedContent: unknown, entryId: string): void;
88
+ /** The carrier a committed frame was riding, as the harness reports it at the transcript append — a tool
89
+ * result with the content it was committed with (after every result transformation). */
90
+ export type CommittedFrameCarrier = {
91
+ kind: "engine_note";
92
+ payload: unknown;
93
+ } | {
94
+ kind: "tool_result";
95
+ toolCallId: string;
96
+ content: unknown;
97
+ };
@@ -0,0 +1,36 @@
1
+ export function createFrameSettlement() {
2
+ return { awaiting: new Map(), receipts: new Map(), withheld: new Set(), hostRewritten: new Set(), hostWithheld: new Set(), compactions: [] };
3
+ }
4
+ export function toolResultReceiptKey(toolCallId) {
5
+ return `tool_result:${toolCallId}`;
6
+ }
7
+ const foldWhitespace = (text) => text.replace(/\s+/g, " ");
8
+ export function frameSurvivesInToolResult(committedContent, frameText) {
9
+ const frame = foldWhitespace(frameText);
10
+ if (typeof committedContent === "string")
11
+ return foldWhitespace(committedContent).includes(frame);
12
+ if (!Array.isArray(committedContent))
13
+ return false;
14
+ return committedContent.some((b) => b.type === "text" && typeof b.text === "string" && foldWhitespace(b.text).includes(frame));
15
+ }
16
+ export function noteHostRewrite(seam, toolCallId) {
17
+ const carrier = toolResultReceiptKey(toolCallId);
18
+ for (const ask of seam.awaiting.values()) {
19
+ if (ask.carrier === carrier) {
20
+ seam.hostRewritten.add(carrier);
21
+ return;
22
+ }
23
+ }
24
+ }
25
+ export function recordFrameReceipt(seam, carrier, committedContent, entryId) {
26
+ const hostRewrote = seam.hostRewritten.delete(carrier);
27
+ for (const [key, ask] of seam.awaiting) {
28
+ if (ask.carrier !== carrier)
29
+ continue;
30
+ seam.awaiting.delete(key);
31
+ if (ask.frameText === undefined || frameSurvivesInToolResult(committedContent, ask.frameText))
32
+ seam.receipts.set(key, entryId);
33
+ else
34
+ (hostRewrote ? seam.hostWithheld : seam.withheld).add(key);
35
+ }
36
+ }
@@ -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", "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"];
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",
@@ -198,6 +199,7 @@ const NOTICE_AUDIENCE_TABLE = {
198
199
  "config.env_timeout_discarded": "operator",
199
200
  "config.materialize_env_discarded": "operator",
200
201
  "config.models_swapped": "operator",
202
+ "config.context_settings_swapped": "operator",
201
203
  "config.read_face_swapped": "operator",
202
204
  "config.read_face_deployment_clamped": "operator",
203
205
  "config.write_face_swapped": "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
  }