@sema-agent/core 5.13.0 → 5.15.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 (108) hide show
  1. package/CHANGELOG.md +418 -0
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +4 -0
  4. package/dist/agents/subagent.js +133 -41
  5. package/dist/brain/anthropic.js +33 -10
  6. package/dist/brain/context-overflow.d.ts +20 -0
  7. package/dist/brain/context-overflow.js +58 -0
  8. package/dist/brain/errors.js +21 -1
  9. package/dist/brain/open-responses.js +24 -10
  10. package/dist/brain/openai.js +29 -11
  11. package/dist/brain/request-params.d.ts +2 -0
  12. package/dist/brain/request-params.js +16 -0
  13. package/dist/brain/retry.d.ts +5 -0
  14. package/dist/brain/retry.js +16 -4
  15. package/dist/brain/stream-engine.d.ts +9 -1
  16. package/dist/brain/stream-engine.js +261 -28
  17. package/dist/brain/timeout.d.ts +1 -0
  18. package/dist/brain/timeout.js +1 -0
  19. package/dist/core/a2a.d.ts +2 -2
  20. package/dist/core/a2a.js +3 -3
  21. package/dist/core/ask-question.d.ts +47 -2
  22. package/dist/core/ask-question.js +209 -28
  23. package/dist/core/background-agent-store.d.ts +2 -0
  24. package/dist/core/checkpoint-store.d.ts +41 -17
  25. package/dist/core/checkpoint-store.js +137 -6
  26. package/dist/core/hooks.d.ts +24 -2
  27. package/dist/core/hooks.js +97 -10
  28. package/dist/core/human-input-projection.d.ts +12 -0
  29. package/dist/core/human-input-projection.js +27 -0
  30. package/dist/core/mcp.d.ts +7 -2
  31. package/dist/core/mcp.js +7 -7
  32. package/dist/core/memory-admission.d.ts +4 -0
  33. package/dist/core/memory-admission.js +3 -0
  34. package/dist/core/memory-engine/engine.d.ts +3 -1
  35. package/dist/core/memory-engine/engine.js +4 -3
  36. package/dist/core/memory-recall.d.ts +1 -1
  37. package/dist/core/memory-recall.js +3 -2
  38. package/dist/core/runner/assemble-result.d.ts +1 -0
  39. package/dist/core/runner/assemble-result.js +1 -1
  40. package/dist/core/runner/prepare-memory.d.ts +1 -0
  41. package/dist/core/runner/prepare-memory.js +3 -3
  42. package/dist/core/runner/prepare-task.d.ts +19 -6
  43. package/dist/core/runner/prepare-task.js +343 -35
  44. package/dist/core/runner/runtask.d.ts +3 -6
  45. package/dist/core/runner/runtask.js +290 -64
  46. package/dist/core/runner/tool-disclosure.d.ts +5 -0
  47. package/dist/core/runner/tool-disclosure.js +65 -16
  48. package/dist/core/runner/tool-output-projection.js +1 -0
  49. package/dist/core/runner/turn-attachments.d.ts +4 -0
  50. package/dist/core/runner/turn-attachments.js +15 -2
  51. package/dist/core/session-store.d.ts +3 -0
  52. package/dist/core/session-store.js +4 -0
  53. package/dist/core/session.d.ts +1 -0
  54. package/dist/core/store-contracts/background-agent-store-contract.js +19 -0
  55. package/dist/core/store-contracts/checkpoint-store-contract.js +62 -3
  56. package/dist/core/task-notification.d.ts +2 -0
  57. package/dist/core/task-notification.js +5 -3
  58. package/dist/core/task-registry-agent.d.ts +1 -0
  59. package/dist/core/task-registry-agent.js +6 -0
  60. package/dist/core/task-registry.d.ts +1 -0
  61. package/dist/core/task-registry.js +4 -1
  62. package/dist/core/task-tool-shape.js +4 -3
  63. package/dist/core/tool-policy.d.ts +5 -0
  64. package/dist/core/tool-policy.js +2 -1
  65. package/dist/core/trace.d.ts +6 -0
  66. package/dist/core/types.d.ts +32 -1
  67. package/dist/core/wiring-manifest.d.ts +97 -0
  68. package/dist/core/wiring-manifest.js +186 -0
  69. package/dist/engine/compaction/compaction.js +2 -2
  70. package/dist/engine/harness/agent-harness.d.ts +2 -1
  71. package/dist/engine/harness/agent-harness.js +8 -1
  72. package/dist/engine/harness/types.d.ts +3 -1
  73. package/dist/engine/llm/types.d.ts +7 -0
  74. package/dist/engine/llm/types.js +8 -1
  75. package/dist/engine/session/import-validate.d.ts +6 -1
  76. package/dist/engine/session/import-validate.js +29 -6
  77. package/dist/engine/session/memory-repo.d.ts +3 -1
  78. package/dist/engine/session/memory-repo.js +2 -2
  79. package/dist/index.d.ts +10 -7
  80. package/dist/index.js +8 -5
  81. package/dist/internal/harness-types.d.ts +1 -1
  82. package/dist/internal/llm.d.ts +2 -2
  83. package/dist/internal/llm.js +1 -1
  84. package/dist/orchestration/run-workflow-tool.d.ts +4 -0
  85. package/dist/orchestration/run-workflow-tool.js +3 -0
  86. package/dist/orchestration/workflow-types.d.ts +8 -0
  87. package/dist/orchestration/workflow-types.js +14 -0
  88. package/dist/orchestration/workflow.d.ts +4 -0
  89. package/dist/orchestration/workflow.js +134 -5
  90. package/dist/prompts/default.js +1 -1
  91. package/dist/prompts/supervisor.d.ts +2 -2
  92. package/dist/prompts/supervisor.js +5 -4
  93. package/dist/stores/file/checkpoint-store.d.ts +3 -5
  94. package/dist/stores/file/checkpoint-store.js +31 -2
  95. package/dist/stores/file/index.js +1 -1
  96. package/dist/stores/file/session-store.d.ts +3 -1
  97. package/dist/stores/file/session-store.js +2 -2
  98. package/dist/stores/file/shared-ledger.js +8 -1
  99. package/dist/tools/fs/bash-readonly-classifier.d.ts +3 -0
  100. package/dist/tools/fs/bash-readonly-classifier.js +94 -0
  101. package/dist/tools/fs/fs-bash.d.ts +1 -0
  102. package/dist/tools/fs/fs-bash.js +32 -13
  103. package/dist/tools/fs/gh-rate-limit.d.ts +1 -1
  104. package/dist/tools/fs/gh-rate-limit.js +4 -3
  105. package/dist/tools/fs/index.d.ts +1 -0
  106. package/dist/tools/fs/index.js +1 -0
  107. package/dist/tools/fs/safety.js +34 -10
  108. package/package.json +1 -1
@@ -1,10 +1,12 @@
1
1
  import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
2
- import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, MAX_SUPPORTED_CHECKPOINT_VERSION, remainingBudgetMicroUsd, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
2
+ import { snapshotActorAssertion } from "../../internal/llm.js";
3
+ import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, MAX_SUPPORTED_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
3
4
  import { engineVersion } from "../version.js";
4
5
  import { CONFIG_CATALOG_VERSION, declarationReasons, resolveEffectiveConfig } from "../../config/catalog.js";
5
6
  import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
6
7
  import { DEFAULT_COMPACTION_INSTRUCTIONS, createRapidRefillState, isCompactionManualCancel, maybeCompact, nextTrimForceBackoff, recordCompactionAndCheckRapidRefill, sanitizeCompactionSettings } from "../auto-compaction.js";
7
- import { ASK_USER_QUESTION_TOOL_NAME, QUESTION_AWAITS_RESUME } from "../ask-question.js";
8
+ import { ASK_USER_QUESTION_TOOL_NAME, canonicalizeCapturedPlainData, classifyQuestionOutcome, isLiveQuestionFace, markBoundOnlyQuestionFace } from "../ask-question.js";
9
+ import { boundInputHashOf } from "../canonical-json.js";
8
10
  import { computeCostMicroUsd, modelCostToPricing } from "../pricing.js";
9
11
  import { emitTrace } from "../trace.js";
10
12
  import { emitTaskOutcome } from "../task-outcome.js";
@@ -31,6 +33,7 @@ import { hasVerifiableStructureSignal } from "./grounding-signal.js";
31
33
  import { hasDestroy, isIsolated } from "../remote-env.js";
32
34
  import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
33
35
  import { formatHookFeedback } from "../hooks.js";
36
+ import { buildHumanInputEvent, projectHumanInput } from "../human-input-projection.js";
34
37
  import { delimitUntrusted, inlineUntrusted, sanitizeUntrustedText } from "../untrusted-text.js";
35
38
  import { reconcileInterruptedSession } from "../session-reconcile.js";
36
39
  import { RunnerSharedToolResultStore } from "../tool-result-store.js";
@@ -52,6 +55,15 @@ function createRunState() {
52
55
  attach: { attachmentsCfg: undefined, agentListingOn: false, skillsListingOn: false, attachState: undefined, dateState: undefined, instrProbe: undefined, instrState: undefined, sizeGuidelineState: undefined, attachmentsInjected: 0 },
53
56
  };
54
57
  }
58
+ const humanInputOrdinals = new WeakMap();
59
+ function nextHumanInputSeq(key) {
60
+ let box = humanInputOrdinals.get(key);
61
+ if (box === undefined) {
62
+ box = { n: 0 };
63
+ humanInputOrdinals.set(key, box);
64
+ }
65
+ return ++box.n;
66
+ }
55
67
  const MAX_CONSECUTIVE_COMPACTION_FAILURES = 3;
56
68
  const STOP_HOOK_BLOCK_CAP = 8;
57
69
  const COMPACTION_REGROWTH_FACTOR = 1.5;
@@ -139,7 +151,15 @@ function deepJsonEqual(a, b) {
139
151
  if (aArr && bArr) {
140
152
  if (a.length !== b.length)
141
153
  return false;
142
- return a.every((v, i) => deepJsonEqual(v, b[i]));
154
+ for (let i = 0; i < a.length; i++) {
155
+ if (!deepJsonEqual(a[i], b[i]))
156
+ return false;
157
+ }
158
+ const aExtra = Object.keys(a).filter((k) => !isCanonicalIndexKey(k, a.length));
159
+ const bExtra = Object.keys(b).filter((k) => !isCanonicalIndexKey(k, b.length));
160
+ if (aExtra.length !== bExtra.length)
161
+ return false;
162
+ return aExtra.every((k) => Object.prototype.hasOwnProperty.call(b, k) && deepJsonEqual(Reflect.get(a, k), Reflect.get(b, k)));
143
163
  }
144
164
  const ao = a;
145
165
  const bo = b;
@@ -149,6 +169,9 @@ function deepJsonEqual(a, b) {
149
169
  return false;
150
170
  return aKeys.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && deepJsonEqual(ao[k], bo[k]));
151
171
  }
172
+ function isCanonicalIndexKey(key, length) {
173
+ return /^(0|[1-9]\d*)$/.test(key) && Number(key) < length;
174
+ }
152
175
  function sameWinner(a, b) {
153
176
  return (a.boundCallId === b.boundCallId &&
154
177
  a.decision === b.decision &&
@@ -160,14 +183,18 @@ function pendingContentAskCallId(cp) {
160
183
  ? cp.pendingAction.toolCallId
161
184
  : undefined;
162
185
  }
163
- function answerFaceForRedeemedCall(answer, redeemedCallId, base) {
164
- return async (req, signal) => {
165
- if (req.toolCallId === redeemedCallId)
186
+ function answerFaceForRedeemedCall(answer, redeemedCallId, base, redeemedQuestionsHash) {
187
+ let consumed = false;
188
+ const face = async (req, signal) => {
189
+ if (!consumed && req.toolCallId === redeemedCallId && boundInputHashOf(req.questions) === redeemedQuestionsHash) {
190
+ consumed = true;
166
191
  return answer;
192
+ }
167
193
  if (base !== undefined)
168
194
  return base(req, signal);
169
195
  throw new Error("this resumed leg's answer was bound to the decided question only — a new question has no answer on this leg");
170
196
  };
197
+ return isLiveQuestionFace(base) ? face : markBoundOnlyQuestionFace(face);
171
198
  }
172
199
  function writeFamilyOfCanonical(name) {
173
200
  if (name === "TaskCreate" || name === "TaskUpdate")
@@ -188,6 +215,14 @@ function toolResultMsg(toolCallId, toolName, text, isError) {
188
215
  timestamp: Date.now(),
189
216
  };
190
217
  }
218
+ function describeSuppliedValue(value) {
219
+ return typeof value === "string" ? value : value === null ? "null" : typeof value;
220
+ }
221
+ function assertReviewText(value, field) {
222
+ if (value !== undefined && typeof value !== "string") {
223
+ throw new CheckpointError("checkpoint.invalid_outcome", `resume \`${field}\` is not a plain string (got ${typeof value}) — a review verdict's text is an operator's plain data, not a live object; refusing pre-CAS, the checkpoint stays pending`);
224
+ }
225
+ }
191
226
  function resumeContinuation(resume) {
192
227
  if (resume.outcome.gate === "wake") {
193
228
  return formatHookFeedback("You were WOKEN from a parked pause by an operator. Before continuing, re-orient from the workspace: " +
@@ -548,6 +583,12 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
548
583
  ...(bgTasks !== undefined ? { backgroundTasks: bgTasks } : {}),
549
584
  ...(pendingTools !== undefined ? { newTools: pendingTools } : {}),
550
585
  ...(prepared.toolMaterializeStatic ? { newToolsStaticFace: true } : {}),
586
+ ...(prepared.toolMaterializeStatic && pendingTools !== undefined
587
+ ? { newToolsSwappedUnderStatic: pendingTools.filter((n) => prepared.staticFaceFor?.(n) !== true) }
588
+ : {}),
589
+ ...(prepared.toolMaterializeStatic && pendingReaddedTools !== undefined
590
+ ? { readdedToolsSwappedUnderStatic: pendingReaddedTools.filter((n) => prepared.staticFaceFor?.(n) !== true) }
591
+ : {}),
551
592
  ...(mcpToolsDelta !== undefined ? { mcpToolsDelta } : {}),
552
593
  ...(rs.attach.agentListingOn && prepared.agentListing !== undefined
553
594
  ? {
@@ -566,6 +607,12 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
566
607
  const exact = renderToolsDelta({
567
608
  ...(pendingTools !== undefined ? { added: pendingTools } : {}),
568
609
  ...(prepared.toolMaterializeStatic ? { staticFace: true } : {}),
610
+ ...(prepared.toolMaterializeStatic && pendingTools !== undefined
611
+ ? { swappedUnderStatic: pendingTools.filter((n) => prepared.staticFaceFor?.(n) !== true) }
612
+ : {}),
613
+ ...(prepared.toolMaterializeStatic && pendingReaddedTools !== undefined
614
+ ? { readdedSwappedUnderStatic: pendingReaddedTools.filter((n) => prepared.staticFaceFor?.(n) !== true) }
615
+ : {}),
569
616
  ...(mcpToolsDelta ?? {}),
570
617
  });
571
618
  if (exact !== undefined && due.some((a) => a.source === "tools_delta" && a.body === exact)) {
@@ -1166,6 +1213,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
1166
1213
  const progress = {
1167
1214
  type: "task_progress",
1168
1215
  taskId: rs.telemetry.taskId,
1216
+ ...(internals?.delegationTaskType !== undefined ? { taskType: internals.delegationTaskType } : {}),
1169
1217
  ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
1170
1218
  ...(subagentName ? { name: subagentName } : {}),
1171
1219
  usage: { totalTokens: stats.tokens, toolUses: stats.toolCalls, durationMs: Date.now() - rs.telemetry.taskStart },
@@ -1332,6 +1380,7 @@ export class Runner {
1332
1380
  return runSideQuery(spec, { brain: this.deps.brain, models: this.deps.models, roles: this.deps.roles });
1333
1381
  }
1334
1382
  runTaskStream(spec, resume, internals) {
1383
+ const entryActor = spec.actor === undefined ? undefined : snapshotActorAssertion(spec.actor);
1335
1384
  const queue = new PushQueue();
1336
1385
  const detachHub = new ToolDetachHub();
1337
1386
  let resultValue;
@@ -1386,7 +1435,7 @@ export class Runner {
1386
1435
  publishReady(h);
1387
1436
  }, (s) => {
1388
1437
  reapHandle = s;
1389
- }, manualCompactRef, taskIdRef, resume, { ...(internals ?? {}), detachHub }, notifyRef);
1438
+ }, manualCompactRef, taskIdRef, resume, { ...(internals ?? {}), detachHub }, notifyRef, entryActor);
1390
1439
  }
1391
1440
  finally {
1392
1441
  releaseLock?.();
@@ -1523,7 +1572,29 @@ export class Runner {
1523
1572
  if (options?.trusted && sanitizeUntrustedText(text) !== text) {
1524
1573
  throw steeringError("trusted steering text must not contain a </system-reminder> tag", "steering.invalid_content");
1525
1574
  }
1526
- const payload = options?.trusted ? formatHookFeedback(text) : text;
1575
+ const actor = options?.actor === undefined ? undefined : snapshotActorAssertion(options.actor);
1576
+ const projected = projectHumanInput({ text, actor, source: "steer" });
1577
+ const payload = options?.trusted ? formatHookFeedback(projected) : projected;
1578
+ const mintsAFrame = payload.trim().length !== 0;
1579
+ const emitAccepted = (h) => {
1580
+ if (!mintsAFrame)
1581
+ return;
1582
+ queue.push({
1583
+ ...buildHumanInputEvent({
1584
+ carrier: "steer",
1585
+ source: "steer",
1586
+ delivery: "queued",
1587
+ sessionSeq: nextHumanInputSeq(h.harness),
1588
+ ...(actor !== undefined ? { actor } : {}),
1589
+ ...(actor?.issuer !== undefined ? { issuer: actor.issuer } : {}),
1590
+ ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
1591
+ }),
1592
+ eventId: uuidv7(),
1593
+ ...(internals?.parentToolCallId !== undefined
1594
+ ? { parentToolCallId: internals.parentToolCallId, ...(spec.taskId !== undefined ? { sourceTaskId: spec.taskId } : {}) }
1595
+ : {}),
1596
+ });
1597
+ };
1527
1598
  const deliver = async () => {
1528
1599
  if (resultValue)
1529
1600
  throw steeringError("the task has already finished");
@@ -1531,7 +1602,8 @@ export class Runner {
1531
1602
  if (!h)
1532
1603
  throw steeringError("the task is not running");
1533
1604
  try {
1534
- await h.harness.steer(payload, { provenance: "engine-note" });
1605
+ await h.harness.steer(payload, { provenance: "engine-note", ...(actor !== undefined ? { actor } : {}) });
1606
+ emitAccepted(h);
1535
1607
  return;
1536
1608
  }
1537
1609
  catch (e) {
@@ -1541,7 +1613,8 @@ export class Runner {
1541
1613
  const birthDeadline = Date.now() + READY_TIMEOUT_MS;
1542
1614
  while (resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
1543
1615
  try {
1544
- await h.harness.steer(payload, { provenance: "engine-note" });
1616
+ await h.harness.steer(payload, { provenance: "engine-note", ...(actor !== undefined ? { actor } : {}) });
1617
+ emitAccepted(h);
1545
1618
  return;
1546
1619
  }
1547
1620
  catch (e2) {
@@ -1658,7 +1731,7 @@ export class Runner {
1658
1731
  })()),
1659
1732
  };
1660
1733
  }
1661
- async runLocked(spec, queue, setResult, onSuggestions, onReady, onSuspend, manualCompactRef, taskIdRef, resume, internals, notifyRef) {
1734
+ async runLocked(spec, queue, setResult, onSuggestions, onReady, onSuspend, manualCompactRef, taskIdRef, resume, internals, notifyRef, entryActor) {
1662
1735
  const prepareResume = resume
1663
1736
  ? {
1664
1737
  leafId: resume.cp.leafId,
@@ -1671,6 +1744,12 @@ export class Runner {
1671
1744
  executesApprovedAction: resume.cp.pendingAction.kind === "tool_approval" &&
1672
1745
  resume.outcome.gate !== "wake" &&
1673
1746
  resume.outcome.decision === "allow",
1747
+ ...(resume.outcome.gate !== "wake" && resume.outcome.decision === "allow" && pendingContentAskCallId(resume.cp) !== undefined
1748
+ ? {
1749
+ redeemedContentAskCallId: pendingContentAskCallId(resume.cp),
1750
+ redeemedContentAskQuestionsHash: boundInputHashOf(resume.cp.pendingAction.args?.questions),
1751
+ }
1752
+ : {}),
1674
1753
  }
1675
1754
  : undefined;
1676
1755
  const taskNotificationQueue = new SystemInjectionQueue();
@@ -1759,6 +1838,7 @@ export class Runner {
1759
1838
  const parentToolCallId = internals?.parentToolCallId;
1760
1839
  const ident = () => parentToolCallId !== undefined ? { eventId: uuidv7(), parentToolCallId, sourceTaskId: runSourceTaskId } : { eventId: uuidv7() };
1761
1840
  notificationIdent = ident;
1841
+ queue.push({ type: "wiring_manifest", manifest: prepared.wiringManifest, ...ident() });
1762
1842
  manualCompactRef.emitMooted = (reason) => {
1763
1843
  queue.push({ type: "compaction_outcome", outcome: "mooted", trigger: "manual", reason, ...ident() });
1764
1844
  };
@@ -2133,6 +2213,7 @@ export class Runner {
2133
2213
  ...(prepared.promptManifest.tools ? { tools: prepared.promptManifest.tools } : {}),
2134
2214
  ...(prepared.promptManifest.snapshot ? { snapshot: prepared.promptManifest.snapshot } : {}),
2135
2215
  ...(prepared.promptManifest.lowering ? { lowering: prepared.promptManifest.lowering } : {}),
2216
+ ...(prepared.promptManifest.toolDisclosure ? { toolDisclosure: prepared.promptManifest.toolDisclosure } : {}),
2136
2217
  totalChars: prepared.promptManifest.blocks.reduce((n, b) => n + b.chars, 0),
2137
2218
  ts: rs.telemetry.taskStart,
2138
2219
  }));
@@ -2595,6 +2676,7 @@ export class Runner {
2595
2676
  let final;
2596
2677
  let threw;
2597
2678
  let abortedLive = false;
2679
+ let strandedHumanAnswers = [];
2598
2680
  try {
2599
2681
  if (prepared.abortController.signal.aborted) {
2600
2682
  const e = new Error("run aborted");
@@ -2647,15 +2729,31 @@ export class Runner {
2647
2729
  }
2648
2730
  }
2649
2731
  const engineSegments = continuation.length > 0 ? [{ start: 0, end: continuation.length }] : [];
2650
- for (const steer of [resume.cp.state.pendingSteer, resume.wakeMessage]) {
2651
- if (steer === undefined)
2652
- continue;
2732
+ const resumeFrames = [
2733
+ ...readPendingSteerQueue(resume.cp.state).map((entry) => ({ entry, source: "steer" })),
2734
+ ...(resume.wakeMessage !== undefined ? [{ entry: resume.wakeMessage, source: "wake" }] : []),
2735
+ ];
2736
+ for (const { entry: steer, source } of resumeFrames) {
2653
2737
  const start = continuation.length;
2738
+ const projected = projectHumanInput({ text: steer.text, actor: steer.actor, source });
2654
2739
  continuation +=
2655
2740
  "\n\n" +
2656
- (steer.trusted ? formatHookFeedback(steer.text) : delimitUntrusted("supervisor steering message", steer.text));
2741
+ (steer.trusted ? formatHookFeedback(projected) : delimitUntrusted("supervisor steering message", projected));
2657
2742
  if (steer.trusted)
2658
2743
  engineSegments.push({ start, end: continuation.length });
2744
+ queue.push({
2745
+ ...buildHumanInputEvent({
2746
+ carrier: source,
2747
+ source,
2748
+ delivery: "applied",
2749
+ sessionSeq: nextHumanInputSeq(prepared.harness),
2750
+ inputId: steer.inputId,
2751
+ ...(steer.actor !== undefined ? { actor: steer.actor } : {}),
2752
+ ...(steer.actor?.issuer !== undefined ? { issuer: steer.actor.issuer } : {}),
2753
+ ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
2754
+ }),
2755
+ ...ident(),
2756
+ });
2659
2757
  }
2660
2758
  if (resume !== undefined)
2661
2759
  resume.decisionDelivered = true;
@@ -2663,7 +2761,9 @@ export class Runner {
2663
2761
  }
2664
2762
  }
2665
2763
  else {
2666
- let effectiveObjective = spec.objective;
2764
+ const objectiveActor = entryActor;
2765
+ const projectedObjective = projectHumanInput({ text: spec.objective, actor: objectiveActor, source: "objective" });
2766
+ let effectiveObjective = projectedObjective;
2667
2767
  let promptBlocked = false;
2668
2768
  const userPromptSubmit = (spec.hooks ?? this.deps.hooks)?.userPromptSubmit;
2669
2769
  if (userPromptSubmit) {
@@ -2674,7 +2774,7 @@ export class Runner {
2674
2774
  promptBlocked = true;
2675
2775
  }
2676
2776
  else if (decision?.additionalContext) {
2677
- effectiveObjective = `${formatHookFeedback(decision.additionalContext)}\n\n${spec.objective}`;
2777
+ effectiveObjective = `${formatHookFeedback(decision.additionalContext)}\n\n${projectedObjective}`;
2678
2778
  }
2679
2779
  }
2680
2780
  catch (hookErr) {
@@ -2775,10 +2875,23 @@ export class Runner {
2775
2875
  rs.attach.attachmentsInjected += firstFrames.length;
2776
2876
  if (firstFrames.length > 0)
2777
2877
  await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
2778
- const enginePrefixChars = effectiveObjective.length - spec.objective.length;
2878
+ const enginePrefixChars = effectiveObjective.length - projectedObjective.length;
2879
+ queue.push({
2880
+ ...buildHumanInputEvent({
2881
+ carrier: "objective",
2882
+ source: "objective",
2883
+ delivery: "applied",
2884
+ sessionSeq: nextHumanInputSeq(prepared.harness),
2885
+ ...(objectiveActor !== undefined ? { actor: objectiveActor } : {}),
2886
+ ...(objectiveActor?.issuer !== undefined ? { issuer: objectiveActor.issuer } : {}),
2887
+ ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
2888
+ }),
2889
+ ...ident(),
2890
+ });
2779
2891
  final = await withBrainSinks(() => prepared.harness.prompt(effectiveObjective, {
2780
2892
  ...(images && images.length > 0 ? { images } : {}),
2781
2893
  ...(enginePrefixChars > 0 ? { enginePrefixChars } : {}),
2894
+ ...(objectiveActor !== undefined ? { actor: objectiveActor } : {}),
2782
2895
  }));
2783
2896
  }
2784
2897
  }
@@ -2801,6 +2914,7 @@ export class Runner {
2801
2914
  prepared.lspDiagnostics?.registry.releaseRun(prepared.lspDiagnostics.runIdent);
2802
2915
  prepared.abortController.abort();
2803
2916
  prepared.releaseSignal();
2917
+ strandedHumanAnswers = prepared.settleContentAskBindings();
2804
2918
  try {
2805
2919
  prepared.harness.recoverUndrainedEngineNotes();
2806
2920
  }
@@ -2939,6 +3053,7 @@ export class Runner {
2939
3053
  model: prepared.model.id,
2940
3054
  unpricedSpend: rs.telemetry.unpricedSpend,
2941
3055
  rewindNotes: prepared.rewindNotes,
3056
+ strandedHumanAnswers,
2942
3057
  remoteEnvFailures: prepared.remoteEnvFailures,
2943
3058
  retryAfterMs: rs.limits.platformTerminal?.retryAfterMs,
2944
3059
  abortedForTimeout: timeout.fired,
@@ -3075,6 +3190,7 @@ export class Runner {
3075
3190
  const terminalTick = {
3076
3191
  type: "task_progress",
3077
3192
  taskId: rs.telemetry.taskId,
3193
+ ...(internals?.delegationTaskType !== undefined ? { taskType: internals.delegationTaskType } : {}),
3078
3194
  ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
3079
3195
  ...(subagentName ? { name: subagentName } : {}),
3080
3196
  usage: { totalTokens: stats.tokens, toolUses: stats.toolCalls, durationMs: Date.now() - rs.telemetry.taskStart },
@@ -3281,6 +3397,9 @@ export class Runner {
3281
3397
  return stream.result();
3282
3398
  }
3283
3399
  async resumeStream(token, outcome, taskConfig, internals) {
3400
+ if (outcome === null || typeof outcome !== "object") {
3401
+ throw new CheckpointError("checkpoint.invalid_outcome", `resume outcome must be an object naming its gate (got ${outcome === null ? "null" : typeof outcome})`);
3402
+ }
3284
3403
  const store = resolveCheckpointStore(taskConfig, this.deps);
3285
3404
  if (!store) {
3286
3405
  throw new CheckpointError("checkpoint.not_found", "no CheckpointStore wired — cannot resume (set RunnerDeps.checkpointStore or taskConfig.checkpointStore)");
@@ -3299,7 +3418,10 @@ export class Runner {
3299
3418
  }
3300
3419
  }
3301
3420
  let wakeMessage;
3302
- if (outcome.gate === "wake") {
3421
+ const outcomeGate = outcome.gate;
3422
+ let suppliedMessage;
3423
+ if (outcomeGate === "wake") {
3424
+ suppliedMessage = outcome.message;
3303
3425
  const gk = cp.gate.kind;
3304
3426
  const decideEntry = gk === "human" || gk === "irreversible_ask"
3305
3427
  ? 'a `policy_ask` outcome (decision allow/deny bound to the pending tool call)'
@@ -3332,56 +3454,147 @@ export class Runner {
3332
3454
  `(newer-version row?) — fail-closed: wake only serves a pure park (pendingAction "task_done")`);
3333
3455
  }
3334
3456
  }
3335
- if (outcome.message !== undefined) {
3336
- wakeMessage = validatePendingSteer(outcome.message);
3457
+ if (suppliedMessage !== undefined) {
3458
+ wakeMessage = validatePendingSteer(suppliedMessage);
3337
3459
  }
3338
- else if (cp.state.pendingSteer === undefined) {
3460
+ else if (readPendingSteerQueue(cp.state).length === 0) {
3339
3461
  throw new CheckpointError("wake.nothing_to_deliver", "cannot wake: no message was supplied and the checkpoint holds no parked pendingSteer — an empty " +
3340
3462
  "wake would burn the checkpoint on a blank continuation; supply `message` or park a steer first");
3341
3463
  }
3342
3464
  }
3343
- const gateMatch = outcome.gate === "wake" ||
3344
- (cp.gate.kind === "human" && outcome.gate === "policy_ask") ||
3345
- (cp.gate.kind === "irreversible_ask" && outcome.gate === "policy_ask") ||
3346
- (cp.gate.kind === "resource_limit" && outcome.gate === "resource_limit") ||
3347
- (cp.gate.kind === "needs_review" && outcome.gate === "dry_run_review") ||
3348
- (cp.gate.kind === "plan_review" && outcome.gate === "plan_review");
3465
+ const gateMatch = outcomeGate === "wake" ||
3466
+ (cp.gate.kind === "human" && outcomeGate === "policy_ask") ||
3467
+ (cp.gate.kind === "irreversible_ask" && outcomeGate === "policy_ask") ||
3468
+ (cp.gate.kind === "resource_limit" && outcomeGate === "resource_limit") ||
3469
+ (cp.gate.kind === "needs_review" && outcomeGate === "dry_run_review") ||
3470
+ (cp.gate.kind === "plan_review" && outcomeGate === "plan_review");
3349
3471
  if (!gateMatch) {
3350
- throw new CheckpointError("checkpoint.gate_mismatch", `resume outcome (gate "${outcome.gate}") does not match checkpoint gate "${cp.gate.kind}" — resume serves human/policy_ask, irreversible_ask/policy_ask, resource_limit/resource_limit, needs_review/dry_run_review, and plan_review/plan_review`);
3351
- }
3352
- const reasonBearing = (outcome.gate === "policy_ask" && outcome.decision === "deny") ||
3353
- (outcome.gate === "dry_run_review" && outcome.decision === "reject") ||
3354
- (outcome.gate === "plan_review" && outcome.decision === "reject");
3355
- if (reasonBearing && outcome.reason && sanitizeUntrustedText(outcome.reason) !== outcome.reason) {
3356
- throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
3472
+ throw new CheckpointError("checkpoint.gate_mismatch", `resume outcome (gate "${describeSuppliedValue(outcomeGate)}") does not match checkpoint gate "${cp.gate.kind}" — resume serves human/policy_ask, irreversible_ask/policy_ask, resource_limit/resource_limit, needs_review/dry_run_review, and plan_review/plan_review`);
3473
+ }
3474
+ let plainReviewOutcome;
3475
+ if (cp.gate.kind === "plan_review") {
3476
+ const decide = outcome;
3477
+ const decision = decide.decision;
3478
+ const editedPlan = decide.editedPlan;
3479
+ const reason = decide.reason;
3480
+ if (decision !== "approve" && decision !== "edit" && decision !== "reject") {
3481
+ throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${describeSuppliedValue(decision)}" is outside the plan_review domain — a plan review is exactly "approve", "edit" or "reject"; refusing pre-CAS, the checkpoint stays pending`);
3482
+ }
3483
+ assertReviewText(editedPlan, "editedPlan");
3484
+ assertReviewText(reason, "reason");
3485
+ plainReviewOutcome = {
3486
+ gate: "plan_review",
3487
+ decision,
3488
+ ...(editedPlan !== undefined ? { editedPlan } : {}),
3489
+ ...(reason !== undefined ? { reason } : {}),
3490
+ };
3357
3491
  }
3358
- if (outcome.gate === "plan_review" &&
3359
- outcome.decision === "edit" &&
3360
- outcome.editedPlan &&
3361
- sanitizeUntrustedText(outcome.editedPlan) !== outcome.editedPlan) {
3362
- throw new CheckpointError("checkpoint.invalid_outcome", "resume editedPlan must not contain a </system-reminder> tag");
3492
+ else if (cp.gate.kind === "needs_review") {
3493
+ const decide = outcome;
3494
+ const decision = decide.decision;
3495
+ const reason = decide.reason;
3496
+ if (decision !== "approve" && decision !== "reject") {
3497
+ throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${describeSuppliedValue(decision)}" is outside the dry_run_review domain — a dry-run review is exactly "approve" or "reject"; refusing pre-CAS, the checkpoint stays pending`);
3498
+ }
3499
+ assertReviewText(reason, "reason");
3500
+ plainReviewOutcome = {
3501
+ gate: "dry_run_review",
3502
+ decision,
3503
+ ...(reason !== undefined ? { reason } : {}),
3504
+ };
3363
3505
  }
3364
- if ((outcome.gate === "plan_review" || outcome.gate === "dry_run_review") && cp.reopenReason === "env_failed") {
3365
- const winner = cp.resolvedOutcome;
3366
- if (winner === undefined) {
3367
- throw new CheckpointError("checkpoint.reopen_revote", "review checkpoint was reopened after an env-restore failure (env_failed) but carries no persisted winner to replay — refusing to resume (inconsistent row, fail-closed)");
3506
+ if (plainReviewOutcome !== undefined) {
3507
+ const reason = plainReviewOutcome.reason;
3508
+ if (plainReviewOutcome.decision === "reject" && reason && sanitizeUntrustedText(reason) !== reason) {
3509
+ throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
3510
+ }
3511
+ const capturedPlan = plainReviewOutcome.gate === "plan_review" ? plainReviewOutcome.editedPlan : undefined;
3512
+ if (plainReviewOutcome.decision === "edit" &&
3513
+ capturedPlan &&
3514
+ sanitizeUntrustedText(capturedPlan) !== capturedPlan) {
3515
+ throw new CheckpointError("checkpoint.invalid_outcome", "resume editedPlan must not contain a </system-reminder> tag");
3516
+ }
3517
+ if (cp.reopenReason === "env_failed") {
3518
+ const winner = cp.resolvedOutcome;
3519
+ if (winner === undefined) {
3520
+ throw new CheckpointError("checkpoint.reopen_revote", "review checkpoint was reopened after an env-restore failure (env_failed) but carries no persisted winner to replay — refusing to resume (inconsistent row, fail-closed)");
3521
+ }
3522
+ if (winner.decision !== plainReviewOutcome.decision) {
3523
+ throw new CheckpointError("checkpoint.reopen_revote", `an env_failed reopen replays the ALREADY-RECORDED review decision ("${winner.decision}") — refusing a re-vote with a different decision ("${plainReviewOutcome.decision}")`);
3524
+ }
3525
+ if (winner.updatedInput !== capturedPlan) {
3526
+ throw new CheckpointError("checkpoint.reopen_revote", "an env_failed reopen replays the ALREADY-RECORDED review decision — refusing a re-vote whose edited plan differs from the recorded one");
3527
+ }
3368
3528
  }
3369
- if (winner.decision !== outcome.decision) {
3370
- throw new CheckpointError("checkpoint.reopen_revote", `an env_failed reopen replays the ALREADY-RECORDED review decision ("${winner.decision}") — refusing a re-vote with a different decision ("${outcome.decision}")`);
3529
+ }
3530
+ let plainParkOutcome;
3531
+ if (outcomeGate === "wake") {
3532
+ plainParkOutcome = {
3533
+ gate: "wake",
3534
+ ...(wakeMessage !== undefined ? { message: wakeMessage } : {}),
3535
+ };
3536
+ }
3537
+ else if (outcomeGate === "resource_limit") {
3538
+ const decision = outcome.decision;
3539
+ if (decision !== "continue") {
3540
+ throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${describeSuppliedValue(decision)}" is outside the resource_limit domain — a slice resume is exactly "continue"; refusing pre-CAS, the checkpoint stays pending`);
3541
+ }
3542
+ plainParkOutcome = { gate: "resource_limit", decision };
3543
+ }
3544
+ let suppliedAnswer;
3545
+ let redeemedAnswer;
3546
+ let plainPolicyOutcome;
3547
+ if (cp.gate.kind === "human" || cp.gate.kind === "irreversible_ask") {
3548
+ const decide = outcome;
3549
+ suppliedAnswer = decide.answer;
3550
+ if (suppliedAnswer !== undefined) {
3551
+ const reading = classifyQuestionOutcome(suppliedAnswer);
3552
+ if (reading.shape !== "answered") {
3553
+ throw new CheckpointError("checkpoint.invalid_outcome", "resume carried a content-ask `answer` that does not read as an answer (malformed, sparse, or not capturable as plain data) — " +
3554
+ "refusing pre-CAS rather than fabricating an empty human response; the checkpoint stays pending", { field: "answer" });
3555
+ }
3556
+ redeemedAnswer = reading.answer;
3557
+ }
3558
+ const updatedInputRaw = decide.updatedInput;
3559
+ let updatedInput;
3560
+ try {
3561
+ updatedInput = updatedInputRaw === undefined ? undefined : structuredClone(updatedInputRaw);
3371
3562
  }
3372
- const replayedPlan = outcome.gate === "plan_review" ? outcome.editedPlan : undefined;
3373
- if (winner.updatedInput !== replayedPlan) {
3374
- throw new CheckpointError("checkpoint.reopen_revote", "an env_failed reopen replays the ALREADY-RECORDED review decision — refusing a re-vote whose edited plan differs from the recorded one");
3563
+ catch {
3564
+ throw new CheckpointError("checkpoint.invalid_outcome", "resume carried an `updatedInput` that could not be captured as plain data — refusing pre-CAS; the checkpoint stays pending");
3565
+ }
3566
+ if (Object.is(updatedInput, -0))
3567
+ updatedInput = 0;
3568
+ if (updatedInput !== undefined && !canonicalizeCapturedPlainData(updatedInput)) {
3569
+ throw new CheckpointError("checkpoint.invalid_outcome", "resume carried an `updatedInput` outside the plain-JSON value domain (a Map/Set/Date/buffer, a sparse or expando-carrying array, a non-finite number, or a cycle) — " +
3570
+ "such a value reads differently on the two sides of the durable store; refusing pre-CAS, the checkpoint stays pending");
3571
+ }
3572
+ const decision = decide.decision;
3573
+ if (decision !== "allow" && decision !== "deny") {
3574
+ throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${String(decision)}" is outside the policy_ask domain — a decide is exactly "allow" or "deny"; refusing pre-CAS, the checkpoint stays pending`);
3575
+ }
3576
+ const reason = decide.reason;
3577
+ plainPolicyOutcome = {
3578
+ gate: "policy_ask",
3579
+ boundCallId: decide.boundCallId,
3580
+ boundInputHash: decide.boundInputHash,
3581
+ decision,
3582
+ ...(updatedInput !== undefined ? { updatedInput } : {}),
3583
+ ...(reason !== undefined ? { reason } : {}),
3584
+ ...(redeemedAnswer !== undefined ? { answer: redeemedAnswer } : {}),
3585
+ };
3586
+ if (plainPolicyOutcome.decision === "deny" && plainPolicyOutcome.reason && sanitizeUntrustedText(plainPolicyOutcome.reason) !== plainPolicyOutcome.reason) {
3587
+ throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
3375
3588
  }
3376
3589
  }
3377
- if (outcome.gate === "policy_ask") {
3590
+ if (plainPolicyOutcome !== undefined) {
3378
3591
  const boundTo = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.toolCallId : undefined;
3379
- if (outcome.boundCallId !== boundTo) {
3380
- throw new CheckpointError("checkpoint.invalid_outcome", `resume boundCallId "${outcome.boundCallId}" does not match the checkpoint's pending tool call "${boundTo ?? "(none)"}" — the decision-action binding (design/80 D-1) failed; refusing to apply a decision bound to a different action`, { field: "boundCallId" });
3592
+ if (plainPolicyOutcome.boundCallId !== boundTo) {
3593
+ throw new CheckpointError("checkpoint.invalid_outcome", `resume boundCallId "${plainPolicyOutcome.boundCallId}" does not match the checkpoint's pending tool call "${boundTo ?? "(none)"}" — the decision-action binding (design/80 D-1) failed; refusing to apply a decision bound to a different action`, { field: "boundCallId" });
3381
3594
  }
3382
3595
  const boundHash = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.boundInputHash : undefined;
3383
3596
  if (boundHash !== undefined) {
3384
- if (outcome.boundInputHash !== boundHash) {
3597
+ if (plainPolicyOutcome.boundInputHash !== boundHash) {
3385
3598
  throw new CheckpointError("checkpoint.invalid_outcome", "resume boundInputHash does not match the checkpoint's pending tool call input — the decision-action input binding (design/80 D-1 §2) failed; refusing to apply an approval bound to a different input (TOCTOU re-mint guard)", { field: "boundInputHash" });
3386
3599
  }
3387
3600
  }
@@ -3392,20 +3605,26 @@ export class Runner {
3392
3605
  if (cp.resolvedOutcome === undefined) {
3393
3606
  throw new CheckpointError("checkpoint.reopen_revote", "checkpoint was reopened after an env-restore failure (env_failed) but carries no persisted winner to replay — refusing to resume (inconsistent row, fail-closed)");
3394
3607
  }
3395
- const incoming = winnerFromOutcome(outcome);
3396
- if (incoming === undefined || !sameWinner(incoming, cp.resolvedOutcome)) {
3608
+ const incoming = winnerFromOutcome(plainPolicyOutcome);
3609
+ const persistedWinner = (() => {
3610
+ const w = cp.resolvedOutcome;
3611
+ if (w?.answer === undefined)
3612
+ return w;
3613
+ const reading = classifyQuestionOutcome(w.answer);
3614
+ return reading.shape === "answered" ? { ...w, answer: reading.answer } : w;
3615
+ })();
3616
+ if (incoming === undefined || !sameWinner(incoming, persistedWinner)) {
3397
3617
  throw new CheckpointError("checkpoint.reopen_revote", `this checkpoint was reopened after an env-restore failure (env_failed) — a re-resume is a system retry of the already-approved action, not a re-vote; the supplied decision must replay the persisted winner (boundCallId "${cp.resolvedOutcome.boundCallId}", decision "${cp.resolvedOutcome.decision}"), refusing a different decision`);
3398
3618
  }
3399
3619
  }
3400
3620
  const contentAskCallId = pendingContentAskCallId(cp);
3401
- if (outcome.answer !== undefined && (contentAskCallId === undefined || outcome.decision !== "allow")) {
3621
+ if (suppliedAnswer !== undefined && (contentAskCallId === undefined || plainPolicyOutcome.decision !== "allow")) {
3402
3622
  throw new CheckpointError("checkpoint.invalid_outcome", contentAskCallId === undefined
3403
3623
  ? "resume carried a content-ask `answer` but the checkpoint's pending call is not the reserved question tool — an answer has no consumer on a side-effecting tool's approval; refusing rather than dropping it silently"
3404
3624
  : "resume carried a content-ask `answer` on a `deny` — a denial injects a refusal, never an answer; refusing rather than dropping it silently", { field: "answer" });
3405
3625
  }
3406
- if (contentAskCallId !== undefined && outcome.decision === "allow" && outcome.answer === undefined) {
3407
- const liveFace = taskConfig.onQuestion ?? this.deps.onQuestion;
3408
- if (liveFace === undefined || liveFace === QUESTION_AWAITS_RESUME) {
3626
+ if (contentAskCallId !== undefined && plainPolicyOutcome.decision === "allow" && suppliedAnswer === undefined) {
3627
+ if (!isLiveQuestionFace(taskConfig.onQuestion ?? this.deps.onQuestion)) {
3409
3628
  throw new CheckpointError("checkpoint.invalid_outcome", "resume approved a content-ask (the reserved question tool) without an `answer`, and this resume has no live answering face — " +
3410
3629
  'executing the question against nothing would hand the model a fabricated "no human is available" default while consuming the approval; ' +
3411
3630
  "re-resume with the operator's answer on the outcome (or deny it), the checkpoint stays pending", { field: "answer" });
@@ -3442,7 +3661,11 @@ export class Runner {
3442
3661
  "rejected pre-CAS (the checkpoint stays pending) — re-resume with the full original chain");
3443
3662
  }
3444
3663
  }
3445
- const won = await store.resolve(token, cp.scope, outcome, { rev: cp.rev ?? 0 });
3664
+ const outcomeForStore = plainPolicyOutcome ?? plainReviewOutcome ?? plainParkOutcome;
3665
+ if (outcomeForStore === undefined) {
3666
+ throw new CheckpointError("checkpoint.invalid_outcome", `resume outcome (gate "${describeSuppliedValue(outcomeGate)}") matched the checkpoint gate but no lane captured it — refusing pre-CAS rather than passing the caller's live object to the store and the resumed run`);
3667
+ }
3668
+ const won = await store.resolve(token, cp.scope, outcomeForStore, { rev: cp.rev ?? 0 });
3446
3669
  if (!won) {
3447
3670
  const live = await store.get(token);
3448
3671
  if (live?.status === "pending") {
@@ -3456,8 +3679,8 @@ export class Runner {
3456
3679
  await internals.afterCheckpointResolve();
3457
3680
  consumeFlipDone = true;
3458
3681
  }
3459
- const answerFace = outcome.gate === "policy_ask" && outcome.answer !== undefined
3460
- ? answerFaceForRedeemedCall(outcome.answer, outcome.boundCallId, taskConfig.onQuestion ?? this.deps.onQuestion)
3682
+ const answerFace = plainPolicyOutcome !== undefined && redeemedAnswer !== undefined
3683
+ ? answerFaceForRedeemedCall(redeemedAnswer, plainPolicyOutcome.boundCallId, taskConfig.onQuestion ?? this.deps.onQuestion, boundInputHashOf(cp.pendingAction.args?.questions))
3461
3684
  : undefined;
3462
3685
  const spec = {
3463
3686
  ...taskConfig,
@@ -3496,7 +3719,7 @@ export class Runner {
3496
3719
  ? new CheckpointError("checkpoint.reopen_failed", "the resume was aborted before the approved action could run AND the store refused to reopen the checkpoint — the approval is terminally consumed and the suspended work was not executed; a retry needs a fresh approval")
3497
3720
  : new CheckpointError("checkpoint.reopen_failed", "the resume was aborted before the approved action could run and the reopen attempt FAILED IN FLIGHT — the checkpoint's state is unprovable from here: it may already be pending again. Re-read it before deciding; do NOT issue a fresh approval on the assumption the old one is dead (the approved action did NOT run either way)");
3498
3721
  }
3499
- return this.runTaskStream(spec, { cp, outcome, onEnvRestoreFailed, ...(wakeMessage !== undefined ? { wakeMessage } : {}) }, internals);
3722
+ return this.runTaskStream(spec, { cp, outcome: outcomeForStore, onEnvRestoreFailed, ...(wakeMessage !== undefined ? { wakeMessage } : {}) }, internals);
3500
3723
  }
3501
3724
  async applyResumeDecision(prepared, resume, emit, emitCommitted, onResolvedToolSuccess, onExecuteStart) {
3502
3725
  const { pendingAction } = resume.cp;
@@ -3552,7 +3775,7 @@ export class Runner {
3552
3775
  const { outcome } = resume;
3553
3776
  if (pendingAction.kind !== "tool_approval" || outcome.gate !== "policy_ask")
3554
3777
  return;
3555
- const resolvedArgs = pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME ? pendingAction.args : (outcome.updatedInput ?? pendingAction.args);
3778
+ const resolvedArgs = pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME ? pendingAction.args : (outcome.updatedInput !== undefined ? outcome.updatedInput : pendingAction.args);
3556
3779
  const pendingLabel = (() => { const l = prepared.tools.find((t) => t.name === pendingAction.toolName)?.label; return l !== undefined && l !== pendingAction.toolName ? { label: l } : {}; })();
3557
3780
  emit({ type: "tool_start", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, args: resolvedArgs });
3558
3781
  const emitEnd = (isError, result) => emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError) });
@@ -3563,6 +3786,9 @@ export class Runner {
3563
3786
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
3564
3787
  return;
3565
3788
  }
3789
+ if (outcome.decision !== "allow") {
3790
+ throw new CheckpointError("checkpoint.invalid_outcome", `pending action reached the resolver with decision "${String(outcome.decision)}" — only "allow" executes and only "deny" injects a denial; refusing to execute`);
3791
+ }
3566
3792
  if (outcome.decision === "allow" && outcome.updatedInput !== undefined && prepared.basePolicyForResumeEdit) {
3567
3793
  const rechecked = refuseOutOfContractDecision(await prepared.basePolicyForResumeEdit.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal));
3568
3794
  if (rechecked.action === "deny") {