@sema-agent/core 7.5.1 → 7.6.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 (132) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/agents/cascade.d.ts +2 -2
  3. package/dist/agents/cascade.js +12 -10
  4. package/dist/agents/repair-loop.d.ts +5 -3
  5. package/dist/agents/repair-loop.js +13 -15
  6. package/dist/agents/subagent.d.ts +24 -42
  7. package/dist/agents/subagent.js +119 -105
  8. package/dist/agents/suspend-guard.d.ts +31 -19
  9. package/dist/agents/suspend-guard.js +14 -8
  10. package/dist/agents/teacher.js +9 -9
  11. package/dist/agents/team.d.ts +4 -3
  12. package/dist/agents/team.js +10 -8
  13. package/dist/agents/verify.d.ts +3 -3
  14. package/dist/agents/verify.js +17 -17
  15. package/dist/core/a2a.js +2 -1
  16. package/dist/core/ask-origin.d.ts +60 -7
  17. package/dist/core/ask-origin.js +26 -1
  18. package/dist/core/checkpoint-store.d.ts +78 -76
  19. package/dist/core/checkpoint-store.js +17 -1
  20. package/dist/core/gate-outcome.d.ts +189 -0
  21. package/dist/core/gate-outcome.js +70 -0
  22. package/dist/core/hooks.d.ts +18 -92
  23. package/dist/core/hooks.js +88 -85
  24. package/dist/core/mcp-failure.d.ts +104 -0
  25. package/dist/core/mcp-failure.js +128 -0
  26. package/dist/core/mcp.d.ts +21 -77
  27. package/dist/core/mcp.js +76 -150
  28. package/dist/core/pause-registry.d.ts +131 -0
  29. package/dist/core/pause-registry.js +27 -0
  30. package/dist/core/protocol-table.d.ts +5 -0
  31. package/dist/core/protocol-table.js +1 -0
  32. package/dist/core/runner/abort-race.d.ts +41 -0
  33. package/dist/core/runner/abort-race.js +38 -0
  34. package/dist/core/runner/assemble-result.d.ts +32 -41
  35. package/dist/core/runner/assemble-result.js +55 -74
  36. package/dist/core/runner/checkpoint-scope.d.ts +15 -3
  37. package/dist/core/runner/checkpoint-scope.js +3 -0
  38. package/dist/core/runner/compaction-call-options.d.ts +1 -1
  39. package/dist/core/runner/content-ask-bindings.d.ts +27 -0
  40. package/dist/core/runner/content-ask-bindings.js +1 -0
  41. package/dist/core/runner/contracts.d.ts +144 -50
  42. package/dist/core/runner/denial-limit-arms.d.ts +24 -1
  43. package/dist/core/runner/denial-limit-arms.js +24 -3
  44. package/dist/core/runner/gate-exit.d.ts +74 -0
  45. package/dist/core/runner/gate-exit.js +55 -0
  46. package/dist/core/runner/inherited-ask-grants.d.ts +46 -0
  47. package/dist/core/runner/inherited-ask-grants.js +29 -0
  48. package/dist/core/runner/park-commit.d.ts +102 -0
  49. package/dist/core/runner/park-commit.js +31 -0
  50. package/dist/core/runner/{prepare-permission-rules.d.ts → permission-rule-lanes.d.ts} +109 -3
  51. package/dist/core/runner/{prepare-permission-rules.js → permission-rule-lanes.js} +47 -1
  52. package/dist/core/runner/prepare-ask-lane.d.ts +107 -0
  53. package/dist/core/runner/prepare-ask-lane.js +131 -0
  54. package/dist/core/runner/prepare-boundary-parks.d.ts +102 -0
  55. package/dist/core/runner/prepare-boundary-parks.js +169 -0
  56. package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
  57. package/dist/core/runner/prepare-context-lane.d.ts +119 -0
  58. package/dist/core/runner/prepare-context-lane.js +230 -0
  59. package/dist/core/runner/prepare-gate-stations.d.ts +174 -0
  60. package/dist/core/runner/prepare-gate-stations.js +265 -0
  61. package/dist/core/runner/prepare-hands-readface.d.ts +4 -4
  62. package/dist/core/runner/prepare-inherited-gate.d.ts +4 -4
  63. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  64. package/dist/core/runner/prepare-memory-engine-session.d.ts +84 -0
  65. package/dist/core/runner/prepare-memory-engine-session.js +233 -0
  66. package/dist/core/runner/prepare-memory.d.ts +44 -26
  67. package/dist/core/runner/prepare-park-ask.d.ts +162 -0
  68. package/dist/core/runner/prepare-park-ask.js +377 -0
  69. package/dist/core/runner/prepare-policy-chain.d.ts +208 -0
  70. package/dist/core/runner/prepare-policy-chain.js +584 -0
  71. package/dist/core/runner/prepare-project-context.d.ts +1 -13
  72. package/dist/core/runner/prepare-project-context.js +1 -3
  73. package/dist/core/runner/prepare-prompt-assembly.d.ts +95 -0
  74. package/dist/core/runner/prepare-prompt-assembly.js +162 -0
  75. package/dist/core/runner/prepare-prompt-inputs.d.ts +1 -20
  76. package/dist/core/runner/prepare-protocol-tools.d.ts +3 -3
  77. package/dist/core/runner/prepare-protocol-tools.js +0 -3
  78. package/dist/core/runner/prepare-question-face.d.ts +3 -21
  79. package/dist/core/runner/prepare-question-face.js +2 -1
  80. package/dist/core/runner/prepare-safety-scan.d.ts +0 -5
  81. package/dist/core/runner/prepare-safety-scan.js +1 -2
  82. package/dist/core/runner/prepare-suspend-saga.d.ts +170 -0
  83. package/dist/core/runner/prepare-suspend-saga.js +308 -0
  84. package/dist/core/runner/prepare-task.d.ts +9 -136
  85. package/dist/core/runner/prepare-task.js +47 -2745
  86. package/dist/core/runner/prepare-turn-wiring.d.ts +154 -0
  87. package/dist/core/runner/prepare-turn-wiring.js +201 -0
  88. package/dist/core/runner/prepare-wiring-manifest.d.ts +17 -17
  89. package/dist/core/runner/prepare-wiring-manifest.js +16 -10
  90. package/dist/core/runner/prepare-workspace-restore.d.ts +2 -29
  91. package/dist/core/runner/prepare-workspace-restore.js +3 -16
  92. package/dist/core/runner/prompt-hash-salt.d.ts +1 -0
  93. package/dist/core/runner/prompt-hash-salt.js +2 -0
  94. package/dist/core/runner/remote-env-retry.d.ts +29 -0
  95. package/dist/core/runner/remote-env-retry.js +16 -0
  96. package/dist/core/runner/runtask.d.ts +16 -31
  97. package/dist/core/runner/runtask.js +110 -121
  98. package/dist/core/runner/terminal-projection.d.ts +22 -0
  99. package/dist/core/runner/terminal-projection.js +28 -0
  100. package/dist/core/session.d.ts +12 -0
  101. package/dist/core/session.js +3 -0
  102. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -1
  103. package/dist/core/store-contracts/checkpoint-store-contract.js +8 -2
  104. package/dist/core/terminal-cause.d.ts +137 -0
  105. package/dist/core/terminal-cause.js +9 -0
  106. package/dist/core/tool-policy.d.ts +43 -139
  107. package/dist/core/tool-policy.js +79 -112
  108. package/dist/core/types.d.ts +67 -164
  109. package/dist/core/wiring-manifest.d.ts +6 -3
  110. package/dist/core/workflow-journal-store.js +3 -4
  111. package/dist/engine/harness/agent-harness.d.ts +1 -1
  112. package/dist/index.d.ts +10 -7
  113. package/dist/index.js +8 -5
  114. package/dist/orchestration/builtin-workflows.d.ts +2 -2
  115. package/dist/orchestration/builtin-workflows.js +1 -1
  116. package/dist/orchestration/goal.js +8 -7
  117. package/dist/orchestration/run-spec.js +5 -3
  118. package/dist/orchestration/run-workflow-tool.d.ts +1 -1
  119. package/dist/orchestration/run-workflow-tool.js +4 -4
  120. package/dist/orchestration/workflow-governance.d.ts +4 -4
  121. package/dist/orchestration/workflow-governance.js +4 -2
  122. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  123. package/dist/orchestration/workflow-primitives.js +1 -1
  124. package/dist/orchestration/workflow.d.ts +11 -0
  125. package/dist/orchestration/workflow.js +64 -39
  126. package/dist/prompts/supervisor.d.ts +1 -1
  127. package/dist/prompts/supervisor.js +3 -3
  128. package/dist/scenarios/scenario-registry.js +1 -1
  129. package/package.json +3 -1
  130. package/test/export-surface.snapshot.json +74 -22
  131. /package/dist/core/runner/{prepare-announce-once.d.ts → announce-once-ledger.d.ts} +0 -0
  132. /package/dist/core/runner/{prepare-announce-once.js → announce-once-ledger.js} +0 -0
@@ -26,9 +26,9 @@ import type { CompactionForkContext } from "../auto-compaction.js";
26
26
  import type { AutoModeArmingRecipe } from "../auto-mode-arming.js";
27
27
  import type { AutoModeDecider, AutoModeDenialTracker } from "../auto-mode.js";
28
28
  import type { CacheBreakDetector, ToolFingerprintInput } from "../cache-break-detector.js";
29
- import type { CheckpointGate, CheckpointState, CheckpointToken, PlatformLimitReason, ResourceLedger, ResourceLimitReason } from "../checkpoint-store.js";
29
+ import type { Checkpoint, CheckpointState, CheckpointToken, PlatformLimitReason, ResourceLedger, ResourceLimitReason, SerializedCheckpointState } from "../checkpoint-store.js";
30
30
  import type { ClearedProjectionLedger, ContextEditMachine, OccurrenceIndex } from "../context-edit.js";
31
- import type { HookInvocationIdentity, Hooks, OrgGateVerdict } from "../hooks.js";
31
+ import type { HookInvocationIdentity, Hooks, OrgGateVerdict, PermissionDeniedPayload, ToolGateInput } from "../hooks.js";
32
32
  import type { MaterializedMcp } from "../mcp.js";
33
33
  import type { MemoryEngine } from "../memory-engine/engine.js";
34
34
  import type { HarvestReport, MemorySessionHandle } from "../memory-engine/types.js";
@@ -36,7 +36,8 @@ import type { SessionPermissionRules } from "../session-policy-store.js";
36
36
  import type { RecoveredOrphan } from "../session-reconcile.js";
37
37
  import type { StoredSession } from "../session.js";
38
38
  import type { TaskNotificationPayload } from "../task-notification.js";
39
- import type { OnAsk, ToolCallRequest, ToolPolicy } from "../tool-policy.js";
39
+ import type { OnAsk, PermissionResult, ResolvedAsk, ToolCallRequest, ToolPolicy } from "../tool-policy.js";
40
+ import type { RemoteExecutionEnv, SnapshotId, WorkspaceHandle } from "../remote-env.js";
40
41
  import type { ToolDisclosureManifest } from "../trace.js";
41
42
  import type { NestedUsageAccum, RunnerDeps, TaskEvent, TaskResult, TaskSpec, ToolActivity, ToolEffect } from "../types.js";
42
43
  import type { WiringManifest } from "../wiring-manifest.js";
@@ -62,7 +63,7 @@ export interface ToolFaceSnapshot {
62
63
  /**
63
64
  * The once-per-session announcement ledger's OPERATOR SINK as the phases see it: the
64
65
  * `RunnerDeps.onError` signature, deduplicated per (code, text) across the session. The ledger itself
65
- * (load, digests, settle) lives in prepare-announce-once.ts; a phase that announces once invokes only
66
+ * (load, digests, settle) lives in announce-once-ledger.ts; a phase that announces once invokes only
66
67
  * this member, so the member's shape is declared here — below every phase — rather than reached for
67
68
  * sideways as the ledger module's type.
68
69
  */
@@ -143,6 +144,18 @@ export interface FileHistoryBoundarySeat {
143
144
  begin(entryId: string): void;
144
145
  settle(): Promise<void>;
145
146
  }
147
+ /**
148
+ * The commit-side facts of the run's ONE durable pause: the terminal cause the result assembles
149
+ * ({@link import("../types.js").PausedCause} — gate, token, checkpoint id, restore mode), the COMMITTED scope
150
+ * (read verbatim by the reap/cancel path — never re-derived from the spec, which the cross-scope re-scope
151
+ * may no longer match), and the id of the tool call the park is holding (only a `tool_approval` pending
152
+ * action parks ON a call; absent, never guessed, for the kinds that hold none).
153
+ */
154
+ export interface CommittedPause {
155
+ cause: import("../types.js").PausedCause;
156
+ scope: string;
157
+ gatedCallId?: string;
158
+ }
146
159
  export interface Prepared {
147
160
  harness: AgentHarness;
148
161
  /** The CONCRETE built-in session (engine-internal: prepare constructs/acquires `StoredSession` itself,
@@ -220,17 +233,12 @@ export interface Prepared {
220
233
  *
221
234
  * It is a sideband and not a field on the tool RESULT because a result is not a trustworthy carrier
222
235
  * for this: `details` is arbitrary tool-authored data that post-tool hooks may also replace, so a
223
- * failing tool could stamp `settledBy:"human"` on itself and tell a consumer's audit view that a
224
- * person approved something nobody was ever shown. Provenance has to travel on a channel only the
236
+ * failing tool could stamp a settlement on itself and tell a consumer's audit view that a person
237
+ * approved something nobody was ever shown. Provenance has to travel on a channel only the
225
238
  * adjudicating layer can write. Same reason the entries are keyed by CALL id: the gate adjudicated
226
239
  * that exact call, and the frame that reads it is that call's own.
227
240
  */
228
- approvalSettlement: Map<string, {
229
- settledBy?: import("../tool-policy.js").ApprovalSettledBy;
230
- approver?: string;
231
- resolution?: import("../tool-policy.js").AskDenyResolution;
232
- autoDenied?: true;
233
- }>;
241
+ gateOutcomes: Map<string, import("../gate-outcome.js").GateOutcome>;
234
242
  /**
235
243
  * The parent-thread human-rejection halt fact (see `maybeHumanRejectionHalt`): present from the
236
244
  * moment a bare human rejection halts the turn's batch until the run ends or the NEXT provider
@@ -553,24 +561,15 @@ export interface Prepared {
553
561
  * and the Runner must tear down on task end. Undefined when the env came from a (caller-owned) static
554
562
  * `deps.executionEnv` or the stub — those outlive the task and must NOT be destroyed here. */
555
563
  ownedEnv?: ExecutionEnv;
556
- /** design/45: a mutable holder the durable-suspend gate writes when a policy `ask` was checkpointed
557
- * (capture + abort). The run loop reads it to assemble `status:"suspended"`. Empty unless a suspension
558
- * fired this run.
559
- *
560
- * `gatedCallId` is the id of the tool call the committed gate is holding read straight off the
561
- * committed checkpoint's `tool_approval` pendingAction, so the id a contaminated sibling frame names
562
- * and the id the checkpoint parks on are the SAME value by construction, not by convention. ABSENT
563
- * (never guessed) for a park that binds no tool call at all — a `resource_limit` slice and a
564
- * `plan_review` pause both have pendingActions with no tool call, so there is no causal id to name.
565
- * Written unconditionally by the commit-side publisher precisely so a later park cannot inherit an
566
- * earlier one's id. */
567
- suspendRef: {
568
- token?: CheckpointToken;
569
- checkpointId?: string;
570
- gate?: CheckpointGate;
571
- scope?: string;
572
- restoreMode?: "snapshot" | "park_only";
573
- gatedCallId?: string;
564
+ /** The run's ONE committed durable pause ({@link CommittedPause}): a mutable holder the commit-side
565
+ * publisher (`publishCommittedSuspend`) writes exactly once per run when a park commits an approval
566
+ * or content ask, a resource slice, a platform pause, a plan or dry-run review — and the run loop reads
567
+ * to assemble the result's `terminal: { kind: "paused", … }`. WHICH pause it is is the cause's own
568
+ * `gate.kind` (a registry row), not the holder it landed in: the two same-shaped holders that used to
569
+ * split the approval family from the review family, and the commit-side routing between them, are
570
+ * gone. `current` is absent until a park commits. */
571
+ pausedRef: {
572
+ current?: CommittedPause;
574
573
  };
575
574
  /** Ruled 2026-08-05 (matrix ruling arm A): set true by the resume engine when this leg EXECUTES the
576
575
  * approved pending call — the restart-loop cap then counts from a fresh base (consecutive
@@ -578,22 +577,6 @@ export interface Prepared {
578
577
  suspendProgressRef: {
579
578
  executedApproved: boolean;
580
579
  };
581
- /** design/76 §2.5 (dry-run / shadow) + design/80 D-B (plan-gate): the DUAL of {@link suspendRef} for the
582
- * REVIEW-PAUSE family — a `{kind:"needs_review"}` pause (a profile's dry-run interception committed a
583
- * checkpoint whose predicted state-diff a human/judge must REVIEW) OR a `{kind:"plan_review"}` pause (a
584
- * profile's plan-gate committed a checkpoint whose proposed PLAN a human must approve/edit/reject). The
585
- * commit-side discriminant (`publishCommittedSuspend`) writes HERE for a `needs_review` OR `plan_review`
586
- * gate and into {@link suspendRef} for every other gate kind — **never both** (else assemble-result's slot
587
- * 8.6 `needs_review` branch is dead code, v4 MAJOR-A). The run loop reads it to assemble
588
- * `status:"needs_review"`. Empty unless a review pause fired this run. */
589
- reviewRef: {
590
- token?: CheckpointToken;
591
- checkpointId?: string;
592
- gate?: CheckpointGate;
593
- scope?: string;
594
- restoreMode?: "snapshot" | "park_only";
595
- gatedCallId?: string;
596
- };
597
580
  /** RB-439-a: the remote-workspace lifecycle failures this run hit, appended in call order and echoed
598
581
  * verbatim on `TaskResult.remoteEnvFailures`. A shared array (not a per-call return) because a suspend
599
582
  * refusal is reported through the deployment's `onError` side channel and the run then continues or
@@ -613,7 +596,7 @@ export interface Prepared {
613
596
  * results, and a remote — or static caller-owned, never per-task-stub — env). The run loop calls it at a
614
597
  * CLEAN turn boundary when a resource limit (turns/budget/walltime) was hit: it mints a `resource_limit`
615
598
  * checkpoint + pauses the workspace + stops the loop cleanly (NOT abort). Returns true iff it committed a
616
- * resumable checkpoint (sets `suspendRef`); false ⇒ caller falls through to normal limit handling.
599
+ * resumable checkpoint (publishes the committed pause); false ⇒ caller falls through to normal limit handling.
617
600
  * `sliceSpend` (Slice 4) is THIS slice's cost/tokens/turns, debited onto the cross-slice ledger. */
618
601
  suspendForResource?: (reason: ResourceLimitReason, sliceSpend: {
619
602
  costMicroUsd: number;
@@ -684,7 +667,7 @@ export interface Prepared {
684
667
  };
685
668
  /** design/80 D-B: present (≠ undefined) ONLY when a `checkpointStore` is wired (the deployment can pause). The
686
669
  * run loop calls it at a CLEAN turn boundary when `reviewRequestRef.pending` is set: it mints a `plan_review`
687
- * checkpoint (`status:"needs_review"`, routes to `reviewRef`) + pauses the workspace + aborts the loop, reusing
670
+ * checkpoint (`status:"needs_review"` by its registry row) + pauses the workspace + aborts the loop, reusing
688
671
  * the SAME commit saga as the human/resource suspends. Returns true iff it committed a resumable checkpoint;
689
672
  * false ⇒ the request could not be honored (caller drops it and continues). */
690
673
  suspendForReview?: (reason?: string) => Promise<boolean>;
@@ -745,7 +728,10 @@ export interface Prepared {
745
728
  * · the driver — pushes CronCreate/CronDelete/CronList (write hands; inert without a daemon);
746
729
  * · lsp — pushes the LSP tool (manager wired);
747
730
  * · project-context — pushes Skill (manifest), the shared-memory pair and the memory-engine tools;
748
- * · defer-classify — SPLICES OUT the excluded names and the retracted pair (the only removals at prepare).
731
+ * · defer-classify — SPLICES OUT the excluded names and the retracted pair (the only removals at prepare);
732
+ * · memory-engine-session — REPLACES entries in place with the content-origin wrappers when it arms (a mounted
733
+ * memory session or a recorder channel); the refresh seam re-runs the same pass through `contentOriginWrapRef`
734
+ * after a re-splice (identity-tracked, so a wrapper is never wrapped again).
749
735
  * tool-disclosure and listings only READ it (`harnessTools` is a derived list; placeholders never enter here). */
750
736
  tools: AgentTool[];
751
737
  /** Name→effect map for every tool this task can call (design/44 §3). Used by the abort-path orphan
@@ -962,7 +948,7 @@ export interface Prepared {
962
948
  * ANNOUNCED model catalog the same way (advanced only when a committed frame carried one), so a
963
949
  * catalog change across a suspend is re-announced as a "Models available…" drift line.
964
950
  * `mcpFailed` / `advisories` are the once-per-SESSION arms of the same snapshot (see
965
- * prepare-announce-once.ts): seeded at prepare from the branch's record, advanced by the run loop
951
+ * announce-once-ledger.ts): seeded at prepare from the branch's record, advanced by the run loop
966
952
  * on intact delivery of the tools_delta `failedServers` arm / the mcp_dropped_tools frame. */
967
953
  announcedListingsRef: AnnouncedListingSnapshotInput;
968
954
  /** env-tail migration (#254 shape) — the git-status frame lane's run-local state: this leg's
@@ -1294,6 +1280,25 @@ export interface InheritedGate {
1294
1280
  hookEnv?: unknown;
1295
1281
  }>;
1296
1282
  }
1283
+ /** The runtime feature flags the stable prompt's harness-context block gates its sections on
1284
+ * (design/64 §6.3: claim only what the task has). Spread into the assembly's StablePromptContext
1285
+ * by the prompt-assembly phase, which re-derives the two orchestration flags against the post-exclusion roster. */
1286
+ export interface PromptFeatureFlags {
1287
+ policyEnabled: boolean;
1288
+ hooksEnabled: boolean;
1289
+ isolationEnabled: boolean;
1290
+ reminderMark: string;
1291
+ readFaceOpen: boolean;
1292
+ orchestrationEnabled: boolean;
1293
+ orchestrationDeferred: boolean;
1294
+ promptProfile: "simple" | "classic";
1295
+ fableMitigations: boolean;
1296
+ goalEnabled: boolean;
1297
+ awarenessEnabled: boolean;
1298
+ worktreeIsolated: boolean;
1299
+ withinTaskCompactionEnabled: boolean;
1300
+ isSubagent: boolean;
1301
+ }
1297
1302
  /**
1298
1303
  * design/78 Slice-1 (MAJOR-3 wiring): a TRUSTED, run-scoped internal channel into {@link prepareTask}, set
1299
1304
  * ONLY by a trusted CORE caller (`runRepairLoop` via the Runner's internal `runTaskStream` arg) — NEVER from
@@ -1876,3 +1881,92 @@ export interface ResolvedWorkspace {
1876
1881
  * which `cwd` is a host path a consumer may diff / merge / remove. */
1877
1882
  remote: boolean;
1878
1883
  }
1884
+ /**
1885
+ * #457 ② — the resolver's answer, with the WITHHELD cause beside the value.
1886
+ *
1887
+ * The old signature returned a bare `unknown`, which collapsed three different worlds into one
1888
+ * `undefined`: "this tool declares no preview at all", "the projection threw / could not be
1889
+ * serialized", and "the projection declined to speak for these args". The middle one is the case an
1890
+ * approval surface must be told about — a person is being asked to approve something whose display
1891
+ * projection could NOT be produced — and it was indistinguishable from the other two.
1892
+ *
1893
+ * `withheld` names the cause; it is ORTHOGONAL to `preview` presence, deliberately: the oversize arm
1894
+ * still ships its truncation stub (a surface that renders it tells the person the size, which is more
1895
+ * useful than nothing), and the stub alone never carried the fact that the real projection is missing.
1896
+ */
1897
+ export interface ResolvedApprovalPreview {
1898
+ /** The display projection, when one could be produced (or the oversize truncation stub). */
1899
+ preview?: unknown;
1900
+ /** Present ⇔ the tool DECLARED a preview and the gate could not present a usable one:
1901
+ * `"oversize"` — produced but past the 16KiB display bound (the stub rides `preview`);
1902
+ * `"unavailable"` — the projection threw, or its value has no JSON serialization.
1903
+ * ABSENT covers both "no preview declared" and "the projection returned nothing to show" — neither
1904
+ * is a withholding, and claiming one would make every preview-less tool look suppressed. */
1905
+ withheld?: "oversize" | "unavailable";
1906
+ }
1907
+ /** The in-stream ask lane: the six closures the park closure and the gate station consume. Built as ONE value so
1908
+ * "the lane exists" is one fact, not six independently-undefined seats. */
1909
+ export interface AskLane {
1910
+ /** The shared run+call signal composition, bound to this task's run signal once. */
1911
+ composedCallSignal: (callSignal: AbortSignal | undefined) => AbortSignal;
1912
+ /** The abort-bound policy check, or undefined when no policy is wired (→ the gate reads allow). */
1913
+ adjudicate: ((req: ToolCallRequest, callSignal?: AbortSignal) => Promise<PermissionResult>) | undefined;
1914
+ /** The approval display projection over the live roster (alias-aware; oversize/unavailable named). */
1915
+ approvalPreviewOf: (toolName: string, args: unknown) => ResolvedApprovalPreview;
1916
+ /** THE synchronous ask mint: resolves a surviving `ask` through the live seat, books the ledger. */
1917
+ resolveAskBound: (decision: PermissionResult, req: ToolCallRequest, callSignal?: AbortSignal) => Promise<ResolvedAsk>;
1918
+ /** The pre-wrapped deny observer, or undefined when the deployment wired no `permissionDenied` hook. */
1919
+ notifyPermissionDenied: ((payload: PermissionDeniedPayload) => Promise<void>) | undefined;
1920
+ /** The deployment-facing half of a crashing PreToolUse hook (swallow-guarded). */
1921
+ notifyHookError: (err: unknown) => void;
1922
+ }
1923
+ /** How the put-commit + put-failure saga ended (see `commitSuspendSaga` inside the phase for each arm's meaning). */
1924
+ export type SuspendSagaOutcome = {
1925
+ tag: "committed";
1926
+ } | {
1927
+ tag: "cut";
1928
+ } | {
1929
+ tag: "absent";
1930
+ reason?: string;
1931
+ } | {
1932
+ tag: "unknown";
1933
+ reason: string;
1934
+ } | {
1935
+ tag: "compensation_failed";
1936
+ reason: string;
1937
+ };
1938
+ /** The durable settlement machinery the boundary parks and the park closure consume. Built as ONE value so "the saga
1939
+ * exists" is one fact, not eight independently-undefined seats. */
1940
+ export interface SuspendSaga {
1941
+ /** THIS leg's spend so far in micro-USD (own + nested delta), read at mint time. */
1942
+ inFlightSpendMicroUsd: () => number;
1943
+ /** The §4.bis correctness-state whitelist, serialized from the live values at suspend. */
1944
+ serializeCheckpointState: (workspaceHandle: WorkspaceHandle | undefined, parkedSpendMicroUsd?: number) => SerializedCheckpointState;
1945
+ /** The shared paused-VM compensation, bound to this task's failure ledger and error face. */
1946
+ compensatePausedVM: (remoteEnv: RemoteExecutionEnv, snapshotId: SnapshotId) => Promise<{
1947
+ ok: true;
1948
+ } | {
1949
+ ok: false;
1950
+ reason: string;
1951
+ }>;
1952
+ /** The put-commit + put-failure split — the point of no return, tagged. */
1953
+ commitSuspendSaga: (token: CheckpointToken, cp: Checkpoint, remoteEnv: RemoteExecutionEnv | undefined, remoteHandle: WorkspaceHandle | undefined, cutSignal?: AbortSignal) => Promise<SuspendSagaOutcome>;
1954
+ /** The shared suspend-loop-cap guard (safety state first, diagnostic second). */
1955
+ suspendLoopCapHit: (count: number, cap: number | undefined, detail: string) => boolean;
1956
+ /** The `suspendVM` target by CAPABILITY, or undefined. */
1957
+ suspendableEnv: RemoteExecutionEnv | undefined;
1958
+ /** The park-only remote target (non-suspendable, externally durable), or undefined. */
1959
+ parkOnlyRemoteEnv: RemoteExecutionEnv | undefined;
1960
+ /** The park-only identity capture (snapshotId stripped, `restoreMode: "park_only"`). */
1961
+ parkOnlyHandle: (env: RemoteExecutionEnv) => WorkspaceHandle;
1962
+ }
1963
+ /** The park closure and the content-ask routing closure the gate station hands to `runToolGate`. Built as ONE value so
1964
+ * "the park lane's closures exist" is one fact; `suspendAsk` stays optional INSIDE it because the park predicate (a store,
1965
+ * and something to park for) is a narrower condition than the gate machinery's activation. */
1966
+ export interface ParkAsk {
1967
+ /** design/174 — the gate's content-ask routing closure (total by construction; answers `unavailable` with no live face). */
1968
+ resolveContentAsk: NonNullable<ToolGateInput["resolveContentAsk"]>;
1969
+ /** design/45 — the durable park closure (twelve positional seats then the structural carry), or undefined when the park
1970
+ * lane is not armed (the gate then keeps the synchronous onAsk path). */
1971
+ suspendAsk: ToolGateInput["suspendAsk"];
1972
+ }
@@ -148,7 +148,7 @@ export declare function judgeInheritedClassifier<A extends InheritedAsk>(opts: {
148
148
  subject: "this call" | "the approved edit";
149
149
  }): Promise<InheritedClassifierJudgment<A>>;
150
150
  /** The resolver's answer as the settlement reads it. */
151
- export type FallbackResolution = Pick<ResolvedAsk, "action" | "resolution" | "approverUnavailable">;
151
+ export type FallbackResolution = Pick<ResolvedAsk, "action" | "settlement" | "approverUnavailable">;
152
152
  /** A deny nobody made, at a wrapper arm: no approver wired, or a blanket `onAsk:"allow"` refused as
153
153
  * no approver. `approver_unavailable` is NOT headless here — it floats to the main gate, which owns
154
154
  * that arm (a park, or the fail-closed refusal). */
@@ -173,3 +173,26 @@ export declare function settleDenialLimitFallback(opts: {
173
173
  toolName: string;
174
174
  toolCallId: string;
175
175
  }): void;
176
+ /**
177
+ * design/384 slice 1 — the observer `resolveAskBound` hands `resolveAsk` for a DETACHED approver's
178
+ * settlement (the race arm released the wait; the approver's promise lives on):
179
+ * · an approval the released wait never consumed becomes the `task.late_approval` notice a host
180
+ * can forward on its own wire (the wire-facing half of "release, not a verdict" — the notice
181
+ * asserts non-consumption only, never an arrival order the resolver cannot prove);
182
+ * · an unconsumed rejection goes to the deployment's error face (`onError(phase:"hook")`) — a
183
+ * callback failure the engine no longer awaits is still a callback failure, never silence.
184
+ */
185
+ export declare function lateAskSettlementObserver(args: {
186
+ toolName: string;
187
+ toolCallId: string;
188
+ sessionId: string;
189
+ runId: string;
190
+ taskId?: string;
191
+ onNotice: import("../types.js").RunnerDeps["onNotice"];
192
+ onError: import("../types.js").RunnerDeps["onError"];
193
+ }): (late: {
194
+ kind: "approve";
195
+ } | {
196
+ kind: "error";
197
+ error: unknown;
198
+ }) => void;
@@ -53,7 +53,7 @@ export function createDenialLimitStop(opts) {
53
53
  };
54
54
  return { gateStopRef, stopForDenialLimit };
55
55
  }
56
- const INHERITED_STATION_FACTS = { contentQuestion: false, markedUnresolvable: false, org: undefined };
56
+ const INHERITED_STATION_FACTS = { contentQuestion: false, markedUnresolvable: false, org: undefined, tightened: undefined };
57
57
  function fallbackCarry(fallback, liveApprover, tracker) {
58
58
  if (fallback === undefined)
59
59
  return {};
@@ -97,10 +97,10 @@ export async function judgeInheritedClassifier(opts) {
97
97
  return { kind: "resolve", ask: minted, fallback, mintedHere: true, origin: askOriginOf(minted, INHERITED_STATION_FACTS) };
98
98
  }
99
99
  export function headlessDenyAtFold(r) {
100
- return r.resolution === "no_approver" || r.resolution === "blanket_allow_refused";
100
+ return r.settlement?.kind === "no_approver" || r.settlement?.kind === "blanket_allow_refused";
101
101
  }
102
102
  export function headlessDenyAtRecheck(r) {
103
- return headlessDenyAtFold(r) || r.resolution === "approver_unavailable" || r.approverUnavailable === true;
103
+ return headlessDenyAtFold(r) || r.settlement?.kind === "approver_unavailable" || r.approverUnavailable === true;
104
104
  }
105
105
  export function settleDenialLimitFallback(opts) {
106
106
  const { fallback, resolved } = opts;
@@ -114,3 +114,24 @@ export function settleDenialLimitFallback(opts) {
114
114
  opts.stop({ toolName: opts.toolName, toolCallId: opts.toolCallId, fallback });
115
115
  }
116
116
  }
117
+ export function lateAskSettlementObserver(args) {
118
+ return (late) => {
119
+ if (late.kind === "approve") {
120
+ deliverEngineNotice(args.onNotice, {
121
+ code: "task.late_approval",
122
+ message: `an approval for "${args.toolName}" was not consumed: a run or turn interrupt released the ask wait, ` +
123
+ `so the tool did NOT run and the approval was not honored (an unconsumed answer is the approver ` +
124
+ `releasing its wait, never a verdict).`,
125
+ detail: {
126
+ toolName: args.toolName,
127
+ toolCallId: args.toolCallId,
128
+ sessionId: args.sessionId,
129
+ runId: args.runId,
130
+ ...(args.taskId !== undefined ? { taskId: args.taskId } : {}),
131
+ },
132
+ });
133
+ return;
134
+ }
135
+ args.onError?.(late.error, { phase: "hook", sessionId: args.sessionId });
136
+ };
137
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The ONE mint of a {@link GateOutcome} — the record of what a tool-gate pass decided, what ask it settled
3
+ * and who asked. The gate accumulates two facts on its way to an exit (WHO refused, written once at the
4
+ * refusing site; WHAT was settled, written at the resolution site) and hands them here; this module
5
+ * assembles the record, runs the invariant screen, and refuses a record the engine composed wrong. Every
6
+ * projection of the outcome (the deny observer's payload, the call's `tool_end` frame, the engine's
7
+ * per-call sideband) is the object minted here, never a re-derivation.
8
+ *
9
+ * A machinery module on the prepare path's first stratum: it reaches down for the vocabulary only.
10
+ */
11
+ import { type DeniedBy, type GateOutcome, type Settlement } from "../gate-outcome.js";
12
+ import type { AskOrigin } from "../ask-origin.js";
13
+ import { type PermissionResult, type ResolvedAsk } from "../tool-policy.js";
14
+ /** The ask a gate pass settled, as the gate captured it at its resolution site: the settlement the
15
+ * resolver composed and the origin word the ask carried. Both or neither (invariant I1). */
16
+ export interface SettledAsk {
17
+ settlement: Settlement;
18
+ origin: AskOrigin;
19
+ }
20
+ /** The facts a gate pass hands the mint. `deniedBy` is present exactly when the pass refused; `settled`
21
+ * exactly when the pass consumed an ask's settlement (a person's yes that was later vetoed included). */
22
+ export interface GateExitFacts {
23
+ deniedBy?: DeniedBy;
24
+ settled?: SettledAsk;
25
+ }
26
+ /**
27
+ * Mint the outcome. A defect here is an ENGINE defect (a refusing site that did not say who refused, a
28
+ * settlement without its origin, a person's yes beside a non-veto layer's deny) and throws: the gate's
29
+ * own composition is not a caller input, so there is no fail-closed "deny" to fall back to that would be
30
+ * more honest than the fault.
31
+ */
32
+ export declare function mintGateOutcome(facts: GateExitFacts): GateOutcome;
33
+ /** A direct refusal by one layer, with no ask settled — the five short-circuit exits (a hook that threw or
34
+ * never answered, the hook's own deny, the compliance lock, the plan-mode block). */
35
+ export declare function directDeny(deniedBy: Exclude<DeniedBy, "ask_resolution">): GateOutcome;
36
+ /**
37
+ * The gate's settlement LEDGER for one pass — the one place "which ask did this pass consume" is written,
38
+ * with ONE rule for every attestation read (the main adjudication, the approval-edit re-check, the gate's
39
+ * own ask resolutions), so the three sites cannot disagree about what an attested decision means:
40
+ * - an engine attestation on a decision object (the approval factory's wait ended inside a policy
41
+ * `check()`; an inherited lane resolved an ancestor's ask) is a settled ask of THIS call — read off the
42
+ * identity-keyed seat, never off a caller-composable member;
43
+ * - a deny carrying a REFUSAL attestation is the ask's own resolution (`ask_resolution`); a deny carrying a
44
+ * person's YES is a later layer's VETO (the yes stays on the settlement, the layer is the disposition —
45
+ * invariant I3);
46
+ * - the LATEST resolution wins (a re-check refusal replaces the first approval; a re-approval renews it);
47
+ * - a SURVIVING ask supersedes any settlement read off an earlier decision (a tighten minted a new
48
+ * question; whoever resolves it settles it afresh).
49
+ * For a resolver's object the attestation outranks the exposed member; the member is the fallback for a
50
+ * resolver seat that composes without attesting (a hand-built resolver; the ask lane's own intercept
51
+ * deny). TRUST BOUNDARY, stated: `ToolGateInput.resolveAsk` is wired by prepare from the engine's own
52
+ * resolver — a wrapper that spreads a resolved object (dropping the identity-keyed attestation) and
53
+ * REPLACES the member is inside the engine's trust line, not a deployment's; making the member
54
+ * attestation-only is the follow-up that migrates the ask lane's composition and the test resolvers
55
+ * onto an engine-owned mint.
56
+ */
57
+ export interface SettlementLedger {
58
+ /** The ask this pass consumed, or undefined when no ask was settled (a direct deny, a plain allow). */
59
+ readonly settled: SettledAsk | undefined;
60
+ /** An engine-composed settlement the gate itself minted (the unshown content ask's refusal). */
61
+ set(settled: SettledAsk): void;
62
+ /** Record an ask's resolution: the resolver's settlement beside the ask's own origin word. */
63
+ fromResolved(resolved: ResolvedAsk, asked: PermissionResult, toolName: string): void;
64
+ /** Read an adjudication's attestation. For a deny, returns the refusing layer word (`ask_resolution`
65
+ * for an attested refusal, `policy` otherwise — a veto when a yes rides the settlement); undefined
66
+ * for an allow or an ask. */
67
+ fromDecision(decision: PermissionResult): DeniedBy | undefined;
68
+ /** A surviving ask supersedes whatever an earlier decision settled. */
69
+ supersede(): void;
70
+ }
71
+ export declare function createSettlementLedger(call: {
72
+ toolCallId: string;
73
+ toolName: string;
74
+ }): SettlementLedger;
@@ -0,0 +1,55 @@
1
+ import { screenGateOutcome, SETTLEMENT_IS_REFUSAL } from "../gate-outcome.js";
2
+ import { engineSettlementOf } from "../tool-policy.js";
3
+ export function mintGateOutcome(facts) {
4
+ const settlement = facts.settled !== undefined ? Object.freeze({ ...facts.settled.settlement, who: Object.freeze({ ...facts.settled.settlement.who }) }) : undefined;
5
+ const outcome = Object.freeze({
6
+ disposition: Object.freeze(facts.deniedBy !== undefined ? { kind: "denied", deniedBy: facts.deniedBy } : { kind: "allowed" }),
7
+ ...(facts.settled !== undefined ? { settlement: settlement, origin: facts.settled.origin } : {}),
8
+ });
9
+ const defects = screenGateOutcome(outcome);
10
+ if (defects.length > 0)
11
+ throw new Error(`the tool gate composed an outcome that violates its own record: ${defects.join("; ")}`);
12
+ return outcome;
13
+ }
14
+ export function directDeny(deniedBy) {
15
+ return mintGateOutcome({ deniedBy });
16
+ }
17
+ export function createSettlementLedger(call) {
18
+ let settled;
19
+ return {
20
+ get settled() {
21
+ return settled;
22
+ },
23
+ set(next) {
24
+ settled = next;
25
+ },
26
+ fromResolved(resolved, asked, toolName) {
27
+ if (asked.action !== "ask" || asked.origin === undefined) {
28
+ if (resolved.settlement === undefined)
29
+ return;
30
+ throw new Error(`an ask for "${toolName}" was resolved without an origin word — the origin stamp precedes every resolution`);
31
+ }
32
+ const attested = engineSettlementOf(resolved, call);
33
+ if (attested !== undefined) {
34
+ settled = { settlement: attested.settlement, origin: asked.origin };
35
+ return;
36
+ }
37
+ if (resolved.settlement === undefined)
38
+ return;
39
+ settled = { settlement: resolved.settlement, origin: asked.origin };
40
+ },
41
+ fromDecision(decision) {
42
+ if (decision.action === "ask")
43
+ return undefined;
44
+ const attested = engineSettlementOf(decision, call);
45
+ if (attested !== undefined)
46
+ settled = { settlement: attested.settlement, origin: attested.origin };
47
+ if (decision.action !== "deny")
48
+ return undefined;
49
+ return attested !== undefined && SETTLEMENT_IS_REFUSAL[attested.settlement.kind] ? "ask_resolution" : "policy";
50
+ },
51
+ supersede() {
52
+ settled = undefined;
53
+ },
54
+ };
55
+ }
@@ -0,0 +1,46 @@
1
+ import { type OnAsk, type PermissionResult, type ResolvedAsk, type ToolCallRequest } from "../tool-policy.js";
2
+ /** RB-40 byte-shape key, throw-proofed: JSON.stringify throws on a cyclic value (a deployment
3
+ * policy's rewrite object carries no acyclicity guarantee) — `undefined` here means "no comparable
4
+ * shape", which every consumer treats as a mismatch (skip the record / skip the reuse ⇒ the child
5
+ * gate just re-asks — the safe direction, never a gate failure). */
6
+ export declare function askGrantShapeOf(args: unknown): string | undefined;
7
+ /**
8
+ * RB-40 — duplicate-frame collapse (single-Runner embedded shape), the CONSUME half: an inherited
9
+ * layer already resolved THIS call as a clean allow at the SAME approver function, on EXACTLY the
10
+ * byte-shape this caller-slot ask now presents. The human's consent covers this consultation
11
+ * verbatim — asking again is the 2-frame UX debt, not a second authority. Guards, all mismatches
12
+ * re-ask (⇒ `undefined`):
13
+ * · SOURCE (review F-2): only a caller-slot POLICY ask may reuse — a safety tighten
14
+ * (egress/irreversible/shellGate stamp `decisionReason:"safety"`), a hook-promoted ask
15
+ * ("hook"), or a classifier ask carries a DIFFERENT authority/context and keeps its frame.
16
+ * · IDENTITY: the approver FUNCTION reference (never a name) — compared through
17
+ * `askApproverIdentity`, not by raw reference (ruled 2026-08-04): a delegated child's approver
18
+ * is the ancestor's own, wrapped with delegation provenance — a DIFFERENT function object, the
19
+ * SAME operator. Bare `===` therefore stopped collapsing the frame the moment the ask seam
20
+ * started forwarding approvers, and one tool call presented the same person the same card
21
+ * twice. The wrapper adds metadata and delegates every decision, so it grants no authority of
22
+ * its own; identity here only answers "same consultation".
23
+ * · SHAPE: byte-equality against the [1462] r5 presented SNAPSHOT; reuse re-emits that snapshot
24
+ * as the executed shape via `presentedInput` (review F-1) — the same schema-revalidated binding
25
+ * as a main-gate approval (shown == executed, by construction).
26
+ * · LEDGER (review F-3): the ONE real consultation (timed on the wrapper arm) is recorded here —
27
+ * one frame happened, one frame is booked.
28
+ * The grant lives only within one gate evaluation (consumed on reuse, swept at the call site) — a
29
+ * REOPENED checkpoint's re-adjudication never sees a stale grant.
30
+ */
31
+ export declare function consumeInheritedAskGrant(grants: Map<string, {
32
+ approver: unknown;
33
+ argsJson: string;
34
+ presented: unknown;
35
+ waitMs: number;
36
+ }>, onAsk: OnAsk | undefined, humanReviewRef: {
37
+ count: number;
38
+ totalWaitMs: number;
39
+ gates: Array<{
40
+ kind: string;
41
+ waitMs: number;
42
+ decision?: string;
43
+ toolName?: string;
44
+ toolArg?: string;
45
+ }>;
46
+ }, decision: PermissionResult, req: ToolCallRequest): ResolvedAsk | undefined;
@@ -0,0 +1,29 @@
1
+ import { primaryActivityArg } from "../arg-summary.js";
2
+ import { askApproverIdentity } from "../tool-policy.js";
3
+ export function askGrantShapeOf(args) {
4
+ try {
5
+ return JSON.stringify(args ?? null);
6
+ }
7
+ catch {
8
+ return undefined;
9
+ }
10
+ }
11
+ export function consumeInheritedAskGrant(grants, onAsk, humanReviewRef, decision, req) {
12
+ if (decision.decisionReason !== undefined && decision.decisionReason !== "rule")
13
+ return undefined;
14
+ const grant = grants.get(req.toolCallId);
15
+ if (grant === undefined || askApproverIdentity(grant.approver) !== askApproverIdentity(onAsk) || grant.argsJson !== askGrantShapeOf(req.args))
16
+ return undefined;
17
+ grants.delete(req.toolCallId);
18
+ humanReviewRef.count += 1;
19
+ humanReviewRef.totalWaitMs += grant.waitMs;
20
+ const toolArg = primaryActivityArg(req.args);
21
+ humanReviewRef.gates.push({
22
+ kind: "human",
23
+ waitMs: grant.waitMs,
24
+ decision: "allow",
25
+ toolName: req.toolName,
26
+ ...(toolArg !== undefined ? { toolArg } : {}),
27
+ });
28
+ return { action: "allow", presentedInput: grant.presented };
29
+ }