@sema-agent/core 5.63.0 → 5.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/dist/agents/cascade.d.ts +5 -1
- package/dist/agents/cascade.js +6 -1
- package/dist/agents/subagent.js +1 -0
- package/dist/agents/verify.d.ts +5 -1
- package/dist/agents/verify.js +5 -2
- package/dist/core/checkpoint-store.d.ts +5 -1
- package/dist/core/fs-write-gate-policy.d.ts +21 -0
- package/dist/core/fs-write-gate-policy.js +14 -3
- package/dist/core/remote-env.d.ts +34 -2
- package/dist/core/runner/prepare-task.d.ts +20 -0
- package/dist/core/runner/prepare-task.js +12 -3
- package/dist/core/runner/prepare-workspace-restore.js +13 -0
- package/dist/core/runner/runtask.js +23 -17
- package/dist/core/types.d.ts +13 -3
- package/dist/core/usage-window-store.d.ts +44 -12
- package/dist/core/usage-window-store.js +11 -3
- package/dist/core/workflow-run-store-contract.js +17 -0
- package/dist/core/workflow-run-store.d.ts +22 -1
- package/dist/core/workflow-run-store.js +1 -0
- package/dist/engine/harness/agent-harness.d.ts +8 -3
- package/dist/engine/harness/agent-harness.js +9 -4
- package/dist/engine/harness/types.d.ts +89 -3
- package/dist/engine/loop/agent-loop.js +39 -15
- package/dist/engine/loop/types.d.ts +43 -22
- package/dist/index.d.ts +1 -1
- package/dist/internal/harness-types.d.ts +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +6 -0
- package/dist/orchestration/run-workflow-tool.js +1 -0
- package/dist/orchestration/workflow.d.ts +6 -0
- package/dist/orchestration/workflow.js +1 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +44 -1
- package/dist/tools/fs/bash-readonly-classifier.js +132 -5
- package/dist/tools/fs/fs-bash.js +9 -2
- package/dist/tools/fs/fs-write.js +19 -8
- package/dist/tools/fs/index.d.ts +1 -0
- package/dist/tools/fs/index.js +1 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.64.0 — 2026-08-27
|
|
4
|
+
|
|
5
|
+
### BREAKING
|
|
6
|
+
- **design/380 batch 1 — device-lane placement identity**: `ExecutionEnvFactoryContext` grows a
|
|
7
|
+
REQUIRED `placementRootSessionId` (the run TREE's fixed point, minted once as
|
|
8
|
+
`placementRoot ?? rootSessionId ?? sessionId`; an empty resolved value is refused loudly as
|
|
9
|
+
`config.placement_root_invalid` before the factory is ever called). Deployment code that
|
|
10
|
+
CONSTRUCTS its own factory context gains a compile-time break — factory *implementors* are purely
|
|
11
|
+
additive. The fixed point threads through cascade rungs, verification legs and every spawn family
|
|
12
|
+
(subagent, workflow), so a descendant's fresh session id can no longer read as a new placement;
|
|
13
|
+
durable-resume carriage is the next batch's seat (`O1③`) and its current fallback is pinned in the
|
|
14
|
+
tree so landing it must flip the pin consciously.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- `WorkspaceHandle.deviceId` — explicit, minter-stated identity of the target a workspace lives on
|
|
18
|
+
(rides the checkpoint's all-string handle whitelist unchanged).
|
|
19
|
+
- `outcome_unknown` joins `ExecutionErrorCode`, `FileErrorCode` and `RemoteExecutionErrorCode`, and
|
|
20
|
+
`target_unavailable` joins `ExecutionErrorCode`: an op COMMITTED to a remote target whose outcome
|
|
21
|
+
is unknowable must never be auto-retried (it is deliberately NOT in
|
|
22
|
+
`RETRYABLE_REMOTE_ERROR_CODES`), while a never-started commit refusal is retry-safe by contract.
|
|
23
|
+
`precondition_failed` joins `FileErrorCode` for the guarded write below. **Downstream exhaustive
|
|
24
|
+
switches over these unions will red — re-pin per the new members.**
|
|
25
|
+
- `FileSystem.writeFileGuarded?` (+ `WriteExpectation` / `WriteReceipt`, both type-only exports):
|
|
26
|
+
optional atomic verify-and-write with a receipt of the real on-disk object, carrying
|
|
27
|
+
`writeFileExclusive`'s degradation law verbatim (a backend without the primitive leaves it
|
|
28
|
+
`undefined` rather than emulating it). New deployment knob `requireGuardedWrite` (default off,
|
|
29
|
+
byte-identical): armed, a covered write on an env lacking the capability is a typed refusal
|
|
30
|
+
instead of a silent fallback.
|
|
31
|
+
- `UsageWindow.maxTokens` becomes optional (#480): a window may govern by `maxCostUsd` alone. A
|
|
32
|
+
window declaring NEITHER ceiling refuses loudly, the exhaustion arithmetic reads declared axes
|
|
33
|
+
only, and a `maxCostUsd` whose micro-USD conversion overflows is refused at the door instead of
|
|
34
|
+
being admitted and never binding (**named narrowing**: a dual window declaring such a ceiling
|
|
35
|
+
previously ran with its money arm silently dead).
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
- **#477 — the `before_agent_start` replacement prompt is run-scoped**: the between-turn rebuild
|
|
39
|
+
now carries it, so a run's prompt is byte-stable across its three legal change points (run start /
|
|
40
|
+
compaction-boundary epoch adoption / clear) instead of silently reverting to the base prompt from
|
|
41
|
+
turn 2 (which also broke prefix-cache stability). Deployments with no handler — or whose handler
|
|
42
|
+
returns no `systemPrompt` — are byte-identical.
|
|
43
|
+
- **#482 / #486 — read-only command judgment, both directions**: the allowlist grows to CC's safe
|
|
44
|
+
command set (with per-verb availability guards), a classify-only superset admits `find`/`sed`/`cd`
|
|
45
|
+
behind CC's own guard grammars, and compound judgment now THREADS the base a `cd` actually moves —
|
|
46
|
+
every segment's containment *and* deny resolve against the base bash will have, and a poll-loop
|
|
47
|
+
body containing a `cd` is judged over its worst-case iterated shift. Two escapes are closed (an
|
|
48
|
+
iterated `cd ..` loop climbing out of the read roots; a `cd` eating the first segment of a
|
|
49
|
+
multi-segment deny pattern). **This is a judgment fix, not a narrowing**: legitimately-configured
|
|
50
|
+
full-read deployments keep zero-ask on the same commands, pinned by positive controls carrying the
|
|
51
|
+
same weight as the escape pins.
|
|
52
|
+
- **#473** the resume verb snapshots its config once at the door (a mutated bag can no longer make
|
|
53
|
+
the identity gate and the resumed leg disagree); **#474** the prompt-too-long recovery lane adopts
|
|
54
|
+
the rebuilt prompt and honours the turn-scoped abort; **#470/#475** a workflow run that overran
|
|
55
|
+
its token ceiling discloses it on the list projection as well as the full record; **#468**
|
|
56
|
+
`SubagentSteerHandle.steer` gains the `inputId` pass-through its workflow twin already had.
|
|
57
|
+
|
|
58
|
+
|
|
3
59
|
## 5.63.0 — 2026-08-27
|
|
4
60
|
|
|
5
61
|
### BREAKING
|
package/dist/agents/cascade.d.ts
CHANGED
|
@@ -199,6 +199,10 @@ export declare function runCascade(runner: Runner, spec: TaskSpec, config: Casca
|
|
|
199
199
|
* what teardown does to the workspace is the factory's business.
|
|
200
200
|
*
|
|
201
201
|
* Absent ⇒ every rung is dispatched exactly as before (the call site passes `undefined`, which is
|
|
202
|
-
* what an omitted optional argument already was)
|
|
202
|
+
* what an omitted optional argument already was) — with ONE design/380 O1② placement delta either
|
|
203
|
+
* way: when neither `placementRoot` nor `rootSessionId` is present, rungs after the first are
|
|
204
|
+
* dispatched with a fresh shallow copy carrying `placementRoot` = the first rung's resolved
|
|
205
|
+
* sessionId (the ladder's placement fixed point); every other field still rides by the same
|
|
206
|
+
* reference, and an internals object that already carries a fixed point is forwarded verbatim.
|
|
203
207
|
*/
|
|
204
208
|
internals?: RunInternals): Promise<CascadeRunResult>;
|
package/dist/agents/cascade.js
CHANGED
|
@@ -57,6 +57,7 @@ export async function runCascade(runner, spec, config, internals) {
|
|
|
57
57
|
});
|
|
58
58
|
let lastResult;
|
|
59
59
|
let passedRung = -1;
|
|
60
|
+
let ladderPlacementRoot;
|
|
60
61
|
const { sessionId: _drop, ...specBase } = spec;
|
|
61
62
|
for (let i = 0; i < maxRungs; i++) {
|
|
62
63
|
if (deadlineAt !== undefined && Date.now() >= deadlineAt)
|
|
@@ -81,14 +82,18 @@ export async function runCascade(runner, spec, config, internals) {
|
|
|
81
82
|
? AbortSignal.any([specBase.signal, rungAbort.signal])
|
|
82
83
|
: rungAbort.signal;
|
|
83
84
|
let result;
|
|
85
|
+
const rungInternals = ladderPlacementRoot === undefined || internals?.placementRoot !== undefined || internals?.rootSessionId !== undefined
|
|
86
|
+
? internals
|
|
87
|
+
: { ...internals, placementRoot: ladderPlacementRoot };
|
|
84
88
|
try {
|
|
85
|
-
result = await runner.runTask({ ...specBase, model: rung.model, ...(rung.overrides ?? {}), ...(rungSignal ? { signal: rungSignal } : {}) },
|
|
89
|
+
result = await runner.runTask({ ...specBase, model: rung.model, ...(rung.overrides ?? {}), ...(rungSignal ? { signal: rungSignal } : {}) }, rungInternals);
|
|
86
90
|
}
|
|
87
91
|
finally {
|
|
88
92
|
if (deadlineTimer !== undefined)
|
|
89
93
|
clearTimeout(deadlineTimer);
|
|
90
94
|
}
|
|
91
95
|
lastResult = result;
|
|
96
|
+
ladderPlacementRoot ??= internals?.placementRoot ?? internals?.rootSessionId ?? result.sessionId;
|
|
92
97
|
const s = result.stats;
|
|
93
98
|
const rungCostKnown = s.costMicroUsd !== undefined && (s.nested === undefined || s.nested.costMicroUsd !== undefined);
|
|
94
99
|
const rungCost = rungCostKnown ? (s.costMicroUsd ?? 0) + (s.nested?.costMicroUsd ?? 0) : undefined;
|
package/dist/agents/subagent.js
CHANGED
|
@@ -1967,6 +1967,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1967
1967
|
...(ctx.taskId !== undefined ? { parentTaskId: ctx.taskId } : {}),
|
|
1968
1968
|
...(ctx.sessionId !== undefined ? { parentSessionId: ctx.sessionId } : {}),
|
|
1969
1969
|
...((ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}),
|
|
1970
|
+
...(ctx.placementRoot !== undefined ? { placementRoot: ctx.placementRoot } : {}),
|
|
1970
1971
|
}),
|
|
1971
1972
|
...(ctx.centerArtifactDigest !== undefined ? { parentCenterArtifactDigest: ctx.centerArtifactDigest } : {}),
|
|
1972
1973
|
...(ctx.centerSourceRevision !== undefined ? { parentCenterSourceRevision: ctx.centerSourceRevision } : {}),
|
package/dist/agents/verify.d.ts
CHANGED
|
@@ -287,7 +287,11 @@ export declare function verifyCompleted(runner: Runner, result: TaskResult, spec
|
|
|
287
287
|
* refusal does and does not undo.
|
|
288
288
|
*
|
|
289
289
|
* Absent ⇒ every leg is dispatched exactly as before (each call site passes `undefined`, which is
|
|
290
|
-
* what an omitted optional argument already was)
|
|
290
|
+
* what an omitted optional argument already was) — with ONE design/380 O1② placement delta either
|
|
291
|
+
* way: when neither `placementRoot` nor `rootSessionId` is present, the verifier and fix legs are
|
|
292
|
+
* dispatched with a fresh shallow copy carrying `placementRoot` = the entry implementation leg's
|
|
293
|
+
* sessionId (the gate's placement fixed point); every other field still rides by the same
|
|
294
|
+
* reference, and an internals object that already carries a fixed point is forwarded verbatim.
|
|
291
295
|
*/
|
|
292
296
|
internals?: RunInternals): Promise<VerificationResult>;
|
|
293
297
|
/**
|
package/dist/agents/verify.js
CHANGED
|
@@ -129,6 +129,9 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
|
|
|
129
129
|
};
|
|
130
130
|
const evidenceMode = config.evidence != null && config.evidence.trim() !== "";
|
|
131
131
|
const verifierPrompt = config.verifierPrompt ?? (evidenceMode ? STATIC_VERIFICATION_PROMPT : VERIFICATION_PROMPT);
|
|
132
|
+
const legInternals = () => internals?.placementRoot !== undefined || internals?.rootSessionId !== undefined
|
|
133
|
+
? internals
|
|
134
|
+
: { ...internals, placementRoot: result.sessionId };
|
|
132
135
|
const runVerifier = async (impl, round) => {
|
|
133
136
|
refuseUnhonorableInternals(internals, "before_leg");
|
|
134
137
|
const roundEvidence = round === 1 ? config.evidence : undefined;
|
|
@@ -162,7 +165,7 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
|
|
|
162
165
|
...(specBase.clientContext !== undefined ? { clientContext: { ...specBase.clientContext } } : {}),
|
|
163
166
|
...(specBase.promptProfile !== undefined ? { promptProfile: specBase.promptProfile } : {}),
|
|
164
167
|
signal: specBase.signal,
|
|
165
|
-
},
|
|
168
|
+
}, legInternals());
|
|
166
169
|
try {
|
|
167
170
|
const cost = (v.stats.costMicroUsd ?? 0) + (v.stats.nested?.costMicroUsd ?? 0);
|
|
168
171
|
return { verdict: v.structuredOutput, cost, ...(isDurablePause(v.status) ? { paused: v } : {}) };
|
|
@@ -242,7 +245,7 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
|
|
|
242
245
|
objective: fixObjective(outcome.findings),
|
|
243
246
|
...(foldedRootsFace ? { readFace: "roots" } : {}),
|
|
244
247
|
...(foldedReadDeny.length > 0 ? { readDenyPatterns: [...foldedReadDeny] } : {}),
|
|
245
|
-
},
|
|
248
|
+
}, legInternals());
|
|
246
249
|
spend += (current.stats.costMicroUsd ?? 0) + (current.stats.nested?.costMicroUsd ?? 0);
|
|
247
250
|
implAccount.add(current.stats);
|
|
248
251
|
if (isDurablePause(current.status)) {
|
|
@@ -349,7 +349,11 @@ export interface PendingSteerEntry {
|
|
|
349
349
|
seq: number;
|
|
350
350
|
/** Caller-supplied correlation/idempotency id (a service passes the message id it already minted);
|
|
351
351
|
* a uuidv7 is minted when absent. Re-appending the SAME `inputId` is a no-op, which is what keeps
|
|
352
|
-
* `setPendingSteer` retry-safe now that it appends instead of overwriting.
|
|
352
|
+
* `setPendingSteer` retry-safe now that it appends instead of overwriting. Because the id lives on
|
|
353
|
+
* the PERSISTED entry and every replayed drain of this row carries it verbatim, `(notice code,
|
|
354
|
+
* sessionId, detail.inputId)` is the parked family's cross-replay OCCURRENCE key — consumers dedup
|
|
355
|
+
* on it directly; no per-delivery key exists or is needed. (Write-path guarantee: a row written by
|
|
356
|
+
* a non-conforming store may lack it, and read points do not re-validate.) */
|
|
353
357
|
inputId: string;
|
|
354
358
|
/** Carried VERBATIM for the serving layer; it does NOT reorder the drain (drain is `seq` order). */
|
|
355
359
|
priority?: SystemInjectionPriority;
|
|
@@ -38,6 +38,27 @@ export interface FsWriteGatePolicyOptions {
|
|
|
38
38
|
* asks for an UNRESOLVABLE target fire before any canonical key exists, so they are never exempted.
|
|
39
39
|
*/
|
|
40
40
|
isExempt?: (toolName: string, canonicalPath: string) => boolean | Promise<boolean>;
|
|
41
|
+
/**
|
|
42
|
+
* design/380 O13 — the deployment HARD-CLOSE knob for guarded writes. Armed (`true`), a covered
|
|
43
|
+
* write whose env does NOT provide `writeFileGuarded` is REFUSED (deny, typed message) — never the
|
|
44
|
+
* plain-writeFile fallback: for a deployment that exists to close the adjudicate→write TOCTOU
|
|
45
|
+
* window, a silent advisory downgrade would open the hole right beside its patch. The version-skew
|
|
46
|
+
* arm is the same refusal (an older adapter without the method IS an env without the capability).
|
|
47
|
+
* Default off = every verdict ACTION exactly as before (the capability's ABSENCE is still disclosed
|
|
48
|
+
* on the ask face either way — see the advisory sentence at the ask sites). The knob lives on the
|
|
49
|
+
* deployment's gate options, not in core-global state: core stays lane-agnostic (no device concept
|
|
50
|
+
* here); the deployment that runs guarded-capable envs arms it.
|
|
51
|
+
*
|
|
52
|
+
* WHAT ARMED DOES AND DOES NOT CLOSE (honesty boundary, codex r1-1): armed guarantees every covered
|
|
53
|
+
* write lands through an ATOMIC precondition-verified backend step — the tool's own
|
|
54
|
+
* canonicalize→write window is closed. It does NOT yet bind THIS GATE's adjudicated canonical key
|
|
55
|
+
* across an approval wait: the tool re-canonicalizes at execute time, so a symlink swapped between
|
|
56
|
+
* this gate's verdict and the tool's execution is re-resolved (and then atomically verified against
|
|
57
|
+
* the NEW resolution). Closing that remaining window needs the adjudicated key carried through a
|
|
58
|
+
* trusted approval receipt into tool execution — a separate seat, deliberately not smuggled into
|
|
59
|
+
* this knob.
|
|
60
|
+
*/
|
|
61
|
+
requireGuardedWrite?: boolean;
|
|
41
62
|
}
|
|
42
63
|
/**
|
|
43
64
|
* Build the file-write approval gate policy. Mount via `combinePolicies` alongside the deployment's
|
|
@@ -6,18 +6,29 @@ export function createFsWriteGatePolicy(opts) {
|
|
|
6
6
|
const gated = PATH_CONFINABLE_WRITE_TOOLS;
|
|
7
7
|
const acceptDirs = opts.acceptDirs && opts.acceptDirs.length > 0 ? opts.acceptDirs : undefined;
|
|
8
8
|
const exemptDirs = opts.exemptDirs && opts.exemptDirs.length > 0 ? opts.exemptDirs : undefined;
|
|
9
|
+
const guardedCapable = env.writeFileGuarded !== undefined;
|
|
10
|
+
const advisory = guardedCapable
|
|
11
|
+
? ""
|
|
12
|
+
: " (advisory adjudication: this environment provides no guarded write, so approval is judged before a separate, non-atomic write step)";
|
|
9
13
|
return {
|
|
10
14
|
async check(req, signal) {
|
|
11
15
|
const canonical = req.toolName;
|
|
12
16
|
if (!gated.has(canonical))
|
|
13
17
|
return { action: "allow" };
|
|
18
|
+
if (opts.requireGuardedWrite === true && !guardedCapable) {
|
|
19
|
+
return {
|
|
20
|
+
action: "deny",
|
|
21
|
+
message: `write tool "${req.toolName}" refused: this deployment requires guarded (atomic precondition-verified) writes (requireGuardedWrite), but the execution environment provides no writeFileGuarded capability — refusing rather than falling back to a non-atomic write`,
|
|
22
|
+
decisionReason: "rule",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
14
25
|
const path = writeTargetPath(canonical, req.args);
|
|
15
26
|
if (path === undefined) {
|
|
16
|
-
return ask(`write tool "${req.toolName}" requires approval: the call has no resolvable path target to confine`);
|
|
27
|
+
return ask(`write tool "${req.toolName}" requires approval: the call has no resolvable path target to confine${advisory}`);
|
|
17
28
|
}
|
|
18
29
|
const canon = await canonicalizeTarget(env, path, signal, req.cwd ?? rootPath);
|
|
19
30
|
if (!canon.ok) {
|
|
20
|
-
return ask(`write to "${path}" requires approval: its real target could not be resolved (${canon.message})`);
|
|
31
|
+
return ask(`write to "${path}" requires approval: its real target could not be resolved (${canon.message})${advisory}`);
|
|
21
32
|
}
|
|
22
33
|
for (const dirs of [exemptDirs, acceptDirs]) {
|
|
23
34
|
if (!dirs)
|
|
@@ -46,7 +57,7 @@ export function createFsWriteGatePolicy(opts) {
|
|
|
46
57
|
}
|
|
47
58
|
if (defaultWrite === "allow")
|
|
48
59
|
return { action: "allow" };
|
|
49
|
-
return ask(`approve write to "${path}"
|
|
60
|
+
return ask(`approve write to "${path}"?${advisory}`);
|
|
50
61
|
},
|
|
51
62
|
};
|
|
52
63
|
}
|
|
@@ -58,6 +58,13 @@ export interface WorkspaceHandle {
|
|
|
58
58
|
* `snapshotId` as corruption (fail-closed) unless the resumed env is itself non-suspendable
|
|
59
59
|
* (tolerance for park handles minted by 1.257.1 before this field existed). */
|
|
60
60
|
restoreMode?: "park_only";
|
|
61
|
+
/**
|
|
62
|
+
* design/380 O1④ — explicit identity of the physical/target device this workspace lives on
|
|
63
|
+
* (device lane; an SSH/ADB adapter MAY also stamp it). MINTER-STATED: consumers read this field,
|
|
64
|
+
* never parse an identity out of `sandboxId`. Plain string → rides CheckpointState.workspaceHandle
|
|
65
|
+
* (all-string whitelist) with zero schema movement.
|
|
66
|
+
*/
|
|
67
|
+
deviceId?: string;
|
|
61
68
|
}
|
|
62
69
|
/**
|
|
63
70
|
* A reference to a secret injected at {@link RemoteExecutionEnv.connect}/{@link RemoteExecutionEnv.postResumeInit}
|
|
@@ -213,6 +220,16 @@ export type RemoteExecutionErrorCode =
|
|
|
213
220
|
* else a retry whitelist misses them.
|
|
214
221
|
*/
|
|
215
222
|
| "transport_lost"
|
|
223
|
+
/**
|
|
224
|
+
* design/380 O12 — the op was COMMITTED to the remote target and its outcome is UNKNOWABLE (the target
|
|
225
|
+
* went unreachable / restarted / was revoked after dispatch-commit; an execStream cut where the target
|
|
226
|
+
* protocol's own server has ruled the outcome unknowable). NEVER auto-retried and deliberately NOT in
|
|
227
|
+
* {@link RETRYABLE_REMOTE_ERROR_CODES}: the command may have already executed — a retry whitelist that
|
|
228
|
+
* contained it would re-drive committed side effects. Distinct from `transport_lost` (connection story
|
|
229
|
+
* known, idempotency-gated retry after reconnect). Message MUST carry the verify-first sentence ("may
|
|
230
|
+
* have already executed on the target; verify its effect before re-running").
|
|
231
|
+
*/
|
|
232
|
+
| "outcome_unknown"
|
|
216
233
|
/** Unclassified provider/transport failure. */
|
|
217
234
|
| "unknown";
|
|
218
235
|
/**
|
|
@@ -220,8 +237,10 @@ export type RemoteExecutionErrorCode =
|
|
|
220
237
|
* can succeed (`auth_transient`: retry-exactly-once after the device is authorized; `connect_failed`: the
|
|
221
238
|
* workspace could not be reached/provisioned this attempt; `timeout`: a liveness bound tripped;
|
|
222
239
|
* `transport_lost`: re-establish the connection). Every OTHER code is permanent for this attempt
|
|
223
|
-
* (`auth_failed`/`unsupported`), caller-driven (`aborted`),
|
|
224
|
-
*
|
|
240
|
+
* (`auth_failed`/`unsupported`), caller-driven (`aborted`), unclassifiable (`unknown`), or
|
|
241
|
+
* outcome-unknowable (`outcome_unknown` — design/380 O12: the op may have ALREADY executed, so a retry
|
|
242
|
+
* re-drives committed side effects) — retrying them burns budget or, worse, re-drives a rejected
|
|
243
|
+
* credential or a committed op.
|
|
225
244
|
*
|
|
226
245
|
* ONE list, two consumers, so a caller's retry decision and its DISCLOSURE can never disagree: the engine
|
|
227
246
|
* retries only IDEMPOTENT ops on these codes (see {@link withRetry}'s red line — a snapshot-taking
|
|
@@ -405,6 +424,19 @@ export interface RemoteExecutionEnv extends ExecutionEnv {
|
|
|
405
424
|
export interface ExecutionEnvFactoryContext {
|
|
406
425
|
/** Resolved session id for the task — the stable identity of its per-task workspace. */
|
|
407
426
|
sessionId: string;
|
|
427
|
+
/**
|
|
428
|
+
* design/380 O1① — the run TREE's placement root (fixed point), minted by prepare AFTER session
|
|
429
|
+
* resolution as `internals.placementRoot ?? internals.rootSessionId ?? sessionId` — a top-level
|
|
430
|
+
* run's own resolved sessionId, every descendant's inherited root, verbatim (delegation and
|
|
431
|
+
* workflow spawn chains re-thread an explicit `placementRoot` — C12; the ONE boundary it does not
|
|
432
|
+
* yet cross is a bare DURABLE RESUME, which falls back to the checkpointed session until
|
|
433
|
+
* design/380 O1③'s checkpoint carriage lands — residual pinned). A target-bound
|
|
434
|
+
* factory (device lane) keys its placement lookup on THIS, never on `sessionId` (a child's fresh
|
|
435
|
+
* session id must not read as a new placement). Always present and non-empty — an empty value is
|
|
436
|
+
* an assembly error the engine refuses loudly at the mint (`config.placement_root_invalid`) and a
|
|
437
|
+
* factory MUST refuse loudly too (loud-bad-value posture), never default around.
|
|
438
|
+
*/
|
|
439
|
+
placementRootSessionId: string;
|
|
408
440
|
/** Caller-supplied task id, when set on the `TaskSpec`. */
|
|
409
441
|
taskId?: string;
|
|
410
442
|
/**
|
|
@@ -1498,6 +1498,26 @@ export interface RunInternals {
|
|
|
1498
1498
|
* after a restart those intermediate sessions are dead ends, and a recovery face enumerating
|
|
1499
1499
|
* "everything under this host session" needs the root anchor, not an alias walk. */
|
|
1500
1500
|
rootSessionId?: string;
|
|
1501
|
+
/**
|
|
1502
|
+
* design/380 O1② — the run tree's PLACEMENT root: the fixed point a target-bound env factory keys
|
|
1503
|
+
* its placement lookup on ({@link import("../remote-env.js").ExecutionEnvFactoryContext.placementRootSessionId}).
|
|
1504
|
+
* A SEPARATE axis from {@link rootSessionId} deliberately: that field means "member of this host
|
|
1505
|
+
* session's DELEGATION tree" and is consumed by the registry access/recovery faces — cascade rungs
|
|
1506
|
+
* and verification legs are intentionally NOT members of that tree (independent cold re-runs in
|
|
1507
|
+
* their own sessions), so widening `rootSessionId` to cover them would corrupt the recovery faces'
|
|
1508
|
+
* reading. This member says only "place me where this session was placed". Producers: the
|
|
1509
|
+
* orchestration entries (`runCascade` / `runWithVerification` family) after their first leg's
|
|
1510
|
+
* sessionId receipt, and — C12 — the delegation/workflow spawn chains, which re-thread a parent's
|
|
1511
|
+
* EXPLICIT value verbatim into child internals (ToolExecuteContext.placementRoot → childInternals;
|
|
1512
|
+
* workflow deps → shared internals base), so every descendant of a placed leg keeps the fixed
|
|
1513
|
+
* point; prepare's mint reads it first (`placementRoot ?? rootSessionId ?? sessionId`). Absent
|
|
1514
|
+
* everywhere else — the delegation lanes' `rootSessionId` fixed point then becomes the placement
|
|
1515
|
+
* root through the middle segment, unchanged. NOT yet durable (C12 residual, pinned): a bare
|
|
1516
|
+
* durable resume cannot recover it until design/380 O1③'s checkpoint carriage lands — the
|
|
1517
|
+
* resumed leg falls back to its own session; a resume caller needing continuity re-supplies the
|
|
1518
|
+
* member itself. TRUSTED run-scoped channel (never a {@link TaskSpec} field).
|
|
1519
|
+
*/
|
|
1520
|
+
placementRoot?: string;
|
|
1501
1521
|
/**
|
|
1502
1522
|
* RB-429 — the REGISTRY SCOPE this run's own background row lives in: the domain its registry-facing
|
|
1503
1523
|
* tools (TaskOutput / TaskStop / SendMessage / AgentTranscript / Monitor, and the announce listing)
|
|
@@ -681,6 +681,15 @@ async function derivedRouteFallsBack(args) {
|
|
|
681
681
|
return false;
|
|
682
682
|
}
|
|
683
683
|
}
|
|
684
|
+
function mintPlacementRootSessionId(internals, sessionId) {
|
|
685
|
+
const placementRootSessionId = internals?.placementRoot ?? internals?.rootSessionId ?? sessionId;
|
|
686
|
+
if (placementRootSessionId === "") {
|
|
687
|
+
const e = new Error("placement root resolved EMPTY (internals.placementRoot / internals.rootSessionId carries an empty string) — an empty fixed point cannot key a placement lookup; fix the spawning lane instead of defaulting around it.");
|
|
688
|
+
e.code = "config.placement_root_invalid";
|
|
689
|
+
throw e;
|
|
690
|
+
}
|
|
691
|
+
return placementRootSessionId;
|
|
692
|
+
}
|
|
684
693
|
export async function prepareTask(spec, deps, sessions, resume, internals, runnerSelf) {
|
|
685
694
|
const doors = prepareConfigDoors({ spec, deps, sessions, resume, internals });
|
|
686
695
|
spec = doors.spec;
|
|
@@ -789,7 +798,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
789
798
|
try {
|
|
790
799
|
ownedEnv = deps.executionEnvFactory
|
|
791
800
|
? await deps.executionEnvFactory({
|
|
792
|
-
sessionId,
|
|
801
|
+
sessionId, placementRootSessionId: mintPlacementRootSessionId(internals, sessionId),
|
|
793
802
|
taskId: spec.taskId,
|
|
794
803
|
...(internals?.isolation ? { isolation: internals.isolation } : {}),
|
|
795
804
|
...(internals?.parentCwd ? { parentCwd: internals.parentCwd } : {}),
|
|
@@ -1173,7 +1182,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1173
1182
|
taskId: hostTaskId,
|
|
1174
1183
|
...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
|
|
1175
1184
|
...(internals?.parentSessionId !== undefined ? { parentSessionId: internals.parentSessionId } : {}),
|
|
1176
|
-
...(internals?.rootSessionId !== undefined ? { rootSessionId: internals.rootSessionId } : {}),
|
|
1185
|
+
...(internals?.rootSessionId !== undefined ? { rootSessionId: internals.rootSessionId } : {}), ...(internals?.placementRoot !== undefined ? { placementRoot: internals.placementRoot } : {}),
|
|
1177
1186
|
...(internals?.explicitAgentName !== undefined ? { spawnedAgentName: internals.explicitAgentName } : {}),
|
|
1178
1187
|
...(internals?.peerSelfRef !== undefined ? { peerSelfRef: internals.peerSelfRef } : {}),
|
|
1179
1188
|
...(internals?.peerInboundChainRef !== undefined ? { peerInboundChainRef: internals.peerInboundChainRef } : {}),
|
|
@@ -1348,7 +1357,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1348
1357
|
scope: taskScope,
|
|
1349
1358
|
notifier: deps.workflowCompletionNotifier,
|
|
1350
1359
|
originatingSessionId: sessionId,
|
|
1351
|
-
rootSessionId: internals?.rootSessionId ?? sessionId,
|
|
1360
|
+
rootSessionId: internals?.rootSessionId ?? sessionId, ...(internals?.placementRoot !== undefined ? { placementRoot: internals.placementRoot } : {}),
|
|
1352
1361
|
taskRegistry: defaultTaskRegistry,
|
|
1353
1362
|
taskNotification: internals?.onTaskNotification,
|
|
1354
1363
|
taskOwner: hostTaskId,
|
|
@@ -49,6 +49,16 @@ export async function prepareWorkspaceRestore(input) {
|
|
|
49
49
|
throw e;
|
|
50
50
|
};
|
|
51
51
|
const handle = resume.workspaceHandle;
|
|
52
|
+
const refuseIdentityMismatch = (resumedHandle) => {
|
|
53
|
+
if (resumedHandle.provider !== handle.provider) {
|
|
54
|
+
failResume(`resume workspace identity mismatch: the checkpoint records provider "${handle.provider}" but the resumed env reports "${resumedHandle.provider}" — refusing to continue a workspace on a different backend (wire the factory to rebuild the checkpointed lane and re-resume)`);
|
|
55
|
+
}
|
|
56
|
+
if (handle.deviceId !== undefined && resumedHandle.deviceId !== handle.deviceId) {
|
|
57
|
+
failResume(resumedHandle.deviceId === undefined
|
|
58
|
+
? `resume workspace identity mismatch: the checkpoint records deviceId "${handle.deviceId}" but the resumed env reports none — a device-bound workspace cannot be verified on a handle that does not state its device (absence is not a match)`
|
|
59
|
+
: `resume workspace identity mismatch: the checkpoint records deviceId "${handle.deviceId}" but the resumed env reports "${resumedHandle.deviceId}" — refusing to continue on a different physical device`);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
52
62
|
if (ownedEnv === undefined || !isRemoteExecutionEnv(ownedEnv)) {
|
|
53
63
|
failResume("resume needs a RemoteExecutionEnv from executionEnvFactory to restore the workspace snapshot");
|
|
54
64
|
}
|
|
@@ -56,6 +66,7 @@ export async function prepareWorkspaceRestore(input) {
|
|
|
56
66
|
if (handle.restoreMode !== "park_only" && ownedEnv.capabilities.suspendable) {
|
|
57
67
|
failResume("checkpoint workspaceHandle has no snapshotId and is not a park_only handle, but the resumed env is suspendable — refusing to resume on a possibly-unrestored workspace (corrupt checkpoint?)");
|
|
58
68
|
}
|
|
69
|
+
refuseIdentityMismatch(ownedEnv.workspaceHandle());
|
|
59
70
|
if (handle.mountPath && handle.mountPath !== taskRootPath) {
|
|
60
71
|
taskRootPath = handle.mountPath;
|
|
61
72
|
}
|
|
@@ -68,6 +79,7 @@ export async function prepareWorkspaceRestore(input) {
|
|
|
68
79
|
if (missingHere.length > 0) {
|
|
69
80
|
failResume(`the resumed execution env cannot restore a workspace snapshot: its adapter does not implement ${missingHere.join(" or ")}. The checkpoint holds snapshot "${handle.snapshotId}" — wire an adapter that implements the full RemoteExecutionEnv restore surface and re-resume.`);
|
|
70
81
|
}
|
|
82
|
+
refuseIdentityMismatch(ownedEnv.workspaceHandle());
|
|
71
83
|
const { outcome: restored, attempts: restoreAttempts } = await restoreWorkspaceWithRetry(ownedEnv, handle.snapshotId, {
|
|
72
84
|
abortSignal: restoreSignal,
|
|
73
85
|
priorHandle: handle,
|
|
@@ -76,6 +88,7 @@ export async function prepareWorkspaceRestore(input) {
|
|
|
76
88
|
failResume(`resumeVM failed after ${restoreAttempts} attempt(s) (${restored.error.code}): ${restored.error.message}`, restored.error, remoteEnvFailureNote("resumeVM", restored.error, restoreAttempts));
|
|
77
89
|
}
|
|
78
90
|
else {
|
|
91
|
+
refuseIdentityMismatch(restored.value);
|
|
79
92
|
const restoredEnv = ownedEnv;
|
|
80
93
|
const canonicalInEnv = async (p) => {
|
|
81
94
|
try {
|
|
@@ -3119,7 +3119,10 @@ export class Runner {
|
|
|
3119
3119
|
}
|
|
3120
3120
|
catch (err) {
|
|
3121
3121
|
if (turnSignal?.aborted === true && !prepared.abortController.signal.aborted) {
|
|
3122
|
-
|
|
3122
|
+
const interruptReason = lane === "rejection"
|
|
3123
|
+
? "prompt-too-long recovery pass cut short by a turn interrupt"
|
|
3124
|
+
: "guard-chain forced compaction cut short by a turn interrupt";
|
|
3125
|
+
queue.push({ type: "compaction_outcome", outcome: "failed", trigger: "forced", reason: interruptReason, ...ident() });
|
|
3123
3126
|
return false;
|
|
3124
3127
|
}
|
|
3125
3128
|
compactionBreaker.failures += 1;
|
|
@@ -3135,9 +3138,11 @@ export class Runner {
|
|
|
3135
3138
|
thinkingOnly: {},
|
|
3136
3139
|
degenerateOutput: { detect: (m) => isDegenerateCutMessage(m) },
|
|
3137
3140
|
promptTooLong: {
|
|
3138
|
-
recover: async (attempt) => {
|
|
3141
|
+
recover: async (attempt, turnSignal) => {
|
|
3139
3142
|
if (prepared.abortController.signal.aborted || prepared.suspendRef.token !== undefined)
|
|
3140
3143
|
return false;
|
|
3144
|
+
if (turnSignal?.aborted === true)
|
|
3145
|
+
return false;
|
|
3141
3146
|
if (prepared.microCompact.clearOnRejection && attempt === 1) {
|
|
3142
3147
|
const proj = prepared.microCompact.projectionRef.current;
|
|
3143
3148
|
const compactionAvailable = (spec.compaction?.enabled ?? true) && compactionBreaker.failures < MAX_CONSECUTIVE_COMPACTION_FAILURES;
|
|
@@ -3174,7 +3179,7 @@ export class Runner {
|
|
|
3174
3179
|
return true;
|
|
3175
3180
|
}
|
|
3176
3181
|
}
|
|
3177
|
-
return runForcedCompactionPass("rejection");
|
|
3182
|
+
return runForcedCompactionPass("rejection", turnSignal);
|
|
3178
3183
|
},
|
|
3179
3184
|
},
|
|
3180
3185
|
});
|
|
@@ -4308,7 +4313,8 @@ export class Runner {
|
|
|
4308
4313
|
if (outcome === null || typeof outcome !== "object") {
|
|
4309
4314
|
throw new CheckpointError("checkpoint.invalid_outcome", `resume outcome must be an object naming its gate (got ${outcome === null ? "null" : typeof outcome})`);
|
|
4310
4315
|
}
|
|
4311
|
-
const
|
|
4316
|
+
const config = typeof taskConfig === "object" && taskConfig !== null ? { ...taskConfig } : taskConfig;
|
|
4317
|
+
const store = resolveCheckpointStore(config, this.deps);
|
|
4312
4318
|
if (!store) {
|
|
4313
4319
|
throw new CheckpointError("checkpoint.not_found", "no CheckpointStore wired — cannot resume (set RunnerDeps.checkpointStore or taskConfig.checkpointStore)");
|
|
4314
4320
|
}
|
|
@@ -4335,7 +4341,7 @@ export class Runner {
|
|
|
4335
4341
|
let wakeMessage;
|
|
4336
4342
|
let wakeHookContext;
|
|
4337
4343
|
const outcomeGate = outcome.gate;
|
|
4338
|
-
const resumeSignal =
|
|
4344
|
+
const resumeSignal = config.signal;
|
|
4339
4345
|
let suppliedMessage;
|
|
4340
4346
|
if (outcomeGate === "wake") {
|
|
4341
4347
|
suppliedMessage = outcome.message;
|
|
@@ -4564,7 +4570,7 @@ export class Runner {
|
|
|
4564
4570
|
: "resume carried a content-ask `answer` on a `deny` — a denial injects a refusal, never an answer; refusing rather than dropping it silently", { field: "answer" });
|
|
4565
4571
|
}
|
|
4566
4572
|
if (contentAskCallId !== undefined && plainPolicyOutcome.decision === "allow" && suppliedAnswer === undefined) {
|
|
4567
|
-
if (!isLiveQuestionFace(
|
|
4573
|
+
if (!isLiveQuestionFace(config.onQuestion ?? this.deps.onQuestion)) {
|
|
4568
4574
|
throw new CheckpointError("checkpoint.invalid_outcome", "resume approved a content-ask (the reserved question tool) without an `answer`, and this resume has no live answering face — " +
|
|
4569
4575
|
'executing the question against nothing would hand the model a fabricated "no human is available" default while consuming the approval; ' +
|
|
4570
4576
|
"re-resume with the operator's answer on the outcome (or deny it), the checkpoint stays pending", { field: "answer" });
|
|
@@ -4675,14 +4681,14 @@ export class Runner {
|
|
|
4675
4681
|
},
|
|
4676
4682
|
}
|
|
4677
4683
|
: internals;
|
|
4678
|
-
if (this.deps.lockedConfig?.keys?.includes("toolPolicy") === true &&
|
|
4684
|
+
if (this.deps.lockedConfig?.keys?.includes("toolPolicy") === true && config.basePolicyForResumeEdit !== undefined) {
|
|
4679
4685
|
throw new CheckpointError("checkpoint.invalid_outcome", "TaskSpec.basePolicyForResumeEdit is administratively locked by this deployment (locked key \"toolPolicy\") — a task-supplied " +
|
|
4680
4686
|
"resume-edit policy is refused pre-CAS (the checkpoint stays pending); remove the field or change the deployment's lock configuration");
|
|
4681
4687
|
}
|
|
4682
4688
|
let recheckGovernanceWindow;
|
|
4683
4689
|
{
|
|
4684
4690
|
const rowPrincipal = cp.principal || undefined;
|
|
4685
|
-
const suppliedPrincipal =
|
|
4691
|
+
const suppliedPrincipal = config.principal || undefined;
|
|
4686
4692
|
if (rowPrincipal !== undefined && suppliedPrincipal !== undefined && suppliedPrincipal !== rowPrincipal) {
|
|
4687
4693
|
throw new CheckpointError("resume.principal_mismatch", `the resume config carries principal ${JSON.stringify(suppliedPrincipal)} but this checkpoint was suspended under principal ${JSON.stringify(rowPrincipal)} — ` +
|
|
4688
4694
|
"running the resumed leg under a different identity would move its usage-ledger bucket, scope derivation and attribution; " +
|
|
@@ -4709,13 +4715,13 @@ export class Runner {
|
|
|
4709
4715
|
}
|
|
4710
4716
|
}
|
|
4711
4717
|
if (outcomeGate === "wake" && wakeMessage !== undefined) {
|
|
4712
|
-
const resumeScreenHooks =
|
|
4718
|
+
const resumeScreenHooks = config.hooks ?? this.deps.hooks;
|
|
4713
4719
|
const screen = resumeScreenHooks?.userPromptSubmit;
|
|
4714
4720
|
if (screen !== undefined && resumeSignal?.aborted !== true) {
|
|
4715
4721
|
const screenedMessage = wakeMessage;
|
|
4716
4722
|
const identity = mintHookInvocationIdentity({
|
|
4717
4723
|
sessionId: cp.sessionId,
|
|
4718
|
-
taskId:
|
|
4724
|
+
taskId: config.taskId ?? cp.sessionId,
|
|
4719
4725
|
legKind: "resume",
|
|
4720
4726
|
isDelegatedChild: effectiveDelegationFacts(internals, cp.state.isDelegatedChild).isDelegatedChild,
|
|
4721
4727
|
...(internals?.insideFork === true ? { insideFork: true } : {}),
|
|
@@ -4784,7 +4790,7 @@ export class Runner {
|
|
|
4784
4790
|
budget: editBudget,
|
|
4785
4791
|
...(cp.state.handsCwd !== undefined ? { cwd: cp.state.handsCwd } : {}),
|
|
4786
4792
|
};
|
|
4787
|
-
const editSignal =
|
|
4793
|
+
const editSignal = config.signal;
|
|
4788
4794
|
const raceEditAbort = async (p) => {
|
|
4789
4795
|
if (editSignal === undefined)
|
|
4790
4796
|
return p;
|
|
@@ -4823,7 +4829,7 @@ export class Runner {
|
|
|
4823
4829
|
const toolPolicyLocked = this.deps.lockedConfig?.keys?.includes("toolPolicy") === true;
|
|
4824
4830
|
const editBasePolicy = toolPolicyLocked
|
|
4825
4831
|
? (this.deps.basePolicyForResumeEdit ?? this.deps.toolPolicy)
|
|
4826
|
-
: (
|
|
4832
|
+
: (config.basePolicyForResumeEdit ?? this.deps.basePolicyForResumeEdit ?? config.toolPolicy ?? this.deps.toolPolicy);
|
|
4827
4833
|
if (editBasePolicy !== undefined) {
|
|
4828
4834
|
let rechecked;
|
|
4829
4835
|
try {
|
|
@@ -4843,7 +4849,7 @@ export class Runner {
|
|
|
4843
4849
|
`refused pre-CAS (the checkpoint stays pending); re-submit the edited action so the rewrite is adjudicated and approved synchronously`);
|
|
4844
4850
|
}
|
|
4845
4851
|
}
|
|
4846
|
-
const resumeHooks =
|
|
4852
|
+
const resumeHooks = config.hooks ?? this.deps.hooks;
|
|
4847
4853
|
if (resumeHooks?.preToolUse !== undefined && resumeHooks.preToolUseObservational === true) {
|
|
4848
4854
|
try {
|
|
4849
4855
|
await raceEditAbort(Promise.resolve(resumeHooks.preToolUse(cp.pendingAction.toolName, cloneObserverInput(editedArgs), {
|
|
@@ -4878,15 +4884,15 @@ export class Runner {
|
|
|
4878
4884
|
consumeFlipDone = true;
|
|
4879
4885
|
}
|
|
4880
4886
|
const answerFace = plainPolicyOutcome !== undefined && redeemedAnswer !== undefined
|
|
4881
|
-
? answerFaceForRedeemedCall(redeemedAnswer, plainPolicyOutcome.boundCallId,
|
|
4887
|
+
? answerFaceForRedeemedCall(redeemedAnswer, plainPolicyOutcome.boundCallId, config.onQuestion ?? this.deps.onQuestion, boundInputHashOf(cp.pendingAction.args?.questions))
|
|
4882
4888
|
: undefined;
|
|
4883
4889
|
const spec = {
|
|
4884
|
-
...
|
|
4885
|
-
...((
|
|
4890
|
+
...config,
|
|
4891
|
+
...((config.principal || undefined) === undefined && (cp.principal || undefined) !== undefined ? { principal: cp.principal } : {}),
|
|
4886
4892
|
objective: "",
|
|
4887
4893
|
sessionId: cp.sessionId,
|
|
4888
4894
|
requireExistingSession: true,
|
|
4889
|
-
preemptSignal:
|
|
4895
|
+
preemptSignal: config.preemptSignal?.aborted ? undefined : config.preemptSignal,
|
|
4890
4896
|
...(answerFace !== undefined ? { onQuestion: answerFace } : {}),
|
|
4891
4897
|
};
|
|
4892
4898
|
const reopenFn = store.reopen?.bind(store);
|
package/dist/core/types.d.ts
CHANGED
|
@@ -552,6 +552,13 @@ export interface ToolExecuteContext {
|
|
|
552
552
|
* fixed point `ctx.rootSessionId ?? ctx.sessionId` at every spawn). Absent for a top-level run
|
|
553
553
|
* (its OWN sessionId is the root). */
|
|
554
554
|
rootSessionId?: string;
|
|
555
|
+
/** design/380 O1② (C12) — the spawning run's EXPLICIT placement fixed point
|
|
556
|
+
* (`RunInternals.placementRoot`), present only when its internals carried one (cascade rungs /
|
|
557
|
+
* verification legs after the first, or a caller-declared root). A delegation tool threads it
|
|
558
|
+
* VERBATIM into child RunInternals so every descendant keeps the ladder/gate placement; absent ⇒
|
|
559
|
+
* the `rootSessionId` chain above IS the placement root through prepare's mint middle segment
|
|
560
|
+
* (`placementRoot ?? rootSessionId ?? sessionId`) and nothing extra is threaded. */
|
|
561
|
+
placementRoot?: string;
|
|
555
562
|
/**
|
|
556
563
|
* design/151 §7 S3c — TRUSTED tier-3 revival claim (same trusted-internals seat as
|
|
557
564
|
* {@link parentTaskId}; never a model argument). Filled ONLY by the auto-mounted SendMessage's
|
|
@@ -5932,9 +5939,12 @@ export interface RunnerDeps {
|
|
|
5932
5939
|
* `TaskSpec.limits`, which is the allowance ONE task asked for — an operator granting "N tokens per 5
|
|
5933
5940
|
* hours" cannot express it as a task limit, because nothing stops the next task from asking again.
|
|
5934
5941
|
*
|
|
5935
|
-
* A window carries a TOKEN ceiling
|
|
5936
|
-
* USD — the `TaskLimits.maxCostUsd` quantity one governance level up)
|
|
5937
|
-
*
|
|
5942
|
+
* A window carries a TOKEN ceiling (`UsageWindow.maxTokens`), a MONEY ceiling (`UsageWindow.maxCostUsd`,
|
|
5943
|
+
* absolute USD — the `TaskLimits.maxCostUsd` quantity one governance level up), or both: each is
|
|
5944
|
+
* optional on its own and at least one is required, so a deployment governed by spend alone declares
|
|
5945
|
+
* only `maxCostUsd` (a window with neither ceiling is refused, `config.usage_window_invalid`). The two
|
|
5946
|
+
* are independent and every declared one binds. A $ ceiling requires a PRICED run: a task whose model
|
|
5947
|
+
* has neither a {@link pricing}
|
|
5938
5948
|
* entry nor a `Model.cost` declaration is refused at the door (`config.usage_window_unpriced`) rather
|
|
5939
5949
|
* than charged the fabricated 0 an unpriced run would otherwise file into an operator's ceiling.
|
|
5940
5950
|
*
|