@sema-agent/core 7.5.1 → 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 +20 -0
- 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 +15 -3
- package/dist/core/runner/checkpoint-scope.js +3 -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 +118 -6
- package/dist/core/runner/denial-limit-arms.d.ts +23 -0
- package/dist/core/runner/denial-limit-arms.js +21 -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-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-context-lane.d.ts +119 -0
- package/dist/core/runner/prepare-context-lane.js +230 -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 +4 -4
- package/dist/core/runner/prepare-inherited-gate.d.ts +4 -4
- 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-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 +1 -13
- package/dist/core/runner/prepare-project-context.js +1 -3
- 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 +1 -20
- package/dist/core/runner/prepare-protocol-tools.d.ts +3 -3
- package/dist/core/runner/prepare-protocol-tools.js +0 -3
- package/dist/core/runner/prepare-question-face.d.ts +3 -21
- package/dist/core/runner/prepare-question-face.js +2 -1
- package/dist/core/runner/prepare-safety-scan.d.ts +0 -5
- 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 +9 -136
- package/dist/core/runner/prepare-task.js +44 -2741
- 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 +11 -3
- package/dist/core/runner/prepare-wiring-manifest.js +9 -2
- package/dist/core/runner/prepare-workspace-restore.d.ts +2 -29
- package/dist/core/runner/prepare-workspace-restore.js +3 -16
- 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/runtask.js +1 -1
- package/dist/core/session.d.ts +12 -0
- package/dist/core/session.js +3 -0
- package/package.json +1 -1
- /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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 7.5.2 — 2026-09-06
|
|
4
|
+
|
|
5
|
+
Maintenance release: design/390 slices S3–S4 complete the `prepareTask` decomposition (4 756 → 1 666 driver lines, 34 phase modules), one settlement-path narrowing (#587), and the wire-manifest tooling that the 7.5.1 review asked for (#588 / #589). **No BREAKING change**: the export surface is byte-identical to 7.5.1; the only behaviour delta is that a reserved-tool-name collision now disposes an acquired protocol handle once instead of twice (below).
|
|
6
|
+
|
|
7
|
+
### Changed — design/390 S3–S4: the rest of `prepareTask` becomes phases
|
|
8
|
+
- Extracted verbatim, each with one Input (every seat classified) and one Result: memory-engine session, prompt assembly (two live accessor cells `systemPromptSeat` / `centerAdoptionRef`), policy chain (1 351 lines; the ask-origin mint sites move, the words and the closed set do not), context lane, turn wiring, the park lane as four phases (`prepare-ask-lane` / `prepare-suspend-saga` / `prepare-boundary-parks` / `prepare-park-ask`), and the two tool-gate stations (`prepare-gate-stations`). The driver holds five direct phase calls where it held nested closures and an `if` block; the block-activation predicate is minted once (`gateMachineryActive`).
|
|
9
|
+
- Layer 1 machinery (reached down for by phases, never a phase): `permission-rule-lanes.ts` (formerly `prepare-permission-rules.ts`), `inherited-ask-grants.ts`, `prompt-hash-salt.ts`, `abort-race.ts`, `remote-env-retry.ts`, `park-commit.ts`, `content-ask-bindings.ts`, `announce-once-ledger.ts` (formerly `prepare-announce-once.ts`), `compaction-call-options.ts`; `docs/LAYERING.json` `uncovered` list is empty.
|
|
10
|
+
- Contract seats: `AskLane` / `SuspendSaga` / `ParkAsk` are one fact each (a lane exists or it does not) rather than several independently-optional closures; `PromptFeatureFlags` / `SuspendSagaOutcome` and the park-lane seat types live in `contracts.ts`.
|
|
11
|
+
- Unchanged and pinned: the suspendAsk 13-parameter `AskCarry`, `ASK_ORIGINS` and its fence, the 23 gate stations (words, order, deny sources), the durable park row, `tool_end` settlement words, the tool roster order (owner table on `Prepared.tools`).
|
|
12
|
+
|
|
13
|
+
### Changed (narrowing, named) — #587 one releaser on the reserved-name path
|
|
14
|
+
- A caller tool whose name collides with a protocol tool's reserved name still makes prepare throw `config.reserved_tool_name`; the acquired MCP / A2A handle is now released exactly once by the RollbackStack unwind (A2A before MCP). 7.5.1 disposed it inline and again on unwind. Pinned as "exactly once" at the transcript and unit level.
|
|
15
|
+
|
|
16
|
+
### Gates / tooling
|
|
17
|
+
- `test/runner-layer-order.test.ts`: the five-layer order (vocabulary < machinery < phases < orchestrator < runner) is asserted over the REAL import graph of `src/core/runner/*.ts`; the three upward exemptions are enumerated and the table cannot grow silently.
|
|
18
|
+
- `test/prepare-prompt-assembly-zero-write-window.test.ts` (design/238 T16): prompt assembly performs no session write between its start and the prompt-epoch pin; unknown storage methods count as writes (fail-closed).
|
|
19
|
+
- `scripts/wire-changes.mjs`: `declarationMoves[]` (#589) lists every module-exported name whose declaring file changed while the name stayed exported (`onSurface` marks barrel names) — informational, never counted as breaking; the CLI says on stderr when `to` resolves to HEAD (#588: amending that commit orphans the manifest; the release order is tag first, then generate against the tag). `wire-changes/core@7.5.1.json` was regenerated against its tag with 16 informational move rows.
|
|
20
|
+
- `scripts/ship-post-lint.mjs`: a checksum label followed by one markdown backtick still labels the digest (the 7.5.1 post had hand-segmented its shasum to get past this leg).
|
|
21
|
+
- G5 phase-contract floor `KNOWN_INPUT_FIELDS` 371 → 667; unannotated-Input exceptions 4 → 1; receiving-side pins added for the context lane's `charsPerToken` wiring.
|
|
22
|
+
|
|
3
23
|
## 7.5.1 — 2026-09-06
|
|
4
24
|
|
|
5
25
|
Maintenance release: the design/390 runner decomposition (slices S0–S2), the post-7.5.0 gate repairs, and one summary hardening (#566). **No BREAKING change**: the export surface is byte-identical to 7.5.0 (export snapshot unchanged); the only observable delta is the label text of a wedged dispose on the prepare-throw path (below).
|
|
@@ -75,6 +75,11 @@ export declare const A2A_NAMESPACE: ProtocolNamespace<typeof A2A_PREFIX_NAME>;
|
|
|
75
75
|
/** Every protocol namespace the engine knows. APPEND-ONLY: adding a protocol must not change how any
|
|
76
76
|
* existing name resolves — prefixes must stay pairwise non-overlapping (pinned in the table test). */
|
|
77
77
|
export declare const PROTOCOL_TABLE: readonly ProtocolNamespace[];
|
|
78
|
+
/** The namespaced-name shapes the protocol table currently owns, rendered for the two messages that have
|
|
79
|
+
* to name them (the caller-name reservation and the policy audit's unprefixed-name arm). Read from the
|
|
80
|
+
* table rather than written out: a message that hard-codes ONE protocol becomes wrong — while staying
|
|
81
|
+
* green — the moment a second one is appended, and both messages tell a deployment what to write. */
|
|
82
|
+
export declare const NAMESPACED_NAME_SHAPES: string;
|
|
78
83
|
/** The namespace owning `name`, or `undefined` for a caller/first-party tool. */
|
|
79
84
|
export declare function protocolOf(name: string): ProtocolNamespace | undefined;
|
|
80
85
|
export {};
|
|
@@ -37,6 +37,7 @@ const A2A_PREFIX_NAME = `a2a${NAME_SEP}`;
|
|
|
37
37
|
export const MCP_NAMESPACE = makeProtocolNamespace("mcp", MCP_PREFIX_NAME);
|
|
38
38
|
export const A2A_NAMESPACE = makeProtocolNamespace("a2a", A2A_PREFIX_NAME);
|
|
39
39
|
export const PROTOCOL_TABLE = [MCP_NAMESPACE, A2A_NAMESPACE];
|
|
40
|
+
export const NAMESPACED_NAME_SHAPES = PROTOCOL_TABLE.map((ns) => `${ns.prefix}<peer>__<tool>`).join(", ");
|
|
40
41
|
export function protocolOf(name) {
|
|
41
42
|
return PROTOCOL_TABLE.find((ns) => name.startsWith(ns.prefix));
|
|
42
43
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The abort-signal RACING vocabulary the park lane's synchronous-wait seats stand on — the per-call signal
|
|
3
|
+
* composition, the value-mapping race and the tagged three-state race — moved whole out of the orchestrator so
|
|
4
|
+
* the ask-lane phase (adjudicate / resolveAsk), the suspend-saga phase (the paused-VM compensation) and the
|
|
5
|
+
* park-ask phase (the content-ask delivery race, the projection re-adjudication, the fence) reach DOWN for the
|
|
6
|
+
* same three helpers instead of each carrying a copy. Pure: no engine state, no clock.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* design/384 slice 1 — the per-call signal composition all three synchronous-wait seats share
|
|
10
|
+
* (adjudicate / resolveAsk / resolveContentAsk): `AbortSignal.any([run, call])` when the gate
|
|
11
|
+
* threaded a per-call signal, the bare run signal otherwise (host-driven gates and unthreaded emits
|
|
12
|
+
* keep the historical binding byte for byte). The run signal is ALSO composed into the loop's turn
|
|
13
|
+
* controller, so the double source is a belt, not a dependency — this layer must not assume the
|
|
14
|
+
* loop's internal wiring. The composed product lives exactly as long as the one wait it bounds
|
|
15
|
+
* (both sources outlive it), so nothing here retains across calls.
|
|
16
|
+
*/
|
|
17
|
+
export declare function composeCallSignal(runSignal: AbortSignal, callSignal: AbortSignal | undefined): AbortSignal;
|
|
18
|
+
/** design/384 slice 2 — how a raced await ended. The generalized sibling of the ask-side
|
|
19
|
+
* `raceAskWaitAgainstSignal` (tool-policy.ts): same three-state form over any value domain.
|
|
20
|
+
* `aborted` means the SIGNAL settled the race — the promise may still be pending and stays the
|
|
21
|
+
* caller's to detach. `threw` reports the rejection AS a rejection, never folded into the abort
|
|
22
|
+
* arm: the value-mapping `raceAbort` below resolves `onAbort()` for a rejection too, which at a
|
|
23
|
+
* DECISION seat would disguise a real failure as a concession (and feed the concession value into
|
|
24
|
+
* whatever door consumes the raced result) — a decision seat must race with THIS form. */
|
|
25
|
+
export type RacedSettlement<T> = {
|
|
26
|
+
tag: "value";
|
|
27
|
+
value: T;
|
|
28
|
+
} | {
|
|
29
|
+
tag: "threw";
|
|
30
|
+
error: unknown;
|
|
31
|
+
} | {
|
|
32
|
+
tag: "aborted";
|
|
33
|
+
};
|
|
34
|
+
/** design/384 slice 2 — settle a raced await the moment `signal` aborts, WITHOUT waiting for the
|
|
35
|
+
* promise to notice. Pre-aborted entry answers at once; the listener is removed on settle
|
|
36
|
+
* whichever side wins (the `raceAbort` hygiene: `{once}` alone leaks a closure on the signal when
|
|
37
|
+
* the promise settles first). The mapped promise attaches BOTH handlers up front, so an abort-arm
|
|
38
|
+
* return never leaves an unhandled rejection behind. */
|
|
39
|
+
export declare function raceSettlementAgainstSignal<T>(p: Promise<T>, signal: AbortSignal): Promise<RacedSettlement<T>>;
|
|
40
|
+
/** Race a promise against an abort signal; on abort, resolve with `onAbort()`. Cleans up its listener. */
|
|
41
|
+
export declare function raceAbort<T>(p: Promise<T>, signal: AbortSignal, onAbort: () => T): Promise<T>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function composeCallSignal(runSignal, callSignal) {
|
|
2
|
+
return callSignal !== undefined ? AbortSignal.any([runSignal, callSignal]) : runSignal;
|
|
3
|
+
}
|
|
4
|
+
export function raceSettlementAgainstSignal(p, signal) {
|
|
5
|
+
const settled = p.then((value) => ({ tag: "value", value }), (error) => ({ tag: "threw", error }));
|
|
6
|
+
if (signal.aborted)
|
|
7
|
+
return Promise.resolve({ tag: "aborted" });
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
let done = false;
|
|
10
|
+
const finish = (r) => {
|
|
11
|
+
if (done)
|
|
12
|
+
return;
|
|
13
|
+
done = true;
|
|
14
|
+
signal.removeEventListener("abort", onAbortEvent);
|
|
15
|
+
resolve(r);
|
|
16
|
+
};
|
|
17
|
+
const onAbortEvent = () => finish({ tag: "aborted" });
|
|
18
|
+
signal.addEventListener("abort", onAbortEvent, { once: true });
|
|
19
|
+
void settled.then(finish);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function raceAbort(p, signal, onAbort) {
|
|
23
|
+
if (signal.aborted)
|
|
24
|
+
return Promise.resolve(onAbort());
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
let settled = false;
|
|
27
|
+
const finish = (v) => {
|
|
28
|
+
if (settled)
|
|
29
|
+
return;
|
|
30
|
+
settled = true;
|
|
31
|
+
signal.removeEventListener("abort", onAbortEvent);
|
|
32
|
+
resolve(v);
|
|
33
|
+
};
|
|
34
|
+
const onAbortEvent = () => finish(onAbort());
|
|
35
|
+
signal.addEventListener("abort", onAbortEvent, { once: true });
|
|
36
|
+
p.then(finish, () => finish(onAbort()));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The durable-suspend checkpoint
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The durable-suspend checkpoint ROW-IDENTITY vocabulary — the default isolation scope, the ONE derivation of a row's
|
|
3
|
+
* scope, and the ONE reading of the placement fields' empty string — moved whole out of the orchestrator so the
|
|
4
|
+
* wiring-manifest phase (the synthesized durableApproval), the park-lane phases (the mint sites and the suspend stamp),
|
|
5
|
+
* the driver (the restore fold) and runtask (the resume rung) reach DOWN for the same names. Pure: a constant and two
|
|
6
|
+
* string functions.
|
|
5
7
|
*/
|
|
6
8
|
/** design/77 §4.4: the multi-tenant scope used when a durable suspend fires for an IRREVERSIBLE tool in an
|
|
7
9
|
* unattended deployment that did NOT opt into `durableApproval` (so there is no caller-supplied scope). The
|
|
@@ -30,3 +32,13 @@ export declare function checkpointScopeOf(spec: {
|
|
|
30
32
|
};
|
|
31
33
|
principal?: string;
|
|
32
34
|
}): string;
|
|
35
|
+
/**
|
|
36
|
+
* rescan C5 — the ONE spelling of the placement fields' empty-string discipline: `""` is absence
|
|
37
|
+
* wearing clothes (the resume entry's principal-rung posture), and every placement read that must
|
|
38
|
+
* treat it so — the resume rung's two sides (runtask), the restore fold's seed and the suspend
|
|
39
|
+
* stamp — goes through THIS helper, so three sites cannot drift into three readings. Deliberately
|
|
40
|
+
* NOT applied to the live `internals.placementRoot`/`rootSessionId` reads of the RESOLUTION fold:
|
|
41
|
+
* an empty supplied claim there is an assembly error the execution-env phase's `mintPlacementRootSessionId` refuses
|
|
42
|
+
* loudly on the factory path, and normalizing it away would silently repair what should be loud.
|
|
43
|
+
*/
|
|
44
|
+
export declare function placementValueOrAbsent(value: string | undefined): string | undefined;
|
|
@@ -2,3 +2,6 @@ export const DEFAULT_IRREVERSIBLE_SCOPE = "irreversible";
|
|
|
2
2
|
export function checkpointScopeOf(spec) {
|
|
3
3
|
return spec.durableApproval?.scope || spec.principal || DEFAULT_IRREVERSIBLE_SCOPE;
|
|
4
4
|
}
|
|
5
|
+
export function placementValueOrAbsent(value) {
|
|
6
|
+
return value || undefined;
|
|
7
|
+
}
|
|
@@ -3,7 +3,7 @@ import type { MaybeCompactOptions } from "../auto-compaction.js";
|
|
|
3
3
|
import type { StaleToolResultOffloadOptions, TaskSpec } from "../types.js";
|
|
4
4
|
import type { Context } from "../../internal/llm.js";
|
|
5
5
|
import { type ToolResultStore } from "../tool-result-store.js";
|
|
6
|
-
import type { Prepared } from "./
|
|
6
|
+
import type { Prepared } from "./contracts.js";
|
|
7
7
|
/**
|
|
8
8
|
* REF-A2 (car R11) — the maybeCompact call arguments that MUST be assembled identically on all three
|
|
9
9
|
* of runtask's compaction lanes (turn boundary / prompt-too-long recovery / end-of-task).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The content-ask BINDING vocabulary: the leak bound the routing lane checks against and the one entry shape the
|
|
3
|
+
* binding table holds. It lived in the question-face phase (which mints the table) while the routing lane was still
|
|
4
|
+
* orchestrator code; the lane is a phase now, and a phase reaching SIDEWAYS into a sibling for a constant and a type
|
|
5
|
+
* is the wrong reading — both phases reach DOWN for them here.
|
|
6
|
+
*/
|
|
7
|
+
import type { QuestionAnswer } from "../ask-question.js";
|
|
8
|
+
export declare const CONTENT_ASK_BINDING_CAP = 32;
|
|
9
|
+
/** One gate-routed content-ask outcome, keyed by the call id it was produced for (see the table's own note). */
|
|
10
|
+
export type ContentAskBinding = ({
|
|
11
|
+
kind: "answered";
|
|
12
|
+
answer: QuestionAnswer;
|
|
13
|
+
} | {
|
|
14
|
+
kind: "failed";
|
|
15
|
+
error: unknown;
|
|
16
|
+
}) & {
|
|
17
|
+
/** Digest of the exact question batch this outcome belongs to. The call id alone is not enough
|
|
18
|
+
* to redeem a binding: it comes from the model/provider and can repeat, so an entry that
|
|
19
|
+
* outlived its call (its call was allowed but never executed) could otherwise be handed to a
|
|
20
|
+
* LATER call that happens to reuse the id. Matching on the batch as well means an outcome can
|
|
21
|
+
* only ever be delivered for the question it was produced for. */
|
|
22
|
+
questionsHash: string;
|
|
23
|
+
/** The engine-minted per-delivery identity (see AskQuestionRequest.deliveryId) — carried so a
|
|
24
|
+
* stranded-answer disclosure names DELIVERIES, not call ids: call ids can repeat, and a
|
|
25
|
+
* disclosure keyed on them would collapse two lost answers into one. */
|
|
26
|
+
deliveryId: string;
|
|
27
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CONTENT_ASK_BINDING_CAP = 32;
|
|
@@ -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, CheckpointGate, 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
|
|
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
|
*/
|
|
@@ -745,7 +746,10 @@ export interface Prepared {
|
|
|
745
746
|
* · the driver — pushes CronCreate/CronDelete/CronList (write hands; inert without a daemon);
|
|
746
747
|
* · lsp — pushes the LSP tool (manager wired);
|
|
747
748
|
* · 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)
|
|
749
|
+
* · defer-classify — SPLICES OUT the excluded names and the retracted pair (the only removals at prepare);
|
|
750
|
+
* · memory-engine-session — REPLACES entries in place with the content-origin wrappers when it arms (a mounted
|
|
751
|
+
* memory session or a recorder channel); the refresh seam re-runs the same pass through `contentOriginWrapRef`
|
|
752
|
+
* after a re-splice (identity-tracked, so a wrapper is never wrapped again).
|
|
749
753
|
* tool-disclosure and listings only READ it (`harnessTools` is a derived list; placeholders never enter here). */
|
|
750
754
|
tools: AgentTool[];
|
|
751
755
|
/** Name→effect map for every tool this task can call (design/44 §3). Used by the abort-path orphan
|
|
@@ -962,7 +966,7 @@ export interface Prepared {
|
|
|
962
966
|
* ANNOUNCED model catalog the same way (advanced only when a committed frame carried one), so a
|
|
963
967
|
* catalog change across a suspend is re-announced as a "Models available…" drift line.
|
|
964
968
|
* `mcpFailed` / `advisories` are the once-per-SESSION arms of the same snapshot (see
|
|
965
|
-
*
|
|
969
|
+
* announce-once-ledger.ts): seeded at prepare from the branch's record, advanced by the run loop
|
|
966
970
|
* on intact delivery of the tools_delta `failedServers` arm / the mcp_dropped_tools frame. */
|
|
967
971
|
announcedListingsRef: AnnouncedListingSnapshotInput;
|
|
968
972
|
/** env-tail migration (#254 shape) — the git-status frame lane's run-local state: this leg's
|
|
@@ -1318,6 +1322,25 @@ export interface InheritedGate {
|
|
|
1318
1322
|
* them; the engine does not defend against it. The untrusted surface is {@link TaskSpec}, whose pre-first-
|
|
1319
1323
|
* await latch is a separate, pinned guarantee (prepare-task-phase-pins T1).
|
|
1320
1324
|
*/
|
|
1325
|
+
/** The runtime feature flags the stable prompt's harness-context block gates its sections on
|
|
1326
|
+
* (design/64 §6.3: claim only what the task has). Spread into the assembly's StablePromptContext
|
|
1327
|
+
* by the prompt-assembly phase, which re-derives the two orchestration flags against the post-exclusion roster. */
|
|
1328
|
+
export interface PromptFeatureFlags {
|
|
1329
|
+
policyEnabled: boolean;
|
|
1330
|
+
hooksEnabled: boolean;
|
|
1331
|
+
isolationEnabled: boolean;
|
|
1332
|
+
reminderMark: string;
|
|
1333
|
+
readFaceOpen: boolean;
|
|
1334
|
+
orchestrationEnabled: boolean;
|
|
1335
|
+
orchestrationDeferred: boolean;
|
|
1336
|
+
promptProfile: "simple" | "classic";
|
|
1337
|
+
fableMitigations: boolean;
|
|
1338
|
+
goalEnabled: boolean;
|
|
1339
|
+
awarenessEnabled: boolean;
|
|
1340
|
+
worktreeIsolated: boolean;
|
|
1341
|
+
withinTaskCompactionEnabled: boolean;
|
|
1342
|
+
isSubagent: boolean;
|
|
1343
|
+
}
|
|
1321
1344
|
export interface RunInternals {
|
|
1322
1345
|
/** The live repair bundle from a `runRepairLoop` attempt in flight (attemptCount>0). Serialized onto a
|
|
1323
1346
|
* checkpoint minted MID-attempt so a resume re-seeds it; undefined for any non-repair run. */
|
|
@@ -1876,3 +1899,92 @@ export interface ResolvedWorkspace {
|
|
|
1876
1899
|
* which `cwd` is a host path a consumer may diff / merge / remove. */
|
|
1877
1900
|
remote: boolean;
|
|
1878
1901
|
}
|
|
1902
|
+
/**
|
|
1903
|
+
* #457 ② — the resolver's answer, with the WITHHELD cause beside the value.
|
|
1904
|
+
*
|
|
1905
|
+
* The old signature returned a bare `unknown`, which collapsed three different worlds into one
|
|
1906
|
+
* `undefined`: "this tool declares no preview at all", "the projection threw / could not be
|
|
1907
|
+
* serialized", and "the projection declined to speak for these args". The middle one is the case an
|
|
1908
|
+
* approval surface must be told about — a person is being asked to approve something whose display
|
|
1909
|
+
* projection could NOT be produced — and it was indistinguishable from the other two.
|
|
1910
|
+
*
|
|
1911
|
+
* `withheld` names the cause; it is ORTHOGONAL to `preview` presence, deliberately: the oversize arm
|
|
1912
|
+
* still ships its truncation stub (a surface that renders it tells the person the size, which is more
|
|
1913
|
+
* useful than nothing), and the stub alone never carried the fact that the real projection is missing.
|
|
1914
|
+
*/
|
|
1915
|
+
export interface ResolvedApprovalPreview {
|
|
1916
|
+
/** The display projection, when one could be produced (or the oversize truncation stub). */
|
|
1917
|
+
preview?: unknown;
|
|
1918
|
+
/** Present ⇔ the tool DECLARED a preview and the gate could not present a usable one:
|
|
1919
|
+
* `"oversize"` — produced but past the 16KiB display bound (the stub rides `preview`);
|
|
1920
|
+
* `"unavailable"` — the projection threw, or its value has no JSON serialization.
|
|
1921
|
+
* ABSENT covers both "no preview declared" and "the projection returned nothing to show" — neither
|
|
1922
|
+
* is a withholding, and claiming one would make every preview-less tool look suppressed. */
|
|
1923
|
+
withheld?: "oversize" | "unavailable";
|
|
1924
|
+
}
|
|
1925
|
+
/** The in-stream ask lane: the six closures the park closure and the gate station consume. Built as ONE value so
|
|
1926
|
+
* "the lane exists" is one fact, not six independently-undefined seats. */
|
|
1927
|
+
export interface AskLane {
|
|
1928
|
+
/** The shared run+call signal composition, bound to this task's run signal once. */
|
|
1929
|
+
composedCallSignal: (callSignal: AbortSignal | undefined) => AbortSignal;
|
|
1930
|
+
/** The abort-bound policy check, or undefined when no policy is wired (→ the gate reads allow). */
|
|
1931
|
+
adjudicate: ((req: ToolCallRequest, callSignal?: AbortSignal) => Promise<PermissionResult>) | undefined;
|
|
1932
|
+
/** The approval display projection over the live roster (alias-aware; oversize/unavailable named). */
|
|
1933
|
+
approvalPreviewOf: (toolName: string, args: unknown) => ResolvedApprovalPreview;
|
|
1934
|
+
/** THE synchronous ask mint: resolves a surviving `ask` through the live seat, books the ledger. */
|
|
1935
|
+
resolveAskBound: (decision: PermissionResult, req: ToolCallRequest, callSignal?: AbortSignal) => Promise<ResolvedAsk>;
|
|
1936
|
+
/** The pre-wrapped deny observer, or undefined when the deployment wired no `permissionDenied` hook. */
|
|
1937
|
+
notifyPermissionDenied: ((payload: PermissionDeniedPayload) => Promise<void>) | undefined;
|
|
1938
|
+
/** The deployment-facing half of a crashing PreToolUse hook (swallow-guarded). */
|
|
1939
|
+
notifyHookError: (err: unknown) => void;
|
|
1940
|
+
}
|
|
1941
|
+
/** How the put-commit + put-failure saga ended (see `commitSuspendSaga` inside the phase for each arm's meaning). */
|
|
1942
|
+
export type SuspendSagaOutcome = {
|
|
1943
|
+
tag: "committed";
|
|
1944
|
+
} | {
|
|
1945
|
+
tag: "cut";
|
|
1946
|
+
} | {
|
|
1947
|
+
tag: "absent";
|
|
1948
|
+
reason?: string;
|
|
1949
|
+
} | {
|
|
1950
|
+
tag: "unknown";
|
|
1951
|
+
reason: string;
|
|
1952
|
+
} | {
|
|
1953
|
+
tag: "compensation_failed";
|
|
1954
|
+
reason: string;
|
|
1955
|
+
};
|
|
1956
|
+
/** The durable settlement machinery the boundary parks and the park closure consume. Built as ONE value so "the saga
|
|
1957
|
+
* exists" is one fact, not eight independently-undefined seats. */
|
|
1958
|
+
export interface SuspendSaga {
|
|
1959
|
+
/** THIS leg's spend so far in micro-USD (own + nested delta), read at mint time. */
|
|
1960
|
+
inFlightSpendMicroUsd: () => number;
|
|
1961
|
+
/** The §4.bis correctness-state whitelist, serialized from the live values at suspend. */
|
|
1962
|
+
serializeCheckpointState: (workspaceHandle: WorkspaceHandle | undefined, parkedSpendMicroUsd?: number) => SerializedCheckpointState;
|
|
1963
|
+
/** The shared paused-VM compensation, bound to this task's failure ledger and error face. */
|
|
1964
|
+
compensatePausedVM: (remoteEnv: RemoteExecutionEnv, snapshotId: SnapshotId) => Promise<{
|
|
1965
|
+
ok: true;
|
|
1966
|
+
} | {
|
|
1967
|
+
ok: false;
|
|
1968
|
+
reason: string;
|
|
1969
|
+
}>;
|
|
1970
|
+
/** The put-commit + put-failure split — the point of no return, tagged. */
|
|
1971
|
+
commitSuspendSaga: (token: CheckpointToken, cp: Checkpoint, remoteEnv: RemoteExecutionEnv | undefined, remoteHandle: WorkspaceHandle | undefined, cutSignal?: AbortSignal) => Promise<SuspendSagaOutcome>;
|
|
1972
|
+
/** The shared suspend-loop-cap guard (safety state first, diagnostic second). */
|
|
1973
|
+
suspendLoopCapHit: (count: number, cap: number | undefined, detail: string) => boolean;
|
|
1974
|
+
/** The `suspendVM` target by CAPABILITY, or undefined. */
|
|
1975
|
+
suspendableEnv: RemoteExecutionEnv | undefined;
|
|
1976
|
+
/** The park-only remote target (non-suspendable, externally durable), or undefined. */
|
|
1977
|
+
parkOnlyRemoteEnv: RemoteExecutionEnv | undefined;
|
|
1978
|
+
/** The park-only identity capture (snapshotId stripped, `restoreMode: "park_only"`). */
|
|
1979
|
+
parkOnlyHandle: (env: RemoteExecutionEnv) => WorkspaceHandle;
|
|
1980
|
+
}
|
|
1981
|
+
/** The park closure and the content-ask routing closure the gate station hands to `runToolGate`. Built as ONE value so
|
|
1982
|
+
* "the park lane's closures exist" is one fact; `suspendAsk` stays optional INSIDE it because the park predicate (a store,
|
|
1983
|
+
* and something to park for) is a narrower condition than the gate machinery's activation. */
|
|
1984
|
+
export interface ParkAsk {
|
|
1985
|
+
/** design/174 — the gate's content-ask routing closure (total by construction; answers `unavailable` with no live face). */
|
|
1986
|
+
resolveContentAsk: NonNullable<ToolGateInput["resolveContentAsk"]>;
|
|
1987
|
+
/** design/45 — the durable park closure (twelve positional seats then the structural carry), or undefined when the park
|
|
1988
|
+
* lane is not armed (the gate then keeps the synchronous onAsk path). */
|
|
1989
|
+
suspendAsk: ToolGateInput["suspendAsk"];
|
|
1990
|
+
}
|
|
@@ -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,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;
|