@sema-agent/core 5.65.0 → 6.0.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 (159) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/agents/subagent.d.ts +2 -2
  3. package/dist/agents/subagent.js +11 -0
  4. package/dist/agents/verify.d.ts +1 -1
  5. package/dist/brain/anthropic.js +1 -1
  6. package/dist/brain/errors.d.ts +29 -0
  7. package/dist/brain/errors.js +20 -0
  8. package/dist/brain/open-responses.js +2 -2
  9. package/dist/brain/route-adjudicator.d.ts +8 -1
  10. package/dist/brain/route-adjudicator.js +1 -0
  11. package/dist/brain/status-sink.js +12 -1
  12. package/dist/brain/stream-engine.js +17 -6
  13. package/dist/core/auto-compaction.d.ts +26 -0
  14. package/dist/core/auto-compaction.js +7 -2
  15. package/dist/core/auto-mode-arming.d.ts +138 -0
  16. package/dist/core/auto-mode-arming.js +181 -0
  17. package/dist/core/auto-mode-defaults.d.ts +13 -0
  18. package/dist/core/auto-mode-defaults.js +5 -0
  19. package/dist/core/auto-mode-prompt.d.ts +14 -3
  20. package/dist/core/auto-mode-prompt.js +10 -7
  21. package/dist/core/auto-mode-rebuild.d.ts +75 -0
  22. package/dist/core/auto-mode-rebuild.js +41 -0
  23. package/dist/core/auto-mode.d.ts +15 -0
  24. package/dist/core/auto-mode.js +4 -2
  25. package/dist/core/checkpoint-store.d.ts +18 -0
  26. package/dist/core/context-edit.d.ts +47 -5
  27. package/dist/core/context-guard.d.ts +1 -1
  28. package/dist/core/file-history-retention.d.ts +106 -0
  29. package/dist/core/file-history-retention.js +36 -0
  30. package/dist/core/file-history-store.d.ts +768 -0
  31. package/dist/core/file-history-store.js +880 -0
  32. package/dist/core/governance-codes.d.ts +2 -1
  33. package/dist/core/governance-codes.js +14 -0
  34. package/dist/core/hooks.d.ts +39 -7
  35. package/dist/core/hooks.js +38 -21
  36. package/dist/core/lsp.d.ts +2 -2
  37. package/dist/core/mcp.d.ts +29 -7
  38. package/dist/core/memory-engine/consolidation-driver.d.ts +11 -0
  39. package/dist/core/memory-engine/consolidation-driver.js +71 -4
  40. package/dist/core/memory-engine/consolidation.d.ts +25 -2
  41. package/dist/core/memory-engine/consolidation.js +4 -1
  42. package/dist/core/memory-engine/distiller.d.ts +84 -1
  43. package/dist/core/memory-engine/distiller.js +68 -0
  44. package/dist/core/memory-engine/dual-root.js +3 -0
  45. package/dist/core/memory-engine/engine.d.ts +328 -15
  46. package/dist/core/memory-engine/engine.js +355 -29
  47. package/dist/core/memory-engine/file-backend.d.ts +30 -0
  48. package/dist/core/memory-engine/file-backend.js +14 -13
  49. package/dist/core/memory-engine/frontmatter.d.ts +22 -1
  50. package/dist/core/memory-engine/frontmatter.js +3 -0
  51. package/dist/core/memory-engine/header-hints.d.ts +5 -0
  52. package/dist/core/memory-engine/index.d.ts +5 -4
  53. package/dist/core/memory-engine/index.js +5 -4
  54. package/dist/core/memory-engine/layout.d.ts +88 -2
  55. package/dist/core/memory-engine/layout.js +112 -3
  56. package/dist/core/memory-engine/provenance-wording.d.ts +7 -0
  57. package/dist/core/memory-engine/provenance-wording.js +3 -0
  58. package/dist/core/memory-engine/tools.d.ts +89 -8
  59. package/dist/core/memory-engine/tools.js +263 -22
  60. package/dist/core/memory-engine/types.d.ts +64 -1
  61. package/dist/core/memory-recall.d.ts +6 -0
  62. package/dist/core/memory.d.ts +27 -1
  63. package/dist/core/memory.js +16 -2
  64. package/dist/core/permission-rule-consent.d.ts +20 -0
  65. package/dist/core/permission-rule-consent.js +12 -3
  66. package/dist/core/permission-rule-model.d.ts +67 -7
  67. package/dist/core/permission-rule-model.js +53 -7
  68. package/dist/core/permission-rule-store.js +15 -10
  69. package/dist/core/permission-rule-sync.js +15 -11
  70. package/dist/core/retention-policy.d.ts +9 -0
  71. package/dist/core/retention-policy.js +5 -2
  72. package/dist/core/retention.d.ts +13 -2
  73. package/dist/core/runner/assemble-result.d.ts +19 -1
  74. package/dist/core/runner/assemble-result.js +17 -2
  75. package/dist/core/runner/compaction-call-options.d.ts +93 -0
  76. package/dist/core/runner/compaction-call-options.js +3 -0
  77. package/dist/core/runner/memory-capture-optout.d.ts +80 -0
  78. package/dist/core/runner/memory-capture-optout.js +53 -0
  79. package/dist/core/runner/prepare-config-doors.d.ts +5 -0
  80. package/dist/core/runner/prepare-config-doors.js +16 -0
  81. package/dist/core/runner/prepare-hands-readface.d.ts +110 -5
  82. package/dist/core/runner/prepare-hands-readface.js +99 -7
  83. package/dist/core/runner/prepare-memory.d.ts +88 -0
  84. package/dist/core/runner/prepare-memory.js +305 -24
  85. package/dist/core/runner/prepare-task.d.ts +141 -1
  86. package/dist/core/runner/prepare-task.js +443 -79
  87. package/dist/core/runner/runtask.d.ts +9 -20
  88. package/dist/core/runner/runtask.js +133 -96
  89. package/dist/core/runner/session-file-state-replay.d.ts +18 -10
  90. package/dist/core/runner/session-file-state-replay.js +52 -1
  91. package/dist/core/runner/tool-disclosure.js +2 -1
  92. package/dist/core/runner/turn-attachments.d.ts +22 -12
  93. package/dist/core/session-store.d.ts +1 -1
  94. package/dist/core/session-store.js +6 -1
  95. package/dist/core/session.d.ts +34 -1
  96. package/dist/core/store-contracts/file-history-store-contract.d.ts +3 -0
  97. package/dist/core/store-contracts/file-history-store-contract.js +720 -0
  98. package/dist/core/task-registry-shared.js +11 -1
  99. package/dist/core/tool-errors.js +1 -0
  100. package/dist/core/tool-policy.d.ts +172 -1
  101. package/dist/core/tool-policy.js +32 -1
  102. package/dist/core/tool-result-store.js +2 -1
  103. package/dist/core/trace.d.ts +24 -0
  104. package/dist/core/types.d.ts +784 -89
  105. package/dist/core/types.js +4 -3
  106. package/dist/core/untrusted-text.d.ts +1 -1
  107. package/dist/core/untrusted-text.js +8 -0
  108. package/dist/core/workflow-run-store-contract.js +6 -2
  109. package/dist/core/workflow-run-store.d.ts +4 -1
  110. package/dist/engine/compaction/compaction.d.ts +88 -10
  111. package/dist/engine/compaction/compaction.js +109 -30
  112. package/dist/engine/execution-env/node-execution-env.d.ts +9 -1
  113. package/dist/engine/execution-env/node-execution-env.js +28 -0
  114. package/dist/engine/harness/agent-harness.d.ts +52 -1
  115. package/dist/engine/harness/agent-harness.js +36 -1
  116. package/dist/engine/harness/types.d.ts +26 -1
  117. package/dist/engine/llm/types.d.ts +50 -4
  118. package/dist/engine/loop/agent-loop.d.ts +5 -1
  119. package/dist/engine/loop/agent-loop.js +25 -0
  120. package/dist/engine/loop/types.d.ts +19 -0
  121. package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
  122. package/dist/engine/session/session.js +1 -1
  123. package/dist/index.d.ts +18 -8
  124. package/dist/index.js +14 -6
  125. package/dist/orchestration/run-workflow-tool.d.ts +20 -2
  126. package/dist/orchestration/run-workflow-tool.js +22 -3
  127. package/dist/orchestration/workflow-governance.d.ts +59 -1
  128. package/dist/orchestration/workflow-governance.js +61 -8
  129. package/dist/orchestration/workflow-meta.d.ts +4 -2
  130. package/dist/orchestration/workflow-primitives.js +56 -13
  131. package/dist/orchestration/workflow-types.d.ts +78 -2
  132. package/dist/orchestration/workflow.d.ts +20 -0
  133. package/dist/orchestration/workflow.js +163 -14
  134. package/dist/prompt-assembly/event-registry.js +1 -1
  135. package/dist/prompts/default.d.ts +7 -7
  136. package/dist/stores/file/file-history-store.d.ts +368 -0
  137. package/dist/stores/file/file-history-store.js +1248 -0
  138. package/dist/stores/file/index.d.ts +22 -13
  139. package/dist/stores/file/index.js +4 -4
  140. package/dist/stores/file/permission-rule-store.js +1 -0
  141. package/dist/stores/file/strategy-store.d.ts +3 -3
  142. package/dist/tools/fs/bash-readonly-classifier.d.ts +87 -3
  143. package/dist/tools/fs/bash-readonly-classifier.js +106 -4
  144. package/dist/tools/fs/fs-bash.js +9 -5
  145. package/dist/tools/fs/fs-shared.d.ts +52 -1
  146. package/dist/tools/fs/fs-shared.js +14 -0
  147. package/dist/tools/fs/fs-write.d.ts +5 -5
  148. package/dist/tools/fs/fs-write.js +71 -14
  149. package/dist/tools/fs/index.d.ts +6 -1
  150. package/dist/tools/fs/index.js +1 -1
  151. package/dist/tools/web.js +2 -1
  152. package/package.json +5 -1
  153. package/test/export-surface.snapshot.json +155 -23
  154. package/dist/core/file-snapshot-store.d.ts +0 -165
  155. package/dist/core/file-snapshot-store.js +0 -259
  156. package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +0 -13
  157. package/dist/core/store-contracts/file-snapshot-store-contract.js +0 -134
  158. package/dist/stores/file/file-snapshot-store.d.ts +0 -58
  159. package/dist/stores/file/file-snapshot-store.js +0 -353
@@ -152,16 +152,11 @@ export declare class Runner {
152
152
  readonly sessions: SessionStore;
153
153
  /** Per-sessionId serialization so two tasks never mutate one session concurrently. */
154
154
  private sessionLocks;
155
- /** (a)roots whose rewind-files snapshot hit `too_large`, with the refusal time.
156
- * A 20G workspace otherwise pays a full bounded tree-walk (stat tens of thousands of files) EVERY
157
- * task just to re-discover the same refusal; once too_large, skip further snapshots for that root
158
- * (announced once via onError). E19 semantics unchanged rewind for that root was already
159
- * impossible. TTL'd (30min), not permanent — a long-lived Runner over a tree the
160
- * user later SHRINKS re-probes instead of being locked out until restart.
161
- * RB-430-b: `skipAnnounced` makes the SKIP itself observable — one announcement per cooldown window,
162
- * so an operator can tell "this turn produced no snapshot" from "snapshots are working", without the
163
- * per-turn log spam the latch exists to prevent. Cleared with the entry when the window elapses. */
164
- private snapshotTooLargeRoots;
155
+ /** #483 rung-1 per-session read-file-state seats (CC parity: readFileState is session-scoped;
156
+ * sema's map stays per-task, so the session scope lives here). A bounded CACHE, never an
157
+ * authority: a durable resume's checkpoint seed wins on its leg, and a miss (cross-process,
158
+ * evicted) falls back to the transcript-replay rung. See {@link SessionReadFileStates}. */
159
+ private readonly sessionReadStates;
165
160
  /** task notifications born BETWEEN turns (run torn down / harness already idle), parked
166
161
  * per session and drained into the session's next run at its first boundary. Runner-lived (outlives any
167
162
  * single run, like the registry handles that produce into it); bounded + drop-disclosing, see
@@ -274,13 +269,6 @@ export declare class Runner {
274
269
  * never rejects. Also fills the budget-excluded `stats.suggestions` line.
275
270
  */
276
271
  private suggestNextPrompts;
277
- /**
278
- * design/101 §E19 — capture a working-tree snapshot of the just-finished turn, keyed by the leaf
279
- * `SessionTreeEntry.id`. Best-effort + never throws (errors route to `onError(phase:"rewind")`). Runs against
280
- * ANY ExecutionEnv (local or remote) when a `fileSnapshotStore` is wired (GATE-SPLIT) — the
281
- * snapshot is portable (env FileSystem ops). A deployment wanting VM-snapshot instead wires a VM-backed store.
282
- */
283
- private snapshotTurn;
284
272
  /** Run a task to completion and return a machine-readable result. */
285
273
  runTask(spec: TaskSpec, internals?: RunInternals): Promise<TaskResult>;
286
274
  /**
@@ -429,9 +417,10 @@ export declare class Runner {
429
417
  * reaper is the backstop for an env orphaned by a rare pre-teardown throw (same posture as `mcp`).
430
418
  *
431
419
  * Runs from the run-loop tail AFTER every env-reader (finish()'s compaction `attachWorkingFiles` +
432
- * `snapshotTurn`'s rewind-files capture), so the working tree is still readable when they run. This ORDER is the
433
- * fix for a teardown-order defect: the old in-`finish` destroy ran BEFORE `snapshotTurn`, so a factory-minted
434
- * remote/worktree lane snapshotted an already-destroyed env (plain host/static lanes were false-green).
420
+ * the file-history boundary settle), so the working tree is still readable when they run. This ORDER is
421
+ * the fix for a teardown-order defect: the old in-`finish` destroy ran BEFORE the capture seat, so a
422
+ * factory-minted remote/worktree lane captured an already-destroyed env (plain host/static lanes were
423
+ * false-green).
435
424
  *
436
425
  * design/49 v1.5 / design/76 §2.5: a SUSPENDED (`suspendRef.token`) or `needs_review` (`reviewRef.token`) pause
437
426
  * `suspendVM`-paused the env and persisted its `workspaceHandle` into the checkpoint — destroying it would
@@ -34,9 +34,10 @@ import { toImageContent } from "./image.js";
34
34
  import { OUTPUT_TOOL_NAME, SKILLS_LISTING_PROBE_HEADER, resolveOutputRetries } from "./synthetic-tools.js";
35
35
  import { cacheFamilyOf, usageCostMicroUsd } from "./usage-accounting.js";
36
36
  import { assembleResult, errorCodeOf } from "./assemble-result.js";
37
- import { ATTACHMENT_BYTE_CAP, CHANGED_FILES_MAX, AGENT_LISTING_REMOVED_HEADER, SKILLS_LISTING_DELTA_HEADER, SKILLS_LISTING_REMOVED_HEADER, advanceCadenceClock, agentListingDeltaHeader, attachmentEnvelopeTags, agentListingInitialHeader, replayAnnouncedListing, replayAnnouncedModels, clipToBytes, collectDateChange, collectDueAttachments, collectInstructionsChange, commitAgentListing, commitInstructionsChange, commitSkillsListing, createAttachmentState, rebaseCadenceWindows, reduceToolEnd, renderAgentListingDelta, renderMcpDroppedTools, renderMcpInstructionsDelta, renderOrphanedBackgroundTasks, selectMcpDroppedBatch, renderSkillsListingDelta, renderToolsDelta, stampWriteAnchor } from "./turn-attachments.js";
38
- import { buildWorkingFileAttachments, centerAdoptionOption, emitInputTruncated, forkContextOption } from "./compaction-call-options.js";
37
+ import { ATTACHMENT_BYTE_CAP, CHANGED_FILES_MAX, TOTAL_TOKENS_REMINDER_DEFAULT_MODE, AGENT_LISTING_REMOVED_HEADER, SKILLS_LISTING_DELTA_HEADER, SKILLS_LISTING_REMOVED_HEADER, advanceCadenceClock, agentListingDeltaHeader, attachmentEnvelopeTags, agentListingInitialHeader, replayAnnouncedListing, replayAnnouncedModels, clipToBytes, collectDateChange, collectDueAttachments, collectInstructionsChange, commitAgentListing, commitInstructionsChange, commitSkillsListing, createAttachmentState, rebaseCadenceWindows, reduceToolEnd, renderAgentListingDelta, renderMcpDroppedTools, renderMcpInstructionsDelta, renderOrphanedBackgroundTasks, selectMcpDroppedBatch, renderSkillsListingDelta, renderToolsDelta, stampWriteAnchor } from "./turn-attachments.js";
38
+ import { buildWorkingFileAttachments, centerAdoptionOption, contextInstructionFilesOption, emitInputTruncated, forkContextOption } from "./compaction-call-options.js";
39
39
  import { effectiveDelegationFacts, gatedCallIdOf, placementValueOrAbsent, prepareTask, resolveCheckpointStore } from "./prepare-task.js";
40
+ import { SessionReadFileStates } from "./prepare-hands-readface.js";
40
41
  import { settleTeardownLeg } from "./teardown-bounded.js";
41
42
  import { TOOL_SEARCH_NAME } from "./tool-disclosure.js";
42
43
  import { hasVerifiableStructureSignal } from "./grounding-signal.js";
@@ -48,7 +49,7 @@ import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntr
48
49
  import { appendInterruptionMarker, reconcileInterruptedSession } from "../session-reconcile.js";
49
50
  import { RunnerSharedToolResultStore } from "../tool-result-store.js";
50
51
  import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
51
- import { checkToolPolicyProjection, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, refuseOutOfContractDecision, screenApproverAttribution, toolPolicyNameSets } from "../tool-policy.js";
52
+ import { checkToolPolicyProjection, constraintChainDigest, constraintChainEntryOfSuppliedLayer, isApprovalSettledBy, refuseOutOfContractDecision, screenApproverAttribution, toolPolicyNameSets } from "../tool-policy.js";
52
53
  import { defaultTaskRegistry } from "../task-registry.js";
53
54
  import { discloseDroppedPending, isDelegatedAgentTerminal, isSystemInjectionPriority, isTerminalTaskNotification, PendingSessionNotifications, renderTaskNotificationXml, SYSTEM_INJECTION_PRIORITIES, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
54
55
  import { ToolDetachHub } from "../tool-detach.js";
@@ -57,7 +58,7 @@ import { workflowSizeGuidelineChangeNotice } from "../../orchestration/workflow-
57
58
  import { structuredFrom, toolOutputFrom } from "./tool-output-projection.js";
58
59
  function createRunState() {
59
60
  return {
60
- telemetry: { cacheFamily: "input-excludes-cached", pricing: { inputPer1M: 0, outputPer1M: 0 }, pricingConfigured: true, unpricedSpend: false, tracer: undefined, taskId: "", taskStart: 0, taskStartMonotonic: 0, cacheBreakReported: false },
61
+ telemetry: { cacheFamily: "input-excludes-cached", pricing: { inputPer1M: 0, outputPer1M: 0 }, pricingConfigured: true, unpricedSpend: false, tracer: undefined, taskId: "", runId: "", taskStart: 0, taskStartMonotonic: 0, cacheBreakReported: false },
61
62
  degrade: { degradeToModel: undefined, degraded: undefined, outputErrorStreak: 0, outputInvalid: false, recordDegraded: () => { } },
62
63
  limits: { turnsExceeded: false, budgetHit: undefined, budgetAxis: undefined, platformTerminal: undefined, outputRetryCap: 0, effectiveMaxTurns: undefined },
63
64
  budget: { remainingMicroUsd: undefined, maxCostMicroUsd: undefined, remainingTokens: undefined, maxTokensWindow: undefined, overBudget: () => undefined, streamCancel: false, callOutputChars: 0, lastStreamBudgetCheck: 0, projectedOverBudget: () => undefined },
@@ -124,8 +125,6 @@ const SUGGESTIONS_DEFAULT_COUNT = 3;
124
125
  const SUGGESTIONS_MAX_COUNT = 8;
125
126
  const SUGGESTIONS_TIMEOUT_MS = 30_000;
126
127
  const SUGGESTIONS_TRANSCRIPT_MESSAGES = 12;
127
- const SNAPSHOT_TIMEOUT_MS = 30_000;
128
- const SNAPSHOT_TOO_LARGE_TTL_MS = 30 * 60_000;
129
128
  const LIMIT_APPROACH_DEFAULT_THRESHOLDS = [0.8, 0.95];
130
129
  const DEFAULT_PRECALL_OUTPUT_TOKENS = 4096;
131
130
  function limitApproachFrames(index, axis, percent, mark) {
@@ -607,7 +606,13 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
607
606
  const mcpRemovals = mcpInstructionsOn && mcpDelta.pendingRemovals.length > 0 ? [...mcpDelta.pendingRemovals] : undefined;
608
607
  const mcpDropped = mcpInstructionsOn && prepared.mcp.droppedTools.length > 0 ? selectMcpDroppedBatch(prepared.mcp.droppedTools) : undefined;
609
608
  const budgetUsdOn = rs.attach.attachmentsCfg?.budgetUsd === true && rs.budget.maxCostMicroUsd !== undefined && rs.turn.lastTurnHadToolCalls;
610
- const totalTokensOn = rs.attach.attachmentsCfg?.totalTokensReminder === true && rs.turn.lastTurnHadToolCalls;
609
+ const totalTokensDeclared = rs.attach.attachmentsCfg?.totalTokensReminder;
610
+ const totalTokensMode = rs.attach.attachmentsCfg?.totalTokensReminderMode ?? TOTAL_TOKENS_REMINDER_DEFAULT_MODE;
611
+ const totalTokensArmedByDefault = totalTokensDeclared === undefined &&
612
+ eventDefaultOn("total_tokens_reminder") &&
613
+ totalTokensMode !== "fixed" &&
614
+ totalTokensMode !== "infinite";
615
+ const totalTokensOn = (totalTokensDeclared === true || totalTokensArmedByDefault) && rs.turn.lastTurnHadToolCalls;
611
616
  const toolSearchReminderOn = rs.attach.attachmentsCfg?.toolSearchReminder === true && prepared.deferredToolNames !== undefined;
612
617
  const undiscoveredTools = toolSearchReminderOn
613
618
  ? [...prepared.deferredToolNames].filter((n) => !prepared.activeTools.has(n)).sort()
@@ -897,6 +902,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
897
902
  overheadTokens: prepared.promptOverheadTokens,
898
903
  ...(prepared.activeTools.size > 0 ? { activeTools: [...prepared.activeTools] } : {}),
899
904
  workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
905
+ ...contextInstructionFilesOption(prepared),
900
906
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
901
907
  ...runnerHooks.seamCCompactionOptions(prepared),
902
908
  ...gitRestateOption(prepared),
@@ -1165,6 +1171,8 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
1165
1171
  if (event.entryId !== undefined && (m.role === "user" || m.role === "assistant" || m.role === "toolResult")) {
1166
1172
  emitCommitted(event.entryId, m.role, m.role === "toolResult" ? m.toolCallId : undefined);
1167
1173
  }
1174
+ if (event.entryId !== undefined && m.role === "user")
1175
+ prepared.fileHistoryBoundary?.begin(event.entryId);
1168
1176
  if (event.entryId !== undefined && m.role === "user" && prepared.gitStatusRef.pendingReceipt !== undefined) {
1169
1177
  const pr = prepared.gitStatusRef.pendingReceipt;
1170
1178
  const c = m.content;
@@ -1453,7 +1461,7 @@ export class Runner {
1453
1461
  deps;
1454
1462
  sessions;
1455
1463
  sessionLocks = new Map();
1456
- snapshotTooLargeRoots = new Map();
1464
+ sessionReadStates = new SessionReadFileStates();
1457
1465
  pendingSessionNotifications = new PendingSessionNotifications();
1458
1466
  parentConstraintRegistry = new Map();
1459
1467
  suspendedEnvReaps = new Map();
@@ -1605,7 +1613,7 @@ export class Runner {
1605
1613
  deliverEngineNotice(this.deps.onNotice, {
1606
1614
  code: "config.models_swapped",
1607
1615
  message: `model catalog swapped: ${Object.keys(next.models).length} model(s), ${tiers ? Object.keys(tiers).length : 0} tier binding(s); in-flight tasks finish on their resolved models, new tasks resolve against the new catalog`,
1608
- detail: { models: Object.keys(next.models).length, tiers: tiers ? Object.keys(tiers).length : 0 },
1616
+ detail: { models: Object.keys(next.models).length, tiers: tiers ? Object.keys(tiers).length : 0, occurrenceId: uuidv7() },
1609
1617
  });
1610
1618
  if (movedEntries.length > 0) {
1611
1619
  const RENDER_CAP = 8;
@@ -1616,7 +1624,7 @@ export class Runner {
1616
1624
  rendered.map((e) => `"${e.modelId}" ${e.from || "(config root)"} → ${e.to || "(config root)"}`).join("; ") +
1617
1625
  (movedEntries.length > rendered.length ? `; +${movedEntries.length - rendered.length} more` : "") +
1618
1626
  " — if the provider changed (not just its domain), update the credential reference in the same step",
1619
- detail: { entries: rendered, total: movedEntries.length },
1627
+ detail: { entries: rendered, total: movedEntries.length, occurrenceId: uuidv7() },
1620
1628
  });
1621
1629
  }
1622
1630
  }
@@ -1629,6 +1637,7 @@ export class Runner {
1629
1637
  "(Note the parameter order: runTaskStream(spec, resume?, internals?) — internals is the THIRD parameter.)");
1630
1638
  }
1631
1639
  const entryActor = spec.actor === undefined ? undefined : snapshotActorAssertion(spec.actor);
1640
+ const entryTracer = { tracer: spec.tracer ?? this.deps.tracer };
1632
1641
  const queue = new PushQueue();
1633
1642
  const detachHub = new ToolDetachHub();
1634
1643
  let resultValue;
@@ -1650,6 +1659,7 @@ export class Runner {
1650
1659
  let steerChain = Promise.resolve();
1651
1660
  const acceptedSteerInputs = new Map();
1652
1661
  const notifyRef = {};
1662
+ const captureOptOutRef = {};
1653
1663
  const manualCompactRef = { requested: false, waiters: [] };
1654
1664
  const drainManualCompactWaiters = (outcome) => {
1655
1665
  manualCompactRef.requested = false;
@@ -1688,7 +1698,7 @@ export class Runner {
1688
1698
  }, (s) => {
1689
1699
  reapHandle = s;
1690
1700
  this.suspendedEnvReaps.set(s.token, s);
1691
- }, manualCompactRef, taskIdRef, resume, { ...(internals ?? {}), detachHub }, notifyRef, entryActor);
1701
+ }, manualCompactRef, taskIdRef, resume, { ...(internals ?? {}), detachHub }, notifyRef, captureOptOutRef, entryActor, entryTracer);
1692
1702
  }
1693
1703
  finally {
1694
1704
  releaseLock?.();
@@ -1759,6 +1769,7 @@ export class Runner {
1759
1769
  resultValue = {
1760
1770
  taskId: taskIdRef.current ?? "unknown",
1761
1771
  sessionId: taskIdRef.sessionId ?? "unknown",
1772
+ ...(taskIdRef.runId !== undefined ? { runId: taskIdRef.runId } : {}),
1762
1773
  status: "failed",
1763
1774
  result: "",
1764
1775
  errorMessage: err instanceof Error ? err.message : String(err),
@@ -1774,10 +1785,11 @@ export class Runner {
1774
1785
  })(),
1775
1786
  stats: { turns: 0, tokens: 0, toolCalls: 0, cachedTokens: 0, costMicroUsd: 0 },
1776
1787
  };
1777
- emitTrace(spec.tracer ?? this.deps.tracer, () => ({
1788
+ emitTrace(entryTracer.tracer, () => ({
1778
1789
  kind: "task.end",
1779
1790
  version: 1,
1780
1791
  taskId: taskIdRef.current ?? "unknown",
1792
+ ...(taskIdRef.runId !== undefined ? { runId: taskIdRef.runId } : {}),
1781
1793
  status: "failed",
1782
1794
  errorCode: code,
1783
1795
  turns: 0,
@@ -1985,6 +1997,7 @@ export class Runner {
1985
1997
  detail: {
1986
1998
  inputId: effectiveInputId,
1987
1999
  sessionId: h.sessionId,
2000
+ runId: h.runId,
1988
2001
  ...(actor?.id !== undefined ? { actorId: actor.id } : {}),
1989
2002
  ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}),
1990
2003
  },
@@ -2065,6 +2078,26 @@ export class Runner {
2065
2078
  }
2066
2079
  notifyRef.inject(payload, priority !== undefined ? { priority } : undefined);
2067
2080
  },
2081
+ optOutMemoryCapture: async (options) => {
2082
+ const reasonIn = options?.reason;
2083
+ if (reasonIn !== undefined && typeof reasonIn !== "string") {
2084
+ const e = new Error(`cannot opt out of memory capture: options.reason must be a string when present`);
2085
+ e.code = "steering.invalid_content";
2086
+ throw e;
2087
+ }
2088
+ const reason = reasonIn;
2089
+ if (resultValue)
2090
+ throw steeringError("the task has already finished");
2091
+ const h = handle ?? (await orTimeout(ready));
2092
+ if (!h)
2093
+ throw steeringError("the task is not running");
2094
+ if (captureOptOutRef.flip === undefined) {
2095
+ const e = new Error(`cannot opt out of memory capture: this run mounted no memory session (no memory spec / no backend / prepare degraded memory-less) — there is nothing to opt out of`);
2096
+ e.code = "memory.capture_optout_unavailable";
2097
+ throw e;
2098
+ }
2099
+ return captureOptOutRef.flip(reason);
2100
+ },
2068
2101
  compact: async (opts) => {
2069
2102
  if (resultValue)
2070
2103
  throw steeringError("the task has already finished");
@@ -2118,6 +2151,60 @@ export class Runner {
2118
2151
  h.abortController.abort();
2119
2152
  void h.harness.abort();
2120
2153
  },
2154
+ halt: async () => {
2155
+ const haltRefused = (msg) => {
2156
+ const e = new Error(`cannot halt: ${msg}`);
2157
+ e.code = "steering.not_running";
2158
+ return e;
2159
+ };
2160
+ if (resultValue)
2161
+ throw haltRefused("the task has already finished");
2162
+ const h = handle ?? (await orTimeout(ready));
2163
+ if (!h)
2164
+ throw haltRefused("the task is not running");
2165
+ const apply = () => {
2166
+ const abortOwnedBeforeHalt = h.abortController.signal.aborted;
2167
+ const receipt = h.harness.halt();
2168
+ if (receipt.accepted && !abortOwnedBeforeHalt)
2169
+ h.loop.userHalted = true;
2170
+ if (receipt.turnCut) {
2171
+ deliverEngineNotice(this.deps.onNotice, {
2172
+ code: "task.turn_interrupted",
2173
+ message: "a bare user halt cut the running turn: in-flight work was cut at a manufactured boundary " +
2174
+ "(finished tool calls keep their real results; never-started ones settle as interrupted) and " +
2175
+ "the run is collecting to a clean, resumable stop — no further model turn will start.",
2176
+ detail: {
2177
+ cause: "user_halt",
2178
+ sessionId: h.sessionId,
2179
+ runId: h.runId,
2180
+ ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}),
2181
+ },
2182
+ });
2183
+ }
2184
+ return { turnCut: receipt.turnCut };
2185
+ };
2186
+ try {
2187
+ if (resultValue !== undefined || h.loop.ended)
2188
+ throw haltRefused("the task is no longer running");
2189
+ return apply();
2190
+ }
2191
+ catch (e) {
2192
+ if (!(e instanceof Error && e.code === "invalid_state"))
2193
+ throw e;
2194
+ }
2195
+ const birthDeadline = Date.now() + READY_TIMEOUT_MS;
2196
+ while (resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
2197
+ try {
2198
+ return apply();
2199
+ }
2200
+ catch (e2) {
2201
+ if (!(e2 instanceof Error && e2.code === "invalid_state"))
2202
+ throw e2;
2203
+ }
2204
+ await new Promise((r) => setTimeout(r, 10));
2205
+ }
2206
+ throw haltRefused("the task is no longer running");
2207
+ },
2121
2208
  destroy: () => (destroyOnce ??= (async () => {
2122
2209
  const h = handle ?? (await orTimeout(ready));
2123
2210
  if (h) {
@@ -2129,7 +2216,7 @@ export class Runner {
2129
2216
  })()),
2130
2217
  };
2131
2218
  }
2132
- async runLocked(spec, queue, setResult, onSuggestions, onReady, onSuspend, manualCompactRef, taskIdRef, resume, internals, notifyRef, entryActor) {
2219
+ async runLocked(spec, queue, setResult, onSuggestions, onReady, onSuspend, manualCompactRef, taskIdRef, resume, internals, notifyRef, captureOptOutRef, entryActor, entryTracer) {
2133
2220
  const prepareResume = resume
2134
2221
  ? {
2135
2222
  leafId: resume.cp.leafId,
@@ -2233,6 +2320,7 @@ export class Runner {
2233
2320
  peerSelfRef,
2234
2321
  peerInboundChainRef,
2235
2322
  detachHub,
2323
+ sessionReadStates: this.sessionReadStates,
2236
2324
  onTaskNotification: (notification, opts) => {
2237
2325
  try {
2238
2326
  upstreamTaskNotification?.(notification, opts);
@@ -2241,7 +2329,7 @@ export class Runner {
2241
2329
  }
2242
2330
  injectTaskNotification(notification, opts);
2243
2331
  },
2244
- }, this);
2332
+ }, this, taskIdRef);
2245
2333
  notificationHarness = prepared.harness;
2246
2334
  notificationSessionId = prepared.sessionId;
2247
2335
  if (taskIdRef)
@@ -2281,7 +2369,7 @@ export class Runner {
2281
2369
  undrainedUserAtEnd = counts;
2282
2370
  return;
2283
2371
  }
2284
- for (const notice of undrainedUserInputNotices(counts, spec.taskId, prepared.sessionId)) {
2372
+ for (const notice of undrainedUserInputNotices(counts, spec.taskId ?? prepared.sessionId, prepared.sessionId, prepared.runId)) {
2285
2373
  deliverEngineNotice(this.deps.onNotice, notice);
2286
2374
  }
2287
2375
  };
@@ -2292,6 +2380,9 @@ export class Runner {
2292
2380
  };
2293
2381
  if (notifyRef)
2294
2382
  notifyRef.inject = injectTaskNotification;
2383
+ if (captureOptOutRef && prepared.memoryEngineSession?.captureOptOut !== undefined) {
2384
+ captureOptOutRef.flip = prepared.memoryEngineSession.captureOptOut.flip;
2385
+ }
2295
2386
  try {
2296
2387
  internals?.onNotifyInjectorReady?.(injectTaskNotification);
2297
2388
  }
@@ -2310,8 +2401,8 @@ export class Runner {
2310
2401
  }
2311
2402
  }
2312
2403
  }
2313
- const loopLatch = { ended: false, userInterrupted: false };
2314
- onReady({ harness: prepared.harness, abortController: prepared.abortController, loop: loopLatch, reminderMark: prepared.reminderMark, sessionId: prepared.sessionId, hookTimeoutMs: prepared.hookTimeoutMs, hookIdentity: prepared.hookIdentity });
2404
+ const loopLatch = { ended: false, userInterrupted: false, userHalted: false };
2405
+ onReady({ harness: prepared.harness, abortController: prepared.abortController, loop: loopLatch, reminderMark: prepared.reminderMark, sessionId: prepared.sessionId, runId: prepared.runId, hookTimeoutMs: prepared.hookTimeoutMs, hookIdentity: prepared.hookIdentity });
2315
2406
  const stats = { turns: 0, tokens: 0, toolCalls: 0, promptTokens: 0, totalInputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cacheWriteTokensLong: 0, outputTokens: 0, costMicroUsd: 0 };
2316
2407
  prepared.liveSpendRef.get = () => ({ costMicroUsd: stats.costMicroUsd, tokens: stats.tokens, turns: stats.turns, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) });
2317
2408
  if (resume &&
@@ -2352,8 +2443,9 @@ export class Runner {
2352
2443
  rs.telemetry.cacheBreakReported = false;
2353
2444
  rs.limits.outputRetryCap = resolveOutputRetries(spec.outputRetries);
2354
2445
  rs.limits.effectiveMaxTurns = resolveMaxTurns(spec.limits);
2355
- rs.telemetry.tracer = spec.tracer ?? this.deps.tracer;
2446
+ rs.telemetry.tracer = entryTracer !== undefined ? entryTracer.tracer : (spec.tracer ?? this.deps.tracer);
2356
2447
  rs.telemetry.taskId = runSourceTaskId;
2448
+ rs.telemetry.runId = prepared.runId;
2357
2449
  if (taskIdRef) {
2358
2450
  taskIdRef.current = rs.telemetry.taskId;
2359
2451
  taskIdRef.sessionId = prepared.sessionId;
@@ -2368,7 +2460,7 @@ export class Runner {
2368
2460
  }
2369
2461
  };
2370
2462
  try {
2371
- void Promise.resolve(this.sessions.noteTaskRun?.(prepared.sessionId, rs.telemetry.taskId)).catch(discloseNoteTaskRunFailure);
2463
+ void Promise.resolve(this.sessions.noteTaskRun?.(prepared.sessionId, rs.telemetry.taskId, rs.telemetry.runId)).catch(discloseNoteTaskRunFailure);
2372
2464
  }
2373
2465
  catch (err) {
2374
2466
  discloseNoteTaskRunFailure(err);
@@ -2654,7 +2746,7 @@ export class Runner {
2654
2746
  const writeFamilyOf = (name) => writeFamilyByName.get(name) ?? writeFamilyOfCanonical(name);
2655
2747
  const toolStartAt = new Map();
2656
2748
  const startedToolCallIds = new Set();
2657
- emitTrace(rs.telemetry.tracer, () => ({ kind: "task.start", version: 1, taskId: rs.telemetry.taskId, model: prepared.model.id, engineVersion: engineVersion(), ts: rs.telemetry.taskStart }));
2749
+ emitTrace(rs.telemetry.tracer, () => ({ kind: "task.start", version: 1, taskId: rs.telemetry.taskId, runId: rs.telemetry.runId, model: prepared.model.id, engineVersion: engineVersion(), ts: rs.telemetry.taskStart }));
2658
2750
  emitTrace(rs.telemetry.tracer, () => ({
2659
2751
  kind: "prompt.assembled",
2660
2752
  version: 1,
@@ -2746,7 +2838,9 @@ export class Runner {
2746
2838
  }
2747
2839
  }
2748
2840
  };
2841
+ const ownCommittedTailRef = { current: undefined };
2749
2842
  const emitCommitted = (entryId, role, toolCallId) => {
2843
+ ownCommittedTailRef.current = entryId;
2750
2844
  if (role === "toolResult" && toolCallId !== undefined)
2751
2845
  startedToolCallIds.delete(toolCallId);
2752
2846
  queue.push({ type: "message_committed", entryId, role, ...(toolCallId !== undefined ? { toolCallId } : {}), ...ident() });
@@ -2762,6 +2856,7 @@ export class Runner {
2762
2856
  ...(s.detail !== undefined ? { detail: s.detail } : {}),
2763
2857
  ...(s.retryInSec !== undefined ? { retryInSec: s.retryInSec } : {}),
2764
2858
  ...(s.retryInMs !== undefined ? { retryInMs: s.retryInMs } : {}),
2859
+ ...(s.retryAtMs !== undefined ? { retryAtMs: s.retryAtMs } : {}),
2765
2860
  ...(s.attempt !== undefined ? { attempt: s.attempt } : {}),
2766
2861
  ...(s.maxRetries !== undefined ? { maxRetries: s.maxRetries } : {}),
2767
2862
  ...(s.errClass !== undefined ? { errClass: s.errClass } : {}),
@@ -3079,6 +3174,7 @@ export class Runner {
3079
3174
  ...(prepared.activeTools.size > 0 ? { activeTools: [...prepared.activeTools] } : {}),
3080
3175
  onInputTruncated: emitInputTruncated(rs.telemetry.tracer, rs.telemetry.taskId),
3081
3176
  workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
3177
+ ...contextInstructionFilesOption(prepared),
3082
3178
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
3083
3179
  ...this.seamCCompactionOptions(prepared),
3084
3180
  ...gitRestateOption(prepared),
@@ -3479,6 +3575,7 @@ export class Runner {
3479
3575
  detail: {
3480
3576
  ...(steer.inputId !== undefined ? { inputId: steer.inputId } : {}),
3481
3577
  sessionId: prepared.sessionId,
3578
+ runId: prepared.runId,
3482
3579
  ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}),
3483
3580
  },
3484
3581
  });
@@ -3859,7 +3956,7 @@ export class Runner {
3859
3956
  steer: Math.max(0, undrainedUserAtEnd.steer - migratedParked.steer),
3860
3957
  followUp: Math.max(0, undrainedUserAtEnd.followUp - migratedParked.followUp),
3861
3958
  };
3862
- for (const notice of undrainedUserInputNotices(remaining, spec.taskId, prepared.sessionId)) {
3959
+ for (const notice of undrainedUserInputNotices(remaining, spec.taskId ?? prepared.sessionId, prepared.sessionId, prepared.runId)) {
3863
3960
  deliverEngineNotice(this.deps.onNotice, notice);
3864
3961
  }
3865
3962
  }
@@ -3867,6 +3964,7 @@ export class Runner {
3867
3964
  emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.announce_dropped", version: 1, taskId: rs.telemetry.taskId, ts: Date.now() }));
3868
3965
  }
3869
3966
  const comp = await this.finish(spec, prepared, {
3967
+ telemetry: { tracer: rs.telemetry.tracer, taskId: rs.telemetry.taskId },
3870
3968
  skipCompaction: durablyPaused || compactionBreaker.failures >= MAX_CONSECUTIVE_COMPACTION_FAILURES,
3871
3969
  minTokens: rs.counters.compactionFloor,
3872
3970
  brain: compactionBrain,
@@ -3951,10 +4049,12 @@ export class Runner {
3951
4049
  }
3952
4050
  const result = assembleResult(spec, prepared.sessionId, final, stats, {
3953
4051
  threw,
4052
+ runId: prepared.runId,
3954
4053
  model: prepared.model.id,
3955
4054
  unpricedSpend: rs.telemetry.unpricedSpend,
3956
4055
  rewindNotes: prepared.rewindNotes,
3957
4056
  haltedOnUserRejection: prepared.batchHaltRef.current !== undefined,
4057
+ userHalted: loopLatch.userHalted,
3958
4058
  strandedHumanAnswers,
3959
4059
  remoteEnvFailures: prepared.remoteEnvFailures,
3960
4060
  effectiveReadFace: prepared.effectiveReadFace,
@@ -4008,6 +4108,7 @@ export class Runner {
4008
4108
  r.errorMessage = `${r.errorMessage ?? "the resume leg did not complete"} — the approved action never started, and reopening the checkpoint FAILED: its state is unprovable from here. Re-read it before deciding; do NOT issue a fresh approval on the assumption the old one is dead`;
4009
4109
  }
4010
4110
  }
4111
+ prepared.sealReadStateSeat?.(ownCommittedTailRef.current);
4011
4112
  {
4012
4113
  const wss = prepared.workspaceStateSettle;
4013
4114
  if (wss !== undefined && result.status !== "suspended" && result.status !== "needs_review") {
@@ -4096,6 +4197,7 @@ export class Runner {
4096
4197
  kind: "task.end",
4097
4198
  version: 1,
4098
4199
  taskId: rs.telemetry.taskId,
4200
+ runId: rs.telemetry.runId,
4099
4201
  status: result.status,
4100
4202
  errorCode: result.errorCode,
4101
4203
  turns: stats.turns,
@@ -4139,9 +4241,7 @@ export class Runner {
4139
4241
  manualCompactRef.emitMooted = undefined;
4140
4242
  queue.push({ type: "done", result });
4141
4243
  queue.close();
4142
- if (spec.rewindFiles && result.status === "completed") {
4143
- await this.snapshotTurn(prepared);
4144
- }
4244
+ await prepared.fileHistoryBoundary?.settle();
4145
4245
  if (spec.suggestNextPrompts && result.status === "completed") {
4146
4246
  onSuggestions(this.suggestNextPrompts(spec, prepared, result));
4147
4247
  }
@@ -4220,7 +4320,7 @@ export class Runner {
4220
4320
  if (!sameRouteIdentity(model, prepared.model)) {
4221
4321
  const verdict = await adjudicateDerivedRoute({ brain: this.deps.brain, model, getApiKeyAndHeaders: spec.getApiKeyAndHeaders });
4222
4322
  if (verdict !== undefined && !verdict.ok) {
4223
- deliverEngineNotice(this.deps.onNotice, fallbackToPrimaryNotice({ seat: "prompt-suggestions", from: model.id, to: prepared.model.id, verdict, sessionId: prepared.sessionId }));
4323
+ deliverEngineNotice(this.deps.onNotice, fallbackToPrimaryNotice({ seat: "prompt-suggestions", from: model.id, to: prepared.model.id, verdict, sessionId: prepared.sessionId, runId: prepared.runId }));
4224
4324
  model = prepared.model;
4225
4325
  thinking = prepared.thinking;
4226
4326
  }
@@ -4253,71 +4353,6 @@ export class Runner {
4253
4353
  clearTimeout(timer);
4254
4354
  }
4255
4355
  }
4256
- async snapshotTurn(prepared) {
4257
- const store = this.deps.fileSnapshotStore;
4258
- const env = prepared.ownedEnv ?? this.deps.executionEnv;
4259
- if (!store || !env)
4260
- return;
4261
- try {
4262
- const leafId = await prepared.session.getLeafId();
4263
- if (!leafId)
4264
- return;
4265
- const root = prepared.taskRootPath;
4266
- const refusal = this.snapshotTooLargeRoots.get(root);
4267
- if (refusal !== undefined) {
4268
- if (Date.now() - refusal.refusedAt < SNAPSHOT_TOO_LARGE_TTL_MS) {
4269
- if (!refusal.skipAnnounced) {
4270
- refusal.skipAnnounced = true;
4271
- try {
4272
- this.deps.onError?.(new Error(`rewind-files snapshot skipped for this working-tree root: a too_large refusal is still inside its ${SNAPSHOT_TOO_LARGE_TTL_MS / 60_000}-minute cooldown (re-probed at ${new Date(refusal.refusedAt + SNAPSHOT_TOO_LARGE_TTL_MS).toISOString()}) — turns completed during the cooldown capture no snapshot and cannot be rewound to. Announced once per cooldown window.`), { phase: "rewind", sessionId: prepared.sessionId, classification: "too_large" });
4273
- }
4274
- catch {
4275
- }
4276
- }
4277
- return;
4278
- }
4279
- this.snapshotTooLargeRoots.delete(root);
4280
- }
4281
- const ac = new AbortController();
4282
- let timer;
4283
- const timeout = new Promise((resolve) => {
4284
- timer = setTimeout(() => {
4285
- ac.abort();
4286
- resolve({ ok: false, error: { code: "aborted", message: "snapshot timed out" } });
4287
- }, SNAPSHOT_TIMEOUT_MS);
4288
- });
4289
- const work = store.snapshot(prepared.sessionId, leafId, env, root, ac.signal);
4290
- work.catch(() => { });
4291
- let r;
4292
- try {
4293
- r = await Promise.race([work, timeout]);
4294
- }
4295
- finally {
4296
- clearTimeout(timer);
4297
- }
4298
- if (!r.ok) {
4299
- const tooLarge = r.error.code === "too_large";
4300
- const refusedAt = Date.now();
4301
- if (tooLarge && !this.snapshotTooLargeRoots.has(root))
4302
- this.snapshotTooLargeRoots.set(root, { refusedAt, skipAnnounced: false });
4303
- try {
4304
- this.deps.onError?.(new Error(`rewind-files snapshot failed (${r.error.code}): ${r.error.message}` +
4305
- (tooLarge
4306
- ? ` — skipping further snapshots for this root (this process) during a ${SNAPSHOT_TOO_LARGE_TTL_MS / 60_000}-minute cooldown, until ${new Date(refusedAt + SNAPSHOT_TOO_LARGE_TTL_MS).toISOString()}, when the tree is re-probed automatically. Raise the store's snapshotBounds to accept a tree this size; shrinking the tree also re-enables rewind, but only at that re-probe — not immediately`
4307
- : "")), { phase: "rewind", sessionId: prepared.sessionId, ...(tooLarge ? { classification: "too_large" } : {}) });
4308
- }
4309
- catch {
4310
- }
4311
- }
4312
- }
4313
- catch (err) {
4314
- try {
4315
- this.deps.onError?.(err, { phase: "rewind", sessionId: prepared.sessionId });
4316
- }
4317
- catch {
4318
- }
4319
- }
4320
- }
4321
4356
  async runTask(spec, internals) {
4322
4357
  const stream = this.runTaskStream(spec, undefined, internals);
4323
4358
  for await (const _ of stream) {
@@ -4687,12 +4722,13 @@ export class Runner {
4687
4722
  "(corrupt / inconsistently-minted row) — the frozen deny data the resume would execute is not the data the " +
4688
4723
  "digest authenticates; rejected pre-CAS (the checkpoint stays pending)");
4689
4724
  }
4690
- const suppliedDigest = constraintChainDigest((internals?.inheritedGate?.parentConstraints ?? []).map((pc) => constraintChainEntryOf(pc.policy, { ...(pc.autoMode !== undefined ? { autoModeArmed: true } : {}), ...(pc.durableMandate === true ? { durableMandate: true } : {}), ...(pc.contentMandate === true ? { contentMandate: true } : {}) })));
4725
+ const suppliedDigest = constraintChainDigest((internals?.inheritedGate?.parentConstraints ?? []).map((pc, i) => constraintChainEntryOfSuppliedLayer(pc, persistedChain[i])));
4691
4726
  if (suppliedDigest !== expectedDigest) {
4692
4727
  throw new CheckpointError("resume.parent_constraint_mismatch", "the re-supplied parent-constraint chain's projection content does not match what the checkpoint recorded " +
4693
4728
  `(recorded ${expectedDigest}, re-supplied ${suppliedDigest}) — a same-shape chain with different frozen ` +
4694
4729
  "deny data would run the resumed leg under a different ancestor gate than it suspended with; " +
4695
- "rejected pre-CAS (the checkpoint stays pending) — re-resume with the original chain's policies");
4730
+ "rejected pre-CAS (the checkpoint stays pending) — re-resume with the original chain's policies " +
4731
+ "(including, when the row recorded one, the ancestor's auto-mode arming recipe verbatim)");
4696
4732
  }
4697
4733
  }
4698
4734
  }
@@ -5370,9 +5406,10 @@ export class Runner {
5370
5406
  minTokens: opts?.minTokens,
5371
5407
  overheadTokens: prepared.promptOverheadTokens,
5372
5408
  ...(prepared.activeTools.size > 0 ? { activeTools: [...prepared.activeTools] } : {}),
5373
- onInputTruncated: emitInputTruncated(spec.tracer ?? this.deps.tracer, spec.taskId ?? prepared.sessionId),
5409
+ onInputTruncated: emitInputTruncated(opts?.telemetry?.tracer ?? spec.tracer ?? this.deps.tracer, opts?.telemetry?.taskId ?? spec.taskId ?? prepared.sessionId),
5374
5410
  ...(opts?.windowSafety ?? {}),
5375
5411
  workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
5412
+ ...contextInstructionFilesOption(prepared),
5376
5413
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
5377
5414
  ...this.seamCCompactionOptions(prepared),
5378
5415
  ...gitRestateOption(prepared),
@@ -5383,17 +5420,17 @@ export class Runner {
5383
5420
  });
5384
5421
  this.recordCompactionReuse(prepared, finishComp);
5385
5422
  if (finishComp.unevaluableWindow) {
5386
- discloseUnevaluableWindow(finishComp.unevaluableWindow, prepared, spec.tracer ?? this.deps.tracer, spec.taskId ?? prepared.sessionId, this.deps.onError);
5423
+ discloseUnevaluableWindow(finishComp.unevaluableWindow, prepared, opts?.telemetry?.tracer ?? spec.tracer ?? this.deps.tracer, opts?.telemetry?.taskId ?? spec.taskId ?? prepared.sessionId, this.deps.onError);
5387
5424
  }
5388
5425
  comp = finishComp;
5389
5426
  }
5390
5427
  catch (err) {
5391
5428
  const failMsg = String(err instanceof Error ? err.message : err);
5392
5429
  const failReason = failMsg.length > 512 ? `${failMsg.slice(0, 512)}…` : failMsg;
5393
- emitTrace(spec.tracer ?? this.deps.tracer, () => ({
5430
+ emitTrace(opts?.telemetry?.tracer ?? spec.tracer ?? this.deps.tracer, () => ({
5394
5431
  kind: "compaction.failed",
5395
5432
  version: 1,
5396
- taskId: spec.taskId ?? prepared.sessionId,
5433
+ taskId: opts?.telemetry?.taskId ?? spec.taskId ?? prepared.sessionId,
5397
5434
  trigger: "auto",
5398
5435
  reason: failReason,
5399
5436
  ts: Date.now(),
@@ -18,11 +18,17 @@
18
18
  * a `git checkout`, a bash command — anything this transcript does not record) therefore fails
19
19
  * the staleness comparison and the edit is refused, exactly as it is inside a single task. A
20
20
  * reconstruction that re-read the file to build its hash would instead wave the dirty edit
21
- * through; that is the one thing this module must never do.
22
- * 2. **Only WHOLE-file records qualify.** A partial read, a notebook projection or an `Edit` result
23
- * does not carry the file's full post-condition text, so there is nothing to hash honestly.
24
- * Those files are simply not reported: the gate then behaves exactly as it does today (the model
25
- * re-reads), which is the pre-existing cost, not a new failure.
21
+ * through; that is the one thing this module must never do. (#483 D1, ruled 2026-08-28: this is
22
+ * deliberately STRICTER than CC's own rehydrate, whose Edit arm re-reads the disk and would
23
+ * admit a between-turns external change same gate satisfaction, no staleness give-up.)
24
+ * 2. **Only records whose full post-condition is PROVABLE qualify.** A whole-file `Read` and a
25
+ * `Write` carry the text verbatim. An `Edit` card (#483 rung-2) carries a diff BASE — the
26
+ * pre-edit whole text plus the resolved mutations — so its post-condition is RECOMPUTED through
27
+ * the same single-source application step the tool itself used (`applyRecordedEdit`); a card
28
+ * that cannot recompute (malformed, offloaded, or its oldString absent from its own base) falls
29
+ * back to RETRACTING the file's record, never to a guess. A partial read or a notebook
30
+ * projection proves nothing and is simply not reported: the gate then behaves exactly as before
31
+ * (the model re-reads), which is the pre-existing cost, not a new failure.
26
32
  */
27
33
  import type { AgentMessage } from "../../internal/harness.js";
28
34
  /** The whole text of one file at the moment the transcript recorded it. */
@@ -39,11 +45,13 @@ export interface TranscriptFileRecord {
39
45
  *
40
46
  * Records are read only from SUCCESSFUL results (an `isError` result changed nothing and proves
41
47
  * nothing). A `Write` proves the post-condition it just persisted; a whole-file `Read` proves what it
42
- * just served. An `Edit` result proves that the file CHANGED without recording its new text, so it
43
- * retracts whatever an earlier record said leaving a stale entry behind would blame the user for
44
- * the agent's own edit ("modified since read") the next turn. Anything else (partial reads, dedup
45
- * stubs, notebook projections, image/document reads, foreign tools) is inert: it neither proves nor
46
- * retracts, because reading a file does not change it.
48
+ * just served. An `Edit` card (#483 rung-2) proves the post-condition RECOMPUTED from its recorded
49
+ * diff base + mutations or, when it cannot prove (malformed / offloaded / inconsistent), retracts
50
+ * whatever an earlier record said (leaving a stale entry behind would blame the user for the agent's
51
+ * own edit, "modified since read", the next turn). A `NotebookEdit` always retracts (deliberate
52
+ * residual see {@link RETRACTING_RESULTS}). Anything else (partial reads, dedup stubs, notebook
53
+ * projections, image/document reads, foreign tools) is inert: it neither proves nor retracts,
54
+ * because reading a file does not change it.
47
55
  *
48
56
  * The parameter is the session's own message lane (`AgentMessage`, custom lanes included) rather than
49
57
  * the narrowed model-facing union: this reads the branch as the session stores it, and every lane that