@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
@@ -40,3 +40,11 @@ export function createRepetitionPoll() {
40
40
  },
41
41
  };
42
42
  }
43
+ export function noteResponseModel(partial, reported, requestedId) {
44
+ if (typeof reported !== "string" || reported === "")
45
+ return;
46
+ if (reported === requestedId)
47
+ delete partial.responseModel;
48
+ else
49
+ partial.responseModel = reported;
50
+ }
@@ -1,9 +1,11 @@
1
1
  import { type AgentMessage, type CompactionSettings, type Session, type ThinkingLevel } from "../internal/harness.js";
2
2
  import type { Message, Model, Tool } from "../internal/llm.js";
3
+ import type { ContextWindowOverlay } from "./context-settings.js";
3
4
  import type { Hooks } from "./hooks.js";
4
5
  import type { TraceEvent } from "./trace.js";
5
6
  import type { Brain } from "./types.js";
6
7
  import type { TaskEvent, TaskEventIdentity } from "./task-event.js";
8
+ import type { CompactionEffect } from "./frame-settlement.js";
7
9
  /**
8
10
  * Default summarization focus, applied when a task does not provide `compaction.instructions`.
9
11
  * Addresses the common fidelity failure where summaries drop specific facts.
@@ -128,6 +130,12 @@ export interface MaybeCompactOptions {
128
130
  };
129
131
  /** Model whose context window bounds the budget (the model that runs the next turn). */
130
132
  model: Model;
133
+ /** The deployment's LIVE per-model window overlay for THIS pass ({@link RunnerDeps.contextSettings}
134
+ * keyed by the leg's catalog word). Read by the caller at the moment of the pass, never snapshotted:
135
+ * every window number below — the trigger, the sanitize clamp, the disclosed `contextUsage` — is
136
+ * measured through it, so an operator who lowers a model's window reaches the legs already running.
137
+ * Absent ⇒ the model card's own declarations, unchanged. */
138
+ contextOverlay?: ContextWindowOverlay;
131
139
  /** Optional cheaper model used to produce the summary. Falls back to `model`.
132
140
  * design/145 §1 (semantics精确化): used only while WINDOW-SAFE and the budget gate allows — when a
133
141
  * dry-run clamp shows the conversation would lose more than `settings.clampTolerance` of its
@@ -352,6 +360,23 @@ export interface MaybeCompactOptions {
352
360
  path: string;
353
361
  content: string;
354
362
  }>, preserveReadState?: ReadonlyArray<string>) => void;
363
+ /**
364
+ * #1036 — THE COMMIT-BEAT EFFECT SINK: what this compaction did to the transcript, reported
365
+ * once, right after the `appendCompaction` that landed it. `summarizedEntryIds` = every entry id
366
+ * that fell into the summarized prefix (before `firstKeptEntryId`); `"unknown"` when the kept-tail
367
+ * anchor is not in the branch — the conservative reading (the same one
368
+ * {@link countSummarizedCrossSessionMessages} already takes) is that the whole branch went into
369
+ * the summary.
370
+ *
371
+ * Why HERE and not at the three call sites: a fact about a landed compaction has exactly one
372
+ * birthplace, and an effect minted per lane would be a rule written three times (and, the two
373
+ * times someone forgets, a lane where a summarized frame is silently never repaired). The three
374
+ * lanes reach it through the options assembly they already share.
375
+ *
376
+ * Observe-only, like every post-landing side effect here: a throwing sink must not fail a landed
377
+ * compaction.
378
+ */
379
+ onCommitted?: (effect: CompactionEffect) => void;
355
380
  /**
356
381
  * env-tail migration — the git-status lane's compaction interaction (two-phase receipt). The git
357
382
  * frame lives on the MESSAGE plane, does not enter the summary, and the projection strips it from
@@ -657,6 +682,8 @@ export interface CompactedFrameSource {
657
682
  fallbackReason?: "window";
658
683
  clampedRatio?: number;
659
684
  clampReason?: "budget" | "tolerance";
685
+ /** Cross-session message frames that sat in the summarized prefix (present iff ≥ 1). */
686
+ summarizedCrossSessionMessages?: number;
660
687
  }
661
688
  /**
662
689
  * THE mint of the `compacted` wire frame — one function, three lanes (the turn boundary, the
@@ -673,6 +700,21 @@ export interface CompactedFrameSource {
673
700
  export declare function mintCompactedEvent(comp: CompactedFrameSource, trigger: "auto" | "manual" | "forced", identity: TaskEventIdentity): Extract<TaskEvent, {
674
701
  type: "compacted";
675
702
  }>;
703
+ /**
704
+ * How many cross-session message frames the summarized prefix held: the engine-note user messages
705
+ * whose text is the drain's `<cross-session-message …>` envelope, before the kept tail begins. The
706
+ * frames were delivered (acked from the box) and are now only whatever the summary kept of them — the
707
+ * count is what lets a consumer say so; `0` is reported as absence.
708
+ */
709
+ export declare function countSummarizedCrossSessionMessages(branch: ReadonlyArray<{
710
+ type: string;
711
+ id: string;
712
+ message?: {
713
+ role: string;
714
+ provenance?: string;
715
+ content: unknown;
716
+ };
717
+ }>, firstKeptEntryId: string): number;
676
718
  /**
677
719
  * design/123 D4 (r5) — trim-force BACKOFF transition (pure; the run-scoped bit lives in the caller).
678
720
  * Spiral protection replacing the §25.2 floor's role on the trim-force path: trim pressure re-arms
@@ -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
@@ -222,6 +232,13 @@ export interface EngineNotice {
222
232
  * occurrenceId }` (rendered list bounded, total always exact); the `occurrenceId` is minted at this
223
233
  * announcement, independently of the `config.models_swapped` line's own, because the bounded render
224
234
  * makes two swaps moving the same first entries identical. Dedup key: (code, occurrenceId).
235
+ * - `"route.response_model_mismatch"` — the provider answered a turn naming a model that is NOT the id
236
+ * the request was sent with (an alias, a pool or a gateway-side substitution looks exactly like this),
237
+ * so it is advisory, never a refusal: the engine keeps asking for the catalog's id and reports the
238
+ * served name beside it (`turn_end.responseModel` / `TaskResult.responseModel`). ONCE PER LEG PER
239
+ * (requested, reported) PAIR — a catalog fact, not a turn fact. Audience `operator`;
240
+ * `detail: { requested, reported, sessionId, runId }`. Absent ⇒ the provider echoed the requested id
241
+ * or named nothing — one reading, not two.
225
242
  * - `"config.read_face_deployment_clamped"` (#237) — a deployment-wide `readFace: "open"` is not
226
243
  * in force beside a read-only (verifier) mount: it clamps to "roots" without throwing
227
244
  * (stricter-wins; the clamp verdict stands, only its occurrence was undisclosed). Announced
@@ -583,7 +600,7 @@ export interface EngineNotice {
583
600
  * disposition table with the error dispositions. The loser MUST NOT retry into the winner's
584
601
  * account. A consumer diffing its own table against the catalog must not add a row for it.
585
602
  *
586
- * - `"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
587
604
  * resolver could not use: outside its legal range, or not a finite number at all (a string off
588
605
  * an untyped host's wiring, `NaN`, `Infinity`). THAT FIELD falls back to its own default (never
589
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
+ }