@sema-agent/core 7.38.3 → 7.39.1

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 (96) hide show
  1. package/CHANGELOG.md +191 -6
  2. package/dist/core/ask-origin.d.ts +16 -7
  3. package/dist/core/ask-origin.js +4 -2
  4. package/dist/core/auto-mode-directive.d.ts +114 -19
  5. package/dist/core/auto-mode-directive.js +67 -19
  6. package/dist/core/checkpoint-store.d.ts +72 -41
  7. package/dist/core/checkpoint-store.js +28 -48
  8. package/dist/core/classifier-unavailable-arm.js +2 -2
  9. package/dist/core/decision-receipt.d.ts +2 -0
  10. package/dist/core/decision-receipt.js +1 -1
  11. package/dist/core/engine-notice.d.ts +41 -17
  12. package/dist/core/engine-notice.js +26 -14
  13. package/dist/core/gate-outcome.d.ts +80 -42
  14. package/dist/core/gate-outcome.js +21 -6
  15. package/dist/core/governance-codes.d.ts +1 -3
  16. package/dist/core/governance-codes.js +0 -3
  17. package/dist/core/hooks.d.ts +1 -1
  18. package/dist/core/hooks.js +2 -2
  19. package/dist/core/human-input-projection.d.ts +12 -0
  20. package/dist/core/human-input-projection.js +12 -0
  21. package/dist/core/memory-engine/engine.js +11 -11
  22. package/dist/core/memory-engine/layout.d.ts +21 -2
  23. package/dist/core/memory-engine/layout.js +17 -5
  24. package/dist/core/mode-policy.d.ts +26 -0
  25. package/dist/core/mode-policy.js +37 -1
  26. package/dist/core/output-style.d.ts +21 -0
  27. package/dist/core/output-style.js +29 -0
  28. package/dist/core/real-approval.d.ts +25 -0
  29. package/dist/core/real-approval.js +27 -0
  30. package/dist/core/reminder-disclosure.d.ts +2 -2
  31. package/dist/core/reminder-mint.d.ts +6 -9
  32. package/dist/core/reminder-mint.js +3 -14
  33. package/dist/core/retired-keys.d.ts +2 -1
  34. package/dist/core/retired-keys.js +3 -1
  35. package/dist/core/runner/assemble-result.d.ts +2 -0
  36. package/dist/core/runner/assemble-result.js +1 -1
  37. package/dist/core/runner/compaction-restate.d.ts +45 -4
  38. package/dist/core/runner/compaction-restate.js +89 -11
  39. package/dist/core/runner/contracts.d.ts +6 -6
  40. package/dist/core/runner/denial-limit-arms.js +4 -4
  41. package/dist/core/runner/inherited-ask-grants.d.ts +4 -2
  42. package/dist/core/runner/inherited-ask-grants.js +0 -3
  43. package/dist/core/runner/permission-rule-lanes.d.ts +18 -0
  44. package/dist/core/runner/permission-rule-lanes.js +35 -25
  45. package/dist/core/runner/prepare-context-lane.d.ts +1 -0
  46. package/dist/core/runner/prepare-context-lane.js +6 -0
  47. package/dist/core/runner/prepare-inherited-gate.js +2 -9
  48. package/dist/core/runner/prepare-task.js +10 -24
  49. package/dist/core/runner/prepare-wiring-manifest.js +2 -2
  50. package/dist/core/runner/prompt-block.d.ts +9 -0
  51. package/dist/core/runner/prompt-block.js +8 -0
  52. package/dist/core/runner/resume-admission.js +4 -1
  53. package/dist/core/runner/resume-apply.js +28 -18
  54. package/dist/core/runner/resume-policy-outcome.js +10 -4
  55. package/dist/core/runner/run-attachment-seats.js +1 -1
  56. package/dist/core/runner/run-identity-wiring.js +9 -2
  57. package/dist/core/runner/run-leg.js +42 -31
  58. package/dist/core/runner/run-settle-and-teardown.js +4 -3
  59. package/dist/core/runner/run-turn-boundary.js +4 -3
  60. package/dist/core/runner/runtask.d.ts +4 -2
  61. package/dist/core/runner/steer-admission.d.ts +9 -0
  62. package/dist/core/runner/steer-admission.js +8 -0
  63. package/dist/core/runner/stream-steer-verb.js +16 -11
  64. package/dist/core/runner/synthetic-tools.d.ts +4 -0
  65. package/dist/core/runner/tool-turn-repair-seat.js +1 -1
  66. package/dist/core/runner/turn-attachments.d.ts +16 -22
  67. package/dist/core/runner/turn-attachments.js +3 -20
  68. package/dist/core/runner-deps.d.ts +1 -1
  69. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +8 -0
  70. package/dist/core/store-contracts/checkpoint-store-contract.js +23 -0
  71. package/dist/core/task-event.d.ts +7 -2
  72. package/dist/core/task-spec.d.ts +29 -7
  73. package/dist/core/terminal-cause.d.ts +44 -3
  74. package/dist/core/terminal-cause.js +5 -0
  75. package/dist/core/tool-policy.d.ts +55 -29
  76. package/dist/core/tool-policy.js +42 -31
  77. package/dist/core/types.d.ts +1 -1
  78. package/dist/core/types.js +1 -1
  79. package/dist/core/wiring-manifest.d.ts +17 -3
  80. package/dist/core/wiring-manifest.js +8 -3
  81. package/dist/engine/harness/agent-harness.d.ts +10 -0
  82. package/dist/engine/harness/agent-harness.js +12 -1
  83. package/dist/engine/llm/tool-turns.d.ts +15 -2
  84. package/dist/engine/llm/tool-turns.js +2 -2
  85. package/dist/index.d.ts +4 -3
  86. package/dist/index.js +4 -3
  87. package/dist/prompt-assembly/event-registry.js +2 -1
  88. package/dist/tools/fs/index.d.ts +2 -1
  89. package/dist/tools/fs/read-deny-home.d.ts +3 -2
  90. package/dist/tools/fs/read-deny-home.js +6 -4
  91. package/dist/tools/fs/shell-read-check.d.ts +10 -3
  92. package/dist/tools/fs/shell-read-check.js +2 -2
  93. package/package.json +1 -1
  94. package/test/export-surface.snapshot.json +21 -1
  95. package/wire-changes/core@7.39.0.json +999 -0
  96. package/wire-changes/core@7.39.1.json +74 -0
@@ -5,21 +5,22 @@ import { CheckpointError, readPendingSteerQueue, realApprovalOrgFact, remainingB
5
5
  import { normalizePersistedRuleHit } from "../gate-lanes.js";
6
6
  import { decidersNote, SETTLEMENT_NOBODY_JUDGED } from "../gate-outcome.js";
7
7
  import { formatHookFeedback, hookSeatExpiredError, runHookSeat } from "../hooks.js";
8
- import { buildHumanInputEvent, projectHumanInput } from "../human-input-projection.js";
8
+ import { buildAppliedHumanInputEvent, buildHumanInputEvent, projectHumanInput } from "../human-input-projection.js";
9
9
  import { ORG_ADJUDICATION_TIMEOUT_MS, settleOrgVerdictWithin } from "../permission-rule-org.js";
10
10
  import { applyPersistedTightening, disclosedRuleSet } from "../persisted-rule-arms.js";
11
11
  import { computeCostMicroUsd } from "../pricing.js";
12
- import { composeApprovalPostureDirective } from "../auto-mode-directive.js";
13
- import { mintSystemReminder, statedReminderBodiesOf } from "../reminder-mint.js";
12
+ import { mintSystemReminder } from "../reminder-mint.js";
14
13
  import { hasDestroy } from "../remote-env.js";
15
- import { advertisedWritableRootsSeat, policyRefusalMessage, refuseOutOfContractDecision } from "../tool-policy.js";
14
+ import { advertisedWritableRootsSeat, policyCannotAnswer, policyRefusalMessage, refuseOutOfContractDecision } from "../tool-policy.js";
16
15
  import { callFaceSeat, judgeParkedToolIdentity, parkedRowLiveFace, rosterEntryNamed } from "../tool-roster.js";
17
16
  import { deliverEngineNotice } from "../types.js";
18
17
  import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText } from "../untrusted-text.js";
19
18
  import { errorCodeOf } from "./assemble-result.js";
20
19
  import { DEFAULT_PRECALL_OUTPUT_TOKENS, platformLimitTerminal } from "./clock-and-limits.js";
20
+ import { mintOutputStyleReminder, owedPostureText } from "./compaction-restate.js";
21
21
  import { deepJsonEqual, resumeContinuation } from "./decide-continuation.js";
22
22
  import { settleExecutionRecord } from "./execution-record.js";
23
+ import { holdPrompt } from "./prompt-block.js";
23
24
  import { mintGateOutcome } from "./gate-exit.js";
24
25
  import { withGateTimeout } from "../gate-bounded-await.js";
25
26
  import { resolveGitLegDelivery, wrapGitFrame } from "./git-leg-delivery.js";
@@ -137,6 +138,12 @@ export async function runLeg(input) {
137
138
  }
138
139
  }
139
140
  }
141
+ const postureText = await owedPostureText(prepared, "always");
142
+ if (postureText !== undefined)
143
+ continuation += "\n\n" + postureText;
144
+ const outputStyleWrapped = mintOutputStyleReminder(prepared);
145
+ if (outputStyleWrapped !== undefined)
146
+ continuation += "\n\n" + outputStyleWrapped;
140
147
  const engineSegments = continuation.length > 0 ? [{ start: 0, end: continuation.length }] : [];
141
148
  const parkedSteers = readPendingSteerQueue(resume.cp.state);
142
149
  const resumeFrames = [
@@ -223,16 +230,7 @@ export async function runLeg(input) {
223
230
  if (steer.trusted)
224
231
  engineSegments.push({ start, end: continuation.length });
225
232
  appliedEvents.push({
226
- ...buildHumanInputEvent({
227
- carrier: source,
228
- source,
229
- delivery: "applied",
230
- sessionSeq: nextHumanInputSeq(prepared.harness),
231
- inputId: steer.inputId,
232
- ...(steer.actor !== undefined ? { actor: steer.actor } : {}),
233
- ...(steer.actor?.issuer !== undefined ? { issuer: steer.actor.issuer } : {}),
234
- ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
235
- }),
233
+ ...buildAppliedHumanInputEvent({ carrier: source, source, sessionSeq: nextHumanInputSeq(prepared.harness), inputId: steer.inputId, actor: steer.actor, principal: spec.principal }),
236
234
  ...ident(),
237
235
  });
238
236
  }
@@ -260,6 +258,10 @@ export async function runLeg(input) {
260
258
  },
261
259
  });
262
260
  }
261
+ if (outputStyleWrapped !== undefined) {
262
+ const preview = redactThenCut(prepared.outputStyleFrame, 220);
263
+ prepared.frameReceipts.pending.push({ source: "output_style", text: continuation, commit: () => void (queue.push({ type: "steering_injected", source: "output_style", preview, ...ident() }), (rs.attach.attachmentsInjected += 1)) });
264
+ }
263
265
  final = await withBrainSinks(() => prepared.harness.prompt(continuation, {
264
266
  engineSegments,
265
267
  settleUnansweredToolCalls: rejectionHalt !== undefined ? { halt: rejectionHalt } : {},
@@ -292,16 +294,18 @@ export async function runLeg(input) {
292
294
  catch {
293
295
  }
294
296
  }
295
- prepared.blockedRef.reason = formatHookFeedback(promptSeat.cause === "timeout"
297
+ const timedOut = promptSeat.cause === "timeout";
298
+ holdPrompt(prepared.blockedRef, prepared.reminderMark, timedOut ? "timeout" : "cancelled", timedOut
296
299
  ? `the deployment's userPromptSubmit hook did not answer within its ${prepared.hookTimeoutMs}ms bound while screening this prompt; ` +
297
300
  `the prompt was NOT submitted (fail-closed)`
298
301
  : `the task was cancelled while the deployment's userPromptSubmit hook was still screening this prompt; ` +
299
- `the prompt was NOT submitted (fail-closed)`, prepared.reminderMark);
302
+ `the prompt was NOT submitted (fail-closed)`);
300
303
  promptBlocked = true;
301
304
  }
302
305
  const decision = promptSeat.expired ? undefined : promptSeat.value;
303
- if (decision?.block) {
304
- prepared.blockedRef.reason = formatHookFeedback(decision.block, prepared.reminderMark);
306
+ const blockWords = decision?.block;
307
+ if (blockWords) {
308
+ holdPrompt(prepared.blockedRef, prepared.reminderMark, "block", blockWords);
305
309
  promptBlocked = true;
306
310
  }
307
311
  else if (decision?.additionalContext) {
@@ -315,8 +319,7 @@ export async function runLeg(input) {
315
319
  }
316
320
  catch {
317
321
  }
318
- prepared.blockedRef.reason = formatHookFeedback(`the deployment's userPromptSubmit hook crashed while screening this prompt (${err.message}); ` +
319
- `the prompt was NOT submitted (fail-closed)`, prepared.reminderMark);
322
+ holdPrompt(prepared.blockedRef, prepared.reminderMark, "crashed", `the deployment's userPromptSubmit hook crashed while screening this prompt (${err.message}); ` + `the prompt was NOT submitted (fail-closed)`);
320
323
  promptBlocked = true;
321
324
  }
322
325
  }
@@ -383,16 +386,14 @@ export async function runLeg(input) {
383
386
  }
384
387
  }
385
388
  }
389
+ if (prepared.outputStyleFrame !== undefined)
390
+ firstFrames.push({ source: "output_style", body: prepared.outputStyleFrame, commit: () => undefined, disclose: () => undefined });
386
391
  if (firstFrames.length > 0) {
387
392
  effectiveObjective = `${firstFrames.map((f) => mintSystemReminder(sanitizeUntrustedText(f.body, attachmentEnvelopeTags(f.source)), prepared.reminderMark)).join("\n")}\n${effectiveObjective}`;
388
393
  }
389
- if (composeApprovalPostureDirective(prepared.approvalPosture) !== undefined) {
390
- const postureDirective = composeApprovalPostureDirective(prepared.approvalPosture, await statedReminderBodiesOf(prepared.session, prepared.reminderMark));
391
- if (postureDirective !== undefined) {
392
- prepared.postureFrameRef.protectedText = mintSystemReminder(postureDirective, prepared.reminderMark);
393
- effectiveObjective = `${prepared.postureFrameRef.protectedText}\n${effectiveObjective}`;
394
- }
395
- }
394
+ const postureText = await owedPostureText(prepared, "always");
395
+ if (postureText !== undefined)
396
+ effectiveObjective = `${postureText}\n${effectiveObjective}`;
396
397
  const gitQueuedChars = gitLegDelivered !== undefined && gitLegDelivered.standalone ? gitLegDelivered.wrapped.length : 0;
397
398
  const precallMicroUsd = rs.budget.maxCostMicroUsd === undefined
398
399
  ? 0
@@ -615,10 +616,17 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
615
616
  ...advertisedWritableRootsSeat(prepared.advertisedWritableDirs),
616
617
  ...callFaceSeat(parkedRowLiveFace(prepared.toolRosterDeltas.current, pendingAction)),
617
618
  }, prepared.abortController.signal));
619
+ const personUnanswered = policyCannotAnswer(rechecked, outcome.hostDecision.decidedBy);
618
620
  const beltUnsafe = rechecked.action === "deny" ||
619
- (rechecked.updatedInput !== undefined && !deepJsonEqual(rechecked.updatedInput, resolvedArgs));
621
+ (rechecked.updatedInput !== undefined && !deepJsonEqual(rechecked.updatedInput, resolvedArgs)) ||
622
+ personUnanswered;
620
623
  if (beltUnsafe) {
621
- const editedDenial = formatHookFeedback(`The approver EDITED this call's input; the edited call is refused by the deployment's tool policy and was not executed${rechecked.action === "deny" && rechecked.message ? `: ${rechecked.message}` : " (the policy rewrote the edit to a value that was not approved)"}.`, prepared.reminderMark);
624
+ const why = rechecked.action === "deny"
625
+ ? (rechecked.message ? `: ${rechecked.message}` : "")
626
+ : personUnanswered
627
+ ? " (the edited call requires a person's approval, and this decision was the deployment's policy)"
628
+ : " (the policy rewrote the edit to a value that was not approved)";
629
+ const editedDenial = formatHookFeedback(`The approver EDITED this call's input; the edited call is refused by the deployment's tool policy and was not executed${why}.`, prepared.reminderMark);
622
630
  await settleEnd(true, { content: editedDenial }, vetoed("policy"));
623
631
  const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, editedDenial, true));
624
632
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
@@ -646,6 +654,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
646
654
  orgWaitCancelled = cause === "aborted";
647
655
  },
648
656
  });
657
+ const unanswered = policyCannotAnswer(org, outcome.hostDecision.decidedBy);
649
658
  const blocked = org.status === "unavailable"
650
659
  ? realApprovalOrgFact(gateRealApproval) === "unavailable"
651
660
  ? undefined
@@ -654,12 +663,14 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
654
663
  : "this deployment is org-governed and cannot currently adjudicate against an organization policy snapshot"
655
664
  : org.verdict?.behavior === "deny"
656
665
  ? `an organization policy rule (${org.verdict.rule}) denies it`
657
- : undefined;
666
+ : unanswered
667
+ ? `an organization policy rule (${org.verdict?.rule})${org.verdict?.unreadable !== undefined ? ` may reach this call, which could not be read against it (${org.verdict.unreadable}), so it` : ""} requires a person's approval for this call, and the decision recorded for it was the deployment's policy, not a person's`
668
+ : undefined;
658
669
  if (blocked !== undefined) {
659
670
  const orgDisclosures = org.status === "unavailable" && org.disclosures.length > 0
660
671
  ? ` Governance disclosures: ${inlineUntrusted(org.disclosures.join("; "), ORG_DISCLOSURE_MAX_CHARS)}.`
661
672
  : "";
662
- const orgDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: ${blocked}.${orgDisclosures} This approval is spent — the call has to be re-issued and approved again once organization policy permits it.`, prepared.reminderMark);
673
+ const orgDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: ${blocked}.${orgDisclosures} This approval is spent — the call has to be re-issued and ${unanswered ? "decided by a person" : "approved again once organization policy permits it"}.`, prepared.reminderMark);
663
674
  await settleEnd(true, { content: orgDenial }, vetoed("org"));
664
675
  const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, orgDenial, true));
665
676
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
@@ -10,7 +10,7 @@ import { hasDestroy, isIsolated } from "../remote-env.js";
10
10
  import { resolveTaskModel } from "../roles.js";
11
11
  import { defaultTaskRegistry } from "../task-registry.js";
12
12
  import { emitTrace } from "../trace.js";
13
- import { deliverEngineNotice, settleOwedUserInput } from "../types.js";
13
+ import { deliverEngineNotice, parentLegIds, settleOwedUserInput } from "../types.js";
14
14
  import { assembleResult } from "./assemble-result.js";
15
15
  import { discloseUnconsumedHalt } from "./halt-attribution.js";
16
16
  import { forwardSafely } from "./forward-sink.js";
@@ -111,6 +111,7 @@ export async function runSettleAndTeardown(input) {
111
111
  budgetHit: rs.limits.budgetHit,
112
112
  budgetAxis: rs.limits.budgetAxis,
113
113
  blockedReason: prepared.blockedRef.reason,
114
+ blockedDetail: prepared.blockedRef.detail,
114
115
  conflict: prepared.conflictRef.hit,
115
116
  gitCoreOverBudget: prepared.gitStatusRef.terminalCode === "irreducible_core_over_budget",
116
117
  outputInvalid: rs.degrade.outputInvalid,
@@ -147,7 +148,7 @@ export async function runSettleAndTeardown(input) {
147
148
  amendTerminal(result, "resume.reopen_failed_unstarted", (m) => `${m ?? "the resume leg did not complete"} — the approved action never started, and reopening the checkpoint FAILED: its state is unprovable from here. Re-read it before deciding; do NOT issue a fresh approval on the assumption the old one is dead`);
148
149
  }
149
150
  }
150
- settleOwedUserInput(owedUserInput, rowReturned, { taskId: spec.taskId ?? prepared.sessionId, sessionId: prepared.sessionId, runId: prepared.runId }, runner.deps.onNotice);
151
+ settleOwedUserInput(owedUserInput, rowReturned, { taskId: spec.taskId ?? prepared.sessionId, sessionId: prepared.sessionId, runId: prepared.runId, parent: parentLegIds(internals) }, runner.deps.onNotice);
151
152
  prepared.sealReadStateSeat?.(ownCommittedTailRef.current);
152
153
  {
153
154
  const wss = prepared.workspaceStateSettle;
@@ -293,7 +294,7 @@ export async function runSettleAndTeardown(input) {
293
294
  }
294
295
  }
295
296
  finally {
296
- settleOwedUserInput(owedUserInput, !prepared.suspendProgressRef.executedApproved, { taskId: spec.taskId ?? prepared.sessionId, sessionId: prepared.sessionId, runId: prepared.runId }, runner.deps.onNotice);
297
+ settleOwedUserInput(owedUserInput, !prepared.suspendProgressRef.executedApproved, { taskId: spec.taskId ?? prepared.sessionId, sessionId: prepared.sessionId, runId: prepared.runId, parent: parentLegIds(internals) }, runner.deps.onNotice);
297
298
  const writeDateAnchor = owedDateAnchor(prepared.session, prepared.dateChange, rs.attach.dateState);
298
299
  if (writeDateAnchor !== undefined)
299
300
  await settleTeardownLeg(writeDateAnchor, "date anchor (settle leg)", (e) => runner.deps.onError?.(e, { phase: "degraded", sessionId: prepared.sessionId }));
@@ -13,7 +13,7 @@ import { droppedToolDigest, withAnnounceDigests } from "./announce-once-ledger.j
13
13
  import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
14
14
  import { workflowSizeGuidelineChangeNotice } from "../../orchestration/workflow-size-guideline.js";
15
15
  import { awaitChargeWithSlowDisclosure, discloseUnevaluableWindow, ENV_DUE_GOVERNANCE_READ_BUDGET_MS, GOVERNANCE_READ_STALLED, LIMIT_APPROACH_DEFAULT_THRESHOLDS, limitApproachFrames, platformLimitTerminal, raceUntilDeadline } from "./clock-and-limits.js";
16
- import { compactionRestateOptions } from "./compaction-restate.js";
16
+ import { appendBoundaryFrames, compactionRestateOptions } from "./compaction-restate.js";
17
17
  import { COMPACTION_FREED_EPSILON, COMPACTION_REGROWTH_FACTOR, MAX_CONSECUTIVE_COMPACTION_FAILURES } from "./compaction-knobs.js";
18
18
  import { compactionHookOptions, recordCompactionReuse, seamCCompactionOptions } from "./compaction-seams.js";
19
19
  import { contextUsageSections } from "../../prompt-assembly/section-usage.js";
@@ -197,6 +197,8 @@ export function createTurnBoundary(input) {
197
197
  }
198
198
  emitTrace(rs.telemetry.tracer, () => ({ kind: "review.dropped", version: 1, taskId: rs.telemetry.taskId, ts: Date.now() }));
199
199
  }
200
+ if (!noNextRequest() && !prepared.abortController.signal.aborted)
201
+ await appendBoundaryFrames(prepared, rs.turn.lastTurnHadToolCalls);
200
202
  if (prepared.lspDiagnostics &&
201
203
  !prepared.lspDiagnostics.registry.isEmpty() &&
202
204
  !prepared.abortController.signal.aborted &&
@@ -283,13 +285,11 @@ export function createTurnBoundary(input) {
283
285
  cadenceTurn: rs.counters.cadenceTurns,
284
286
  todoMounted: todoToolMounted,
285
287
  taskListMounted: taskToolsMounted,
286
- planActive: prepared.permissionMode.current === "plan",
287
288
  config: {
288
289
  todoReminder: rs.attach.attachmentsCfg?.todoReminder === true,
289
290
  ...(rs.attach.attachmentsCfg?.todoReminderMode !== undefined ? { todoReminderMode: rs.attach.attachmentsCfg.todoReminderMode } : {}),
290
291
  toolSearchReminder: toolSearchReminderOn,
291
292
  changedFiles: changedFilesOn,
292
- planModeReminder: rs.attach.attachmentsCfg?.planModeReminder === true,
293
293
  budgetUsd: budgetUsdOn,
294
294
  totalTokensReminder: totalTokensOn,
295
295
  ...(rs.attach.attachmentsCfg?.totalTokensReminderMode !== undefined
@@ -301,6 +301,7 @@ export function createTurnBoundary(input) {
301
301
  skillsListing: rs.attach.skillsListingOn,
302
302
  mcpInstructions: mcpInstructionsOn,
303
303
  },
304
+ ...(prepared.outputStyleFrame !== undefined && rs.turn.lastTurnHadToolCalls ? { outputStyle: prepared.outputStyleFrame } : {}),
304
305
  ...(undiscoveredTools !== undefined && undiscoveredTools.length > 0
305
306
  ? { undiscoveredTools, deferDirectCall: prepared.deferDirectCall }
306
307
  : {}),
@@ -198,8 +198,10 @@ export declare class Runner implements RunnerSelfSeat {
198
198
  * during the resume — pre-CAS/edit-re-adjudication legs leave the checkpoint pending and resumable,
199
199
  * the post-claim leg leaves it consumed; the message says which), `reopen_failed` (an aborted resume
200
200
  * needed to reopen the checkpoint and the store refused, or the reopen failed in flight — the message
201
- * distinguishes terminally-consumed from state-unprovable). Each code's full contract is on
202
- * {@link CheckpointError.code}.
201
+ * distinguishes terminally-consumed from state-unprovable), `config_refused` (pre-CAS: the re-supplied
202
+ * config or a mode the row recorded is one prepare would refuse — `detail.code` is prepare's own code,
203
+ * `detail.field` says whether the config or the row carries it; the checkpoint stays pending and
204
+ * unchanged). Each code's full contract is on {@link CheckpointError.code}.
203
205
  */
204
206
  resume(token: CheckpointToken, outcome: ResumeOutcome, taskConfig: ResumeTaskConfig,
205
207
  /** Same TRUSTED run-internals seam as {@link resumeStream} — the convenience wrapper
@@ -10,6 +10,15 @@
10
10
  */
11
11
  import type { AcceptedSteerInput } from "./contracts.js";
12
12
  export declare function nextHumanInputSeq(key: object): number;
13
+ /** The identity keys a published `queued` frame carried beyond its own `eventId` (`parentToolCallId`, `sourceTaskId`): one fact, stated
14
+ * once by the frame's publisher and handed to whoever mints that input's other frames, so two frames of one input are never stamped
15
+ * by two computations. */
16
+ export type AcceptanceIdentity = {
17
+ parentToolCallId?: string;
18
+ sourceTaskId?: string;
19
+ };
20
+ export declare function armAcceptance(record: object, publish: () => AcceptanceIdentity): void;
21
+ export declare function announceAcceptance(record: object): AcceptanceIdentity | undefined;
13
22
  /** Is a repeat under an already-accepted `inputId` the SAME
14
23
  * instruction (an idempotent retry) or a different one wearing a used key? Compared field for field,
15
24
  * exactly like the parked leg's replay check: the key alone is never evidence of a replay, and two
@@ -7,6 +7,14 @@ export function nextHumanInputSeq(key) {
7
7
  }
8
8
  return ++box.n;
9
9
  }
10
+ const acceptanceSeats = new WeakMap();
11
+ export function armAcceptance(record, publish) {
12
+ let published;
13
+ acceptanceSeats.set(record, () => (published ??= publish()));
14
+ }
15
+ export function announceAcceptance(record) {
16
+ return acceptanceSeats.get(record)?.();
17
+ }
10
18
  export function sameAcceptedSteerInput(a, b) {
11
19
  return (a.payload === b.payload &&
12
20
  a.trusted === b.trusted &&
@@ -6,7 +6,7 @@ import { buildHumanInputEvent, frameMidTurnUserInput, projectHumanInput } from "
6
6
  import { isSystemInjectionPriority, SYSTEM_INJECTION_PRIORITIES } from "../task-notification.js";
7
7
  import { deliverEngineNotice } from "../types.js";
8
8
  import { inlineUntrusted, sanitizeUntrustedText } from "../untrusted-text.js";
9
- import { nextHumanInputSeq, sameAcceptedSteerInput } from "./steer-admission.js";
9
+ import { announceAcceptance, armAcceptance, nextHumanInputSeq, sameAcceptedSteerInput } from "./steer-admission.js";
10
10
  import { asThrownError } from "../thrown-value.js";
11
11
  export function streamSteerVerb(input) {
12
12
  const { spec, internals, queue, acceptedSteerInputs, live, ready, orTimeout, readyTimeoutMs: READY_TIMEOUT_MS, steeringError, runner } = input;
@@ -44,11 +44,8 @@ export function streamSteerVerb(input) {
44
44
  let payload;
45
45
  let mintsAFrame;
46
46
  let replay;
47
- const noteAccepted = (h) => {
48
- if (!mintsAFrame)
49
- return;
50
- if (typeof inputId === "string")
51
- acceptedSteerInputs.set(inputId, replay);
47
+ const publishQueued = (h) => {
48
+ const identity = internals?.parentToolCallId !== undefined ? { parentToolCallId: internals.parentToolCallId, ...(spec.taskId !== undefined ? { sourceTaskId: spec.taskId } : {}) } : {};
52
49
  queue.push({
53
50
  ...buildHumanInputEvent({
54
51
  carrier: "steer",
@@ -61,10 +58,16 @@ export function streamSteerVerb(input) {
61
58
  ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
62
59
  }),
63
60
  eventId: uuidv7(),
64
- ...(internals?.parentToolCallId !== undefined
65
- ? { parentToolCallId: internals.parentToolCallId, ...(spec.taskId !== undefined ? { sourceTaskId: spec.taskId } : {}) }
66
- : {}),
61
+ ...identity,
67
62
  });
63
+ return identity;
64
+ };
65
+ const noteAccepted = () => {
66
+ if (!mintsAFrame)
67
+ return;
68
+ if (typeof inputId === "string")
69
+ acceptedSteerInputs.set(inputId, replay);
70
+ announceAcceptance(parkRecord);
68
71
  };
69
72
  const deliver = async () => {
70
73
  if (live.resultValue)
@@ -130,13 +133,15 @@ export function streamSteerVerb(input) {
130
133
  }
131
134
  const injectFramed = async () => {
132
135
  const noteOptions = { provenance: "engine-note", callerAuthored: true, parkRecord, ...(actor !== undefined ? { actor } : {}) };
136
+ if (mintsAFrame)
137
+ armAcceptance(parkRecord, () => publishQueued(h));
133
138
  if (priority === "later") {
134
139
  await h.harness.followUp(payload, noteOptions);
135
- noteAccepted(h);
140
+ noteAccepted();
136
141
  return;
137
142
  }
138
143
  const frame = await h.harness.steer(payload, { ...noteOptions, ...(priority === "now" ? { immediate: true } : {}) });
139
- noteAccepted(h);
144
+ noteAccepted();
140
145
  if (priority === "now" && frame !== undefined && h.harness.interruptTurn(frame)) {
141
146
  deliverEngineNotice(runner.deps.onNotice, {
142
147
  code: "task.turn_interrupted",
@@ -1,6 +1,7 @@
1
1
  import { type TSchema } from "typebox";
2
2
  import type { AgentTool } from "../../internal/harness-types.js";
3
3
  import type { SkillManifest } from "../types.js";
4
+ import type { TerminalBlockedDetail } from "../terminal-cause.js";
4
5
  import { type ResolvedSkillListingBudget } from "../skill-listing-budget.js";
5
6
  import type { ActiveSkillScope } from "./active-skill-scope.js";
6
7
  /** Reserved name of the injected structured-output tool (CC 2.1.187 `StructuredOutput`; batch-A rename — old
@@ -21,6 +22,9 @@ export declare const OUTPUT_DEFAULT_RETRIES = 2;
21
22
  export declare function resolveOutputRetries(outputRetries?: number): number;
22
23
  export interface BlockedRef {
23
24
  reason?: string;
25
+ /** Written by the engine when a hook seat kept the prompt back (never by the `ReportBlocked` tool): the terminal's
26
+ * `detail`. Absent = the model's own report. */
27
+ detail?: TerminalBlockedDetail;
24
28
  }
25
29
  /** Captures the validated object a `submit_output` call submitted (structured output, 1.41). `set` is
26
30
  * flipped so we can tell "model submitted {}" from "never called". Mirrors `BlockedRef`. */
@@ -40,7 +40,7 @@ export function createToolTurnRepairSeat(onNotice, ids) {
40
40
  const announced = new Set();
41
41
  const incomplete = createToolTurnIncompleteSeat(onNotice, ids);
42
42
  return (repairs) => {
43
- const missing = repairs.filter((repair) => repair.form === "missing_result").flatMap((repair) => repair.toolCallIds);
43
+ const missing = repairs.filter((repair) => repair.form === "missing_result" && repair.beforeRun !== true).flatMap((repair) => repair.toolCallIds);
44
44
  if (missing.length > 0)
45
45
  incomplete.announce("missing_result_synthesized", missing.length, missing);
46
46
  const fresh = repairs.filter((repair) => {
@@ -18,10 +18,6 @@ export declare const TODO_REMINDER_CONFIG: {
18
18
  readonly TURNS_SINCE_WRITE: 10;
19
19
  readonly TURNS_BETWEEN_REMINDERS: 10;
20
20
  };
21
- export declare const PLAN_MODE_ATTACHMENT_CONFIG: {
22
- readonly TURNS_BETWEEN_ATTACHMENTS: 5;
23
- readonly FULL_REMINDER_EVERY_N_ATTACHMENTS: 5;
24
- };
25
21
  /** CC 2.1.220 tool-search reminder cadence, the shipped defaults of its `juniper_shoal`
26
22
  * config block (`sru = 15` / `aru = 10` @185913-185914, read as `{everyNTurns, maxNames}` @185836-185849).
27
23
  * `EVERY_N_TURNS` gates BOTH windows (since-last-ToolSearch and since-last-reminder, @608920);
@@ -80,7 +76,7 @@ export declare const CHANGED_FILES_MTIME_EPS_MS = 2000;
80
76
  /** Per-boundary byte cap over the bundle's CLIPPABLE frames (weak-model context economics). Intact-class
81
77
  * frames ride outside it — see {@link ATTACHMENT_DELIVERY_CLASS}. */
82
78
  export declare const ATTACHMENT_BYTE_CAP: number;
83
- export type AttachmentSource = "todo_reminder" | "task_reminder" | "tool_search_usage_reminder" | "changed_files" | "plan_mode" | "date_change" | "instructions_change" | "workflow_size_guideline_change" | "budget_usd" | "total_tokens_reminder" | "background_tasks" | "tools_delta" | "agent_listing" | "skills_listing" | "mcp_instructions" | "mcp_dropped_tools";
79
+ export type AttachmentSource = "todo_reminder" | "task_reminder" | "tool_search_usage_reminder" | "changed_files" | "output_style" | "date_change" | "instructions_change" | "workflow_size_guideline_change" | "budget_usd" | "total_tokens_reminder" | "background_tasks" | "tools_delta" | "agent_listing" | "skills_listing" | "mcp_instructions" | "mcp_dropped_tools";
84
80
  /** How the bundle cap treats a source — see {@link ATTACHMENT_DELIVERY_CLASS}. */
85
81
  export type AttachmentDeliveryClass = {
86
82
  readonly class: "clippable";
@@ -114,7 +110,7 @@ export declare const ATTACHMENT_DELIVERY_CLASS: {
114
110
  readonly changed_files: {
115
111
  readonly class: "clippable";
116
112
  };
117
- readonly plan_mode: {
113
+ readonly output_style: {
118
114
  readonly class: "clippable";
119
115
  };
120
116
  readonly budget_usd: {
@@ -238,10 +234,6 @@ export interface AttachmentState {
238
234
  /** Cadence-clock value at the last tool_search_usage_reminder emitted (CC 220 `cFy`'s
239
235
  * `turnsSinceLastReminder`, found by scanning back for the attachment entry itself). */
240
236
  lastToolSearchReminderTurn: number;
241
- /** plan_mode attachments emitted so far — drives CC's full/sparse rotation (1st, 6th, 11th… = full).
242
- * 0 also encodes "never attached": CC always attaches on the FIRST plan-mode boundary, then throttles. */
243
- planAttachmentCount: number;
244
- lastPlanReminderTurn: number;
245
237
  /** changed_files per-path dedup: the last mtime already surfaced for a path — the same (path, mtime)
246
238
  * is never reported twice. Evicted alongside readFileState's ENOENT eviction (LOW-9: bounded by it). */
247
239
  surfacedMtime: Map<string, number>;
@@ -296,7 +288,7 @@ export interface AttachmentState {
296
288
  * leg's real remaining — the CC re-anchor arm, not a floor violation; the floor deliberately
297
289
  * does NOT ride the checkpoint, since a carried floor against a fresh slice window would freeze
298
290
  * the readout at "0 tokens left" over an allowance the run genuinely holds). Same lifecycle
299
- * reasoning as the plan_mode lane's `countPlanModeAttachmentsSinceLastExit` note.
291
+ * reasoning applies to every per-leg counter here.
300
292
  * - the ROLLOVER (`rollOverContext`, which adds each compacted-away context back so the count keeps
301
293
  * advancing) is likewise not owed: sema feeds a CUMULATIVE spend coordinate (`stats.tokens`),
302
294
  * which a compaction never rewinds, where CC feeds the live context size.
@@ -412,17 +404,19 @@ export interface AttachmentInputs {
412
404
  * multi-block turn consumes one unit PER block, so the windows run 2-3× faster than physical
413
405
  * turns on real transcripts (text+tool_use is the norm). Write anchors ({@link stampWriteAnchor})
414
406
  * are stamped in this SAME coordinate. Omitted ⇒ falls back to `turn` (correct only for the
415
- * degenerate one-non-thinking-block-per-turn transcript). plan_mode keeps `turn` (its cadence is
416
- * a recorded sema deviation already — assistant turns, not CC human turns). */
407
+ * degenerate one-non-thinking-block-per-turn transcript). */
417
408
  cadenceTurn?: number;
418
409
  /** Whether TodoWrite is mounted (CC gate: no tool ⇒ no reminder, ever). */
419
410
  todoMounted: boolean;
420
411
  /** Whether the task-family tools are mounted. Both mounted ⇒ task_reminder wins (CC isTodoV2 branch). */
421
412
  taskListMounted: boolean;
422
- /** Whether the leg's effective permission mode is `plan` right now (the one mode seat, whichever entrance set it:
423
- * the spec's word, a steer, the model's EnterPlanMode). A false here simply stops the producer — no re-entry cycle
424
- * reset (documented deviation from CC's countPlanModeAttachmentsSinceLastExit). */
425
- planActive: boolean;
413
+ /**
414
+ * The output-style frame's body, rendered once at prepare from `TaskSpec.outputStyle` (output-style.ts);
415
+ * present ⇒ the frame is due at this boundary. The reference implementation's producer has no window: it
416
+ * attaches at every attachment cycle while a non-default style is active, so the run loop passes it at every
417
+ * continuing boundary and this producer holds no state for it.
418
+ */
419
+ outputStyle?: string;
426
420
  /** SR-3 budget_usd lane (CC 198 `QFm` producer :480424-480428 + renderer :600225; 220 `_Fy` @609096
427
421
  * with the mount at @607474): the run's cost progress against its $ ceiling, in USD. Only passed
428
422
  * when `config.budgetUsd` opted in AND the task actually carries a ceiling (CC gate
@@ -466,7 +460,6 @@ export interface AttachmentInputs {
466
460
  * `toolSearchReminder` config resolves non-null, @607450-607456 / `cXi` @185859). */
467
461
  toolSearchReminder?: boolean;
468
462
  changedFiles: boolean;
469
- planModeReminder: boolean;
470
463
  /** SR-3: budget_usd progressive reminder lane (opt-in, default OFF like every 133 member). CC fires
471
464
  * it on EVERY attachment cycle while `maxBudgetUsd` is set; there is NO threshold ladder and no
472
465
  * throttle window (QFm/`_Fy` are unconditional) — the "progression" is the numbers advancing with
@@ -625,11 +618,11 @@ export interface AttachmentInputs {
625
618
  * splitting "decide" from "mark sent" would just invite a drift between the two.
626
619
  *
627
620
  * Returns attachments in EVIDENCE-STRENGTH order (todo/task → tool_search_usage_reminder →
628
- * plan_mode → budget_usd → total_tokens_reminder → background_tasks → tools_delta → agent_listing →
621
+ * output_style → budget_usd → total_tokens_reminder → background_tasks → tools_delta → agent_listing →
629
622
  * skills_listing → mcp_instructions → mcp_dropped_tools → changed_files), which is also the §R3
630
623
  * MED-6 truncation order read backwards: under the 8KB cap changed_files is sacrificed first, the
631
624
  * todo/task list last. The G1/续批 members are registry/engine ground truth (strong), but the
632
- * todo/plan lane keeps priority: it carries the run's own work-tracking state, which the model can
625
+ * todo/task lane keeps priority: it carries the run's own work-tracking state, which the model can
633
626
  * least afford to lose.
634
627
  */
635
628
  export declare function collectDueAttachments(state: AttachmentState, inp: AttachmentInputs): readonly TurnAttachment[];
@@ -856,8 +849,9 @@ export declare function agentListingDeltaHeader(toolName: string): string;
856
849
  *
857
850
  * ShowConcurrencyNote, honest attribution: `entries.length > 1` is a sema APPROXIMATION, not
858
851
  * CC semantics. 220's real predicate (@607882) is `Aa() !== "pro" && Cq() === "default"` — subscription
859
- * tier is not "pro" AND the output style is the default one. Neither axis exists in core (BYOM has no
860
- * subscription plane; there is no output-style plane at all), so the predicate is untranslatable, and
852
+ * tier is not "pro" AND the output style is the default one. The tier axis does not exist in core (BYOM has no
853
+ * subscription plane) and the style axis is only a declared seat (`TaskSpec.outputStyle`), so the predicate is
854
+ * untranslatable as a whole, and
861
855
  * 220's intent — "show the parallel-launch hint to users who have not already been told it elsewhere" —
862
856
  * has no sema carrier either. The roster-size proxy is kept because it is the one honest thing core can
863
857
  * say: a single-type roster cannot be launched concurrently with a different type, so the note would be
@@ -1,4 +1,3 @@
1
- import { PRESENT_PLAN_TOOL_NAME } from "../present-plan-tool.js";
2
1
  import { readMember } from "../thrown-value.js";
3
2
  import { delimitUntrustedWithClip, sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
4
3
  import { protocolOf } from "../protocol-table.js";
@@ -10,10 +9,6 @@ export const TODO_REMINDER_CONFIG = {
10
9
  TURNS_SINCE_WRITE: 10,
11
10
  TURNS_BETWEEN_REMINDERS: 10,
12
11
  };
13
- export const PLAN_MODE_ATTACHMENT_CONFIG = {
14
- TURNS_BETWEEN_ATTACHMENTS: 5,
15
- FULL_REMINDER_EVERY_N_ATTACHMENTS: 5,
16
- };
17
12
  export const TOOL_SEARCH_REMINDER_CONFIG = {
18
13
  EVERY_N_TURNS: 15,
19
14
  MAX_NAMES: 10,
@@ -35,7 +30,7 @@ export const ATTACHMENT_DELIVERY_CLASS = {
35
30
  task_reminder: { class: "clippable" },
36
31
  tool_search_usage_reminder: { class: "clippable" },
37
32
  changed_files: { class: "clippable" },
38
- plan_mode: { class: "clippable" },
33
+ output_style: { class: "clippable" },
39
34
  budget_usd: { class: "clippable" },
40
35
  total_tokens_reminder: { class: "clippable" },
41
36
  background_tasks: { class: "clippable" },
@@ -68,8 +63,6 @@ export function createAttachmentState() {
68
63
  lastTodoReminderTurn: 0,
69
64
  lastTaskReminderTurn: 0,
70
65
  lastToolSearchReminderTurn: 0,
71
- planAttachmentCount: 0,
72
- lastPlanReminderTurn: 0,
73
66
  surfacedMtime: new Map(),
74
67
  postCompactPending: false,
75
68
  };
@@ -193,16 +186,8 @@ export function collectDueAttachments(state, inp) {
193
186
  (out ??= []).push({ source: "tool_search_usage_reminder", body: renderToolSearchUsageReminder(inp.undiscoveredTools, inp.deferDirectCall !== false) });
194
187
  }
195
188
  }
196
- if (inp.config.planModeReminder && inp.planActive) {
197
- const due = state.planAttachmentCount === 0 ||
198
- inp.turn - state.lastPlanReminderTurn >= PLAN_MODE_ATTACHMENT_CONFIG.TURNS_BETWEEN_ATTACHMENTS;
199
- if (due) {
200
- state.planAttachmentCount += 1;
201
- state.lastPlanReminderTurn = inp.turn;
202
- const full = state.planAttachmentCount % PLAN_MODE_ATTACHMENT_CONFIG.FULL_REMINDER_EVERY_N_ATTACHMENTS === 1;
203
- (out ??= []).push({ source: "plan_mode", body: full ? PLAN_MODE_FULL_BODY : PLAN_MODE_SPARSE_BODY });
204
- }
205
- }
189
+ if (inp.outputStyle !== undefined)
190
+ (out ??= []).push({ source: "output_style", body: inp.outputStyle });
206
191
  if (inp.config.budgetUsd && inp.budgetUsd !== undefined) {
207
192
  (out ??= []).push({ source: "budget_usd", body: renderBudgetUsd(inp.budgetUsd.used, inp.budgetUsd.total) });
208
193
  }
@@ -761,8 +746,6 @@ function mcpDroppedToolsBound(inp) {
761
746
  const single = first === undefined ? 0 : MCP_DROPPED_FIXED_MAX_BYTES + bytes(first.tool) + bytes(first.server) + bytes(first.reason) + 32;
762
747
  return Math.max(MCP_DROPPED_BATCH_MAX_BYTES, single);
763
748
  }
764
- const PLAN_MODE_FULL_BODY = `Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system — every such call asks the user first, and is refused where nobody can answer. This supercedes any other instructions you have received. Instead, you should: research and analyze with read-only tools, and when you have formed a concrete plan, present it with the ${PRESENT_PLAN_TOOL_NAME} tool for approval. That tool inherently requests plan approval — never ask for approval via plain text.`;
765
- const PLAN_MODE_SPARSE_BODY = `Plan mode still active (see full instructions earlier in conversation). Read-only: no edits, commits, or config changes. When your plan is ready, present it with the ${PRESENT_PLAN_TOOL_NAME} tool for approval. Never ask about plan approval via plain text.`;
766
749
  const TRUNCATION_MARKER = "\n…[truncated]";
767
750
  const MIN_KEEP_BYTES = 160;
768
751
  function capAttachments(atts) {
@@ -718,7 +718,7 @@ export interface RunnerDeps {
718
718
  */
719
719
  /**
720
720
  * The deployment's A2A peer switches. `isolatePeers: true` makes every A2A peer skill's egress default an
721
- * OPERATOR mandate: its ask is `operator_forced` (kept under `bypassPermissions`, not settled by `dontAsk`)
721
+ * OPERATOR mandate: its ask is `operator_forced` (kept under `bypassPermissions`; `dontAsk` refuses it like every ask)
722
722
  * instead of the releasable `tool_marks_a2a`. Absent ⇒ the default (a permission mode may release it, disclosed).
723
723
  */
724
724
  a2a?: {
@@ -27,6 +27,14 @@ export declare const ALLOW: Extract<ResumeOutcome, {
27
27
  export declare const EXECUTED_ALLOW: GateOutcome;
28
28
  export declare const EXECUTED_ALLOW_REORDERED: GateOutcome;
29
29
  export declare const VETOED_BY_POLICY: GateOutcome;
30
+ /** The other decider: the record a `policy_ask` allow the deployment's own POLICY decided files
31
+ * (`policy_allowed`, `who.party: "policy"` with the seat that answered) — the decision's own record on the
32
+ * resolved winner, and the execution record once the call ran — and the decide that carries it. A backend keeps
33
+ * the record verbatim; nothing in it may be re-derived from the person's shape. */
34
+ export declare const POLICY_ALLOWED: GateOutcome;
35
+ export declare const POLICY_ALLOW: Extract<ResumeOutcome, {
36
+ gate: "policy_ask";
37
+ }>;
30
38
  /**
31
39
  * The full cross-backend contract for {@link CheckpointStore} — the SAME assertions for every
32
40
  * backend. Entries are a 1:1 port of the vitest originals (assertion semantics unchanged).
@@ -41,6 +41,12 @@ const SETTLED = { kind: "human_allowed", who: { party: "person" }, when: 1_700_0
41
41
  export const EXECUTED_ALLOW = Object.freeze({ disposition: Object.freeze({ kind: "allowed" }), settlement: Object.freeze({ ...SETTLED, who: Object.freeze({ ...SETTLED.who }) }), origin: "policy" });
42
42
  export const EXECUTED_ALLOW_REORDERED = Object.freeze({ origin: "policy", settlement: Object.freeze({ when: 1_700_000_000_500, who: Object.freeze({ party: "person" }), kind: "human_allowed" }), disposition: Object.freeze({ kind: "allowed" }) });
43
43
  export const VETOED_BY_POLICY = Object.freeze({ disposition: Object.freeze({ kind: "denied", deniedBy: "policy" }), settlement: Object.freeze({ ...SETTLED, who: Object.freeze({ ...SETTLED.who }) }), origin: "policy" });
44
+ export const POLICY_ALLOWED = Object.freeze({
45
+ disposition: Object.freeze({ kind: "allowed" }),
46
+ settlement: Object.freeze({ kind: "policy_allowed", who: Object.freeze({ party: "policy", approver: "unattended_class_answer" }), when: 1_700_000_000_500 }),
47
+ origin: "policy",
48
+ });
49
+ export const POLICY_ALLOW = { ...ALLOW, hostDecision: { decidedBy: "policy", approver: "unattended_class_answer" }, gateOutcome: POLICY_ALLOWED };
44
50
  export async function checkpointStoreContract(make, runAssertion) {
45
51
  const { run, settle } = beginContract(runAssertion);
46
52
  run("kit prerequisites: reopen/setPendingSteer/listByScope are implemented (REQUIRED by this kit)", async () => {
@@ -399,6 +405,23 @@ export async function checkpointStoreContract(make, runAssertion) {
399
405
  assert.equal("askClass" in bare, false, "an absent askClass must stay OMITTED on the row");
400
406
  assert.equal("classes" in bare, false, "an absent classes must stay OMITTED on the row");
401
407
  });
408
+ run("decider pair: a policy-decided allow resolves like a person's — its `policy_allowed` record (party `policy`, the attribution) round-trips verbatim on the resolved winner and as the execution record; the same record is already_recorded, a person's record for that row conflicts", async () => {
409
+ const store = make();
410
+ const cp = createCheckpointFixture();
411
+ await store.put(cp.token, cp);
412
+ assert.equal(await store.resolve(cp.token, cp.scope, POLICY_ALLOW), true, "a policy-decided allow is a decision the CAS takes like any other");
413
+ const resolved = (await store.get(cp.token));
414
+ assert.equal(resolved.status, "resolved");
415
+ assert.equal(resolved.resolvedOutcome.decision, "allow");
416
+ assert.deepEqual(resolved.resolvedOutcome.gateOutcome, POLICY_ALLOWED, "the decision's record rides the winner verbatim — the party word and the attribution included");
417
+ assert.equal(await store.recordExecutionOutcome(cp.token, cp.scope, POLICY_ALLOWED), "recorded");
418
+ const filed = (await store.get(cp.token));
419
+ assert.deepEqual(filed.executionOutcome, POLICY_ALLOWED, "the policy's execution record is kept verbatim");
420
+ assert.deepEqual(executionVerdict(filed), { kind: "executed", gate: POLICY_ALLOWED });
421
+ assert.equal(await store.recordExecutionOutcome(cp.token, cp.scope, structuredClone(POLICY_ALLOWED)), "already_recorded", "an unfrozen structural twin is the same record");
422
+ await assert.rejects(store.recordExecutionOutcome(cp.token, cp.scope, EXECUTED_ALLOW), (e) => e.code === "checkpoint.execution_outcome_conflict", "a person's record for a row the policy decided is a different record");
423
+ assert.deepEqual((await store.get(cp.token)).executionOutcome, POLICY_ALLOWED);
424
+ });
402
425
  await settle();
403
426
  }
404
427
  const sortByToken = (a, b) => a.token.localeCompare(b.token);