@sema-agent/core 7.8.0 → 7.9.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 (103) hide show
  1. package/CHANGELOG.md +75 -1
  2. package/dist/agents/subagent.d.ts +1 -1
  3. package/dist/core/ask-origin.d.ts +16 -1
  4. package/dist/core/ask-origin.js +5 -1
  5. package/dist/core/checkpoint-store.d.ts +36 -35
  6. package/dist/core/effective-path-target.d.ts +43 -0
  7. package/dist/core/effective-path-target.js +56 -0
  8. package/dist/core/engine-notice.d.ts +8 -0
  9. package/dist/core/fs-write-gate-policy.js +2 -1
  10. package/dist/core/gate-lanes.d.ts +1 -0
  11. package/dist/core/gate-lanes.js +60 -28
  12. package/dist/core/gate-outcome.d.ts +8 -4
  13. package/dist/core/gate-outcome.js +2 -1
  14. package/dist/core/governance-codes.d.ts +1 -1
  15. package/dist/core/governance-codes.js +2 -0
  16. package/dist/core/hooks.d.ts +66 -32
  17. package/dist/core/mcp-injection-drop.d.ts +74 -0
  18. package/dist/core/mcp-injection-drop.js +27 -0
  19. package/dist/core/permission-rule-consent.d.ts +29 -36
  20. package/dist/core/permission-rule-consent.js +110 -60
  21. package/dist/core/permission-rule-model.d.ts +311 -96
  22. package/dist/core/permission-rule-model.js +309 -50
  23. package/dist/core/permission-rule-org.d.ts +19 -12
  24. package/dist/core/permission-rule-org.js +13 -16
  25. package/dist/core/permission-rule-provider.d.ts +10 -6
  26. package/dist/core/permission-rule-provider.js +12 -8
  27. package/dist/core/permission-rule-session.d.ts +7 -6
  28. package/dist/core/permission-rule-session.js +29 -10
  29. package/dist/core/permission-rule-store.d.ts +40 -18
  30. package/dist/core/permission-rule-store.js +68 -42
  31. package/dist/core/permission-rule-sync.d.ts +9 -3
  32. package/dist/core/permission-rule-sync.js +29 -25
  33. package/dist/core/permission-rule-syntax.d.ts +30 -0
  34. package/dist/core/permission-rule-syntax.js +44 -0
  35. package/dist/core/permission-rules.d.ts +56 -33
  36. package/dist/core/permission-rules.js +88 -55
  37. package/dist/core/persisted-rule-arms.d.ts +61 -0
  38. package/dist/core/persisted-rule-arms.js +50 -0
  39. package/dist/core/runner/active-skill-scope.js +2 -1
  40. package/dist/core/runner/contracts.d.ts +21 -2
  41. package/dist/core/runner/permission-rule-lanes.d.ts +42 -27
  42. package/dist/core/runner/permission-rule-lanes.js +25 -21
  43. package/dist/core/runner/prepare-gate-stations.js +1 -1
  44. package/dist/core/runner/prepare-safety-scan.js +8 -2
  45. package/dist/core/runner/prepare-task.js +1 -1
  46. package/dist/core/runner/resume-admission.d.ts +53 -0
  47. package/dist/core/runner/resume-admission.js +83 -0
  48. package/dist/core/runner/resume-apply.d.ts +50 -0
  49. package/dist/core/runner/resume-apply.js +184 -0
  50. package/dist/core/runner/resume-checkpoint-screen.d.ts +18 -0
  51. package/dist/core/runner/resume-checkpoint-screen.js +108 -0
  52. package/dist/core/runner/resume-claim.d.ts +32 -0
  53. package/dist/core/runner/resume-claim.js +27 -0
  54. package/dist/core/runner/resume-internals-and-config.d.ts +33 -0
  55. package/dist/core/runner/resume-internals-and-config.js +50 -0
  56. package/dist/core/runner/resume-policy-outcome.d.ts +31 -0
  57. package/dist/core/runner/resume-policy-outcome.js +127 -0
  58. package/dist/core/runner/resume-preflight.d.ts +40 -0
  59. package/dist/core/runner/resume-preflight.js +122 -0
  60. package/dist/core/runner/resume-review-outcome.d.ts +30 -0
  61. package/dist/core/runner/resume-review-outcome.js +88 -0
  62. package/dist/core/runner/run-harness-handlers.js +1 -1
  63. package/dist/core/runner/runtask.d.ts +20 -0
  64. package/dist/core/runner/runtask.js +85 -734
  65. package/dist/core/runner/session-rule-policy.js +2 -1
  66. package/dist/core/runner/tool-end-body.d.ts +12 -5
  67. package/dist/core/runner/tool-end-body.js +5 -5
  68. package/dist/core/runner/tool-face-overlay.js +22 -3
  69. package/dist/core/runner/tool-output-projection.d.ts +9 -6
  70. package/dist/core/runner/tool-output-projection.js +1 -18
  71. package/dist/core/runner/turn-attachments.d.ts +2 -2
  72. package/dist/core/sensitive-path-policy.js +5 -3
  73. package/dist/core/shell-lexer.d.ts +47 -0
  74. package/dist/core/shell-lexer.js +478 -0
  75. package/dist/core/shell-scan.d.ts +60 -0
  76. package/dist/core/shell-scan.js +183 -0
  77. package/dist/core/shell-wrapper-table.d.ts +297 -0
  78. package/dist/core/shell-wrapper-table.js +58 -0
  79. package/dist/core/store-contracts/permission-rule-sync-contract.js +29 -10
  80. package/dist/core/tool-catalog-entries.js +6 -6
  81. package/dist/core/tool-face.d.ts +80 -4
  82. package/dist/core/tool-face.js +10 -0
  83. package/dist/core/tool-policy.d.ts +1 -6
  84. package/dist/core/tool-registry.d.ts +8 -11
  85. package/dist/core/tool-registry.js +5 -2
  86. package/dist/core/tool-roster.d.ts +20 -2
  87. package/dist/core/tool-roster.js +36 -8
  88. package/dist/index.d.ts +7 -4
  89. package/dist/index.js +6 -3
  90. package/dist/stores/file/checkpoint-store.js +1 -1
  91. package/dist/stores/file/permission-rule-store.d.ts +28 -23
  92. package/dist/stores/file/permission-rule-store.js +64 -16
  93. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
  94. package/dist/tools/fs/fs-pdf.d.ts +1 -1
  95. package/dist/tools/fs/fs-search-tools.d.ts +3 -2
  96. package/dist/tools/fs/fs-search-tools.js +17 -9
  97. package/dist/tools/fs/index.js +1 -1
  98. package/dist/tools/fs/notebook.d.ts +1 -1
  99. package/dist/tools/fs/safety.d.ts +1 -1
  100. package/dist/tools/fs/search.d.ts +0 -8
  101. package/dist/tools/fs/search.js +0 -23
  102. package/package.json +1 -1
  103. package/test/export-surface.snapshot.json +99 -11
@@ -9,13 +9,15 @@
9
9
  *
10
10
  * ## One read per adjudicated call
11
11
  *
12
- * The gate consults the org lane (`orgRules.adjudicate`) on every decision and the personal lane
13
- * (`persistedRules.admits`) only on a surviving ask, in that order. The org lane's read is therefore
12
+ * The gate consults the org lane (`orgRules.adjudicate`) and then the personal lane
13
+ * (`persistedRules.adjudicate`) on every decision, in that order (the personal lane's deny/ask rows must
14
+ * be able to tighten an allow, so it can no longer wait for a surviving ask). The org lane's read is
14
15
  * HANDED to the personal lane by `toolCallId` (taken once, then forgotten), so a governed deployment pays
15
16
  * one store query — one org-source fetch — per call, exactly as it did with two seams. An entry the
16
- * personal lane never claims (an allow, a deny) is evicted first-in-first-out past a small bound; an
17
- * evicted or never-handed entry simply costs the personal lane its own read. The handoff is a cost
18
- * device, not a semantic one: both lanes judge one snapshot, which is the honest shape for one decision.
17
+ * personal lane never claims (a call on a tool it does not speak for) is evicted first-in-first-out past
18
+ * a small bound; an evicted or never-handed entry simply costs the personal lane its own read. The
19
+ * handoff is a cost device, not a semantic one: both lanes judge one snapshot, which is the honest shape
20
+ * for one decision.
19
21
  *
20
22
  * ## Where the lanes do NOT go
21
23
  *
@@ -24,15 +26,13 @@
24
26
  * silence the ungated-write warning for deployments that wired no policy at all.
25
27
  */
26
28
  import type { AskRuleEvidence, ToolCallRequest } from "../tool-policy.js";
27
- import { persistedRuleMandateOf, type OrgGateVerdict, type PersistedRuleAnswer, type PersistedRuleHit } from "../hooks.js";
28
- import type { PersistedAllowRule, RuleOffer, SegmentCoverage } from "../permission-rule-model.js";
29
+ import { persistedRuleMandateOf, type OrgGateVerdict, type PersistedRuleAnswer, type PersistedRuleHit, type PersistedRuleUnreadable } from "../hooks.js";
30
+ import type { PersistedRule, PersistedRuleVerdict, RuleOffer, SegmentCoverage } from "../permission-rule-model.js";
29
31
  import { type OrgRuleResolution } from "../permission-rule-org.js";
30
32
  import type { PermissionRuleStoreProvider } from "../permission-rule-provider.js";
31
- /** The one SHELL tool the persisted-rule lane speaks for. */
33
+ /** The one SHELL tool the persisted-rule lane's COMMAND grammar speaks for (the rule-offer face mints for
34
+ * it alone). The PATH grammar's tools are derived from the catalog (`ruleToolGrammarOf`). */
32
35
  export declare const PERSISTED_RULE_TOOL = "Bash";
33
- /** design/382 §2.5 — the READ tool the directory-rule family speaks for (its one other admission
34
- * point, the cd-segment notch, lives inside the model's own conjunction/coverage arms). */
35
- export declare const DIRECTORY_RULE_TOOL = "Read";
36
36
  /**
37
37
  * design/252 G-2 — the evidence an INHERITED-lane ask carries.
38
38
  *
@@ -67,28 +67,43 @@ export declare function orgRevisionEvidenceOf(resolution: OrgRuleResolution, onD
67
67
  revision?: number;
68
68
  };
69
69
  /**
70
- * design/252 G-2 / design/375 — project the admitting COVERAGE SET onto the gate's rule-lane answer:
71
- * each member's canonical text plus its own add DOTS. A single rule admitting the whole command is a
72
- * one-element set; a compound admitted by the per-segment conjunction is the set of covering rules in
73
- * segment order (the model's own reporting-order contract).
70
+ * design/252 G-2 / design/375 — project the model's verdict onto the gate's rule-lane answer: the
71
+ * behavior that spoke, and each member's canonical text plus its own add DOTS. For an allow the set is
72
+ * the admitting COVERAGE SET (a single rule admitting the whole command is a one-element set; a compound
73
+ * admitted by the per-segment conjunction is the set of covering rules in segment order the model's
74
+ * own reporting-order contract); for a deny/ask it is the one reaching rule the precedence walk chose.
74
75
  *
75
76
  * `adds` on a listed rule are the LIVE adds — the store's read already dropped tombstoned dots — so
76
- * this is the set that actually admitted the call, which is the identity a later audit reconciles a
77
+ * this is the set that actually decided the call, which is the identity a later audit reconciles a
77
78
  * shadowed ask against. The sanitized display text deliberately is not one. Copied rather than
78
79
  * aliased: the evidence must not change under a store that reuses its row objects.
79
80
  */
80
- export declare function persistedRuleHitOf(admitting: readonly PersistedAllowRule[] | undefined): PersistedRuleHit | undefined;
81
+ export declare function persistedRuleHitOf(verdict: PersistedRuleVerdict | undefined): PersistedRuleHit | undefined;
82
+ /** The COMMAND arm's projection of a verdict: a call the lexer could not read against a standing deny/ask
83
+ * row is the lane's UNREADABLE answer — the same fail-closed shape an unreadable store takes (a
84
+ * real-approval ask no automatic lane clears), with the lexer's reason in place of the store's — never a
85
+ * matched ask rule, which would let a blanket `onAsk: "allow"` clear a question nobody can answer. Every
86
+ * other verdict is {@link persistedRuleHitOf}'s hit. */
87
+ export declare function persistedRuleAnswerOf(verdict: PersistedRuleVerdict | undefined): PersistedRuleHit | PersistedRuleUnreadable | undefined;
81
88
  /**
82
- * design/382 §2.5-1 — the persisted-rule lane's READ arm: the call's target path in the SAME lexical
83
- * identity the rule family is defined over — absolute as spelled, relative resolved against the LIVE
84
- * tracked working directory (the very base the Read tool itself resolves against; task root when no
85
- * tracker moved), then normalized lexically, zero IO. One admission predicate
86
- * ({@link directoryRuleAdmits}), the one eligibility predicate for scope/liveness, first table-order hit
87
- * reported. Everything this lane does NOT do is structural: it clears ASKS only — the read fences
88
- * (workspace containment, the sensitive-read deny face) judge inside the tool exactly as they would with
89
- * no rule anywhere, and an unresolvable path is simply no admission (fail toward asking).
89
+ * design/382 §2.5-1, widened to every path-targeting tool — the persisted-rule lane's PATH arm: the call's
90
+ * target path in the SAME lexical identity the rule family is defined over, resolved by the ONE reader every
91
+ * fence and every tool goes through (`effectivePathTargetOf`), so the lane judges the path the tool will
92
+ * really open. The tool's own declaration decides which base a relative spelling takes and what an ABSENT
93
+ * slot means the lane no longer guesses `liveCwd ?? root` for every tool, which was wrong for the two
94
+ * search tools in three separate shapes (see effective-path-target.ts). Still zero IO.
95
+ *
96
+ * TWO readings of the call's path slot, one per direction (design/388 B6/B17): the tightening arms judge
97
+ * the PROTECTIVE target (the object face's declaration, the catalog's for the name as the floor — reading
98
+ * the catalog for an undeclared same-name tool can only tighten a deny); the allow arm judges the DECLARED
99
+ * target (the face's OWN declaration only — a same-name tool that declares no path target is unconfirmable
100
+ * and no grant admits it). The precedence walk, the admission predicates and the Read-only allow are the
101
+ * model's (`adjudicatePersistedPathRules`). An ALLOW here clears ASKS only — the read fences (workspace
102
+ * containment, the sensitive-read deny face) judge inside the tool exactly as they would with no rule
103
+ * anywhere — and an unresolvable path is simply no verdict (fail toward asking on the allow side, no
104
+ * refusal on the tightening side: a rule about a path speaks about no other shape).
90
105
  */
91
- export declare function directoryRuleLaneAnswer(table: readonly PersistedAllowRule[], args: unknown, ctx: {
106
+ export declare function pathRuleLaneAnswer(table: readonly PersistedRule[], req: Pick<ToolCallRequest, "toolName" | "args" | "face">, ctx: {
92
107
  root: string | undefined;
93
108
  sessionId: string | undefined;
94
109
  liveCwd: string | undefined;
@@ -97,7 +112,7 @@ export declare function directoryRuleLaneAnswer(table: readonly PersistedAllowRu
97
112
  * a deployment without one keeps a decision path byte-identical to a build without the feature. */
98
113
  export interface PermissionRuleLanes {
99
114
  personal: {
100
- admits: (req: ToolCallRequest) => Promise<PersistedRuleAnswer>;
115
+ adjudicate: (req: ToolCallRequest) => Promise<PersistedRuleAnswer>;
101
116
  } | undefined;
102
117
  org: {
103
118
  adjudicate: (req: ToolCallRequest) => Promise<OrgGateVerdict>;
@@ -1,8 +1,9 @@
1
1
  import { persistedRuleMandateOf } from "../hooks.js";
2
- import { directoryRuleAdmits, eligiblePersisted, findAdmittingRule, lexicalNormalAbsolutePathOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
2
+ import { adjudicatePersistedPathRules, adjudicatePersistedRules, ruleToolGrammarOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
3
+ import { effectivePathTargetOf } from "../effective-path-target.js";
4
+ import { declaredPathTargetOf, protectivePathTargetOf } from "../tool-registry.js";
3
5
  import { orgRuleVerdictFor } from "../permission-rule-org.js";
4
6
  export const PERSISTED_RULE_TOOL = "Bash";
5
- export const DIRECTORY_RULE_TOOL = "Read";
6
7
  const HANDOFF_BOUND = 256;
7
8
  export function inheritedAskRuleEvidence(provider) {
8
9
  const org = provider?.partitions.org === true ? "not_adjudicated" : "not_wired";
@@ -19,21 +20,23 @@ export function orgRevisionEvidenceOf(resolution, onDefect) {
19
20
  `the adjudication stands, but the ask carries no revision evidence for this call`);
20
21
  return {};
21
22
  }
22
- export function persistedRuleHitOf(admitting) {
23
- return admitting === undefined
23
+ export function persistedRuleHitOf(verdict) {
24
+ return verdict === undefined
24
25
  ? undefined
25
- : { rules: admitting.map((r) => ({ rule: r.rule, dots: r.adds.map((a) => ({ actor: a.dot.actor, counter: a.dot.counter })) })) };
26
+ : { behavior: verdict.behavior, rules: verdict.rules.map((r) => ({ rule: r.rule, dots: r.adds.map((a) => ({ actor: a.dot.actor, counter: a.dot.counter })) })) };
26
27
  }
27
- export function directoryRuleLaneAnswer(table, args, ctx) {
28
- const filePath = args?.file_path;
29
- if (typeof filePath !== "string" || filePath === "")
30
- return undefined;
31
- const spelled = filePath.startsWith("/") ? filePath : `${(ctx.liveCwd ?? ctx.root ?? "").replace(/\/+$/, "")}/${filePath}`;
32
- const target = lexicalNormalAbsolutePathOf(spelled);
33
- if (target === undefined)
28
+ export function persistedRuleAnswerOf(verdict) {
29
+ if (verdict?.unreadable !== undefined)
30
+ return { unreadable: true, reason: verdict.unreadable };
31
+ return persistedRuleHitOf(verdict);
32
+ }
33
+ export function pathRuleLaneAnswer(table, req, ctx) {
34
+ const bases = { ...(ctx.root !== undefined ? { root: ctx.root } : {}), ...(ctx.liveCwd !== undefined ? { cwd: ctx.liveCwd } : {}) };
35
+ const tighten = effectivePathTargetOf(req, protectivePathTargetOf(req), bases);
36
+ const allow = effectivePathTargetOf(req, declaredPathTargetOf(req), bases);
37
+ if (tighten === undefined && allow === undefined)
34
38
  return undefined;
35
- const hit = table.find((r) => eligiblePersisted(r, { tool: DIRECTORY_RULE_TOOL, cwd: ctx.root, sessionId: ctx.sessionId }) && directoryRuleAdmits(r, target));
36
- return hit !== undefined ? persistedRuleHitOf([hit]) : undefined;
39
+ return persistedRuleHitOf(adjudicatePersistedPathRules(table, { tool: req.toolName, cwd: ctx.root, sessionId: ctx.sessionId }, { ...(tighten !== undefined ? { tighten } : {}), ...(allow !== undefined ? { allow } : {}) }, bases));
37
40
  }
38
41
  export function createPermissionRuleLanes(cfg) {
39
42
  const provider = cfg.provider;
@@ -103,10 +106,11 @@ export function createPermissionRuleLanes(cfg) {
103
106
  : undefined;
104
107
  const personal = provider.partitions.durable
105
108
  ? {
106
- admits: async (req) => {
109
+ adjudicate: async (req) => {
107
110
  if (anonymous && !cfg.localOwnerDeclared)
108
111
  return undefined;
109
- if (req.toolName !== PERSISTED_RULE_TOOL && req.toolName !== DIRECTORY_RULE_TOOL)
112
+ const grammar = ruleToolGrammarOf(req.toolName);
113
+ if (grammar === undefined)
110
114
  return undefined;
111
115
  let view;
112
116
  try {
@@ -122,16 +126,16 @@ export function createPermissionRuleLanes(cfg) {
122
126
  }
123
127
  const table = view.rules;
124
128
  const liveCwd = cfg.liveCwd();
125
- if (req.toolName === DIRECTORY_RULE_TOOL) {
126
- return directoryRuleLaneAnswer(table, req.args, { root: cfg.root, sessionId: cfg.sessionId, liveCwd });
129
+ if (grammar === "path") {
130
+ return pathRuleLaneAnswer(table, req, { root: cfg.root, sessionId: cfg.sessionId, liveCwd });
127
131
  }
128
132
  const command = req.args?.command;
129
133
  if (typeof command !== "string")
130
134
  return undefined;
131
135
  const execCwd = liveCwd !== undefined ? { execCwd: liveCwd } : {};
132
- const admitting = findAdmittingRule(table, { tool: req.toolName, command, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd });
133
- if (admitting !== undefined)
134
- return persistedRuleHitOf(admitting);
136
+ const verdict = adjudicatePersistedRules(table, { tool: req.toolName, command, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd });
137
+ if (verdict !== undefined)
138
+ return persistedRuleAnswerOf(verdict);
135
139
  const coverage = segmentCoverageOf(command, { persisted: table }, { tool: req.toolName, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd });
136
140
  return coverage !== undefined ? { segmentCoverage: coverage } : undefined;
137
141
  },
@@ -123,7 +123,7 @@ export function prepareGateStations(input) {
123
123
  ...(permissionRuleLane
124
124
  ? {
125
125
  persistedRules: {
126
- admits: permissionRuleLane.admits,
126
+ adjudicate: permissionRuleLane.adjudicate,
127
127
  onResolved: (info) => emitTrace(deps.tracer, () => ({
128
128
  kind: "permission.persisted_rule_allowed",
129
129
  version: 2,
@@ -1,7 +1,7 @@
1
1
  import { NAMESPACED_NAME_SHAPES } from "../protocol-table.js";
2
- import { handBandEffects } from "../tool-registry.js";
2
+ import { engineCardTypes, handBandEffects } from "../tool-registry.js";
3
3
  import { TOOL_CONTRACT_MAX_CHARS, TOOL_WIRE_NAME_MAX_CHARS } from "../tool-face.js";
4
- import { schemaKeyBoundProblem } from "../tool-roster.js";
4
+ import { callerCardIdOf, offendingResultCard, schemaKeyBoundProblem } from "../tool-roster.js";
5
5
  import { toolFaceProblem } from "./tool-face-overlay.js";
6
6
  import { OFFLOAD_TOOL_NAME } from "../tool-result-store.js";
7
7
  import { PRESENT_PLAN_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME } from "../present-plan-tool.js";
@@ -52,6 +52,12 @@ export function prepareSafetyScan(input) {
52
52
  e.code = "config.tool_face_invalid";
53
53
  throw e;
54
54
  }
55
+ const foreignCard = offendingResultCard(t.renderHints, callerCardIdOf(t), engineCardTypes());
56
+ if (foreignCard !== undefined) {
57
+ const e = new Error(`Tool "${t.name}" declares a result card outside its own namespace: ${JSON.stringify(foreignCard)} (a caller card is "${callerCardIdOf(t)}/<card>"; engine cards and other contracts' namespaces are reserved).`);
58
+ e.code = "config.tool_face_invalid";
59
+ throw e;
60
+ }
55
61
  }
56
62
  if (t.name.includes("__")) {
57
63
  const e = new Error(`Tool name "${t.name}" is invalid: "__" is reserved for the engine's protocol tool namespaces (${NAMESPACED_NAME_SHAPES}) and must not appear in a caller tool name.`);
@@ -686,7 +686,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
686
686
  const preparedHolder = {};
687
687
  const { cacheBreakDetector, cacheFingerprint, promptOverheadTokens, readTaskFile, normalizeAttachmentPath, isDedupStubResult, recentlyReadFiles, onCompactionApplied, detectExternalChanges, listBackgroundTasks, centerCompactionCandidate, effectiveReadFaceObserved, effectiveReadDenyObserved } = prepareTurnWiring({ deps, systemPromptSeat, harnessTools, fpRef, model, epochArtifactDigestForSnapshot, promptProfile, fableMitigations, systemBlocks, thinking, spec, sessionId, turnSnapshotRef, promptManifest, charsPerToken, handsEnabled, executionEnv, attachmentRootCanonical, additionalRootsCanonical, additionalReadRootsCanonical, readDenyMatcher, resolvedReadFace, handsCwdRef, readFileStateForCheckpoint, denyNarrowingPolicy, abortController, hostTaskId, taskScope, buildAssembleInputs, centerAdoptionRef, harnessRef, epochDeclaredSections, carrierReadFace, readDenyAdditionsNormalized, preparedHolder });
688
688
  await onceLedger.settle(session, announcedListingsRef);
689
- const buildPrepared = () => ({ harness, session, sessionId, runId, reminderMark, reminderDisclosureCounts, editedFilesSnapshot, taskRootPath: taskRootFinal, model, thinking, compModel: effectiveCompModel, mcp, ...(a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, gateOutcomes, batchHaltRef, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(fileHistoryBoundary !== undefined ? { fileHistoryBoundary } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), ...(sealReadStateSeat !== undefined ? { sealReadStateSeat } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, pausedRef, suspendProgressRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, gateStopRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolRoster, toolRosterDeltas: rosterSeat, structuredProjector, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, microCompact, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, stopRequestedRef, announcedSnapshotRecovered: onceLedger.recovered, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(projectInstructionContent !== undefined ? { projectInstructionContent } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, hookIdentity, hookTimeoutMs, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
689
+ const buildPrepared = () => ({ harness, session, sessionId, runId, reminderMark, reminderDisclosureCounts, editedFilesSnapshot, taskRootPath: taskRootFinal, model, thinking, compModel: effectiveCompModel, mcp, ...(a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, gateOutcomes, batchHaltRef, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(fileHistoryBoundary !== undefined ? { fileHistoryBoundary } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), ...(sealReadStateSeat !== undefined ? { sealReadStateSeat } : {}), denyNarrowingPolicy, ...(permissionRuleLane !== undefined ? { persistedRuleLane: permissionRuleLane } : {}), ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, pausedRef, suspendProgressRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, gateStopRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolRoster, toolRosterDeltas: rosterSeat, structuredProjector, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, microCompact, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, stopRequestedRef, announcedSnapshotRecovered: onceLedger.recovered, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(projectInstructionContent !== undefined ? { projectInstructionContent } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, hookIdentity, hookTimeoutMs, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
690
690
  rollback.commit();
691
691
  const prepared = buildPrepared();
692
692
  preparedHolder.current = prepared;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * design/393 S3 — the resume ladder's ADMISSION rung (E1) and the wake's shaping (E2), verbatim from
3
+ * `Runner.resumeStream`: the outcome container's domain, the one-read snapshots of the caller's config and
4
+ * the trusted internals bag, the store resolution, the row read and its terminal-status confirm, the
5
+ * same-process parent-constraint re-supply, then the wake pre-ladder (gate purity, pendingAction purity,
6
+ * the message's persist-time validation) and the one read of the outcome discriminant and the signal slot.
7
+ *
8
+ * Async (two store reads); every refusal throws a typed `CheckpointError` pre-CAS. It ends by handing what it
9
+ * admitted to the rung after it (`next`) in its LAST continuation — no suspension point between the
10
+ * registry read and the wake pre-ladder's first read of the row — and returns what that rung returns.
11
+ * The same seat shape the tool gate's fold uses to enter its lanes (gate-fold.ts `afterFold`).
12
+ */
13
+ import { type Checkpoint, type CheckpointStore, type CheckpointToken, type PendingSteerEntry, type ResumeOutcome } from "../checkpoint-store.js";
14
+ import type { InheritedGate, ResumeTaskConfig, RunInternals, RunnerDepsSeat } from "./contracts.js";
15
+ /** What the admission rung was called with. */
16
+ export interface ResumeAdmissionInput<R> {
17
+ /** borrowed-readonly — the checkpoint token the caller is redeeming. */
18
+ token: CheckpointToken;
19
+ /** borrowed-readonly — the caller's outcome object, read exactly where the driver read it (the container test,
20
+ * the discriminant once, the wake message once); never stored, never handed on as itself. */
21
+ outcome: ResumeOutcome;
22
+ /** borrowed-readonly — the caller's config bag; snapshotted ONCE here (own enumerable fields), the snapshot is what
23
+ * every later rung and the resumed leg read. */
24
+ taskConfig: ResumeTaskConfig;
25
+ /** borrowed-readonly — the trusted run-internals bag as the caller supplied it; shallow-snapshotted here, the
26
+ * snapshot (with the parked chain re-supplied when the registry holds it) is what the ladder reads. */
27
+ internals: RunInternals | undefined;
28
+ /** borrowed-readonly — the Runner's deployment deps, read LIVE through the Runner (the store resolution reads `checkpointStore`); the one home of why it is a
29
+ * seat and not a captured object is {@link RunnerDepsSeat}. */
30
+ runner: RunnerDepsSeat;
31
+ /** borrowed-readonly — the Runner's same-process parked-constraint registry; read for this token (never written here). */
32
+ parentConstraintRegistry: ReadonlyMap<CheckpointToken, NonNullable<InheritedGate["parentConstraints"]>>;
33
+ /** borrowed-readonly — the rung after admission, entered in this rung's last continuation with what it admitted. */
34
+ next: (admitted: ResumeAdmissionResult) => Promise<R>;
35
+ }
36
+ /** What admission established — the constants every later rung reads. */
37
+ export interface ResumeAdmissionResult {
38
+ /** The caller's config, snapshotted once (own enumerable fields). */
39
+ config: ResumeTaskConfig;
40
+ /** The trusted internals snapshot, with the parked parent chain re-supplied when this Runner spawned the leg. */
41
+ internals: RunInternals | undefined;
42
+ /** The checkpoint store the row lives in. */
43
+ store: CheckpointStore;
44
+ /** The pending row, read once; a non-pending snapshot was refused before this result existed. */
45
+ cp: Checkpoint;
46
+ /** The ONE read of the outcome's discriminant. */
47
+ outcomeGate: ResumeOutcome["gate"];
48
+ /** The ONE read of the config's signal slot (`.aborted` stays a live read on this object). */
49
+ resumeSignal: AbortSignal | undefined;
50
+ /** The validated wake message (wake outcomes that carried one), never the caller's object. */
51
+ wakeMessage: Omit<PendingSteerEntry, "seq"> | undefined;
52
+ }
53
+ export declare function resumeAdmission<R>(input: ResumeAdmissionInput<R>): Promise<R>;
@@ -0,0 +1,83 @@
1
+ import { CheckpointError, readPendingSteerQueue, resolveCheckpointStore, validatePendingSteer } from "../checkpoint-store.js";
2
+ import { describeSuppliedValue } from "./decide-continuation.js";
3
+ export async function resumeAdmission(input) {
4
+ const { token, outcome, taskConfig, runner, parentConstraintRegistry } = input;
5
+ let internals = input.internals;
6
+ if (outcome === null || typeof outcome !== "object") {
7
+ throw new CheckpointError("checkpoint.invalid_outcome", `resume outcome must be an object naming its gate (got ${outcome === null ? "null" : typeof outcome})`);
8
+ }
9
+ const config = typeof taskConfig === "object" && taskConfig !== null ? { ...taskConfig } : taskConfig;
10
+ internals = internals !== undefined ? { ...internals } : undefined;
11
+ const store = resolveCheckpointStore(config, runner.deps);
12
+ if (!store) {
13
+ throw new CheckpointError("checkpoint.not_found", "no CheckpointStore wired — cannot resume (set RunnerDeps.checkpointStore or taskConfig.checkpointStore)");
14
+ }
15
+ let cp = await store.get(token);
16
+ if (!cp) {
17
+ throw new CheckpointError("checkpoint.not_found", "no checkpoint found for the supplied token");
18
+ }
19
+ if (cp.status !== "pending") {
20
+ const live = await store.get(token);
21
+ if (live?.status === "pending") {
22
+ throw new CheckpointError("checkpoint.reopened_concurrently", "checkpoint changed concurrently (a resolve/reopen cycle landed between this resume's read of the row and its state check) — nothing was validated or executed; re-resume against the current state");
23
+ }
24
+ throw new CheckpointError("checkpoint.already_resolved", `checkpoint is ${describeSuppliedValue(live?.status ?? cp.status)} — the token is consumed, so this resume was neither validated nor executed (idempotent); a different outcome cannot redeem it`);
25
+ }
26
+ if ((internals?.inheritedGate?.parentConstraints?.length ?? 0) === 0) {
27
+ const parked = parentConstraintRegistry.get(token);
28
+ if (parked !== undefined) {
29
+ internals = {
30
+ ...(internals ?? {}),
31
+ inheritedGate: { ...(internals?.inheritedGate ?? {}), parentConstraints: parked },
32
+ };
33
+ }
34
+ }
35
+ let wakeMessage;
36
+ const outcomeGate = outcome.gate;
37
+ const resumeSignal = config.signal;
38
+ let suppliedMessage;
39
+ if (outcomeGate === "wake") {
40
+ suppliedMessage = outcome.message;
41
+ const gk = cp.gate.kind;
42
+ const decideEntry = gk === "human" || gk === "irreversible_ask"
43
+ ? 'a `policy_ask` outcome (decision allow/deny bound to the pending tool call)'
44
+ : gk === "needs_review"
45
+ ? 'a `dry_run_review` outcome (decision approve/reject)'
46
+ : gk === "plan_review"
47
+ ? 'a `plan_review` outcome (decision approve/edit/reject)'
48
+ : gk === "resource_limit"
49
+ ? 'a `resource_limit` outcome (decision "continue")'
50
+ : undefined;
51
+ if (gk !== "task_done") {
52
+ throw new CheckpointError("wake.gate_pending", `cannot wake: this checkpoint awaits a pending gate decision (gate "${gk}") — wake is not a decision ` +
53
+ `channel; resume it with ${decideEntry ?? "its own gate outcome"} instead`);
54
+ }
55
+ const pa = cp.pendingAction;
56
+ switch (pa.kind) {
57
+ case "task_done":
58
+ break;
59
+ case "tool_approval":
60
+ case "resource_limit":
61
+ case "review":
62
+ case "plan_review": {
63
+ throw new CheckpointError("wake.gate_pending", `cannot wake: this checkpoint still carries a pending gate-typed action (pendingAction "${pa.kind}" ` +
64
+ `on a "${gk}" gate — inconsistent row) — wake is not a decision channel; resolve it through its ` +
65
+ `own decide entry instead`);
66
+ }
67
+ default: {
68
+ const _exhaustive = pa;
69
+ void _exhaustive;
70
+ throw new CheckpointError("wake.gate_pending", `cannot wake: this checkpoint carries an unrecognized pendingAction kind "${pa.kind}" ` +
71
+ `(newer-version row?) — fail-closed: wake only serves a pure park (pendingAction "task_done")`);
72
+ }
73
+ }
74
+ if (suppliedMessage !== undefined) {
75
+ wakeMessage = validatePendingSteer(suppliedMessage);
76
+ }
77
+ else if (readPendingSteerQueue(cp.state).length === 0) {
78
+ throw new CheckpointError("wake.nothing_to_deliver", "cannot wake: no message was supplied and the checkpoint holds no parked pendingSteer — an empty " +
79
+ "wake would burn the checkpoint on a blank continuation; supply `message` or park a steer first");
80
+ }
81
+ }
82
+ return await input.next({ config, internals, store, cp, outcomeGate, resumeSignal, wakeMessage });
83
+ }
@@ -0,0 +1,50 @@
1
+ import { type Checkpoint, type PendingSteerEntry, type ResumeOutcome } from "../checkpoint-store.js";
2
+ import type { ResumeRun, ResumeTaskConfig, RunInternals, RunnerDepsSeat } from "./contracts.js";
3
+ export interface ResumeApplyInput<R> {
4
+ /** borrowed-readonly — the pending row (session id, pending call, inherited chain, ledger, hands cwd). */
5
+ cp: Checkpoint;
6
+ /** borrowed-readonly — the config snapshot (`hooks`, `taskId`, `signal`, the policy seats). */
7
+ config: ResumeTaskConfig;
8
+ /** borrowed-readonly — the internals snapshot (delegation identity facts for the screen's envelope). */
9
+ internals: RunInternals | undefined;
10
+ /** borrowed-readonly — the ONE read of the outcome discriminant. */
11
+ outcomeGate: ResumeOutcome["gate"];
12
+ /** borrowed-readonly — the validated wake message the screen judges. */
13
+ wakeMessage: Omit<PendingSteerEntry, "seq"> | undefined;
14
+ /** borrowed-readonly — the ONE signal read from admission; the screen and the edit legs are bounded by it. */
15
+ resumeSignal: AbortSignal | undefined;
16
+ /** borrowed-readonly — the window verdict to re-assert after the screen's await (undefined when unarmed). */
17
+ recheckGovernanceWindow: (() => Promise<void>) | undefined;
18
+ /** borrowed-readonly — the approval lane's twin (its edit is what the rechecks judge; the settlement is minted onto a copy). */
19
+ plainPolicyOutcome: Extract<ResumeOutcome, {
20
+ gate: "policy_ask";
21
+ }> | undefined;
22
+ /** borrowed-readonly — the review lane's twin. */
23
+ plainReviewOutcome: Extract<ResumeOutcome, {
24
+ gate: "plan_review" | "dry_run_review";
25
+ }> | undefined;
26
+ /** borrowed-readonly — the tool-less park lanes' twin. */
27
+ plainParkOutcome: Extract<ResumeOutcome, {
28
+ gate: "wake" | "resource_limit";
29
+ }> | undefined;
30
+ /** borrowed-readonly — the Runner's deployment deps, read LIVE through the Runner (reads `hooks`, `onError`, `lockedConfig`, `basePolicyForResumeEdit`, `toolPolicy` — the last three AFTER this rung's awaits); the one home of why it is a
31
+ * seat and not a captured object is {@link RunnerDepsSeat}. */
32
+ runner: RunnerDepsSeat;
33
+ /** borrowed-readonly — the orchestrator's delegation-facts fold (a lane may not name the orchestrator; the driver hands it in). */
34
+ effectiveDelegationFacts: (internals: Pick<RunInternals, "isDelegatedChild" | "insideFork"> | undefined, seedIsDelegatedChild: boolean | undefined) => {
35
+ isDelegatedChild: boolean;
36
+ };
37
+ /** borrowed-readonly — the rung after apply, entered in this rung's last continuation. */
38
+ next: (applied: ResumeApplyResult) => Promise<R>;
39
+ }
40
+ export interface ResumeApplyResult {
41
+ /** The screen's `additionalContext` for the wake message, when a screen ran and supplied one. */
42
+ wakeHookContext: string | undefined;
43
+ /** The approval lane's twin WITH its minted settlement record (undefined on every other lane). */
44
+ plainPolicyOutcome: Extract<ResumeOutcome, {
45
+ gate: "policy_ask";
46
+ }> | undefined;
47
+ /** The twin the store persists and the resumed leg reads — exactly one lane captured it. */
48
+ outcomeForStore: ResumeRun["outcome"];
49
+ }
50
+ export declare function resumeApply<R>(input: ResumeApplyInput<R>): Promise<R>;