@sema-agent/core 5.13.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 (86) hide show
  1. package/CHANGELOG.md +296 -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/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 +41 -17
  22. package/dist/core/checkpoint-store.js +114 -3
  23. package/dist/core/hooks.d.ts +24 -2
  24. package/dist/core/hooks.js +97 -10
  25. package/dist/core/human-input-projection.d.ts +12 -0
  26. package/dist/core/human-input-projection.js +27 -0
  27. package/dist/core/mcp.d.ts +7 -2
  28. package/dist/core/mcp.js +7 -7
  29. package/dist/core/memory-admission.d.ts +4 -0
  30. package/dist/core/memory-admission.js +3 -0
  31. package/dist/core/runner/assemble-result.d.ts +1 -0
  32. package/dist/core/runner/assemble-result.js +1 -1
  33. package/dist/core/runner/prepare-task.d.ts +16 -6
  34. package/dist/core/runner/prepare-task.js +301 -24
  35. package/dist/core/runner/runtask.d.ts +3 -6
  36. package/dist/core/runner/runtask.js +186 -36
  37. package/dist/core/runner/tool-output-projection.js +1 -0
  38. package/dist/core/session-store.d.ts +3 -0
  39. package/dist/core/session-store.js +4 -0
  40. package/dist/core/session.d.ts +1 -0
  41. package/dist/core/store-contracts/background-agent-store-contract.js +19 -0
  42. package/dist/core/store-contracts/checkpoint-store-contract.js +62 -3
  43. package/dist/core/task-notification.d.ts +2 -0
  44. package/dist/core/task-notification.js +5 -3
  45. package/dist/core/task-registry-agent.d.ts +1 -0
  46. package/dist/core/task-registry-agent.js +6 -0
  47. package/dist/core/task-registry.d.ts +1 -0
  48. package/dist/core/task-registry.js +4 -1
  49. package/dist/core/tool-policy.d.ts +5 -0
  50. package/dist/core/tool-policy.js +2 -1
  51. package/dist/core/types.d.ts +32 -1
  52. package/dist/core/wiring-manifest.d.ts +97 -0
  53. package/dist/core/wiring-manifest.js +186 -0
  54. package/dist/engine/compaction/compaction.js +2 -2
  55. package/dist/engine/harness/agent-harness.d.ts +2 -1
  56. package/dist/engine/harness/agent-harness.js +8 -1
  57. package/dist/engine/harness/types.d.ts +3 -1
  58. package/dist/engine/llm/types.d.ts +7 -0
  59. package/dist/engine/llm/types.js +8 -1
  60. package/dist/engine/session/import-validate.d.ts +6 -1
  61. package/dist/engine/session/import-validate.js +29 -6
  62. package/dist/engine/session/memory-repo.d.ts +3 -1
  63. package/dist/engine/session/memory-repo.js +2 -2
  64. package/dist/index.d.ts +7 -4
  65. package/dist/index.js +7 -4
  66. package/dist/internal/harness-types.d.ts +1 -1
  67. package/dist/internal/llm.d.ts +2 -2
  68. package/dist/internal/llm.js +1 -1
  69. package/dist/orchestration/run-workflow-tool.d.ts +4 -0
  70. package/dist/orchestration/run-workflow-tool.js +3 -0
  71. package/dist/orchestration/workflow-types.d.ts +8 -0
  72. package/dist/orchestration/workflow-types.js +14 -0
  73. package/dist/orchestration/workflow.d.ts +4 -0
  74. package/dist/orchestration/workflow.js +134 -5
  75. package/dist/prompts/default.js +1 -1
  76. package/dist/stores/file/checkpoint-store.d.ts +3 -5
  77. package/dist/stores/file/checkpoint-store.js +31 -2
  78. package/dist/stores/file/index.js +1 -1
  79. package/dist/stores/file/session-store.d.ts +3 -1
  80. package/dist/stores/file/session-store.js +2 -2
  81. package/dist/stores/file/shared-ledger.js +8 -1
  82. package/dist/tools/fs/bash-readonly-classifier.d.ts +3 -0
  83. package/dist/tools/fs/bash-readonly-classifier.js +94 -0
  84. package/dist/tools/fs/fs-bash.js +31 -12
  85. package/dist/tools/fs/safety.js +34 -10
  86. 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?.();
@@ -1523,7 +1552,29 @@ export class Runner {
1523
1552
  if (options?.trusted && sanitizeUntrustedText(text) !== text) {
1524
1553
  throw steeringError("trusted steering text must not contain a </system-reminder> tag", "steering.invalid_content");
1525
1554
  }
1526
- 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
+ };
1527
1578
  const deliver = async () => {
1528
1579
  if (resultValue)
1529
1580
  throw steeringError("the task has already finished");
@@ -1531,7 +1582,8 @@ export class Runner {
1531
1582
  if (!h)
1532
1583
  throw steeringError("the task is not running");
1533
1584
  try {
1534
- await h.harness.steer(payload, { provenance: "engine-note" });
1585
+ await h.harness.steer(payload, { provenance: "engine-note", ...(actor !== undefined ? { actor } : {}) });
1586
+ emitAccepted(h);
1535
1587
  return;
1536
1588
  }
1537
1589
  catch (e) {
@@ -1541,7 +1593,8 @@ export class Runner {
1541
1593
  const birthDeadline = Date.now() + READY_TIMEOUT_MS;
1542
1594
  while (resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
1543
1595
  try {
1544
- await h.harness.steer(payload, { provenance: "engine-note" });
1596
+ await h.harness.steer(payload, { provenance: "engine-note", ...(actor !== undefined ? { actor } : {}) });
1597
+ emitAccepted(h);
1545
1598
  return;
1546
1599
  }
1547
1600
  catch (e2) {
@@ -1658,7 +1711,7 @@ export class Runner {
1658
1711
  })()),
1659
1712
  };
1660
1713
  }
1661
- 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) {
1662
1715
  const prepareResume = resume
1663
1716
  ? {
1664
1717
  leafId: resume.cp.leafId,
@@ -1671,6 +1724,12 @@ export class Runner {
1671
1724
  executesApprovedAction: resume.cp.pendingAction.kind === "tool_approval" &&
1672
1725
  resume.outcome.gate !== "wake" &&
1673
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
+ : {}),
1674
1733
  }
1675
1734
  : undefined;
1676
1735
  const taskNotificationQueue = new SystemInjectionQueue();
@@ -1759,6 +1818,7 @@ export class Runner {
1759
1818
  const parentToolCallId = internals?.parentToolCallId;
1760
1819
  const ident = () => parentToolCallId !== undefined ? { eventId: uuidv7(), parentToolCallId, sourceTaskId: runSourceTaskId } : { eventId: uuidv7() };
1761
1820
  notificationIdent = ident;
1821
+ queue.push({ type: "wiring_manifest", manifest: prepared.wiringManifest, ...ident() });
1762
1822
  manualCompactRef.emitMooted = (reason) => {
1763
1823
  queue.push({ type: "compaction_outcome", outcome: "mooted", trigger: "manual", reason, ...ident() });
1764
1824
  };
@@ -2595,6 +2655,7 @@ export class Runner {
2595
2655
  let final;
2596
2656
  let threw;
2597
2657
  let abortedLive = false;
2658
+ let strandedHumanAnswers = [];
2598
2659
  try {
2599
2660
  if (prepared.abortController.signal.aborted) {
2600
2661
  const e = new Error("run aborted");
@@ -2647,15 +2708,31 @@ export class Runner {
2647
2708
  }
2648
2709
  }
2649
2710
  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;
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) {
2653
2716
  const start = continuation.length;
2717
+ const projected = projectHumanInput({ text: steer.text, actor: steer.actor, source });
2654
2718
  continuation +=
2655
2719
  "\n\n" +
2656
- (steer.trusted ? formatHookFeedback(steer.text) : delimitUntrusted("supervisor steering message", steer.text));
2720
+ (steer.trusted ? formatHookFeedback(projected) : delimitUntrusted("supervisor steering message", projected));
2657
2721
  if (steer.trusted)
2658
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
+ });
2659
2736
  }
2660
2737
  if (resume !== undefined)
2661
2738
  resume.decisionDelivered = true;
@@ -2663,7 +2740,9 @@ export class Runner {
2663
2740
  }
2664
2741
  }
2665
2742
  else {
2666
- let effectiveObjective = spec.objective;
2743
+ const objectiveActor = entryActor;
2744
+ const projectedObjective = projectHumanInput({ text: spec.objective, actor: objectiveActor, source: "objective" });
2745
+ let effectiveObjective = projectedObjective;
2667
2746
  let promptBlocked = false;
2668
2747
  const userPromptSubmit = (spec.hooks ?? this.deps.hooks)?.userPromptSubmit;
2669
2748
  if (userPromptSubmit) {
@@ -2674,7 +2753,7 @@ export class Runner {
2674
2753
  promptBlocked = true;
2675
2754
  }
2676
2755
  else if (decision?.additionalContext) {
2677
- effectiveObjective = `${formatHookFeedback(decision.additionalContext)}\n\n${spec.objective}`;
2756
+ effectiveObjective = `${formatHookFeedback(decision.additionalContext)}\n\n${projectedObjective}`;
2678
2757
  }
2679
2758
  }
2680
2759
  catch (hookErr) {
@@ -2775,10 +2854,23 @@ export class Runner {
2775
2854
  rs.attach.attachmentsInjected += firstFrames.length;
2776
2855
  if (firstFrames.length > 0)
2777
2856
  await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
2778
- 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
+ });
2779
2870
  final = await withBrainSinks(() => prepared.harness.prompt(effectiveObjective, {
2780
2871
  ...(images && images.length > 0 ? { images } : {}),
2781
2872
  ...(enginePrefixChars > 0 ? { enginePrefixChars } : {}),
2873
+ ...(objectiveActor !== undefined ? { actor: objectiveActor } : {}),
2782
2874
  }));
2783
2875
  }
2784
2876
  }
@@ -2801,6 +2893,7 @@ export class Runner {
2801
2893
  prepared.lspDiagnostics?.registry.releaseRun(prepared.lspDiagnostics.runIdent);
2802
2894
  prepared.abortController.abort();
2803
2895
  prepared.releaseSignal();
2896
+ strandedHumanAnswers = prepared.settleContentAskBindings();
2804
2897
  try {
2805
2898
  prepared.harness.recoverUndrainedEngineNotes();
2806
2899
  }
@@ -2939,6 +3032,7 @@ export class Runner {
2939
3032
  model: prepared.model.id,
2940
3033
  unpricedSpend: rs.telemetry.unpricedSpend,
2941
3034
  rewindNotes: prepared.rewindNotes,
3035
+ strandedHumanAnswers,
2942
3036
  remoteEnvFailures: prepared.remoteEnvFailures,
2943
3037
  retryAfterMs: rs.limits.platformTerminal?.retryAfterMs,
2944
3038
  abortedForTimeout: timeout.fired,
@@ -3075,6 +3169,7 @@ export class Runner {
3075
3169
  const terminalTick = {
3076
3170
  type: "task_progress",
3077
3171
  taskId: rs.telemetry.taskId,
3172
+ ...(internals?.delegationTaskType !== undefined ? { taskType: internals.delegationTaskType } : {}),
3078
3173
  ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
3079
3174
  ...(subagentName ? { name: subagentName } : {}),
3080
3175
  usage: { totalTokens: stats.tokens, toolUses: stats.toolCalls, durationMs: Date.now() - rs.telemetry.taskStart },
@@ -3335,7 +3430,7 @@ export class Runner {
3335
3430
  if (outcome.message !== undefined) {
3336
3431
  wakeMessage = validatePendingSteer(outcome.message);
3337
3432
  }
3338
- else if (cp.state.pendingSteer === undefined) {
3433
+ else if (readPendingSteerQueue(cp.state).length === 0) {
3339
3434
  throw new CheckpointError("wake.nothing_to_deliver", "cannot wake: no message was supplied and the checkpoint holds no parked pendingSteer — an empty " +
3340
3435
  "wake would burn the checkpoint on a blank continuation; supply `message` or park a steer first");
3341
3436
  }
@@ -3349,8 +3444,7 @@ export class Runner {
3349
3444
  if (!gateMatch) {
3350
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`);
3351
3446
  }
3352
- const reasonBearing = (outcome.gate === "policy_ask" && outcome.decision === "deny") ||
3353
- (outcome.gate === "dry_run_review" && outcome.decision === "reject") ||
3447
+ const reasonBearing = (outcome.gate === "dry_run_review" && outcome.decision === "reject") ||
3354
3448
  (outcome.gate === "plan_review" && outcome.decision === "reject");
3355
3449
  if (reasonBearing && outcome.reason && sanitizeUntrustedText(outcome.reason) !== outcome.reason) {
3356
3450
  throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
@@ -3374,14 +3468,60 @@ export class Runner {
3374
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");
3375
3469
  }
3376
3470
  }
3377
- 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) {
3378
3518
  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" });
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" });
3381
3521
  }
3382
3522
  const boundHash = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.boundInputHash : undefined;
3383
3523
  if (boundHash !== undefined) {
3384
- if (outcome.boundInputHash !== boundHash) {
3524
+ if (plainPolicyOutcome.boundInputHash !== boundHash) {
3385
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" });
3386
3526
  }
3387
3527
  }
@@ -3392,20 +3532,26 @@ export class Runner {
3392
3532
  if (cp.resolvedOutcome === undefined) {
3393
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)");
3394
3534
  }
3395
- const incoming = winnerFromOutcome(outcome);
3396
- 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)) {
3397
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`);
3398
3545
  }
3399
3546
  }
3400
3547
  const contentAskCallId = pendingContentAskCallId(cp);
3401
- if (outcome.answer !== undefined && (contentAskCallId === undefined || outcome.decision !== "allow")) {
3548
+ if (suppliedAnswer !== undefined && (contentAskCallId === undefined || plainPolicyOutcome.decision !== "allow")) {
3402
3549
  throw new CheckpointError("checkpoint.invalid_outcome", contentAskCallId === undefined
3403
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"
3404
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" });
3405
3552
  }
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) {
3553
+ if (contentAskCallId !== undefined && plainPolicyOutcome.decision === "allow" && suppliedAnswer === undefined) {
3554
+ if (!isLiveQuestionFace(taskConfig.onQuestion ?? this.deps.onQuestion)) {
3409
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 — " +
3410
3556
  'executing the question against nothing would hand the model a fabricated "no human is available" default while consuming the approval; ' +
3411
3557
  "re-resume with the operator's answer on the outcome (or deny it), the checkpoint stays pending", { field: "answer" });
@@ -3442,7 +3588,8 @@ export class Runner {
3442
3588
  "rejected pre-CAS (the checkpoint stays pending) — re-resume with the full original chain");
3443
3589
  }
3444
3590
  }
3445
- 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 });
3446
3593
  if (!won) {
3447
3594
  const live = await store.get(token);
3448
3595
  if (live?.status === "pending") {
@@ -3456,8 +3603,8 @@ export class Runner {
3456
3603
  await internals.afterCheckpointResolve();
3457
3604
  consumeFlipDone = true;
3458
3605
  }
3459
- const answerFace = outcome.gate === "policy_ask" && outcome.answer !== undefined
3460
- ? 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))
3461
3608
  : undefined;
3462
3609
  const spec = {
3463
3610
  ...taskConfig,
@@ -3496,7 +3643,7 @@ export class Runner {
3496
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")
3497
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)");
3498
3645
  }
3499
- 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);
3500
3647
  }
3501
3648
  async applyResumeDecision(prepared, resume, emit, emitCommitted, onResolvedToolSuccess, onExecuteStart) {
3502
3649
  const { pendingAction } = resume.cp;
@@ -3552,7 +3699,7 @@ export class Runner {
3552
3699
  const { outcome } = resume;
3553
3700
  if (pendingAction.kind !== "tool_approval" || outcome.gate !== "policy_ask")
3554
3701
  return;
3555
- 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);
3556
3703
  const pendingLabel = (() => { const l = prepared.tools.find((t) => t.name === pendingAction.toolName)?.label; return l !== undefined && l !== pendingAction.toolName ? { label: l } : {}; })();
3557
3704
  emit({ type: "tool_start", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, args: resolvedArgs });
3558
3705
  const emitEnd = (isError, result) => emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError) });
@@ -3563,6 +3710,9 @@ export class Runner {
3563
3710
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
3564
3711
  return;
3565
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
+ }
3566
3716
  if (outcome.decision === "allow" && outcome.updatedInput !== undefined && prepared.basePolicyForResumeEdit) {
3567
3717
  const rechecked = refuseOutOfContractDecision(await prepared.basePolicyForResumeEdit.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal));
3568
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",
@@ -1,5 +1,6 @@
1
1
  import { type SessionRepo } from "../internal/harness.js";
2
2
  import { type AcquiredSession, type SessionStore, type SessionStoreSummary } from "./session.js";
3
+ import type { StoreDurability } from "./checkpoint-store.js";
3
4
  import { SESSION_DEFAULT_TTL_DAYS } from "../config/defaults.js";
4
5
  export { SESSION_DEFAULT_TTL_DAYS };
5
6
  export type EvictPolicy = "delete" | "forget";
@@ -8,9 +9,11 @@ export interface TtlSessionStoreOptions {
8
9
  sweepIntervalMs?: number;
9
10
  repo?: SessionRepo;
10
11
  evict?: EvictPolicy;
12
+ durability?: StoreDurability;
11
13
  }
12
14
  export declare class TtlSessionStore implements SessionStore {
13
15
  readonly retention: "none";
16
+ readonly durability?: StoreDurability;
14
17
  private repo;
15
18
  private entries;
16
19
  private owners;
@@ -5,6 +5,7 @@ import { SESSION_DEFAULT_TTL_DAYS } from "../config/defaults.js";
5
5
  export { SESSION_DEFAULT_TTL_DAYS };
6
6
  export class TtlSessionStore {
7
7
  retention = "none";
8
+ durability;
8
9
  repo;
9
10
  entries = new Map();
10
11
  owners = new Map();
@@ -15,6 +16,9 @@ export class TtlSessionStore {
15
16
  sweepTimer;
16
17
  constructor(opts = {}) {
17
18
  this.repo = opts.repo ?? new InMemorySessionRepo();
19
+ const durability = opts.durability ?? (opts.repo === undefined ? "process-local" : undefined);
20
+ if (durability !== undefined)
21
+ this.durability = durability;
18
22
  this.defaultTtlMs = (opts.defaultTtlDays ?? SESSION_DEFAULT_TTL_DAYS) * DAY_MS;
19
23
  this.evictPolicy = opts.evict ?? (opts.repo ? "forget" : "delete");
20
24
  if (opts.sweepIntervalMs !== undefined && opts.sweepIntervalMs > 0) {
@@ -20,6 +20,7 @@ export interface SessionStoreSummary {
20
20
  forkedFrom?: string;
21
21
  }
22
22
  export interface SessionStore {
23
+ readonly durability?: import("./checkpoint-store.js").StoreDurability;
23
24
  acquire(sessionId?: string, opts?: {
24
25
  requireExisting?: boolean;
25
26
  }): Promise<AcquiredSession>;
@@ -52,6 +52,25 @@ export async function backgroundAgentStoreContract(mk, runAssertion) {
52
52
  assert.equal(await store.get("a1", "tenant-C"), null, "an unrelated scope must not see the row");
53
53
  assert.equal(await store.get("a-missing", S), null);
54
54
  });
55
+ run("the org-admission fields round-trip, and ABSENT stays absent (a backend must not invent a verdict)", async (make) => {
56
+ const store = make();
57
+ await store.put(record({ handle: "a1", admittedOrgScopes: ["org:acme", "org:beta"], admittedOrgWriteScope: "org:acme" }));
58
+ const withVerdict = await held(store, "a1");
59
+ assert.deepEqual(withVerdict.admittedOrgScopes, ["org:acme", "org:beta"]);
60
+ assert.equal(withVerdict.admittedOrgWriteScope, "org:acme");
61
+ await store.put(record({ handle: "a2", admittedOrgScopes: [], admittedOrgWriteScope: null }));
62
+ const readOnly = await held(store, "a2");
63
+ assert.deepEqual(readOnly.admittedOrgScopes, []);
64
+ assert.equal(readOnly.admittedOrgWriteScope, null);
65
+ await store.put(record({ handle: "a3" }));
66
+ const untouched = await held(store, "a3");
67
+ assert.equal(untouched.admittedOrgScopes, undefined);
68
+ assert.equal(untouched.admittedOrgWriteScope, undefined);
69
+ await store.update("a3", S, { ...untouched, admittedOrgScopes: ["org:acme"], admittedOrgWriteScope: null }, { rev: untouched.rev });
70
+ const afterCas = await held(store, "a3");
71
+ assert.deepEqual(afterCas.admittedOrgScopes, ["org:acme"]);
72
+ assert.equal(afterCas.admittedOrgWriteScope, null);
73
+ });
55
74
  run("put refuses a scopeless row (a row with no tenant partition would be world-readable)", async (make) => {
56
75
  const store = make();
57
76
  await assert.rejects(store.put(record({ scope: "" })), "an empty scope must be refused loudly, not stored");
@@ -1,5 +1,5 @@
1
1
  import { strict as assert } from "node:assert";
2
- import { mintCheckpointToken, } from "../checkpoint-store.js";
2
+ import { MAX_PENDING_STEER_ENTRIES, mintCheckpointToken, readPendingSteerQueue, } from "../checkpoint-store.js";
3
3
  import { beginContract } from "./contract-harness.js";
4
4
  export function createCheckpointFixture(over = {}) {
5
5
  const token = over.token ?? mintCheckpointToken();
@@ -90,9 +90,68 @@ export async function checkpointStoreContract(make, runAssertion) {
90
90
  const cp = createCheckpointFixture();
91
91
  await store.put(cp.token, cp);
92
92
  await assert.rejects(store.setPendingSteer(cp.token, cp.scope, { text: "</SYSTEM-REMINDER>", trusted: true }), (e) => e.code === "steering.invalid_content");
93
- assert.equal((await store.get(cp.token)).state.pendingSteer, undefined);
93
+ assert.deepEqual(readPendingSteerQueue((await store.get(cp.token)).state), []);
94
94
  assert.equal(await store.setPendingSteer(cp.token, cp.scope, { text: "go", trusted: false }), true);
95
- assert.deepEqual((await store.get(cp.token)).state.pendingSteer, { text: "go", trusted: false });
95
+ const q = readPendingSteerQueue((await store.get(cp.token)).state);
96
+ assert.equal(q.length, 1);
97
+ assert.equal(q[0].text, "go");
98
+ assert.equal(q[0].trusted, false);
99
+ assert.equal(q[0].seq, 1);
100
+ assert.equal((await store.get(cp.token)).state.pendingSteer, undefined);
101
+ });
102
+ run("setPendingSteer APPENDS: two steers both survive in issue order, each keeping its frozen trust", async () => {
103
+ const store = make();
104
+ const cp = createCheckpointFixture();
105
+ await store.put(cp.token, cp);
106
+ assert.equal(await store.setPendingSteer(cp.token, cp.scope, { text: "first", trusted: false }), true);
107
+ assert.equal(await store.setPendingSteer(cp.token, cp.scope, { text: "second", trusted: true }), true);
108
+ const q = readPendingSteerQueue((await store.get(cp.token)).state);
109
+ assert.deepEqual(q.map((e) => e.text), ["first", "second"]);
110
+ assert.deepEqual(q.map((e) => e.trusted), [false, true]);
111
+ assert.deepEqual(q.map((e) => e.seq), [1, 2]);
112
+ });
113
+ run("setPendingSteer is idempotent on inputId (a retry with the caller's own message id adds nothing)", async () => {
114
+ const store = make();
115
+ const cp = createCheckpointFixture();
116
+ await store.put(cp.token, cp);
117
+ assert.equal(await store.setPendingSteer(cp.token, cp.scope, { text: "once", trusted: false, inputId: "m1" }), true);
118
+ assert.equal(await store.setPendingSteer(cp.token, cp.scope, { text: "once", trusted: false, inputId: "m1" }), true);
119
+ assert.equal(readPendingSteerQueue((await store.get(cp.token)).state).length, 1);
120
+ });
121
+ run("the queue bound is fail-loud steering.queue_full and evicts nothing", async () => {
122
+ const store = make();
123
+ const cp = createCheckpointFixture();
124
+ await store.put(cp.token, cp);
125
+ for (let i = 0; i < MAX_PENDING_STEER_ENTRIES; i++) {
126
+ assert.equal(await store.setPendingSteer(cp.token, cp.scope, { text: `s${i}`, trusted: false }), true);
127
+ }
128
+ await assert.rejects(store.setPendingSteer(cp.token, cp.scope, { text: "overflow", trusted: false }), (e) => e.code === "steering.queue_full");
129
+ const q = readPendingSteerQueue((await store.get(cp.token)).state);
130
+ assert.equal(q.length, MAX_PENDING_STEER_ENTRIES);
131
+ assert.deepEqual(q.map((e) => e.text), Array.from({ length: MAX_PENDING_STEER_ENTRIES }, (_, i) => `s${i}`));
132
+ });
133
+ run("a queue entry round-trips every FROZEN field (actor/inputId/priority included)", async () => {
134
+ const store = make();
135
+ const cp = createCheckpointFixture();
136
+ await store.put(cp.token, cp);
137
+ assert.equal(await store.setPendingSteer(cp.token, cp.scope, {
138
+ text: "attributed",
139
+ trusted: true,
140
+ actor: { id: "slack:U1", hostAsserted: true, issuer: "slack-app:A1" },
141
+ inputId: "m-42",
142
+ priority: "now",
143
+ }), true);
144
+ const q = readPendingSteerQueue((await store.get(cp.token)).state);
145
+ assert.deepEqual(q, [
146
+ { text: "attributed", trusted: true, actor: { id: "slack:U1", hostAsserted: true, issuer: "slack-app:A1" }, inputId: "m-42", priority: "now", seq: 1 },
147
+ ]);
148
+ });
149
+ run("an unknown steering field is REFUSED (a producer must not have its new field silently dropped)", async () => {
150
+ const store = make();
151
+ const cp = createCheckpointFixture();
152
+ await store.put(cp.token, cp);
153
+ await assert.rejects(store.setPendingSteer(cp.token, cp.scope, { text: "go", trusted: false, futureField: 1 }), (e) => e.code === "steering.invalid_content");
154
+ assert.deepEqual(readPendingSteerQueue((await store.get(cp.token)).state), []);
96
155
  });
97
156
  run("expire vs resolve on the same row → exactly one wins; reap expires past-deadline pending in scope", async () => {
98
157
  const store = make();
@@ -41,6 +41,8 @@ export interface SystemInjection<TPayload = unknown> {
41
41
  }
42
42
  export declare function taskNotificationDedupKey(n: Pick<TaskNotificationPayload, "task_id" | "task_type" | "status" | "seq">): string;
43
43
  export declare function taskNotificationLaneKey(n: Pick<TaskNotificationPayload, "task_id" | "task_type">): string;
44
+ export declare function attrEscape(value: string): string;
45
+ export declare const EXTERNAL_SOURCE_MAX = 120;
44
46
  export declare function renderTaskNotificationXml(n: TaskNotificationPayload): string;
45
47
  export declare const MAX_PENDING_EVENTS_PER_TASK = 50;
46
48
  export declare const MAX_PENDING_EVENTS_PER_SESSION = 200;