@sema-agent/core 5.63.0 → 5.65.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 +80 -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 +100 -5
- package/dist/core/fs-write-gate-policy.d.ts +21 -0
- package/dist/core/fs-write-gate-policy.js +14 -3
- package/dist/core/hooks.d.ts +9 -1
- package/dist/core/hooks.js +1 -1
- package/dist/core/memory-engine/dual-root.js +3 -0
- package/dist/core/memory-engine/engine.d.ts +1 -0
- package/dist/core/memory-engine/engine.js +9 -5
- package/dist/core/memory-engine/types.d.ts +16 -0
- package/dist/core/remote-env.d.ts +34 -2
- package/dist/core/runner/prepare-hands-readface.d.ts +9 -0
- package/dist/core/runner/prepare-hands-readface.js +4 -1
- package/dist/core/runner/prepare-memory.js +1 -1
- package/dist/core/runner/prepare-task.d.ts +31 -0
- package/dist/core/runner/prepare-task.js +53 -18
- package/dist/core/runner/prepare-workspace-restore.js +13 -0
- package/dist/core/runner/runtask.d.ts +18 -0
- package/dist/core/runner/runtask.js +173 -20
- package/dist/core/stub-env.d.ts +4 -0
- package/dist/core/stub-env.js +1 -0
- package/dist/core/task-registry-shared.js +20 -2
- package/dist/core/types.d.ts +104 -11
- 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 +30 -0
- package/dist/core/workflow-run-store.d.ts +39 -1
- package/dist/core/workflow-run-store.js +2 -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 +107 -3
- package/dist/engine/loop/agent-loop.js +39 -15
- package/dist/engine/loop/types.d.ts +43 -22
- package/dist/index.d.ts +2 -2
- 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-types.d.ts +35 -0
- package/dist/orchestration/workflow-types.js +2 -2
- package/dist/orchestration/workflow.d.ts +6 -0
- package/dist/orchestration/workflow.js +21 -1
- package/dist/prompts/default.d.ts +8 -0
- package/dist/prompts/default.js +3 -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 +9 -1
|
@@ -36,13 +36,13 @@ import { cacheFamilyOf, usageCostMicroUsd } from "./usage-accounting.js";
|
|
|
36
36
|
import { assembleResult, errorCodeOf } from "./assemble-result.js";
|
|
37
37
|
import { ATTACHMENT_BYTE_CAP, CHANGED_FILES_MAX, AGENT_LISTING_REMOVED_HEADER, SKILLS_LISTING_DELTA_HEADER, SKILLS_LISTING_REMOVED_HEADER, advanceCadenceClock, agentListingDeltaHeader, attachmentEnvelopeTags, agentListingInitialHeader, replayAnnouncedListing, replayAnnouncedModels, clipToBytes, collectDateChange, collectDueAttachments, collectInstructionsChange, commitAgentListing, commitInstructionsChange, commitSkillsListing, createAttachmentState, rebaseCadenceWindows, reduceToolEnd, renderAgentListingDelta, renderMcpDroppedTools, renderMcpInstructionsDelta, renderOrphanedBackgroundTasks, selectMcpDroppedBatch, renderSkillsListingDelta, renderToolsDelta, stampWriteAnchor } from "./turn-attachments.js";
|
|
38
38
|
import { buildWorkingFileAttachments, centerAdoptionOption, emitInputTruncated, forkContextOption } from "./compaction-call-options.js";
|
|
39
|
-
import { effectiveDelegationFacts, gatedCallIdOf, prepareTask, resolveCheckpointStore } from "./prepare-task.js";
|
|
39
|
+
import { effectiveDelegationFacts, gatedCallIdOf, placementValueOrAbsent, prepareTask, resolveCheckpointStore } from "./prepare-task.js";
|
|
40
40
|
import { settleTeardownLeg } from "./teardown-bounded.js";
|
|
41
41
|
import { TOOL_SEARCH_NAME } from "./tool-disclosure.js";
|
|
42
42
|
import { hasVerifiableStructureSignal } from "./grounding-signal.js";
|
|
43
43
|
import { hasDestroy, isIsolated } from "../remote-env.js";
|
|
44
44
|
import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
|
|
45
|
-
import { cloneObserverInput, formatHookFeedback, hookSeatExpiredError, mintHookInvocationIdentity, resolveHookTimeoutMs, runHookSeat } from "../hooks.js";
|
|
45
|
+
import { cloneObserverInput, formatHookFeedback, hookSeatExpiredError, MAX_HOOK_TIMEOUT_MS, mintHookInvocationIdentity, resolveHookTimeoutMs, runHookSeat } from "../hooks.js";
|
|
46
46
|
import { buildHumanInputEvent, frameMidTurnUserInput, projectHumanInput } from "../human-input-projection.js";
|
|
47
47
|
import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
|
|
48
48
|
import { appendInterruptionMarker, reconcileInterruptedSession } from "../session-reconcile.js";
|
|
@@ -84,6 +84,7 @@ function sameAcceptedSteerInput(a, b) {
|
|
|
84
84
|
a.actor?.issuer === b.actor?.issuer);
|
|
85
85
|
}
|
|
86
86
|
const MAX_CONSECUTIVE_COMPACTION_FAILURES = 3;
|
|
87
|
+
const RESUME_PREFLIGHT_DEFAULT_TIMEOUT_MS = 10_000;
|
|
87
88
|
const STOP_HOOK_BLOCK_CAP = 8;
|
|
88
89
|
const COMPACTION_REGROWTH_FACTOR = 1.5;
|
|
89
90
|
const COMPACTION_FREED_EPSILON = 256;
|
|
@@ -1455,6 +1456,8 @@ export class Runner {
|
|
|
1455
1456
|
snapshotTooLargeRoots = new Map();
|
|
1456
1457
|
pendingSessionNotifications = new PendingSessionNotifications();
|
|
1457
1458
|
parentConstraintRegistry = new Map();
|
|
1459
|
+
suspendedEnvReaps = new Map();
|
|
1460
|
+
locallyClaimedTokens = new Set();
|
|
1458
1461
|
static PARENT_CONSTRAINT_REGISTRY_CAP = 1024;
|
|
1459
1462
|
constructor(deps) {
|
|
1460
1463
|
this.deps = deps;
|
|
@@ -1663,8 +1666,11 @@ export class Runner {
|
|
|
1663
1666
|
await this.runLocked(spec, queue, (r) => {
|
|
1664
1667
|
resultValue = r;
|
|
1665
1668
|
const pcs = internals?.inheritedGate?.parentConstraints;
|
|
1669
|
+
if (resume !== undefined)
|
|
1670
|
+
this.locallyClaimedTokens.delete(resume.cp.token);
|
|
1666
1671
|
if (resume !== undefined && r.checkpointToken !== resume.cp.token) {
|
|
1667
1672
|
this.parentConstraintRegistry.delete(resume.cp.token);
|
|
1673
|
+
this.suspendedEnvReaps.delete(resume.cp.token);
|
|
1668
1674
|
}
|
|
1669
1675
|
if (r.status === "suspended" && r.checkpointToken !== undefined && pcs !== undefined && pcs.length > 0) {
|
|
1670
1676
|
if (this.parentConstraintRegistry.size >= Runner.PARENT_CONSTRAINT_REGISTRY_CAP) {
|
|
@@ -1681,6 +1687,7 @@ export class Runner {
|
|
|
1681
1687
|
publishReady(h);
|
|
1682
1688
|
}, (s) => {
|
|
1683
1689
|
reapHandle = s;
|
|
1690
|
+
this.suspendedEnvReaps.set(s.token, s);
|
|
1684
1691
|
}, manualCompactRef, taskIdRef, resume, { ...(internals ?? {}), detachHub }, notifyRef, entryActor);
|
|
1685
1692
|
}
|
|
1686
1693
|
finally {
|
|
@@ -1698,6 +1705,7 @@ export class Runner {
|
|
|
1698
1705
|
const reopenReason = code === "resume.tool_unavailable" ? "tool_unavailable" : "env_failed";
|
|
1699
1706
|
const reopenable = resume !== undefined && resume.pendingActionStarted !== true && reopenReason !== undefined && !(resumeDecisionWasNegative(resume) && resume.decisionDelivered === true);
|
|
1700
1707
|
let reopenCommitted = false;
|
|
1708
|
+
let reopenStateUnknown = false;
|
|
1701
1709
|
if (reopenable && resume?.onEnvRestoreFailed) {
|
|
1702
1710
|
try {
|
|
1703
1711
|
await resume.onEnvRestoreFailed(reopenReason);
|
|
@@ -1708,15 +1716,26 @@ export class Runner {
|
|
|
1708
1716
|
const original = err instanceof Error ? err : new Error(String(err));
|
|
1709
1717
|
const reopenMsg = reopenErr instanceof Error ? reopenErr.message : String(reopenErr);
|
|
1710
1718
|
const definitive = errorCodeOf(reopenErr) === "checkpoint.reopen_failed";
|
|
1719
|
+
reopenStateUnknown = !definitive;
|
|
1711
1720
|
err = Object.assign(new Error(definitive
|
|
1712
1721
|
? `${reopenMsg} (original resume failure: ${original.message})`
|
|
1713
1722
|
: `checkpoint reopen threw mid-flight — state UNKNOWN, confirm via the checkpoint store before retrying: ${reopenMsg} (original resume failure: ${original.message})`, { cause: original }), { code: "checkpoint.reopen_failed" });
|
|
1714
1723
|
code = "checkpoint.reopen_failed";
|
|
1715
1724
|
}
|
|
1716
1725
|
}
|
|
1726
|
+
if (resume)
|
|
1727
|
+
this.locallyClaimedTokens.delete(resume.cp.token);
|
|
1717
1728
|
if (resume && !reopenCommitted) {
|
|
1718
1729
|
this.parentConstraintRegistry.delete(resume.cp.token);
|
|
1719
1730
|
await settleTeardownLeg(() => this.sessions.unpin?.(resume.cp.sessionId), "sessions.unpin (resume-failure leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: resume.cp.sessionId }));
|
|
1731
|
+
if (!reopenStateUnknown) {
|
|
1732
|
+
const failedReap = this.suspendedEnvReaps.get(resume.cp.token);
|
|
1733
|
+
this.suspendedEnvReaps.delete(resume.cp.token);
|
|
1734
|
+
if (failedReap?.env !== undefined && hasDestroy(failedReap.env)) {
|
|
1735
|
+
const failedEnv = failedReap.env;
|
|
1736
|
+
await settleTeardownLeg(() => failedEnv.destroy(), "terminal resume-failure env destroy", (e) => this.deps.onError?.(e, { phase: "config", sessionId: resume.cp.sessionId }));
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1720
1739
|
}
|
|
1721
1740
|
if (resultValue !== undefined) {
|
|
1722
1741
|
try {
|
|
@@ -1802,9 +1821,13 @@ export class Runner {
|
|
|
1802
1821
|
reportErr(err);
|
|
1803
1822
|
return;
|
|
1804
1823
|
}
|
|
1805
|
-
if (!won)
|
|
1824
|
+
if (!won) {
|
|
1825
|
+
if (!this.locallyClaimedTokens.has(rh.token))
|
|
1826
|
+
this.suspendedEnvReaps.delete(rh.token);
|
|
1806
1827
|
return;
|
|
1828
|
+
}
|
|
1807
1829
|
this.parentConstraintRegistry.delete(rh.token);
|
|
1830
|
+
this.suspendedEnvReaps.delete(rh.token);
|
|
1808
1831
|
if (rh.env && hasDestroy(rh.env)) {
|
|
1809
1832
|
try {
|
|
1810
1833
|
await rh.env.destroy();
|
|
@@ -3119,7 +3142,10 @@ export class Runner {
|
|
|
3119
3142
|
}
|
|
3120
3143
|
catch (err) {
|
|
3121
3144
|
if (turnSignal?.aborted === true && !prepared.abortController.signal.aborted) {
|
|
3122
|
-
|
|
3145
|
+
const interruptReason = lane === "rejection"
|
|
3146
|
+
? "prompt-too-long recovery pass cut short by a turn interrupt"
|
|
3147
|
+
: "guard-chain forced compaction cut short by a turn interrupt";
|
|
3148
|
+
queue.push({ type: "compaction_outcome", outcome: "failed", trigger: "forced", reason: interruptReason, ...ident() });
|
|
3123
3149
|
return false;
|
|
3124
3150
|
}
|
|
3125
3151
|
compactionBreaker.failures += 1;
|
|
@@ -3135,9 +3161,11 @@ export class Runner {
|
|
|
3135
3161
|
thinkingOnly: {},
|
|
3136
3162
|
degenerateOutput: { detect: (m) => isDegenerateCutMessage(m) },
|
|
3137
3163
|
promptTooLong: {
|
|
3138
|
-
recover: async (attempt) => {
|
|
3164
|
+
recover: async (attempt, turnSignal) => {
|
|
3139
3165
|
if (prepared.abortController.signal.aborted || prepared.suspendRef.token !== undefined)
|
|
3140
3166
|
return false;
|
|
3167
|
+
if (turnSignal?.aborted === true)
|
|
3168
|
+
return false;
|
|
3141
3169
|
if (prepared.microCompact.clearOnRejection && attempt === 1) {
|
|
3142
3170
|
const proj = prepared.microCompact.projectionRef.current;
|
|
3143
3171
|
const compactionAvailable = (spec.compaction?.enabled ?? true) && compactionBreaker.failures < MAX_CONSECUTIVE_COMPACTION_FAILURES;
|
|
@@ -3174,7 +3202,7 @@ export class Runner {
|
|
|
3174
3202
|
return true;
|
|
3175
3203
|
}
|
|
3176
3204
|
}
|
|
3177
|
-
return runForcedCompactionPass("rejection");
|
|
3205
|
+
return runForcedCompactionPass("rejection", turnSignal);
|
|
3178
3206
|
},
|
|
3179
3207
|
},
|
|
3180
3208
|
});
|
|
@@ -4308,7 +4336,9 @@ export class Runner {
|
|
|
4308
4336
|
if (outcome === null || typeof outcome !== "object") {
|
|
4309
4337
|
throw new CheckpointError("checkpoint.invalid_outcome", `resume outcome must be an object naming its gate (got ${outcome === null ? "null" : typeof outcome})`);
|
|
4310
4338
|
}
|
|
4311
|
-
const
|
|
4339
|
+
const config = typeof taskConfig === "object" && taskConfig !== null ? { ...taskConfig } : taskConfig;
|
|
4340
|
+
internals = internals !== undefined ? { ...internals } : undefined;
|
|
4341
|
+
const store = resolveCheckpointStore(config, this.deps);
|
|
4312
4342
|
if (!store) {
|
|
4313
4343
|
throw new CheckpointError("checkpoint.not_found", "no CheckpointStore wired — cannot resume (set RunnerDeps.checkpointStore or taskConfig.checkpointStore)");
|
|
4314
4344
|
}
|
|
@@ -4335,7 +4365,7 @@ export class Runner {
|
|
|
4335
4365
|
let wakeMessage;
|
|
4336
4366
|
let wakeHookContext;
|
|
4337
4367
|
const outcomeGate = outcome.gate;
|
|
4338
|
-
const resumeSignal =
|
|
4368
|
+
const resumeSignal = config.signal;
|
|
4339
4369
|
let suppliedMessage;
|
|
4340
4370
|
if (outcomeGate === "wake") {
|
|
4341
4371
|
suppliedMessage = outcome.message;
|
|
@@ -4564,7 +4594,7 @@ export class Runner {
|
|
|
4564
4594
|
: "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
4595
|
}
|
|
4566
4596
|
if (contentAskCallId !== undefined && plainPolicyOutcome.decision === "allow" && suppliedAnswer === undefined) {
|
|
4567
|
-
if (!isLiveQuestionFace(
|
|
4597
|
+
if (!isLiveQuestionFace(config.onQuestion ?? this.deps.onQuestion)) {
|
|
4568
4598
|
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
4599
|
'executing the question against nothing would hand the model a fabricated "no human is available" default while consuming the approval; ' +
|
|
4570
4600
|
"re-resume with the operator's answer on the outcome (or deny it), the checkpoint stays pending", { field: "answer" });
|
|
@@ -4675,19 +4705,28 @@ export class Runner {
|
|
|
4675
4705
|
},
|
|
4676
4706
|
}
|
|
4677
4707
|
: internals;
|
|
4678
|
-
if (this.deps.lockedConfig?.keys?.includes("toolPolicy") === true &&
|
|
4708
|
+
if (this.deps.lockedConfig?.keys?.includes("toolPolicy") === true && config.basePolicyForResumeEdit !== undefined) {
|
|
4679
4709
|
throw new CheckpointError("checkpoint.invalid_outcome", "TaskSpec.basePolicyForResumeEdit is administratively locked by this deployment (locked key \"toolPolicy\") — a task-supplied " +
|
|
4680
4710
|
"resume-edit policy is refused pre-CAS (the checkpoint stays pending); remove the field or change the deployment's lock configuration");
|
|
4681
4711
|
}
|
|
4682
4712
|
let recheckGovernanceWindow;
|
|
4683
4713
|
{
|
|
4684
4714
|
const rowPrincipal = cp.principal || undefined;
|
|
4685
|
-
const suppliedPrincipal =
|
|
4715
|
+
const suppliedPrincipal = config.principal || undefined;
|
|
4686
4716
|
if (rowPrincipal !== undefined && suppliedPrincipal !== undefined && suppliedPrincipal !== rowPrincipal) {
|
|
4687
4717
|
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
4718
|
"running the resumed leg under a different identity would move its usage-ledger bucket, scope derivation and attribution; " +
|
|
4689
4719
|
"refused pre-CAS (the checkpoint stays pending): re-resume with the original principal, or omit the field to inherit the recorded one");
|
|
4690
4720
|
}
|
|
4721
|
+
{
|
|
4722
|
+
const recordedPlacementRoot = placementValueOrAbsent(cp.state.placementRootSessionId);
|
|
4723
|
+
const suppliedPlacementRoot = placementValueOrAbsent(internals?.placementRoot);
|
|
4724
|
+
if (recordedPlacementRoot !== undefined && suppliedPlacementRoot !== undefined && suppliedPlacementRoot !== recordedPlacementRoot) {
|
|
4725
|
+
throw new CheckpointError("resume.placement_mismatch", `the resume supplied explicit placement root ${JSON.stringify(suppliedPlacementRoot)} but this checkpoint recorded placement root ${JSON.stringify(recordedPlacementRoot)} at suspend — ` +
|
|
4726
|
+
"resuming under a different placement fixed point would re-place the leg (and its descendants) on another target; " +
|
|
4727
|
+
"refused pre-CAS (the checkpoint stays pending): re-resume with the recorded root, or omit internals.placementRoot to inherit it");
|
|
4728
|
+
}
|
|
4729
|
+
}
|
|
4691
4730
|
const owesDelivery = outcomeGate !== "resource_limit" ||
|
|
4692
4731
|
readPendingSteerQueue(cp.state).length > 0 ||
|
|
4693
4732
|
(cp.state.runningBackgroundTasks?.length ?? 0) > 0;
|
|
@@ -4709,13 +4748,13 @@ export class Runner {
|
|
|
4709
4748
|
}
|
|
4710
4749
|
}
|
|
4711
4750
|
if (outcomeGate === "wake" && wakeMessage !== undefined) {
|
|
4712
|
-
const resumeScreenHooks =
|
|
4751
|
+
const resumeScreenHooks = config.hooks ?? this.deps.hooks;
|
|
4713
4752
|
const screen = resumeScreenHooks?.userPromptSubmit;
|
|
4714
4753
|
if (screen !== undefined && resumeSignal?.aborted !== true) {
|
|
4715
4754
|
const screenedMessage = wakeMessage;
|
|
4716
4755
|
const identity = mintHookInvocationIdentity({
|
|
4717
4756
|
sessionId: cp.sessionId,
|
|
4718
|
-
taskId:
|
|
4757
|
+
taskId: config.taskId ?? cp.sessionId,
|
|
4719
4758
|
legKind: "resume",
|
|
4720
4759
|
isDelegatedChild: effectiveDelegationFacts(internals, cp.state.isDelegatedChild).isDelegatedChild,
|
|
4721
4760
|
...(internals?.insideFork === true ? { insideFork: true } : {}),
|
|
@@ -4784,7 +4823,7 @@ export class Runner {
|
|
|
4784
4823
|
budget: editBudget,
|
|
4785
4824
|
...(cp.state.handsCwd !== undefined ? { cwd: cp.state.handsCwd } : {}),
|
|
4786
4825
|
};
|
|
4787
|
-
const editSignal =
|
|
4826
|
+
const editSignal = config.signal;
|
|
4788
4827
|
const raceEditAbort = async (p) => {
|
|
4789
4828
|
if (editSignal === undefined)
|
|
4790
4829
|
return p;
|
|
@@ -4823,7 +4862,7 @@ export class Runner {
|
|
|
4823
4862
|
const toolPolicyLocked = this.deps.lockedConfig?.keys?.includes("toolPolicy") === true;
|
|
4824
4863
|
const editBasePolicy = toolPolicyLocked
|
|
4825
4864
|
? (this.deps.basePolicyForResumeEdit ?? this.deps.toolPolicy)
|
|
4826
|
-
: (
|
|
4865
|
+
: (config.basePolicyForResumeEdit ?? this.deps.basePolicyForResumeEdit ?? config.toolPolicy ?? this.deps.toolPolicy);
|
|
4827
4866
|
if (editBasePolicy !== undefined) {
|
|
4828
4867
|
let rechecked;
|
|
4829
4868
|
try {
|
|
@@ -4843,7 +4882,7 @@ export class Runner {
|
|
|
4843
4882
|
`refused pre-CAS (the checkpoint stays pending); re-submit the edited action so the rewrite is adjudicated and approved synchronously`);
|
|
4844
4883
|
}
|
|
4845
4884
|
}
|
|
4846
|
-
const resumeHooks =
|
|
4885
|
+
const resumeHooks = config.hooks ?? this.deps.hooks;
|
|
4847
4886
|
if (resumeHooks?.preToolUse !== undefined && resumeHooks.preToolUseObservational === true) {
|
|
4848
4887
|
try {
|
|
4849
4888
|
await raceEditAbort(Promise.resolve(resumeHooks.preToolUse(cp.pendingAction.toolName, cloneObserverInput(editedArgs), {
|
|
@@ -4863,7 +4902,121 @@ export class Runner {
|
|
|
4863
4902
|
if (outcomeForStore === undefined) {
|
|
4864
4903
|
throw new CheckpointError("checkpoint.invalid_outcome", `resume outcome (gate "${describeSuppliedValue(outcomeGate)}") matched the checkpoint gate but no lane captured it — refusing pre-CAS rather than passing the caller's live object to the store and the resumed run`);
|
|
4865
4904
|
}
|
|
4905
|
+
if (this.deps.resumePreflight !== undefined) {
|
|
4906
|
+
const preflight = this.deps.resumePreflight;
|
|
4907
|
+
const rawDeadline = this.deps.resumePreflightTimeoutMs;
|
|
4908
|
+
let preflightTimeoutMs = RESUME_PREFLIGHT_DEFAULT_TIMEOUT_MS;
|
|
4909
|
+
if (rawDeadline !== undefined) {
|
|
4910
|
+
if (typeof rawDeadline === "number" && Number.isFinite(rawDeadline) && rawDeadline > 0 && rawDeadline <= MAX_HOOK_TIMEOUT_MS) {
|
|
4911
|
+
preflightTimeoutMs = rawDeadline;
|
|
4912
|
+
}
|
|
4913
|
+
else {
|
|
4914
|
+
try {
|
|
4915
|
+
this.deps.onError?.(new Error(`RunnerDeps.resumePreflightTimeoutMs is not a positive finite number within the seat ceiling of ${MAX_HOOK_TIMEOUT_MS}ms (got ${describeSuppliedValue(rawDeadline)}) — the resume preflight falls back to its own ${RESUME_PREFLIGHT_DEFAULT_TIMEOUT_MS}ms default`), { phase: "config", sessionId: cp.sessionId });
|
|
4916
|
+
}
|
|
4917
|
+
catch {
|
|
4918
|
+
}
|
|
4919
|
+
}
|
|
4920
|
+
}
|
|
4921
|
+
const info = {
|
|
4922
|
+
token,
|
|
4923
|
+
sessionId: cp.sessionId,
|
|
4924
|
+
...(cp.state.placementRootSessionId !== undefined ? { placementRootSessionId: cp.state.placementRootSessionId } : {}),
|
|
4925
|
+
...(cp.principal ? { principal: cp.principal } : {}),
|
|
4926
|
+
...(cp.state.workspaceHandle !== undefined ? { workspaceHandle: structuredClone(cp.state.workspaceHandle) } : {}),
|
|
4927
|
+
gateKind: cp.gate.kind,
|
|
4928
|
+
};
|
|
4929
|
+
const refusalTail = "; the checkpoint stays pending and the same token is redeemable once the obstacle clears";
|
|
4930
|
+
let verdict;
|
|
4931
|
+
try {
|
|
4932
|
+
const seat = await runHookSeat("resumePreflight", {
|
|
4933
|
+
timeoutMs: preflightTimeoutMs,
|
|
4934
|
+
...(resumeSignal !== undefined ? { signal: resumeSignal } : {}),
|
|
4935
|
+
abortEnds: true,
|
|
4936
|
+
onBadTimeout: (badErr) => {
|
|
4937
|
+
try {
|
|
4938
|
+
this.deps.onError?.(badErr instanceof Error ? badErr : new Error(String(badErr)), { phase: "hook", sessionId: cp.sessionId });
|
|
4939
|
+
}
|
|
4940
|
+
catch {
|
|
4941
|
+
}
|
|
4942
|
+
},
|
|
4943
|
+
owner: this.deps,
|
|
4944
|
+
}, (sig) => preflight(info, sig));
|
|
4945
|
+
if (seat.expired) {
|
|
4946
|
+
throw new CheckpointError("resume.preflight_rejected", seat.cause === "timeout"
|
|
4947
|
+
? `the deployment's resumePreflight did not answer within its ${preflightTimeoutMs}ms deadline while screening this resume — refused fail-closed (retry-later arm)${refusalTail}`
|
|
4948
|
+
: `the resume was cancelled while the deployment's resumePreflight was still screening it — refused (retry-later arm)${refusalTail}`);
|
|
4949
|
+
}
|
|
4950
|
+
verdict = seat.value;
|
|
4951
|
+
}
|
|
4952
|
+
catch (preflightErr) {
|
|
4953
|
+
if (preflightErr instanceof CheckpointError)
|
|
4954
|
+
throw preflightErr;
|
|
4955
|
+
const err = preflightErr instanceof Error ? preflightErr : new Error(String(preflightErr));
|
|
4956
|
+
try {
|
|
4957
|
+
this.deps.onError?.(err, { phase: "hook", sessionId: cp.sessionId });
|
|
4958
|
+
}
|
|
4959
|
+
catch {
|
|
4960
|
+
}
|
|
4961
|
+
throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight crashed while screening this resume (${inlineUntrusted(err.message)}) — refused fail-closed (retry-later arm)${refusalTail}`);
|
|
4962
|
+
}
|
|
4963
|
+
const verdictOk = verdict !== null && typeof verdict === "object" ? verdict.ok : undefined;
|
|
4964
|
+
if (verdictOk !== true) {
|
|
4965
|
+
const bag = verdict !== null && typeof verdict === "object" ? verdict : undefined;
|
|
4966
|
+
const disposition = bag?.disposition;
|
|
4967
|
+
const suppliedMessage = bag?.message;
|
|
4968
|
+
const retryAfterMs = bag?.retryAfterMs;
|
|
4969
|
+
const readableMessage = typeof suppliedMessage === "string" && suppliedMessage !== "" ? suppliedMessage : undefined;
|
|
4970
|
+
const said = readableMessage !== undefined ? `: ${inlineUntrusted(readableMessage)}` : " (no readable verdict — fail-closed)";
|
|
4971
|
+
if (disposition === "terminal" && readableMessage !== undefined) {
|
|
4972
|
+
this.locallyClaimedTokens.add(token);
|
|
4973
|
+
let settled;
|
|
4974
|
+
try {
|
|
4975
|
+
settled = await store.expire(token, cp.scope);
|
|
4976
|
+
}
|
|
4977
|
+
catch (expireErr) {
|
|
4978
|
+
this.locallyClaimedTokens.delete(token);
|
|
4979
|
+
throw expireErr;
|
|
4980
|
+
}
|
|
4981
|
+
if (!settled) {
|
|
4982
|
+
this.locallyClaimedTokens.delete(token);
|
|
4983
|
+
const live = await store.get(token);
|
|
4984
|
+
if (live?.status === "pending") {
|
|
4985
|
+
throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight ruled this resume PERMANENTLY blocked${said}, but the row moved under this refusal (a concurrent resolve/reopen cycle) — nothing was settled here; the row is pending again, re-resume against the current state (the preflight screens every attempt)`);
|
|
4986
|
+
}
|
|
4987
|
+
throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight ruled this resume PERMANENTLY blocked${said} — a concurrent actor had already settled the row (${describeSuppliedValue(live?.status ?? "consumed")}); nothing was settled by this refusal`);
|
|
4988
|
+
}
|
|
4989
|
+
this.parentConstraintRegistry.delete(token);
|
|
4990
|
+
const terminalReap = this.suspendedEnvReaps.get(token);
|
|
4991
|
+
this.suspendedEnvReaps.delete(token);
|
|
4992
|
+
this.locallyClaimedTokens.delete(token);
|
|
4993
|
+
if (terminalReap?.env !== undefined && hasDestroy(terminalReap.env)) {
|
|
4994
|
+
const terminalEnv = terminalReap.env;
|
|
4995
|
+
await settleTeardownLeg(() => terminalEnv.destroy(), "terminal-preflight env destroy", (envErr) => {
|
|
4996
|
+
try {
|
|
4997
|
+
this.deps.onError?.(envErr, { phase: "config", sessionId: terminalReap.sessionId });
|
|
4998
|
+
}
|
|
4999
|
+
catch {
|
|
5000
|
+
}
|
|
5001
|
+
});
|
|
5002
|
+
}
|
|
5003
|
+
try {
|
|
5004
|
+
this.deps.onError?.(new Error(`resumePreflight ruled this row PERMANENTLY blocked${said} — the row was settled terminally (expired) by this refusal's single-shot CAS (token no longer redeemable)`), { phase: "hook", sessionId: cp.sessionId });
|
|
5005
|
+
}
|
|
5006
|
+
catch {
|
|
5007
|
+
}
|
|
5008
|
+
throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight ruled this resume PERMANENTLY blocked${said} — the row was settled terminally (expired by this refusal's single-shot CAS); the token is not redeemable`);
|
|
5009
|
+
}
|
|
5010
|
+
const waitHint = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0 ? retryAfterMs : undefined;
|
|
5011
|
+
throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight refused this resume${said}${refusalTail}${waitHint !== undefined ? ` (deployment wait hint: ${String(waitHint)}ms)` : ""}`, waitHint !== undefined ? { retryAfterMs: waitHint } : undefined);
|
|
5012
|
+
}
|
|
5013
|
+
if (recheckGovernanceWindow !== undefined)
|
|
5014
|
+
await recheckGovernanceWindow();
|
|
5015
|
+
}
|
|
5016
|
+
this.locallyClaimedTokens.add(token);
|
|
4866
5017
|
const won = await store.resolve(token, cp.scope, outcomeForStore, { rev: cp.rev ?? 0 });
|
|
5018
|
+
if (!won)
|
|
5019
|
+
this.locallyClaimedTokens.delete(token);
|
|
4867
5020
|
if (!won) {
|
|
4868
5021
|
const live = await store.get(token);
|
|
4869
5022
|
if (live?.status === "pending") {
|
|
@@ -4878,15 +5031,15 @@ export class Runner {
|
|
|
4878
5031
|
consumeFlipDone = true;
|
|
4879
5032
|
}
|
|
4880
5033
|
const answerFace = plainPolicyOutcome !== undefined && redeemedAnswer !== undefined
|
|
4881
|
-
? answerFaceForRedeemedCall(redeemedAnswer, plainPolicyOutcome.boundCallId,
|
|
5034
|
+
? answerFaceForRedeemedCall(redeemedAnswer, plainPolicyOutcome.boundCallId, config.onQuestion ?? this.deps.onQuestion, boundInputHashOf(cp.pendingAction.args?.questions))
|
|
4882
5035
|
: undefined;
|
|
4883
5036
|
const spec = {
|
|
4884
|
-
...
|
|
4885
|
-
...((
|
|
5037
|
+
...config,
|
|
5038
|
+
...((config.principal || undefined) === undefined && (cp.principal || undefined) !== undefined ? { principal: cp.principal } : {}),
|
|
4886
5039
|
objective: "",
|
|
4887
5040
|
sessionId: cp.sessionId,
|
|
4888
5041
|
requireExistingSession: true,
|
|
4889
|
-
preemptSignal:
|
|
5042
|
+
preemptSignal: config.preemptSignal?.aborted ? undefined : config.preemptSignal,
|
|
4890
5043
|
...(answerFace !== undefined ? { onQuestion: answerFace } : {}),
|
|
4891
5044
|
};
|
|
4892
5045
|
const reopenFn = store.reopen?.bind(store);
|
package/dist/core/stub-env.d.ts
CHANGED
|
@@ -8,6 +8,10 @@ import { type ExecResult, type ExecutionEnv, ExecutionError, FileError, type Fil
|
|
|
8
8
|
*/
|
|
9
9
|
export declare class StubExecutionEnv implements ExecutionEnv {
|
|
10
10
|
cwd: string;
|
|
11
|
+
/** design/380 O9a — declared on the class so the prepare fold can read the optional interface
|
|
12
|
+
* member off a `ExecutionEnv | StubExecutionEnv` union; the no-I/O stub produces no target
|
|
13
|
+
* content, so it never declares (always `undefined` — the trusted-side default). */
|
|
14
|
+
readonly externalContentTarget?: boolean;
|
|
11
15
|
constructor(cwd?: string);
|
|
12
16
|
private fsErr;
|
|
13
17
|
absolutePath(path: string): Promise<Result<string, FileError>>;
|
package/dist/core/stub-env.js
CHANGED
|
@@ -166,6 +166,22 @@ function workflowAgentRow(a, ordinal) {
|
|
|
166
166
|
...(a.replayed === true ? { replayed: true } : {}),
|
|
167
167
|
};
|
|
168
168
|
}
|
|
169
|
+
function budgetOvershootNote(overshoot) {
|
|
170
|
+
if (overshoot === undefined)
|
|
171
|
+
return "";
|
|
172
|
+
const unsettled = overshoot.unsettledTokens ?? 0;
|
|
173
|
+
const total = overshoot.spentTokens + unsettled;
|
|
174
|
+
return (` token budget OVERSHOT: this run spent ${total.toLocaleString()} total tokens against a ${overshoot.budgetTokens.toLocaleString()} ceiling ` +
|
|
175
|
+
`(over by ${(total - overshoot.budgetTokens).toLocaleString()}` +
|
|
176
|
+
`${unsettled > 0 ? `, of which ${unsettled.toLocaleString()} was observed on agents still in flight at the terminal and never settled` : ""}).`);
|
|
177
|
+
}
|
|
178
|
+
function timeoutInterruptionNote(interruption) {
|
|
179
|
+
if (interruption === undefined)
|
|
180
|
+
return "";
|
|
181
|
+
return (` INTERRUPTED by the workflow's total timeout (${interruption.timeoutMs.toLocaleString()}ms): when the deadline fired ` +
|
|
182
|
+
`${interruption.agentsCompleted} agent(s) had completed, ${interruption.agentsFailed} had failed, and ` +
|
|
183
|
+
`${interruption.agentsInFlight} were still in flight — the failed run status is the deadline's, not a verdict on those agents.`);
|
|
184
|
+
}
|
|
169
185
|
export function formatWorkflowRun(run) {
|
|
170
186
|
const summary = summarizeWorkflowRun(run);
|
|
171
187
|
const done = run.agents.filter((a) => a.status === "completed" || a.status === "failed").length;
|
|
@@ -223,7 +239,7 @@ export function formatWorkflowRun(run) {
|
|
|
223
239
|
: {}),
|
|
224
240
|
}
|
|
225
241
|
: {}),
|
|
226
|
-
note: run.status === "running"
|
|
242
|
+
note: (run.status === "running"
|
|
227
243
|
? "still running — poll again shortly."
|
|
228
244
|
: run.status === "completed"
|
|
229
245
|
? run.result !== undefined
|
|
@@ -231,7 +247,9 @@ export function formatWorkflowRun(run) {
|
|
|
231
247
|
: "completed — this run predates result persistence; its result was delivered on the completion notification."
|
|
232
248
|
: finishedAgents.length > 0
|
|
233
249
|
? `failed — see error. ${finishedAgents.length} agent(s) had already completed before the run ended; their outputs are inlined under \`partial_results\`.`
|
|
234
|
-
: "failed — see error."
|
|
250
|
+
: "failed — see error.") +
|
|
251
|
+
timeoutInterruptionNote(summary.timeoutInterruption) +
|
|
252
|
+
budgetOvershootNote(summary.budgetOvershoot),
|
|
235
253
|
}),
|
|
236
254
|
details: {
|
|
237
255
|
task_id: run.id,
|
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
|
|
@@ -5209,8 +5216,13 @@ export interface EngineNotice {
|
|
|
5209
5216
|
* two numbers are read off the report's rows, never subtracted from each other). One notice PER
|
|
5210
5217
|
* HARVEST that withheld at least one entry file (a checkpoint harvest and the terminal harvest
|
|
5211
5218
|
* are distinct facts), never minted for a clean session; `detail: { count, moved, escalated,
|
|
5212
|
-
* reason?, sessionId? }` (`reason` absent ⇔ the pollution marker could not be re-read
|
|
5213
|
-
* time — the withheld count stays true either way).
|
|
5219
|
+
* reportId?, reason?, sessionId? }` (`reason` absent ⇔ the pollution marker could not be re-read
|
|
5220
|
+
* at report time — the withheld count stays true either way). `reportId` (#479) is the minting
|
|
5221
|
+
* harvest's own occurrence identity ({@link import("./memory-engine/types.js").HarvestReport}
|
|
5222
|
+
* `.reportId`, uuidv7): two harvests whose counts and paths coincide carry DISTINCT ids, a
|
|
5223
|
+
* durable replay of the same notice carries the SAME id, and the hold family below shares this
|
|
5224
|
+
* harvest's id — dedup on `(code, detail.reportId)`, never on `reportId` alone (absent only for
|
|
5225
|
+
* a hand-built report; every engine-minted report carries it). The formerly registered index-only gap is
|
|
5214
5226
|
* CLOSED under `memoryProvenance: "carry"` (design/336 §6.3, #331): the mint condition reads
|
|
5215
5227
|
* `HarvestReport.containment`, so a containment whose only act was the derived-index rollback
|
|
5216
5228
|
* announces with `count: 0` and `detail.indexRolledBack: true`; under `"off"` the pre-336
|
|
@@ -5219,15 +5231,18 @@ export interface EngineNotice {
|
|
|
5219
5231
|
* §4/§6.3) — the instruction-hold lifecycle, derived from the same structured
|
|
5220
5232
|
* `HarvestReport.containment` signal (hold seats are only ever filled under
|
|
5221
5233
|
* `memoryProvenance: "carry"`): a PENDING session's instruction-form entry files were captured
|
|
5222
|
-
* off the model-visible plane (`hold_opened`, `detail: { count, paths, sessionId? }`
|
|
5223
|
-
* neutralized/length-bounded); previously held entries re-walked the full gate set and
|
|
5234
|
+
* off the model-visible plane (`hold_opened`, `detail: { count, paths, reportId?, sessionId? }`
|
|
5235
|
+
* — paths neutralized/length-bounded); previously held entries re-walked the full gate set and
|
|
5224
5236
|
* committed after their writer session settled clean or a host valve released them
|
|
5225
|
-
* (`hold_released
|
|
5226
|
-
* `detail.disposed: [{ path, terminal }]` with the closed terminal set
|
|
5237
|
+
* (`hold_released`, same detail shape); held entries moved to control-plane quarantine
|
|
5238
|
+
* (`hold_disposed`, `detail.disposed: [{ path, terminal }]` with the closed terminal set
|
|
5227
5239
|
* dirty/expired/conflict/capture_lost/discarded — an `expired` terminal is explicitly a
|
|
5228
5240
|
* TIMEOUT, not a conviction, and the message names `resolveHold` as the recovery valve). At
|
|
5229
5241
|
* most one notice per family per harvest report; wording is factual, never threat-flavored
|
|
5230
|
-
* (the design/336 §13-2 model-psyche guardrail).
|
|
5242
|
+
* (the design/336 §13-2 model-psyche guardrail). `detail.reportId` (#479) is shared by all
|
|
5243
|
+
* three families AND `"memory.harvest_quarantined"` when they derive from the same harvest
|
|
5244
|
+
* report — that sharing is the contract (one report, several distinct facts): dedup on
|
|
5245
|
+
* `(code, detail.reportId)`; a repeat with the same pair is a replay, a new pair is a new fact.
|
|
5231
5246
|
* - `"memory.delegation_static_mark_waived"` (design/324, #324 ruling ①) — the deployment set
|
|
5232
5247
|
* {@link RunnerDeps.memoryDelegationEvidence} to `"attested-only"` and a delegation call whose
|
|
5233
5248
|
* STATIC tool-face verdict would have marked this session's memory polluted (attestation
|
|
@@ -5291,7 +5306,9 @@ export interface EngineNotice {
|
|
|
5291
5306
|
* session count crossed the consolidation thresholds (time gate open ∧ enough distinct
|
|
5292
5307
|
* sessions); minted at most once per crossing (a completed run re-arms the edge), NEVER when
|
|
5293
5308
|
* the deployment leaves consolidation off. ADVISORY: the host owns the verbs, nothing runs
|
|
5294
|
-
* automatically; `detail: { scope, sessionsSince, sessionId? }
|
|
5309
|
+
* automatically; `detail: { scope, sessionsSince, reportId?, sessionId? }` — `reportId` (#479)
|
|
5310
|
+
* is the minting harvest report's occurrence identity, shared with the harvest/hold family's
|
|
5311
|
+
* notices of the same report (dedup on `(code, detail.reportId)`).
|
|
5295
5312
|
* - `"memory.consolidation_committed"` (design/339 §6.2) — a consolidation plan reached
|
|
5296
5313
|
* `completed`: products landed, superseded targets left the default read face (retained as
|
|
5297
5314
|
* evidence), intents settled; `detail: { planId, scope, products, superseded, intents }` —
|
|
@@ -5369,6 +5386,56 @@ export declare function undrainedUserInputNotices(counts: {
|
|
|
5369
5386
|
steer: number;
|
|
5370
5387
|
followUp: number;
|
|
5371
5388
|
}, taskId?: string, sessionId?: string): EngineNotice[];
|
|
5389
|
+
/**
|
|
5390
|
+
* design/380 O2 — the facts handed to a deployment's {@link RunnerDeps.resumePreflight}: the row's
|
|
5391
|
+
* own recorded identity + placement record, read off the persisted checkpoint (never off the resume
|
|
5392
|
+
* caller's bag), so the hook judges the SAME row the CAS is about to consume.
|
|
5393
|
+
*/
|
|
5394
|
+
export interface ResumePreflightInfo {
|
|
5395
|
+
token: import("./checkpoint-store.js").CheckpointToken;
|
|
5396
|
+
sessionId: string;
|
|
5397
|
+
/** design/380 O1③ — the row's recorded placement root, when stamped. */
|
|
5398
|
+
placementRootSessionId?: string;
|
|
5399
|
+
/** The row's recorded identity ({@link import("./checkpoint-store.js").Checkpoint.principal}), when stamped —
|
|
5400
|
+
* the resume entry's identity-continuity rung already refused a contradicting supplied principal
|
|
5401
|
+
* before this hook runs, so a present value IS the resumed leg's identity. */
|
|
5402
|
+
principal?: string;
|
|
5403
|
+
/** provider / deviceId / mountPath — the placement facts. */
|
|
5404
|
+
workspaceHandle?: import("./remote-env.js").WorkspaceHandle;
|
|
5405
|
+
gateKind: import("./checkpoint-store.js").CheckpointGate["kind"];
|
|
5406
|
+
}
|
|
5407
|
+
/** design/380 O2 — a {@link RunnerDeps.resumePreflight} answer. */
|
|
5408
|
+
export type ResumePreflightVerdict = {
|
|
5409
|
+
ok: true;
|
|
5410
|
+
}
|
|
5411
|
+
/** Transient obstacle (device offline, dependency briefly down): the checkpoint stays `pending`,
|
|
5412
|
+
* the SAME token is redeemable later; retryAfterMs threads the wait hint (the #449 G1 carrier). */
|
|
5413
|
+
| {
|
|
5414
|
+
ok: false;
|
|
5415
|
+
disposition?: "retry_later";
|
|
5416
|
+
message: string;
|
|
5417
|
+
retryAfterMs?: number;
|
|
5418
|
+
}
|
|
5419
|
+
/** PERMANENT obstacle (binding revoked, placement identity gone): the row is settled TERMINALLY —
|
|
5420
|
+
* core CASes it out of `pending` before answering, so a dead binding cannot
|
|
5421
|
+
* be redialed forever (prose-terminal + mechanically-retriable = unbounded redeem). The `expire`
|
|
5422
|
+
* CAS is a pure STATUS flip — the row records no reason (the store seam has no seat for one);
|
|
5423
|
+
* the REASON travels on the typed `resume.preflight_rejected` refusal (the hook's `message`
|
|
5424
|
+
* inlined), the `onError` disclosure sink, and the deployment hook's own audit plane — it
|
|
5425
|
+
* authored the verdict and owns the durable record of why. The exact
|
|
5426
|
+
* terminal CAS form is the single-shot `expire` CAS (reaper parity — exactly one settler wins; a
|
|
5427
|
+
* LOST race is re-read and reported as concurrent movement, never claimed as this refusal's
|
|
5428
|
+
* settle); the CONTRACT is: terminal verdict ⇒ atomic single-shot settle, never a silent
|
|
5429
|
+
* pending-forever. PHYSICAL reclamation of a suspended workspace stays with the DEPLOYMENT that
|
|
5430
|
+
* ruled the binding dead — core holds no env pre-restore and a factory cannot reconnect to a
|
|
5431
|
+
* revoked target; the session pin follows the store contract's documented reap posture. Trust
|
|
5432
|
+
* grant is acceptable: the hook lives on RunnerDeps, the same deployment plane that owns the
|
|
5433
|
+
* checkpoint store itself. */
|
|
5434
|
+
| {
|
|
5435
|
+
ok: false;
|
|
5436
|
+
disposition: "terminal";
|
|
5437
|
+
message: string;
|
|
5438
|
+
};
|
|
5372
5439
|
/** Runtime dependencies shared across tasks. */
|
|
5373
5440
|
export interface RunnerDeps {
|
|
5374
5441
|
brain: Brain;
|
|
@@ -5632,6 +5699,29 @@ export interface RunnerDeps {
|
|
|
5632
5699
|
* `checkpointStore` overrides this. Omitted → no durable suspension (policy `ask` stays synchronous).
|
|
5633
5700
|
*/
|
|
5634
5701
|
checkpointStore?: import("./checkpoint-store.js").CheckpointStore;
|
|
5702
|
+
/**
|
|
5703
|
+
* design/380 O2 — deployment-supplied resume preflight, called INSIDE the pre-CAS ladder (after the
|
|
5704
|
+
* row-integrity rungs, immediately before the CAS) with a bounded deadline
|
|
5705
|
+
* ({@link resumePreflightTimeoutMs}). Refusal / throw / timeout ⇒ typed `resume.preflight_rejected`
|
|
5706
|
+
* (CheckpointError closed-set addition); the DEFAULT/absent disposition is `retry_later` (the safe
|
|
5707
|
+
* arm — fail-closed hooks that just throw can never accidentally terminalize a row) — the
|
|
5708
|
+
* entrance-screen posture of the wake-message hook (design/373 D2), generalized from "screen the
|
|
5709
|
+
* wake message" to "screen the resume". A deployment binding runs to targets (a device lane) checks
|
|
5710
|
+
* its placement/binding tables here: a transient obstacle answers `retry_later` (+`retryAfterMs`,
|
|
5711
|
+
* the #449 G1 wait-hint carrier — the row stays `pending`, the SAME token redeems later); a
|
|
5712
|
+
* PERMANENT one answers `terminal` (the row is settled by the single-shot `expire` CAS — never a
|
|
5713
|
+
* silent pending-forever; bounded waiting for a target to come back also belongs HERE, inside the
|
|
5714
|
+
* deadline, never in the env factory). Absent ⇒ no preflight, byte-identical resume behavior.
|
|
5715
|
+
*/
|
|
5716
|
+
resumePreflight?: (info: ResumePreflightInfo, signal: AbortSignal) => Promise<ResumePreflightVerdict>;
|
|
5717
|
+
/**
|
|
5718
|
+
* design/380 O2 — the {@link resumePreflight} deadline in ms. Default 10s (its own bound,
|
|
5719
|
+
* deliberately NOT the hooks-record default: a resume preflight sits on every redeem attempt of a
|
|
5720
|
+
* parked row and must answer promptly or get out of the way). Bad values (non-finite, ≤ 0,
|
|
5721
|
+
* non-number) are DISCLOSED loudly through `onError` and fall back to the default (the bad-value
|
|
5722
|
+
* loudness default: announce, never silently absorb).
|
|
5723
|
+
*/
|
|
5724
|
+
resumePreflightTimeoutMs?: number;
|
|
5635
5725
|
/**
|
|
5636
5726
|
* design/101 §E19 — working-tree snapshot backend for {@link TaskSpec.rewindFiles}. A task with
|
|
5637
5727
|
* `rewindFiles` captures a snapshot per completed turn (keyed by the leaf `SessionTreeEntry.id`) and, when it
|
|
@@ -5932,9 +6022,12 @@ export interface RunnerDeps {
|
|
|
5932
6022
|
* `TaskSpec.limits`, which is the allowance ONE task asked for — an operator granting "N tokens per 5
|
|
5933
6023
|
* hours" cannot express it as a task limit, because nothing stops the next task from asking again.
|
|
5934
6024
|
*
|
|
5935
|
-
* A window carries a TOKEN ceiling
|
|
5936
|
-
* USD — the `TaskLimits.maxCostUsd` quantity one governance level up)
|
|
5937
|
-
*
|
|
6025
|
+
* A window carries a TOKEN ceiling (`UsageWindow.maxTokens`), a MONEY ceiling (`UsageWindow.maxCostUsd`,
|
|
6026
|
+
* absolute USD — the `TaskLimits.maxCostUsd` quantity one governance level up), or both: each is
|
|
6027
|
+
* optional on its own and at least one is required, so a deployment governed by spend alone declares
|
|
6028
|
+
* only `maxCostUsd` (a window with neither ceiling is refused, `config.usage_window_invalid`). The two
|
|
6029
|
+
* are independent and every declared one binds. A $ ceiling requires a PRICED run: a task whose model
|
|
6030
|
+
* has neither a {@link pricing}
|
|
5938
6031
|
* entry nor a `Model.cost` declaration is refused at the door (`config.usage_window_unpriced`) rather
|
|
5939
6032
|
* than charged the fabricated 0 an unpriced run would otherwise file into an operator's ceiling.
|
|
5940
6033
|
*
|