@sema-agent/core 5.12.0 → 5.14.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 (101) hide show
  1. package/CHANGELOG.md +320 -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 +137 -43
  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/open-responses.js +24 -10
  9. package/dist/brain/openai.js +29 -11
  10. package/dist/brain/request-params.d.ts +2 -0
  11. package/dist/brain/request-params.js +16 -0
  12. package/dist/brain/stream-engine.d.ts +9 -1
  13. package/dist/brain/stream-engine.js +256 -27
  14. package/dist/brain/timeout.d.ts +1 -0
  15. package/dist/brain/timeout.js +1 -0
  16. package/dist/core/a2a.d.ts +2 -2
  17. package/dist/core/a2a.js +3 -3
  18. package/dist/core/ask-question.d.ts +47 -2
  19. package/dist/core/ask-question.js +209 -28
  20. package/dist/core/background-agent-store.d.ts +2 -0
  21. package/dist/core/checkpoint-store.d.ts +49 -18
  22. package/dist/core/checkpoint-store.js +117 -4
  23. package/dist/core/compliance.d.ts +11 -0
  24. package/dist/core/compliance.js +34 -0
  25. package/dist/core/governance-codes.d.ts +12 -0
  26. package/dist/core/governance-codes.js +24 -0
  27. package/dist/core/hooks.d.ts +24 -2
  28. package/dist/core/hooks.js +97 -10
  29. package/dist/core/human-input-projection.d.ts +12 -0
  30. package/dist/core/human-input-projection.js +27 -0
  31. package/dist/core/locked-config.d.ts +27 -0
  32. package/dist/core/locked-config.js +42 -0
  33. package/dist/core/mcp.d.ts +7 -2
  34. package/dist/core/mcp.js +7 -7
  35. package/dist/core/memory-admission.d.ts +51 -0
  36. package/dist/core/memory-admission.js +159 -0
  37. package/dist/core/memory.d.ts +2 -0
  38. package/dist/core/memory.js +3 -2
  39. package/dist/core/retention.d.ts +36 -0
  40. package/dist/core/retention.js +31 -0
  41. package/dist/core/runner/assemble-result.d.ts +1 -0
  42. package/dist/core/runner/assemble-result.js +2 -2
  43. package/dist/core/runner/prepare-memory.d.ts +9 -0
  44. package/dist/core/runner/prepare-memory.js +28 -2
  45. package/dist/core/runner/prepare-task.d.ts +18 -6
  46. package/dist/core/runner/prepare-task.js +428 -38
  47. package/dist/core/runner/runtask.d.ts +3 -6
  48. package/dist/core/runner/runtask.js +258 -77
  49. package/dist/core/runner/tool-output-projection.js +1 -0
  50. package/dist/core/session-store.d.ts +4 -0
  51. package/dist/core/session-store.js +5 -0
  52. package/dist/core/session.d.ts +1 -0
  53. package/dist/core/store-contracts/background-agent-store-contract.js +19 -0
  54. package/dist/core/store-contracts/checkpoint-store-contract.js +62 -3
  55. package/dist/core/task-notification.d.ts +2 -0
  56. package/dist/core/task-notification.js +5 -3
  57. package/dist/core/task-registry-agent.d.ts +1 -0
  58. package/dist/core/task-registry-agent.js +6 -0
  59. package/dist/core/task-registry.d.ts +1 -0
  60. package/dist/core/task-registry.js +4 -1
  61. package/dist/core/tool-policy.d.ts +5 -0
  62. package/dist/core/tool-policy.js +2 -1
  63. package/dist/core/tool-result-store.d.ts +2 -0
  64. package/dist/core/tool-result-store.js +1 -0
  65. package/dist/core/types.d.ts +38 -2
  66. package/dist/core/wiring-manifest.d.ts +97 -0
  67. package/dist/core/wiring-manifest.js +186 -0
  68. package/dist/engine/compaction/compaction.js +2 -2
  69. package/dist/engine/harness/agent-harness.d.ts +2 -1
  70. package/dist/engine/harness/agent-harness.js +19 -2
  71. package/dist/engine/harness/types.d.ts +3 -1
  72. package/dist/engine/llm/types.d.ts +7 -0
  73. package/dist/engine/llm/types.js +8 -1
  74. package/dist/engine/llm/validation.js +11 -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 +12 -4
  80. package/dist/index.js +12 -4
  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/stores/file/checkpoint-store.d.ts +3 -5
  92. package/dist/stores/file/checkpoint-store.js +31 -2
  93. package/dist/stores/file/index.js +1 -1
  94. package/dist/stores/file/session-store.d.ts +3 -1
  95. package/dist/stores/file/session-store.js +2 -2
  96. package/dist/stores/file/shared-ledger.js +8 -1
  97. package/dist/tools/fs/bash-readonly-classifier.d.ts +3 -0
  98. package/dist/tools/fs/bash-readonly-classifier.js +94 -0
  99. package/dist/tools/fs/fs-bash.js +31 -12
  100. package/dist/tools/fs/safety.js +34 -10
  101. 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")
@@ -1166,6 +1193,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
1166
1193
  const progress = {
1167
1194
  type: "task_progress",
1168
1195
  taskId: rs.telemetry.taskId,
1196
+ ...(internals?.delegationTaskType !== undefined ? { taskType: internals.delegationTaskType } : {}),
1169
1197
  ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
1170
1198
  ...(subagentName ? { name: subagentName } : {}),
1171
1199
  usage: { totalTokens: stats.tokens, toolUses: stats.toolCalls, durationMs: Date.now() - rs.telemetry.taskStart },
@@ -1332,6 +1360,7 @@ export class Runner {
1332
1360
  return runSideQuery(spec, { brain: this.deps.brain, models: this.deps.models, roles: this.deps.roles });
1333
1361
  }
1334
1362
  runTaskStream(spec, resume, internals) {
1363
+ const entryActor = spec.actor === undefined ? undefined : snapshotActorAssertion(spec.actor);
1335
1364
  const queue = new PushQueue();
1336
1365
  const detachHub = new ToolDetachHub();
1337
1366
  let resultValue;
@@ -1386,7 +1415,7 @@ export class Runner {
1386
1415
  publishReady(h);
1387
1416
  }, (s) => {
1388
1417
  reapHandle = s;
1389
- }, manualCompactRef, taskIdRef, resume, { ...(internals ?? {}), detachHub }, notifyRef);
1418
+ }, manualCompactRef, taskIdRef, resume, { ...(internals ?? {}), detachHub }, notifyRef, entryActor);
1390
1419
  }
1391
1420
  finally {
1392
1421
  releaseLock?.();
@@ -1450,6 +1479,12 @@ export class Runner {
1450
1479
  errorMessage: err instanceof Error ? err.message : String(err),
1451
1480
  errorCode: code,
1452
1481
  ...(remoteEnvFailure !== undefined ? { remoteEnvFailures: remoteEnvFailure } : {}),
1482
+ ...(() => {
1483
+ const hinted = err.retryAfterMs;
1484
+ return code === "memory.admission_required" && typeof hinted === "number" && Number.isFinite(hinted) && hinted > 0
1485
+ ? { retryAfterMs: hinted }
1486
+ : {};
1487
+ })(),
1453
1488
  stats: { turns: 0, tokens: 0, toolCalls: 0, cachedTokens: 0, costMicroUsd: 0 },
1454
1489
  };
1455
1490
  emitTrace(spec.tracer ?? this.deps.tracer, () => ({
@@ -1517,7 +1552,29 @@ export class Runner {
1517
1552
  if (options?.trusted && sanitizeUntrustedText(text) !== text) {
1518
1553
  throw steeringError("trusted steering text must not contain a </system-reminder> tag", "steering.invalid_content");
1519
1554
  }
1520
- const payload = options?.trusted ? formatHookFeedback(text) : text;
1555
+ const actor = options?.actor === undefined ? undefined : snapshotActorAssertion(options.actor);
1556
+ const projected = projectHumanInput({ text, actor, source: "steer" });
1557
+ const payload = options?.trusted ? formatHookFeedback(projected) : projected;
1558
+ const mintsAFrame = payload.trim().length !== 0;
1559
+ const emitAccepted = (h) => {
1560
+ if (!mintsAFrame)
1561
+ return;
1562
+ queue.push({
1563
+ ...buildHumanInputEvent({
1564
+ carrier: "steer",
1565
+ source: "steer",
1566
+ delivery: "queued",
1567
+ sessionSeq: nextHumanInputSeq(h.harness),
1568
+ ...(actor !== undefined ? { actor } : {}),
1569
+ ...(actor?.issuer !== undefined ? { issuer: actor.issuer } : {}),
1570
+ ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
1571
+ }),
1572
+ eventId: uuidv7(),
1573
+ ...(internals?.parentToolCallId !== undefined
1574
+ ? { parentToolCallId: internals.parentToolCallId, ...(spec.taskId !== undefined ? { sourceTaskId: spec.taskId } : {}) }
1575
+ : {}),
1576
+ });
1577
+ };
1521
1578
  const deliver = async () => {
1522
1579
  if (resultValue)
1523
1580
  throw steeringError("the task has already finished");
@@ -1525,7 +1582,8 @@ export class Runner {
1525
1582
  if (!h)
1526
1583
  throw steeringError("the task is not running");
1527
1584
  try {
1528
- await h.harness.steer(payload, { provenance: "engine-note" });
1585
+ await h.harness.steer(payload, { provenance: "engine-note", ...(actor !== undefined ? { actor } : {}) });
1586
+ emitAccepted(h);
1529
1587
  return;
1530
1588
  }
1531
1589
  catch (e) {
@@ -1535,7 +1593,8 @@ export class Runner {
1535
1593
  const birthDeadline = Date.now() + READY_TIMEOUT_MS;
1536
1594
  while (resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
1537
1595
  try {
1538
- await h.harness.steer(payload, { provenance: "engine-note" });
1596
+ await h.harness.steer(payload, { provenance: "engine-note", ...(actor !== undefined ? { actor } : {}) });
1597
+ emitAccepted(h);
1539
1598
  return;
1540
1599
  }
1541
1600
  catch (e2) {
@@ -1652,7 +1711,7 @@ export class Runner {
1652
1711
  })()),
1653
1712
  };
1654
1713
  }
1655
- async runLocked(spec, queue, setResult, onSuggestions, onReady, onSuspend, manualCompactRef, taskIdRef, resume, internals, notifyRef) {
1714
+ async runLocked(spec, queue, setResult, onSuggestions, onReady, onSuspend, manualCompactRef, taskIdRef, resume, internals, notifyRef, entryActor) {
1656
1715
  const prepareResume = resume
1657
1716
  ? {
1658
1717
  leafId: resume.cp.leafId,
@@ -1665,6 +1724,12 @@ export class Runner {
1665
1724
  executesApprovedAction: resume.cp.pendingAction.kind === "tool_approval" &&
1666
1725
  resume.outcome.gate !== "wake" &&
1667
1726
  resume.outcome.decision === "allow",
1727
+ ...(resume.outcome.gate !== "wake" && resume.outcome.decision === "allow" && pendingContentAskCallId(resume.cp) !== undefined
1728
+ ? {
1729
+ redeemedContentAskCallId: pendingContentAskCallId(resume.cp),
1730
+ redeemedContentAskQuestionsHash: boundInputHashOf(resume.cp.pendingAction.args?.questions),
1731
+ }
1732
+ : {}),
1668
1733
  }
1669
1734
  : undefined;
1670
1735
  const taskNotificationQueue = new SystemInjectionQueue();
@@ -1703,9 +1768,7 @@ export class Runner {
1703
1768
  queue.push({ type: "task_notification", notification: item.payload, ...notificationIdent() });
1704
1769
  if (notificationHarness) {
1705
1770
  const xml = renderTaskNotificationXml(item.payload);
1706
- const deliver = item.priority === "later"
1707
- ? notificationHarness.followUp(xml, { provenance: "engine-note", enginePayload: item.payload })
1708
- : notificationHarness.steer(xml, { provenance: "engine-note", enginePayload: item.payload });
1771
+ const deliver = notificationHarness.steer(xml, { provenance: "engine-note", enginePayload: item.payload });
1709
1772
  void deliver.then(() => item.onDisposition?.("queued"), () => {
1710
1773
  parkTaskNotification(item.payload, item.priority);
1711
1774
  item.onDisposition?.("parked");
@@ -1755,6 +1818,7 @@ export class Runner {
1755
1818
  const parentToolCallId = internals?.parentToolCallId;
1756
1819
  const ident = () => parentToolCallId !== undefined ? { eventId: uuidv7(), parentToolCallId, sourceTaskId: runSourceTaskId } : { eventId: uuidv7() };
1757
1820
  notificationIdent = ident;
1821
+ queue.push({ type: "wiring_manifest", manifest: prepared.wiringManifest, ...ident() });
1758
1822
  manualCompactRef.emitMooted = (reason) => {
1759
1823
  queue.push({ type: "compaction_outcome", outcome: "mooted", trigger: "manual", reason, ...ident() });
1760
1824
  };
@@ -2323,6 +2387,22 @@ export class Runner {
2323
2387
  });
2324
2388
  const stopHook = (spec.hooks ?? this.deps.hooks)?.stop;
2325
2389
  const finalVerificationOn = spec.finalVerification === true;
2390
+ const finalVerifyBudgetFill = () => {
2391
+ let worst = 0;
2392
+ if (rs.budget.maxTokensWindow !== undefined && rs.budget.maxTokensWindow > 0)
2393
+ worst = Math.max(worst, stats.tokens / rs.budget.maxTokensWindow);
2394
+ if (rs.budget.maxCostMicroUsd !== undefined && rs.budget.maxCostMicroUsd > 0)
2395
+ worst = Math.max(worst, stats.costMicroUsd / rs.budget.maxCostMicroUsd);
2396
+ if (walltimeMonotonicDeadline !== undefined && prepared.suspendForResource === undefined) {
2397
+ const windowMs = walltimeMonotonicDeadline - rs.telemetry.taskStartMonotonic;
2398
+ if (windowMs > 0)
2399
+ worst = Math.max(worst, (performance.now() - rs.telemetry.taskStartMonotonic) / windowMs);
2400
+ }
2401
+ return worst;
2402
+ };
2403
+ const emitFinalVerifyEcho = (body) => {
2404
+ queue.push({ type: "steering_injected", source: "final_verification", preview: body.slice(0, 220), ...ident() });
2405
+ };
2326
2406
  if (stopHook || finalVerificationOn) {
2327
2407
  let consecutiveBlocks = 0;
2328
2408
  prepared.harness.setStopGate(async () => {
@@ -2332,58 +2412,69 @@ export class Runner {
2332
2412
  (rs.counters.finalVerifyInjections === 0 || (rs.counters.finalVerifyInjections === 1 && rs.counters.groundingSignalPreR9 && !rs.counters.groundingSignalPostR9)) &&
2333
2413
  rs.counters.wroteThisRun &&
2334
2414
  prepared.outputRef.set !== true &&
2335
- !(rs.limits.effectiveMaxTurns !== undefined && rs.limits.effectiveMaxTurns > 0 && stats.turns >= rs.limits.effectiveMaxTurns - 1)) {
2415
+ !(rs.limits.effectiveMaxTurns !== undefined && rs.limits.effectiveMaxTurns > 0 && stats.turns >= rs.limits.effectiveMaxTurns - 1) &&
2416
+ finalVerifyBudgetFill() < 0.9) {
2336
2417
  rs.counters.finalVerifyInjections += 1;
2337
2418
  if (rs.counters.finalVerifyInjections === 2) {
2419
+ const reentryBody = "<system-reminder>[final verification] Your tool calls in this run worked with raw bytes, structural parsing, " +
2420
+ "or checksum/digest computation — the deliverable very likely embeds verifiable structure (structural fields, an " +
2421
+ "embedded checksum-family value, reference data it must match, or a replayable deterministic path). You MUST " +
2422
+ "execute the grounding check that structure supports — recompute the embedded value and compare it against the " +
2423
+ "declared one, re-parse the structure from the raw bytes and reconcile it with your output, compare against the " +
2424
+ "reference data, or replay the deterministic path — and REPORT the check's concrete result before finishing. " +
2425
+ "A closing statement without a reported check result is not verification. If you already ran such a check, state " +
2426
+ "its concrete result now; if the check mismatches, fix the deliverable first. This is the final reminder from " +
2427
+ "this verification gate — it will not intervene again.</system-reminder>";
2428
+ emitFinalVerifyEcho(reentryBody);
2338
2429
  return [
2339
2430
  {
2340
2431
  role: "user",
2341
2432
  engineMinted: true,
2342
- content: "<system-reminder>[final verification] Your tool calls in this run worked with raw bytes, structural parsing, " +
2343
- "or checksum/digest computation — the deliverable very likely embeds verifiable structure (structural fields, an " +
2344
- "embedded checksum-family value, reference data it must match, or a replayable deterministic path). You MUST " +
2345
- "execute the grounding check that structure supports — recompute the embedded value and compare it against the " +
2346
- "declared one, re-parse the structure from the raw bytes and reconcile it with your output, compare against the " +
2347
- "reference data, or replay the deterministic path — and REPORT the check's concrete result before finishing. " +
2348
- "A closing statement without a reported check result is not verification. If you already ran such a check, state " +
2349
- "its concrete result now; if the check mismatches, fix the deliverable first. This is the final reminder from " +
2350
- "this verification gate — it will not intervene again.</system-reminder>",
2433
+ content: reentryBody,
2351
2434
  timestamp: Date.now(),
2352
2435
  },
2353
2436
  ];
2354
2437
  }
2438
+ const nudgeBody = "<system-reminder>[final verification] Before finishing: re-verify the FINAL deliverable through its REAL entry point, " +
2439
+ "exactly as the acceptance criteria would exercise it — execute the binary/function/endpoint directly and read the ACTUAL " +
2440
+ "output and exit code. Do NOT rely on earlier self-tests, shell redirections, or assumptions (a program that prints to " +
2441
+ "stdout is not a program that writes the required file). If anything mismatches the task's requirements, fix it before " +
2442
+ "finishing. " +
2443
+ "Treat verification writes as state-harmless: when the deliverable itself is a persisted final " +
2444
+ "state (for example a committed or pushed file, a deployed artifact, or a required output file), " +
2445
+ "do not change that state merely to test it. This constrains HOW you verify — it is never a " +
2446
+ "license to skip the real acceptance path or to check a substitute of your own making: expected " +
2447
+ "values must come from the task's requirements, never from content you generated. If the real " +
2448
+ "acceptance path requires a write, use disposable inputs or an isolated target, end in the exact " +
2449
+ "required final state, and verify that final state before finishing. " +
2450
+ "If the work relied on a third-party API, library, or model, check the usage contract the object itself declares " +
2451
+ "(docstrings, metadata, configuration — e.g. prompt conventions shipped with a model) and confirm your calls follow " +
2452
+ "it rather than a default symmetric usage. Verify not only that the deliverable EXISTS but that the METHOD that " +
2453
+ "produced it matches the task's requirements. " +
2454
+ "Choose the verification SURFACE deliberately: check against the reference data, oracle, or evaluation tooling the " +
2455
+ "task itself provides — re-running your own implementation and getting the same answer is self-consistency, not " +
2456
+ "correctness — and cross-check through an independent second path where the task or environment offers one " +
2457
+ "(checksums, runtime artifacts). Verify the PERSISTED artifact — re-read what is actually on disk or committed, " +
2458
+ "not in-memory state — against every hard constraint from the original task text (numeric bounds, allowed-value " +
2459
+ "lists, naming semantics, required files), reconciling whole-set completeness: nothing missing, nothing duplicated. " +
2460
+ "If the deliverable embeds verifiable structure — structural fields, an embedded checksum-family value, " +
2461
+ "reference data it must match, or a replayable deterministic path — you MUST execute the grounding check " +
2462
+ "that structure supports and REPORT its concrete result in your closing summary: for such a deliverable, " +
2463
+ "no reported check result means the work is not finished. " +
2464
+ "If the task produced neither an executable deliverable nor any verifiable structure or acceptance " +
2465
+ "oracle to check against, briefly confirm completion and stop. " +
2466
+ "Residue YOUR OWN testing created (scratch files, running processes, generated outputs the task does not ask for) " +
2467
+ "is not protected state — if the task's required final state is a clean target, removing your own residue is part " +
2468
+ "of delivering it. " +
2469
+ "Verification must never LAUNDER uncertainty: if part of your conclusion was uncertain before this check, keep " +
2470
+ "reporting it as uncertain unless the check you actually ran resolved it — a re-stated conclusion is not new " +
2471
+ "evidence.</system-reminder>";
2472
+ emitFinalVerifyEcho(nudgeBody);
2355
2473
  return [
2356
2474
  {
2357
2475
  role: "user",
2358
2476
  engineMinted: true,
2359
- content: "<system-reminder>[final verification] Before finishing: re-verify the FINAL deliverable through its REAL entry point, " +
2360
- "exactly as the acceptance criteria would exercise it — execute the binary/function/endpoint directly and read the ACTUAL " +
2361
- "output and exit code. Do NOT rely on earlier self-tests, shell redirections, or assumptions (a program that prints to " +
2362
- "stdout is not a program that writes the required file). If anything mismatches the task's requirements, fix it before " +
2363
- "finishing. " +
2364
- "Treat verification writes as state-harmless: when the deliverable itself is a persisted final " +
2365
- "state (for example a committed or pushed file, a deployed artifact, or a required output file), " +
2366
- "do not change that state merely to test it. This constrains HOW you verify — it is never a " +
2367
- "license to skip the real acceptance path or to check a substitute of your own making: expected " +
2368
- "values must come from the task's requirements, never from content you generated. If the real " +
2369
- "acceptance path requires a write, use disposable inputs or an isolated target, end in the exact " +
2370
- "required final state, and verify that final state before finishing. " +
2371
- "If the work relied on a third-party API, library, or model, check the usage contract the object itself declares " +
2372
- "(docstrings, metadata, configuration — e.g. prompt conventions shipped with a model) and confirm your calls follow " +
2373
- "it rather than a default symmetric usage. Verify not only that the deliverable EXISTS but that the METHOD that " +
2374
- "produced it matches the task's requirements. " +
2375
- "Choose the verification SURFACE deliberately: check against the reference data, oracle, or evaluation tooling the " +
2376
- "task itself provides — re-running your own implementation and getting the same answer is self-consistency, not " +
2377
- "correctness — and cross-check through an independent second path where the task or environment offers one " +
2378
- "(checksums, runtime artifacts). Verify the PERSISTED artifact — re-read what is actually on disk or committed, " +
2379
- "not in-memory state — against every hard constraint from the original task text (numeric bounds, allowed-value " +
2380
- "lists, naming semantics, required files), reconciling whole-set completeness: nothing missing, nothing duplicated. " +
2381
- "If the deliverable embeds verifiable structure — structural fields, an embedded checksum-family value, " +
2382
- "reference data it must match, or a replayable deterministic path — you MUST execute the grounding check " +
2383
- "that structure supports and REPORT its concrete result in your closing summary: for such a deliverable, " +
2384
- "no reported check result means the work is not finished. " +
2385
- "If the task produced neither an executable deliverable nor any verifiable structure or acceptance " +
2386
- "oracle to check against, briefly confirm completion and stop.</system-reminder>",
2477
+ content: nudgeBody,
2387
2478
  timestamp: Date.now(),
2388
2479
  },
2389
2480
  ];
@@ -2564,6 +2655,7 @@ export class Runner {
2564
2655
  let final;
2565
2656
  let threw;
2566
2657
  let abortedLive = false;
2658
+ let strandedHumanAnswers = [];
2567
2659
  try {
2568
2660
  if (prepared.abortController.signal.aborted) {
2569
2661
  const e = new Error("run aborted");
@@ -2616,15 +2708,31 @@ export class Runner {
2616
2708
  }
2617
2709
  }
2618
2710
  const engineSegments = continuation.length > 0 ? [{ start: 0, end: continuation.length }] : [];
2619
- for (const steer of [resume.cp.state.pendingSteer, resume.wakeMessage]) {
2620
- if (steer === undefined)
2621
- continue;
2711
+ const resumeFrames = [
2712
+ ...readPendingSteerQueue(resume.cp.state).map((entry) => ({ entry, source: "steer" })),
2713
+ ...(resume.wakeMessage !== undefined ? [{ entry: resume.wakeMessage, source: "wake" }] : []),
2714
+ ];
2715
+ for (const { entry: steer, source } of resumeFrames) {
2622
2716
  const start = continuation.length;
2717
+ const projected = projectHumanInput({ text: steer.text, actor: steer.actor, source });
2623
2718
  continuation +=
2624
2719
  "\n\n" +
2625
- (steer.trusted ? formatHookFeedback(steer.text) : delimitUntrusted("supervisor steering message", steer.text));
2720
+ (steer.trusted ? formatHookFeedback(projected) : delimitUntrusted("supervisor steering message", projected));
2626
2721
  if (steer.trusted)
2627
2722
  engineSegments.push({ start, end: continuation.length });
2723
+ queue.push({
2724
+ ...buildHumanInputEvent({
2725
+ carrier: source,
2726
+ source,
2727
+ delivery: "applied",
2728
+ sessionSeq: nextHumanInputSeq(prepared.harness),
2729
+ inputId: steer.inputId,
2730
+ ...(steer.actor !== undefined ? { actor: steer.actor } : {}),
2731
+ ...(steer.actor?.issuer !== undefined ? { issuer: steer.actor.issuer } : {}),
2732
+ ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
2733
+ }),
2734
+ ...ident(),
2735
+ });
2628
2736
  }
2629
2737
  if (resume !== undefined)
2630
2738
  resume.decisionDelivered = true;
@@ -2632,7 +2740,9 @@ export class Runner {
2632
2740
  }
2633
2741
  }
2634
2742
  else {
2635
- let effectiveObjective = spec.objective;
2743
+ const objectiveActor = entryActor;
2744
+ const projectedObjective = projectHumanInput({ text: spec.objective, actor: objectiveActor, source: "objective" });
2745
+ let effectiveObjective = projectedObjective;
2636
2746
  let promptBlocked = false;
2637
2747
  const userPromptSubmit = (spec.hooks ?? this.deps.hooks)?.userPromptSubmit;
2638
2748
  if (userPromptSubmit) {
@@ -2643,7 +2753,7 @@ export class Runner {
2643
2753
  promptBlocked = true;
2644
2754
  }
2645
2755
  else if (decision?.additionalContext) {
2646
- effectiveObjective = `${formatHookFeedback(decision.additionalContext)}\n\n${spec.objective}`;
2756
+ effectiveObjective = `${formatHookFeedback(decision.additionalContext)}\n\n${projectedObjective}`;
2647
2757
  }
2648
2758
  }
2649
2759
  catch (hookErr) {
@@ -2744,10 +2854,23 @@ export class Runner {
2744
2854
  rs.attach.attachmentsInjected += firstFrames.length;
2745
2855
  if (firstFrames.length > 0)
2746
2856
  await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
2747
- const enginePrefixChars = effectiveObjective.length - spec.objective.length;
2857
+ const enginePrefixChars = effectiveObjective.length - projectedObjective.length;
2858
+ queue.push({
2859
+ ...buildHumanInputEvent({
2860
+ carrier: "objective",
2861
+ source: "objective",
2862
+ delivery: "applied",
2863
+ sessionSeq: nextHumanInputSeq(prepared.harness),
2864
+ ...(objectiveActor !== undefined ? { actor: objectiveActor } : {}),
2865
+ ...(objectiveActor?.issuer !== undefined ? { issuer: objectiveActor.issuer } : {}),
2866
+ ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
2867
+ }),
2868
+ ...ident(),
2869
+ });
2748
2870
  final = await withBrainSinks(() => prepared.harness.prompt(effectiveObjective, {
2749
2871
  ...(images && images.length > 0 ? { images } : {}),
2750
2872
  ...(enginePrefixChars > 0 ? { enginePrefixChars } : {}),
2873
+ ...(objectiveActor !== undefined ? { actor: objectiveActor } : {}),
2751
2874
  }));
2752
2875
  }
2753
2876
  }
@@ -2770,6 +2893,7 @@ export class Runner {
2770
2893
  prepared.lspDiagnostics?.registry.releaseRun(prepared.lspDiagnostics.runIdent);
2771
2894
  prepared.abortController.abort();
2772
2895
  prepared.releaseSignal();
2896
+ strandedHumanAnswers = prepared.settleContentAskBindings();
2773
2897
  try {
2774
2898
  prepared.harness.recoverUndrainedEngineNotes();
2775
2899
  }
@@ -2908,6 +3032,7 @@ export class Runner {
2908
3032
  model: prepared.model.id,
2909
3033
  unpricedSpend: rs.telemetry.unpricedSpend,
2910
3034
  rewindNotes: prepared.rewindNotes,
3035
+ strandedHumanAnswers,
2911
3036
  remoteEnvFailures: prepared.remoteEnvFailures,
2912
3037
  retryAfterMs: rs.limits.platformTerminal?.retryAfterMs,
2913
3038
  abortedForTimeout: timeout.fired,
@@ -3044,6 +3169,7 @@ export class Runner {
3044
3169
  const terminalTick = {
3045
3170
  type: "task_progress",
3046
3171
  taskId: rs.telemetry.taskId,
3172
+ ...(internals?.delegationTaskType !== undefined ? { taskType: internals.delegationTaskType } : {}),
3047
3173
  ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
3048
3174
  ...(subagentName ? { name: subagentName } : {}),
3049
3175
  usage: { totalTokens: stats.tokens, toolUses: stats.toolCalls, durationMs: Date.now() - rs.telemetry.taskStart },
@@ -3304,7 +3430,7 @@ export class Runner {
3304
3430
  if (outcome.message !== undefined) {
3305
3431
  wakeMessage = validatePendingSteer(outcome.message);
3306
3432
  }
3307
- else if (cp.state.pendingSteer === undefined) {
3433
+ else if (readPendingSteerQueue(cp.state).length === 0) {
3308
3434
  throw new CheckpointError("wake.nothing_to_deliver", "cannot wake: no message was supplied and the checkpoint holds no parked pendingSteer — an empty " +
3309
3435
  "wake would burn the checkpoint on a blank continuation; supply `message` or park a steer first");
3310
3436
  }
@@ -3318,8 +3444,7 @@ export class Runner {
3318
3444
  if (!gateMatch) {
3319
3445
  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`);
3320
3446
  }
3321
- const reasonBearing = (outcome.gate === "policy_ask" && outcome.decision === "deny") ||
3322
- (outcome.gate === "dry_run_review" && outcome.decision === "reject") ||
3447
+ const reasonBearing = (outcome.gate === "dry_run_review" && outcome.decision === "reject") ||
3323
3448
  (outcome.gate === "plan_review" && outcome.decision === "reject");
3324
3449
  if (reasonBearing && outcome.reason && sanitizeUntrustedText(outcome.reason) !== outcome.reason) {
3325
3450
  throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
@@ -3343,14 +3468,60 @@ export class Runner {
3343
3468
  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");
3344
3469
  }
3345
3470
  }
3346
- if (outcome.gate === "policy_ask") {
3471
+ let suppliedAnswer;
3472
+ let redeemedAnswer;
3473
+ let plainPolicyOutcome;
3474
+ if (cp.gate.kind === "human" || cp.gate.kind === "irreversible_ask") {
3475
+ const decide = outcome;
3476
+ suppliedAnswer = decide.answer;
3477
+ if (suppliedAnswer !== undefined) {
3478
+ const reading = classifyQuestionOutcome(suppliedAnswer);
3479
+ if (reading.shape !== "answered") {
3480
+ 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) — " +
3481
+ "refusing pre-CAS rather than fabricating an empty human response; the checkpoint stays pending", { field: "answer" });
3482
+ }
3483
+ redeemedAnswer = reading.answer;
3484
+ }
3485
+ const updatedInputRaw = decide.updatedInput;
3486
+ let updatedInput;
3487
+ try {
3488
+ updatedInput = updatedInputRaw === undefined ? undefined : structuredClone(updatedInputRaw);
3489
+ }
3490
+ catch {
3491
+ 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");
3492
+ }
3493
+ if (Object.is(updatedInput, -0))
3494
+ updatedInput = 0;
3495
+ if (updatedInput !== undefined && !canonicalizeCapturedPlainData(updatedInput)) {
3496
+ 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) — " +
3497
+ "such a value reads differently on the two sides of the durable store; refusing pre-CAS, the checkpoint stays pending");
3498
+ }
3499
+ const decision = decide.decision;
3500
+ if (decision !== "allow" && decision !== "deny") {
3501
+ 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`);
3502
+ }
3503
+ const reason = decide.reason;
3504
+ plainPolicyOutcome = {
3505
+ gate: "policy_ask",
3506
+ boundCallId: decide.boundCallId,
3507
+ boundInputHash: decide.boundInputHash,
3508
+ decision,
3509
+ ...(updatedInput !== undefined ? { updatedInput } : {}),
3510
+ ...(reason !== undefined ? { reason } : {}),
3511
+ ...(redeemedAnswer !== undefined ? { answer: redeemedAnswer } : {}),
3512
+ };
3513
+ if (plainPolicyOutcome.decision === "deny" && plainPolicyOutcome.reason && sanitizeUntrustedText(plainPolicyOutcome.reason) !== plainPolicyOutcome.reason) {
3514
+ throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
3515
+ }
3516
+ }
3517
+ if (plainPolicyOutcome !== undefined) {
3347
3518
  const boundTo = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.toolCallId : undefined;
3348
- if (outcome.boundCallId !== boundTo) {
3349
- 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" });
3519
+ if (plainPolicyOutcome.boundCallId !== boundTo) {
3520
+ 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" });
3350
3521
  }
3351
3522
  const boundHash = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.boundInputHash : undefined;
3352
3523
  if (boundHash !== undefined) {
3353
- if (outcome.boundInputHash !== boundHash) {
3524
+ if (plainPolicyOutcome.boundInputHash !== boundHash) {
3354
3525
  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" });
3355
3526
  }
3356
3527
  }
@@ -3361,20 +3532,26 @@ export class Runner {
3361
3532
  if (cp.resolvedOutcome === undefined) {
3362
3533
  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)");
3363
3534
  }
3364
- const incoming = winnerFromOutcome(outcome);
3365
- if (incoming === undefined || !sameWinner(incoming, cp.resolvedOutcome)) {
3535
+ const incoming = winnerFromOutcome(plainPolicyOutcome);
3536
+ const persistedWinner = (() => {
3537
+ const w = cp.resolvedOutcome;
3538
+ if (w?.answer === undefined)
3539
+ return w;
3540
+ const reading = classifyQuestionOutcome(w.answer);
3541
+ return reading.shape === "answered" ? { ...w, answer: reading.answer } : w;
3542
+ })();
3543
+ if (incoming === undefined || !sameWinner(incoming, persistedWinner)) {
3366
3544
  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`);
3367
3545
  }
3368
3546
  }
3369
3547
  const contentAskCallId = pendingContentAskCallId(cp);
3370
- if (outcome.answer !== undefined && (contentAskCallId === undefined || outcome.decision !== "allow")) {
3548
+ if (suppliedAnswer !== undefined && (contentAskCallId === undefined || plainPolicyOutcome.decision !== "allow")) {
3371
3549
  throw new CheckpointError("checkpoint.invalid_outcome", contentAskCallId === undefined
3372
3550
  ? "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"
3373
3551
  : "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" });
3374
3552
  }
3375
- if (contentAskCallId !== undefined && outcome.decision === "allow" && outcome.answer === undefined) {
3376
- const liveFace = taskConfig.onQuestion ?? this.deps.onQuestion;
3377
- if (liveFace === undefined || liveFace === QUESTION_AWAITS_RESUME) {
3553
+ if (contentAskCallId !== undefined && plainPolicyOutcome.decision === "allow" && suppliedAnswer === undefined) {
3554
+ if (!isLiveQuestionFace(taskConfig.onQuestion ?? this.deps.onQuestion)) {
3378
3555
  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 — " +
3379
3556
  'executing the question against nothing would hand the model a fabricated "no human is available" default while consuming the approval; ' +
3380
3557
  "re-resume with the operator's answer on the outcome (or deny it), the checkpoint stays pending", { field: "answer" });
@@ -3411,7 +3588,8 @@ export class Runner {
3411
3588
  "rejected pre-CAS (the checkpoint stays pending) — re-resume with the full original chain");
3412
3589
  }
3413
3590
  }
3414
- const won = await store.resolve(token, cp.scope, outcome, { rev: cp.rev ?? 0 });
3591
+ const outcomeForStore = plainPolicyOutcome ?? outcome;
3592
+ const won = await store.resolve(token, cp.scope, outcomeForStore, { rev: cp.rev ?? 0 });
3415
3593
  if (!won) {
3416
3594
  const live = await store.get(token);
3417
3595
  if (live?.status === "pending") {
@@ -3425,8 +3603,8 @@ export class Runner {
3425
3603
  await internals.afterCheckpointResolve();
3426
3604
  consumeFlipDone = true;
3427
3605
  }
3428
- const answerFace = outcome.gate === "policy_ask" && outcome.answer !== undefined
3429
- ? answerFaceForRedeemedCall(outcome.answer, outcome.boundCallId, taskConfig.onQuestion ?? this.deps.onQuestion)
3606
+ const answerFace = plainPolicyOutcome !== undefined && redeemedAnswer !== undefined
3607
+ ? answerFaceForRedeemedCall(redeemedAnswer, plainPolicyOutcome.boundCallId, taskConfig.onQuestion ?? this.deps.onQuestion, boundInputHashOf(cp.pendingAction.args?.questions))
3430
3608
  : undefined;
3431
3609
  const spec = {
3432
3610
  ...taskConfig,
@@ -3465,7 +3643,7 @@ export class Runner {
3465
3643
  ? 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")
3466
3644
  : 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)");
3467
3645
  }
3468
- return this.runTaskStream(spec, { cp, outcome, onEnvRestoreFailed, ...(wakeMessage !== undefined ? { wakeMessage } : {}) }, internals);
3646
+ return this.runTaskStream(spec, { cp, outcome: plainPolicyOutcome ?? outcome, onEnvRestoreFailed, ...(wakeMessage !== undefined ? { wakeMessage } : {}) }, internals);
3469
3647
  }
3470
3648
  async applyResumeDecision(prepared, resume, emit, emitCommitted, onResolvedToolSuccess, onExecuteStart) {
3471
3649
  const { pendingAction } = resume.cp;
@@ -3521,7 +3699,7 @@ export class Runner {
3521
3699
  const { outcome } = resume;
3522
3700
  if (pendingAction.kind !== "tool_approval" || outcome.gate !== "policy_ask")
3523
3701
  return;
3524
- const resolvedArgs = pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME ? pendingAction.args : (outcome.updatedInput ?? pendingAction.args);
3702
+ const resolvedArgs = pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME ? pendingAction.args : (outcome.updatedInput !== undefined ? outcome.updatedInput : pendingAction.args);
3525
3703
  const pendingLabel = (() => { const l = prepared.tools.find((t) => t.name === pendingAction.toolName)?.label; return l !== undefined && l !== pendingAction.toolName ? { label: l } : {}; })();
3526
3704
  emit({ type: "tool_start", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, args: resolvedArgs });
3527
3705
  const emitEnd = (isError, result) => emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError) });
@@ -3532,6 +3710,9 @@ export class Runner {
3532
3710
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
3533
3711
  return;
3534
3712
  }
3713
+ if (outcome.decision !== "allow") {
3714
+ 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`);
3715
+ }
3535
3716
  if (outcome.decision === "allow" && outcome.updatedInput !== undefined && prepared.basePolicyForResumeEdit) {
3536
3717
  const rechecked = refuseOutOfContractDecision(await prepared.basePolicyForResumeEdit.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal));
3537
3718
  if (rechecked.action === "deny") {
@@ -57,6 +57,7 @@ export const toolOutputFrom = (result) => {
57
57
  return { output: raw, truncated: true, totalChars };
58
58
  };
59
59
  const CC_DETAIL_TYPES = new Set([
60
+ "ask-question",
60
61
  "edit", "create", "update", "bash", "notebook-edit", "notebook", "file_unchanged", "worktree", "text", "grep", "glob", "mcp",
61
62
  "agent", "task", "task-list", "task-output", "workflow-run",
62
63
  "web-fetch", "web-search", "todo", "cron-create", "cron-delete", "cron-list", "image",