@sema-agent/core 7.5.0 → 7.5.2
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.
- package/CHANGELOG.md +44 -0
- package/dist/core/auto-mode.d.ts +9 -0
- package/dist/core/auto-mode.js +11 -0
- package/dist/core/checkpoint-store.js +5 -1
- package/dist/core/protocol-table.d.ts +5 -0
- package/dist/core/protocol-table.js +1 -0
- package/dist/core/runner/abort-race.d.ts +41 -0
- package/dist/core/runner/abort-race.js +38 -0
- package/dist/core/runner/checkpoint-scope.d.ts +44 -0
- package/dist/core/runner/checkpoint-scope.js +7 -0
- package/dist/core/runner/compaction-call-options.d.ts +1 -1
- package/dist/core/runner/content-ask-bindings.d.ts +27 -0
- package/dist/core/runner/content-ask-bindings.js +1 -0
- package/dist/core/runner/contracts.d.ts +1990 -0
- package/dist/core/runner/contracts.js +1 -0
- package/dist/core/runner/denial-limit-arms.d.ts +24 -1
- package/dist/core/runner/denial-limit-arms.js +21 -0
- package/dist/core/runner/derived-route-fallback.d.ts +34 -0
- package/dist/core/runner/derived-route-fallback.js +16 -0
- package/dist/core/runner/inherited-ask-grants.d.ts +46 -0
- package/dist/core/runner/inherited-ask-grants.js +29 -0
- package/dist/core/runner/park-commit.d.ts +108 -0
- package/dist/core/runner/park-commit.js +32 -0
- package/dist/core/runner/{prepare-permission-rules.d.ts → permission-rule-lanes.d.ts} +109 -3
- package/dist/core/runner/{prepare-permission-rules.js → permission-rule-lanes.js} +47 -1
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +1 -1
- package/dist/core/runner/prepare-ask-lane.d.ts +110 -0
- package/dist/core/runner/prepare-ask-lane.js +133 -0
- package/dist/core/runner/prepare-boundary-parks.d.ts +105 -0
- package/dist/core/runner/prepare-boundary-parks.js +169 -0
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +170 -0
- package/dist/core/runner/prepare-caps-and-workflow.js +255 -0
- package/dist/core/runner/prepare-config-doors.d.ts +2 -10
- package/dist/core/runner/prepare-context-lane.d.ts +119 -0
- package/dist/core/runner/prepare-context-lane.js +230 -0
- package/dist/core/runner/prepare-defer-classify.d.ts +86 -0
- package/dist/core/runner/prepare-defer-classify.js +107 -0
- package/dist/core/runner/prepare-delegation-surface.d.ts +104 -0
- package/dist/core/runner/prepare-delegation-surface.js +144 -0
- package/dist/core/runner/prepare-execution-env.d.ts +54 -0
- package/dist/core/runner/prepare-execution-env.js +86 -0
- package/dist/core/runner/prepare-file-history.d.ts +95 -0
- package/dist/core/runner/prepare-file-history.js +383 -0
- package/dist/core/runner/prepare-gate-stations.d.ts +177 -0
- package/dist/core/runner/prepare-gate-stations.js +290 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +9 -11
- package/dist/core/runner/prepare-hands-readface.js +1 -1
- package/dist/core/runner/prepare-inherited-gate.d.ts +268 -0
- package/dist/core/runner/prepare-inherited-gate.js +266 -0
- package/dist/core/runner/prepare-listings.d.ts +77 -0
- package/dist/core/runner/prepare-listings.js +76 -0
- package/dist/core/runner/prepare-lsp.d.ts +55 -0
- package/dist/core/runner/prepare-lsp.js +27 -0
- package/dist/core/runner/prepare-memory-engine-session.d.ts +84 -0
- package/dist/core/runner/prepare-memory-engine-session.js +233 -0
- package/dist/core/runner/prepare-memory.d.ts +1 -1
- package/dist/core/runner/prepare-offload-wrappers.d.ts +62 -0
- package/dist/core/runner/prepare-offload-wrappers.js +45 -0
- package/dist/core/runner/prepare-park-ask.d.ts +164 -0
- package/dist/core/runner/prepare-park-ask.js +377 -0
- package/dist/core/runner/prepare-policy-chain.d.ts +208 -0
- package/dist/core/runner/prepare-policy-chain.js +584 -0
- package/dist/core/runner/prepare-project-context.d.ts +119 -0
- package/dist/core/runner/prepare-project-context.js +148 -0
- package/dist/core/runner/prepare-prompt-assembly.d.ts +95 -0
- package/dist/core/runner/prepare-prompt-assembly.js +162 -0
- package/dist/core/runner/prepare-prompt-inputs.d.ts +119 -0
- package/dist/core/runner/prepare-prompt-inputs.js +141 -0
- package/dist/core/runner/prepare-protocol-tools.d.ts +91 -0
- package/dist/core/runner/prepare-protocol-tools.js +179 -0
- package/dist/core/runner/prepare-question-face.d.ts +101 -0
- package/dist/core/runner/prepare-question-face.js +84 -0
- package/dist/core/runner/prepare-run-refs.d.ts +89 -0
- package/dist/core/runner/prepare-run-refs.js +39 -0
- package/dist/core/runner/prepare-safety-scan.d.ts +3 -7
- package/dist/core/runner/prepare-safety-scan.js +1 -2
- package/dist/core/runner/prepare-suspend-saga.d.ts +170 -0
- package/dist/core/runner/prepare-suspend-saga.js +308 -0
- package/dist/core/runner/prepare-task.d.ts +14 -1976
- package/dist/core/runner/prepare-task.js +182 -5162
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +111 -0
- package/dist/core/runner/prepare-tool-disclosure-mount.js +219 -0
- package/dist/core/runner/prepare-turn-wiring.d.ts +154 -0
- package/dist/core/runner/prepare-turn-wiring.js +201 -0
- package/dist/core/runner/prepare-wiring-manifest.d.ts +192 -0
- package/dist/core/runner/prepare-wiring-manifest.js +247 -0
- package/dist/core/runner/prepare-workspace-restore.d.ts +3 -56
- package/dist/core/runner/prepare-workspace-restore.js +4 -38
- package/dist/core/runner/prompt-hash-salt.d.ts +1 -0
- package/dist/core/runner/prompt-hash-salt.js +2 -0
- package/dist/core/runner/remote-env-retry.d.ts +29 -0
- package/dist/core/runner/remote-env-retry.js +16 -0
- package/dist/core/runner/rollback-stack.d.ts +32 -0
- package/dist/core/runner/rollback-stack.js +30 -0
- package/dist/core/runner/runtask.js +1 -1
- package/dist/core/runner/workspace-path.d.ts +33 -0
- package/dist/core/runner/workspace-path.js +22 -0
- package/dist/core/session.d.ts +12 -0
- package/dist/core/session.js +3 -0
- package/dist/core/tool-policy.d.ts +16 -0
- package/dist/core/tool-policy.js +3 -0
- package/dist/core/types.d.ts +2 -2
- package/dist/core/write-protect.js +3 -2
- package/package.json +6 -2
- /package/dist/core/runner/{prepare-announce-once.d.ts → announce-once-ledger.d.ts} +0 -0
- /package/dist/core/runner/{prepare-announce-once.js → announce-once-ledger.js} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -3,7 +3,7 @@ import { type AutoModeArmingRecipe } from "../auto-mode-arming.js";
|
|
|
3
3
|
import type { PermissionResult, ResolvedAsk, ToolCallRequest } from "../tool-policy.js";
|
|
4
4
|
import { type EngineNotice } from "../types.js";
|
|
5
5
|
import { type AskOrigin } from "../ask-origin.js";
|
|
6
|
-
import type { Prepared } from "./
|
|
6
|
+
import type { Prepared } from "./contracts.js";
|
|
7
7
|
/**
|
|
8
8
|
* #548 — the classifier DENIAL-LIMIT arms of the tool gate's inherited (delegation) lane, extracted
|
|
9
9
|
* from `prepareTask` as a phase module (design/238 D-7: extract, don't accrete). The gate's OWN
|
|
@@ -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;
|
|
@@ -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,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The derived-leg route pre-flight shared by prepareTask's two derived seats: the compaction-summary model (the
|
|
3
|
+
* driver's own seat, resolved before the offload prelude) and the auto-mode classifier model (the caps-and-workflow
|
|
4
|
+
* phase's arming). Moved whole out of the orchestrator so the phase reaches DOWN for it instead of the two callers
|
|
5
|
+
* spelling it twice. The judgment law itself lives in the brain's route adjudicator; this is the one guarded seat
|
|
6
|
+
* around it.
|
|
7
|
+
*/
|
|
8
|
+
import type { Model } from "../../internal/llm.js";
|
|
9
|
+
import type { RunnerDeps, TaskSpec } from "../types.js";
|
|
10
|
+
/**
|
|
11
|
+
* The derived-leg pairing pre-flight as ONE guarded seat: judge the derived model where its route
|
|
12
|
+
* differs from the primary's, announce the fallback on a broken pairing, and ABSTAIN (keep the
|
|
13
|
+
* derived model) on ANY throw. The pre-flight is advisory — the brain's request gate re-runs the
|
|
14
|
+
* same law — so neither a throwing judge face nor a garbage catalog value (the route-identity
|
|
15
|
+
* normalization throws a TypeError on a non-string baseUrl) may widen into "task preparation
|
|
16
|
+
* failed": at the compaction seat the session is already acquired and the throw-cleanup contract
|
|
17
|
+
* is not yet armed, so a throw here would leak the acquired session view, and the garbage value
|
|
18
|
+
* still earns its loud refusal at the request gate the moment the derived leg is actually used.
|
|
19
|
+
* Returns whether the seat should fall back to the primary model.
|
|
20
|
+
*/
|
|
21
|
+
export declare function derivedRouteFallsBack(args: {
|
|
22
|
+
seat: string;
|
|
23
|
+
derived: Model;
|
|
24
|
+
primary: Model;
|
|
25
|
+
brain: RunnerDeps["brain"];
|
|
26
|
+
getApiKeyAndHeaders: TaskSpec["getApiKeyAndHeaders"];
|
|
27
|
+
onNotice: RunnerDeps["onNotice"];
|
|
28
|
+
/** #433 — the run this seat resolution belongs to; carried onto the notice as its routing key
|
|
29
|
+
* (the audience stays operator: correlation, not entitlement). Absent ⇒ nothing is fabricated. */
|
|
30
|
+
sessionId?: string;
|
|
31
|
+
/** #499 — the INVOCATION this seat resolution belongs to. The session cannot stand in for it: two
|
|
32
|
+
* runs of one session that both fall back on the same seat mint otherwise byte-identical lines. */
|
|
33
|
+
runId?: string;
|
|
34
|
+
}): Promise<boolean>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { adjudicateDerivedRoute, fallbackToPrimaryNotice, sameRouteIdentity } from "../../brain/route-adjudicator.js";
|
|
2
|
+
import { deliverEngineNotice } from "../types.js";
|
|
3
|
+
export async function derivedRouteFallsBack(args) {
|
|
4
|
+
try {
|
|
5
|
+
if (sameRouteIdentity(args.derived, args.primary))
|
|
6
|
+
return false;
|
|
7
|
+
const verdict = await adjudicateDerivedRoute({ brain: args.brain, model: args.derived, getApiKeyAndHeaders: args.getApiKeyAndHeaders });
|
|
8
|
+
if (verdict === undefined || verdict.ok)
|
|
9
|
+
return false;
|
|
10
|
+
deliverEngineNotice(args.onNotice, fallbackToPrimaryNotice({ seat: args.seat, from: args.derived.id, to: args.primary.id, verdict, ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}), ...(args.runId !== undefined ? { runId: args.runId } : {}) }));
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The commit-side vocabulary of a durable park: which holder a COMMITTED checkpoint publishes to, the abort-classification
|
|
3
|
+
* marker the loop stamps on the contaminated siblings, and the deadline arithmetic every mint shares (the retention
|
|
4
|
+
* defaults and the one TTL sanitizer). Moved whole out of the orchestrator so the boundary-parks phase (resource /
|
|
5
|
+
* platform / review) and the park-ask phase (the approval park) reach DOWN for the same publication seam and the same
|
|
6
|
+
* numbers, and the driver keeps reaching down for the marker its harness stamps. Pure: no engine state; the only clock
|
|
7
|
+
* is the caller's.
|
|
8
|
+
*/
|
|
9
|
+
import type { CheckpointGate, CheckpointToken, PendingAction } from "../checkpoint-store.js";
|
|
10
|
+
import type { WorkspaceHandle } from "../remote-env.js";
|
|
11
|
+
import type { Prepared } from "./contracts.js";
|
|
12
|
+
/** design/74 (HIGH-2): default resource-checkpoint retention (30 days, the spec's
|
|
13
|
+
* `RESOURCE_SUSPEND_TTL_SEC`). Stamped onto the checkpoint `deadline` so the durable reaper can GC an
|
|
14
|
+
* ORPHANED resource suspend; a legitimate auto-resume fires long before this. Override via
|
|
15
|
+
* `TaskSpec.resourceSuspend.ttlMs`. */
|
|
16
|
+
export declare const DEFAULT_RESOURCE_TTL_MS: number;
|
|
17
|
+
/**
|
|
18
|
+
* design/164 件五 — how long AFTER a governance window frees a `usage_window` checkpoint stays reapable-free.
|
|
19
|
+
* The retention TTL answers "was this abandoned?"; a usage-window suspend is not abandoned while the window
|
|
20
|
+
* it waits on is still full, so its deadline is pushed to `retryAfterMs + this`. One hour gives a host's
|
|
21
|
+
* scheduler a realistic chance to pick the resume up (a cron tick, a queue drain) before the row is
|
|
22
|
+
* treated as garbage — small next to the 30-day retention it is compared against, and large next to any
|
|
23
|
+
* polling interval a driver would sanely use.
|
|
24
|
+
*/
|
|
25
|
+
export declare const USAGE_WINDOW_REAP_MARGIN_MS: number;
|
|
26
|
+
/** design/80 D-2 (BLOCKER): abandonment TTL stamped on an UNATTENDED human/irreversible_ask suspend (no
|
|
27
|
+
* `durableApproval` = no operator/resolver wired). Without it the checkpoint has `deadline=undefined` and the
|
|
28
|
+
* reaper (which filters on a non-null deadline) can NEVER GC it → a pinned session + a stranded paused VM +
|
|
29
|
+
* an orphan checkpoint leak forever. A `durableApproval` suspend keeps the operator's `ttlMs` (undefined =
|
|
30
|
+
* unbounded by the operator's choice; D-D `terminalAt` is the eventual liveness backstop there). 30 days. */
|
|
31
|
+
export declare const DEFAULT_UNATTENDED_APPROVAL_TTL_MS: number;
|
|
32
|
+
/**
|
|
33
|
+
* RB-68 (2026-07-25, red probe): a caller-supplied TTL is only a TTL if it is a finite, positive number.
|
|
34
|
+
* `TaskSpec.durableApproval.ttlMs` / `resourceSuspend.ttlMs` are typed as a bare `number`, and the mint
|
|
35
|
+
* arithmetic was `Date.now() + ttlMs` with NO validation — so `Number(process.env.APPROVAL_TTL_MS)` on an
|
|
36
|
+
* unset/malformed env var (a config-center miss, an upstream arithmetic slip) silently produced
|
|
37
|
+
* `deadline = NaN`. Every reaper comparison is `deadline <= cutoff`, and `NaN <= anything` is FALSE, so a
|
|
38
|
+
* checkpoint that LOOKS bounded (it carries a ttlMs and a deadline) became permanently IMMORTAL: no reap
|
|
39
|
+
* cutoff, however far in the future, could ever expire it → a pinned session + a stranded paused VM, and
|
|
40
|
+
* a misconfigured deployment silently defeating its own abandoned-approval cleanup with no error anywhere.
|
|
41
|
+
*
|
|
42
|
+
* Handling follows the codebase's established treatment of malformed numeric config — sanitize to the
|
|
43
|
+
* documented default rather than throw — `task-registry-shared.ts`'s `TASK_OUTPUT_DEFAULT_CHARS` fallbacks
|
|
44
|
+
* (non-numeric / non-finite / non-positive all return the documented default) and `media-byte-cap.ts`'s
|
|
45
|
+
* `!Number.isFinite(limitBytes) || limitBytes <= 0` guard are the two live precedents: garbage in a bound
|
|
46
|
+
* field must never widen the bound, and
|
|
47
|
+
* killing an in-flight task over it would cost more than it saves. `undefined` (the deliberate
|
|
48
|
+
* "unbounded, D-D terminalAt is the backstop" choice) is preserved as-is and is NOT what this rejects.
|
|
49
|
+
*/
|
|
50
|
+
export declare function sanitizedTtlMs(ttlMs: number | undefined): number | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* WHICH tool call a committed durable park is holding this run — `undefined` when nothing parked, or
|
|
53
|
+
* when the park that did commit holds no call (a resource slice, a plan review).
|
|
54
|
+
*
|
|
55
|
+
* SINGLE derivation on purpose. Two consumers need this answer: the abort-classification seam, which
|
|
56
|
+
* stamps it into the `details` of the results the loop mints for the contaminated siblings, and the
|
|
57
|
+
* `tool_end` projection, which puts it on the wire. Reading it from one function keeps the two from
|
|
58
|
+
* disagreeing about WHICH HOLDER WINS or about what a holder with no call means. They are not otherwise
|
|
59
|
+
* interchangeable: the frame face applies strictly narrower conditions on top of this answer (see
|
|
60
|
+
* `tool_end.gatedCallId`), so a frame may omit an id this function returns — never the reverse.
|
|
61
|
+
*
|
|
62
|
+
* The frame side must not read the id back out of a tool RESULT even though the marker is there: a
|
|
63
|
+
* result's `details` is written by the tool (and replaceable by post-tool hooks), so lifting a
|
|
64
|
+
* cross-call attribution from it would let any failing tool name an arbitrary call and put a phantom
|
|
65
|
+
* approval wait on someone else's frame. Same rule, same reason, as `settledBy`.
|
|
66
|
+
*
|
|
67
|
+
* The two holders are never both set (the commit-side discriminant writes exactly one); they are read
|
|
68
|
+
* here in assemble-result's slot order so the winner is the same one the terminal status is built from.
|
|
69
|
+
*/
|
|
70
|
+
export declare function gatedCallIdOf(p: Pick<Prepared, "suspendRef" | "reviewRef">): string | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* The abort-classification marker for the loop's short-circuit "Operation aborted" family: when a gate
|
|
73
|
+
* committed a durable park this run (`suspendRef` for human/irreversible/content asks, `reviewRef` for
|
|
74
|
+
* the dry-run/plan-review family), the contaminated sibling frames carry something a consumer can key
|
|
75
|
+
* on instead of parsing the (contract-stable) result text (ruled 2026-08-04). The commit is published
|
|
76
|
+
* BEFORE the abort fires, so this read is race-free at mint time.
|
|
77
|
+
*
|
|
78
|
+
* `gatedCallId` NAMES the call the gate is holding, so a consumer can attribute a contaminated frame to
|
|
79
|
+
* its cause instead of inferring the pair from batch adjacency. Absent, never approximated: a park that
|
|
80
|
+
* holds no tool call (a resource slice, a plan review) publishes no id, and this marker does not invent
|
|
81
|
+
* one from whatever call happens to be in flight.
|
|
82
|
+
*
|
|
83
|
+
* Module-level rather than a closure in `prepareTask` (design/238 D-7: extract, don't accrete) — the
|
|
84
|
+
* marker's shape and its rules belong beside {@link gatedCallIdOf}, the derivation it reads.
|
|
85
|
+
*/
|
|
86
|
+
export declare function parkContaminationMarker(refs: Pick<Prepared, "suspendRef" | "reviewRef">): Record<string, unknown> | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* design/76 §2.5 — THE COMMIT-SIDE DISCRIMINANT (v4 MAJOR-A, the load-bearing dry-run fix). Called
|
|
89
|
+
* AFTER `commitSuspendSaga` returns true (the checkpoint is durably committed) to publish which holder
|
|
90
|
+
* the run loop reads for the terminal status. Branch on the COMMITTED `gate.kind`: a REVIEW-PAUSE gate
|
|
91
|
+
* writes `reviewRef` (→ assemble-result slot 8.6 → `status:"needs_review"` / `review.pending`); every
|
|
92
|
+
* approval/resource gate (human / irreversible_ask / resource_limit / task_done) writes `suspendRef` as
|
|
93
|
+
* before (→ slot 8.5 → `status:"suspended"`). NEVER both — a review pause that also set `suspendRef`
|
|
94
|
+
* would assemble as `"suspended"` and make slot 8.6 dead code. The review-pause family is BOTH the
|
|
95
|
+
* dry-run `{kind:"needs_review"}` gate (design/76 §2.5) AND the design/80 D-B `{kind:"plan_review"}`
|
|
96
|
+
* gate — both are human-review pauses that share the `needs_review` terminal (a `plan_review` resumes
|
|
97
|
+
* with a `plan_review` outcome, a `needs_review` with `dry_run_review`; the distinct gate.kind keeps the
|
|
98
|
+
* resume discriminant unambiguous). (The review checkpoints themselves are minted by a PROFILE, out of
|
|
99
|
+
* core scope; core only owns this routing seam + the gate/status/resume discriminants.)
|
|
100
|
+
*
|
|
101
|
+
* Module-level, taking the holders as a parameter (design/238 D-7: extract, don't accrete) — the
|
|
102
|
+
* closure form captured nothing else, so hoisting it costs nothing and puts the discriminant beside the
|
|
103
|
+
* two readers of what it publishes ({@link gatedCallIdOf}, {@link parkContaminationMarker}).
|
|
104
|
+
*/
|
|
105
|
+
export declare function publishCommittedSuspend(refs: Pick<Prepared, "suspendRef" | "reviewRef">, token: CheckpointToken, gate: CheckpointGate, scope: string, remoteHandle: WorkspaceHandle | undefined, checkpointId?: string,
|
|
106
|
+
/** The COMMITTED row's pendingAction — passed whole, not as a pre-extracted id, so no call site can
|
|
107
|
+
* hand this seam an id the committed row does not actually park on. */
|
|
108
|
+
pendingAction?: PendingAction): void;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const DEFAULT_RESOURCE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
2
|
+
export const USAGE_WINDOW_REAP_MARGIN_MS = 60 * 60 * 1000;
|
|
3
|
+
export const DEFAULT_UNATTENDED_APPROVAL_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
4
|
+
export function sanitizedTtlMs(ttlMs) {
|
|
5
|
+
if (ttlMs === undefined)
|
|
6
|
+
return undefined;
|
|
7
|
+
return Number.isFinite(ttlMs) && ttlMs > 0 ? ttlMs : DEFAULT_RESOURCE_TTL_MS;
|
|
8
|
+
}
|
|
9
|
+
export function gatedCallIdOf(p) {
|
|
10
|
+
if (p.suspendRef.token !== undefined)
|
|
11
|
+
return p.suspendRef.gatedCallId;
|
|
12
|
+
if (p.reviewRef.token !== undefined)
|
|
13
|
+
return p.reviewRef.gatedCallId;
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
export function parkContaminationMarker(refs) {
|
|
17
|
+
if (refs.suspendRef.token === undefined && refs.reviewRef.token === undefined)
|
|
18
|
+
return undefined;
|
|
19
|
+
const gatedCallId = gatedCallIdOf(refs);
|
|
20
|
+
return { code: "gate.parked", ...(gatedCallId !== undefined ? { gatedCallId } : {}) };
|
|
21
|
+
}
|
|
22
|
+
export function publishCommittedSuspend(refs, token, gate, scope, remoteHandle, checkpointId, pendingAction) {
|
|
23
|
+
const ref = gate.kind === "needs_review" || gate.kind === "plan_review" ? refs.reviewRef : refs.suspendRef;
|
|
24
|
+
ref.token = token;
|
|
25
|
+
if (checkpointId !== undefined)
|
|
26
|
+
ref.checkpointId = checkpointId;
|
|
27
|
+
ref.gate = gate;
|
|
28
|
+
ref.gatedCallId = pendingAction?.kind === "tool_approval" ? pendingAction.toolCallId : undefined;
|
|
29
|
+
if (remoteHandle !== undefined)
|
|
30
|
+
ref.restoreMode = remoteHandle.restoreMode === "park_only" ? "park_only" : "snapshot";
|
|
31
|
+
ref.scope = scope;
|
|
32
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* design/389 —
|
|
2
|
+
* design/389 — the permission-rule LANES the unified store feeds (prepare-path machinery, design/390 L1): the two gate
|
|
3
|
+
* lanes, and — since the policy-chain phase took the chain assembly — the rule-offer factory every ask-mint site calls.
|
|
3
4
|
*
|
|
4
5
|
* `prepareTask` used to assemble the personal lane inline (a store read + a hand-spliced session
|
|
5
6
|
* overlay + two admission arms) and the org lane from a second seam. Both now read the ONE store query —
|
|
@@ -23,8 +24,8 @@
|
|
|
23
24
|
* silence the ungated-write warning for deployments that wired no policy at all.
|
|
24
25
|
*/
|
|
25
26
|
import type { AskRuleEvidence, ToolCallRequest } from "../tool-policy.js";
|
|
26
|
-
import type
|
|
27
|
-
import type { PersistedAllowRule } from "../permission-rule-model.js";
|
|
27
|
+
import { persistedRuleMandateOf, type OrgGateVerdict, type PersistedRuleAnswer, type PersistedRuleHit } from "../hooks.js";
|
|
28
|
+
import type { PersistedAllowRule, RuleOffer, SegmentCoverage } from "../permission-rule-model.js";
|
|
28
29
|
import { type OrgRuleResolution } from "../permission-rule-org.js";
|
|
29
30
|
import type { PermissionRuleStoreProvider } from "../permission-rule-provider.js";
|
|
30
31
|
/** The one SHELL tool the persisted-rule lane speaks for. */
|
|
@@ -130,3 +131,108 @@ export declare function createPermissionRuleLanes(cfg: {
|
|
|
130
131
|
* lane took the read. */
|
|
131
132
|
onDisclosure: (message: string) => void;
|
|
132
133
|
}): PermissionRuleLanes;
|
|
134
|
+
/** The closed reason set for the rule-offer factory's empty answer (#490 修②) — the same three
|
|
135
|
+
* spellings the `AskRequest` and durable-row seats declare, kept at the one factory that fills both.
|
|
136
|
+
* Deliberately not exported: this is a wire vocabulary, and its two faces declare it literally (the
|
|
137
|
+
* `previewWithheld` precedent) so a consumer reads the closed set on the type it is holding rather
|
|
138
|
+
* than through an import. */
|
|
139
|
+
type RuleOffersAbsence = "mandated" | "lane_cannot_speak" | "shadowed";
|
|
140
|
+
/** The surviving-ask facts the rule-offer factory reads (see {@link createRuleOffersOf}): the
|
|
141
|
+
* synchronous mint sites spread the surviving decision, which carries them; the park leg threads
|
|
142
|
+
* each as its own parameter. */
|
|
143
|
+
interface RuleOffersAskFacts {
|
|
144
|
+
requiresRealApproval?: boolean;
|
|
145
|
+
persistedRuleShadowed?: string;
|
|
146
|
+
decisionReason?: import("../tool-policy.js").DecisionReason;
|
|
147
|
+
/** #457 ④: an EXPLICIT `ask` permission rule matched this call (design/127 DSL, stamped at
|
|
148
|
+
* the rule policy's three matched-ask exits — bare, covering and param). */
|
|
149
|
+
matchedAskRule?: string;
|
|
150
|
+
/** #502: the engine-stamped probe mandate (the reversibility probe declared this call's
|
|
151
|
+
* demotion structural — for the built-in shell probe, a read outside the session's
|
|
152
|
+
* allowed directories). */
|
|
153
|
+
probeMandated?: boolean;
|
|
154
|
+
inheritedUnresolved?: boolean;
|
|
155
|
+
/** The ask is being resolved at an ANCESTOR's frozen approver (the three inherited-lane
|
|
156
|
+
* mint sites pass it literally) — this task's rule lane never adjudicates it. */
|
|
157
|
+
ancestorResolved?: boolean;
|
|
158
|
+
/** design/375 §5.2②: the surviving ask's engine-stamped per-segment coverage table — the
|
|
159
|
+
* batch offer carries exactly the segments this table calls uncovered. Absent ⇒ all
|
|
160
|
+
* segments read as uncovered (over-offer, the safe direction). */
|
|
161
|
+
segmentCoverage?: readonly SegmentCoverage[];
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* design/179 §4 — the rule forms that could cover this exact call, for a surface's "stop asking me
|
|
165
|
+
* this" option (extracted from the prepare body, the spliceSessionOverlayRows precedent; the maker
|
|
166
|
+
* captures the per-task wiring once and the returned factory serves all five ask-mint sites — the
|
|
167
|
+
* approval-preview field is a standing example of what happens otherwise: it reached two of the
|
|
168
|
+
* five, so a delegated child's card was silently poorer than the top-level one).
|
|
169
|
+
*
|
|
170
|
+
* Empty unless a rule lane is armed — offering an option that redemption would refuse is worse than
|
|
171
|
+
* offering none — and empty for any call the lane cannot speak for (a compound, a redirection, a
|
|
172
|
+
* substitution, or a tool that is not the shell tool).
|
|
173
|
+
*
|
|
174
|
+
* Empty, too, on any ask a persisted rule could never CLEAR — "allow rules silence the classifier's
|
|
175
|
+
* questions, never a mandated one". Two doors, matching the two halves of that boundary:
|
|
176
|
+
* · the marks half, judged from the SAME resolved sources the gate input is built from, through the
|
|
177
|
+
* gate's own single-source predicate (an operator's shellGate:"always", the tool's own
|
|
178
|
+
* egress/irreversibility marks, and — #502 — the PER-CALL member that predicate also takes: a
|
|
179
|
+
* demotion this call's reversibility probe declared structural, read off the ask the gate
|
|
180
|
+
* stamped rather than off the tool seat, because it is true of one call and not of the seat) —
|
|
181
|
+
* a drift here would offer a rule the lane then refuses to honor;
|
|
182
|
+
* · the decision half (`ask`, REQUIRED at every mint site so a site added later cannot skip it):
|
|
183
|
+
* `requiresRealApproval` (org/governance — only judgment clears it), the shadowed-rule
|
|
184
|
+
* disclosure (the standing proof that a matching rule does not clear THIS ask), a hook-raised
|
|
185
|
+
* ask (`decisionReason === "hook"` — the lane's own clearing conjuncts refuse hook asks, so a
|
|
186
|
+
* rule minted from one would never silence it), an EXPLICIT `ask` rule match (`matchedAskRule`
|
|
187
|
+
* — see below), and an inherited-unresolved marked call (the
|
|
188
|
+
* ask is an ANCESTOR's authority, which this task's rule lane never adjudicates). Offering
|
|
189
|
+
* "stop asking me this" on a card that will keep asking lets a person mint a rule that never
|
|
190
|
+
* takes effect where they minted it.
|
|
191
|
+
*
|
|
192
|
+
* #457 ④ (CC 2.1.245 toolPolicy 对表, F11: "wildcard `ask` … keeps every matching tool behind a
|
|
193
|
+
* per-call prompt (no persistent always-allow)") — `matchedAskRule` was the one conjunct of the
|
|
194
|
+
* lane's own clearing predicate this door did not mirror, and the asymmetry was live: the
|
|
195
|
+
* persisted-rule lane refuses to clear ANY ask carrying it (`hooks.ts`, the
|
|
196
|
+
* `decision.matchedAskRule === undefined` conjunct on its allow arm), so a person who wrote a
|
|
197
|
+
* standing `Bash` `ask` rule and had no allow rule yet was still offered "allow, and stop asking
|
|
198
|
+
* me this" — and the rule they minted came back on the very next call as a SHADOWED match
|
|
199
|
+
* (`persistedRuleShadowed`, which this door was already refusing). The offer was only ever
|
|
200
|
+
* suppressed one call too late, after the useless rule existed. The shadowed conjunct stays: it
|
|
201
|
+
* covers the person who ALREADY has the allow rule; this one covers the person about to mint it.
|
|
202
|
+
*
|
|
203
|
+
* #490 修② — every door above that answers "no offers" now also names WHICH door, on the
|
|
204
|
+
* `ruleOffersAbsence` seat (`AskRequest`'s and the durable row's, ONE factory so the two faces
|
|
205
|
+
* cannot disagree). The doctrine it serves is the loud-bad-value one, applied to an absence: a
|
|
206
|
+
* surface reading an empty card could not tell "there is nothing this lane could offer" from
|
|
207
|
+
* "offers were deliberately suppressed", and a person reading it could not tell "write the rule
|
|
208
|
+
* yourself" from "no rule can excuse this approval". The seat is CLOSED
|
|
209
|
+
* (`mandated | lane_cannot_speak | shadowed`), mutually exclusive with `ruleOffers` by
|
|
210
|
+
* construction, and structural-door silent (see the first two guards).
|
|
211
|
+
*/
|
|
212
|
+
export declare function createRuleOffersOf(cfg: {
|
|
213
|
+
/** `permissionRuleLane !== undefined` at wiring time — the armed-lane structural door. */
|
|
214
|
+
laneArmed: boolean;
|
|
215
|
+
principal: string | undefined;
|
|
216
|
+
/** `deps.localOwnerRules === true` — the declared local-owner exception to the anonymous floor. */
|
|
217
|
+
localOwnerDeclared: boolean;
|
|
218
|
+
/** The live mark sources the gate input is built from — passed as REFERENCES (read per call, so a
|
|
219
|
+
* late-registered tool seat is seen exactly as the gate sees it). */
|
|
220
|
+
egressTools: {
|
|
221
|
+
has(name: string): boolean;
|
|
222
|
+
};
|
|
223
|
+
irreversibilityTier: {
|
|
224
|
+
get(name: string): Parameters<typeof persistedRuleMandateOf>[0]["irreversibility"];
|
|
225
|
+
};
|
|
226
|
+
shellGatedBash: boolean;
|
|
227
|
+
taskRoot: string | undefined;
|
|
228
|
+
/** The live tracked-cwd ref (read per call — the relative-cd resolution base, adversarial-review P1/r3). */
|
|
229
|
+
cwdRef: {
|
|
230
|
+
current: string;
|
|
231
|
+
} | undefined;
|
|
232
|
+
deniesDirectoryRead: ((directory: string) => boolean) | undefined;
|
|
233
|
+
}): (toolName: string, args: unknown, ask: RuleOffersAskFacts | undefined) => {
|
|
234
|
+
ruleOffers?: readonly RuleOffer[];
|
|
235
|
+
ruleOffersAbsence?: RuleOffersAbsence;
|
|
236
|
+
execCwd?: string;
|
|
237
|
+
};
|
|
238
|
+
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { persistedRuleMandateOf } from "../hooks.js";
|
|
2
|
+
import { directoryRuleAdmits, eligiblePersisted, findAdmittingRule, lexicalNormalAbsolutePathOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
|
|
2
3
|
import { orgRuleVerdictFor } from "../permission-rule-org.js";
|
|
3
4
|
export const PERSISTED_RULE_TOOL = "Bash";
|
|
4
5
|
export const DIRECTORY_RULE_TOOL = "Read";
|
|
@@ -138,3 +139,48 @@ export function createPermissionRuleLanes(cfg) {
|
|
|
138
139
|
: undefined;
|
|
139
140
|
return { personal, org };
|
|
140
141
|
}
|
|
142
|
+
export function createRuleOffersOf(cfg) {
|
|
143
|
+
return (toolName, args, ask) => {
|
|
144
|
+
if (!cfg.laneArmed || toolName !== PERSISTED_RULE_TOOL)
|
|
145
|
+
return {};
|
|
146
|
+
if ((cfg.principal === undefined || cfg.principal === "") && !cfg.localOwnerDeclared)
|
|
147
|
+
return {};
|
|
148
|
+
const closedDoor = (() => {
|
|
149
|
+
if (ask?.requiresRealApproval === true)
|
|
150
|
+
return "mandated";
|
|
151
|
+
if (ask?.persistedRuleShadowed !== undefined)
|
|
152
|
+
return "shadowed";
|
|
153
|
+
if (ask?.decisionReason === "hook")
|
|
154
|
+
return "mandated";
|
|
155
|
+
if (ask?.matchedAskRule !== undefined)
|
|
156
|
+
return "shadowed";
|
|
157
|
+
if (ask?.inheritedUnresolved === true)
|
|
158
|
+
return "mandated";
|
|
159
|
+
if (ask?.ancestorResolved === true)
|
|
160
|
+
return "mandated";
|
|
161
|
+
return undefined;
|
|
162
|
+
})();
|
|
163
|
+
if (closedDoor !== undefined)
|
|
164
|
+
return { ruleOffersAbsence: closedDoor };
|
|
165
|
+
if (persistedRuleMandateOf({
|
|
166
|
+
egress: cfg.egressTools.has(toolName),
|
|
167
|
+
irreversibility: cfg.irreversibilityTier.get(toolName),
|
|
168
|
+
shellGated: cfg.shellGatedBash,
|
|
169
|
+
probeMandated: ask?.probeMandated === true,
|
|
170
|
+
}) !== undefined) {
|
|
171
|
+
return { ruleOffersAbsence: "mandated" };
|
|
172
|
+
}
|
|
173
|
+
const command = args?.command;
|
|
174
|
+
if (typeof command !== "string")
|
|
175
|
+
return { ruleOffersAbsence: "lane_cannot_speak" };
|
|
176
|
+
const offers = suggestRulesForCommand(command, {
|
|
177
|
+
...(ask?.segmentCoverage !== undefined ? { coverage: ask.segmentCoverage } : {}),
|
|
178
|
+
...(cfg.taskRoot !== undefined ? { scope: { kind: "project", root: cfg.taskRoot }, cwd: cfg.taskRoot } : {}),
|
|
179
|
+
...(cfg.cwdRef?.current !== undefined ? { execCwd: cfg.cwdRef.current } : {}),
|
|
180
|
+
...(cfg.deniesDirectoryRead !== undefined ? { deniesDirectoryRead: cfg.deniesDirectoryRead } : {}),
|
|
181
|
+
});
|
|
182
|
+
return offers.length > 0
|
|
183
|
+
? { ruleOffers: offers, ...(cfg.cwdRef?.current !== undefined ? { execCwd: cfg.cwdRef.current } : {}) }
|
|
184
|
+
: { ruleOffersAbsence: "lane_cannot_speak" };
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -22,7 +22,7 @@ import { StoredSession } from "../session.js";
|
|
|
22
22
|
import type { AcquiredSession, SessionStore } from "../session.js";
|
|
23
23
|
import { type RecoveredOrphan } from "../session-reconcile.js";
|
|
24
24
|
import type { TaskSpec, ToolEffect } from "../types.js";
|
|
25
|
-
import type { PrepareResume } from "./
|
|
25
|
+
import type { PrepareResume } from "./contracts.js";
|
|
26
26
|
export interface PrepareAcquireReconcileInput {
|
|
27
27
|
/** borrowed-mutable (service port) — the deployment's session store. This phase's verbs on it are
|
|
28
28
|
* the acquire/forget half of the session resource protocol: `acquire` per attempt, `forget` to
|