@sema-agent/core 7.5.0 → 7.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/dist/core/auto-mode.d.ts +9 -0
- package/dist/core/auto-mode.js +11 -0
- package/dist/core/checkpoint-store.js +5 -1
- package/dist/core/protocol-table.d.ts +5 -0
- package/dist/core/protocol-table.js +1 -0
- package/dist/core/runner/abort-race.d.ts +41 -0
- package/dist/core/runner/abort-race.js +38 -0
- package/dist/core/runner/checkpoint-scope.d.ts +44 -0
- package/dist/core/runner/checkpoint-scope.js +7 -0
- package/dist/core/runner/compaction-call-options.d.ts +1 -1
- package/dist/core/runner/content-ask-bindings.d.ts +27 -0
- package/dist/core/runner/content-ask-bindings.js +1 -0
- package/dist/core/runner/contracts.d.ts +1990 -0
- package/dist/core/runner/contracts.js +1 -0
- package/dist/core/runner/denial-limit-arms.d.ts +24 -1
- package/dist/core/runner/denial-limit-arms.js +21 -0
- package/dist/core/runner/derived-route-fallback.d.ts +34 -0
- package/dist/core/runner/derived-route-fallback.js +16 -0
- package/dist/core/runner/inherited-ask-grants.d.ts +46 -0
- package/dist/core/runner/inherited-ask-grants.js +29 -0
- package/dist/core/runner/park-commit.d.ts +108 -0
- package/dist/core/runner/park-commit.js +32 -0
- package/dist/core/runner/{prepare-permission-rules.d.ts → permission-rule-lanes.d.ts} +109 -3
- package/dist/core/runner/{prepare-permission-rules.js → permission-rule-lanes.js} +47 -1
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +1 -1
- package/dist/core/runner/prepare-ask-lane.d.ts +110 -0
- package/dist/core/runner/prepare-ask-lane.js +133 -0
- package/dist/core/runner/prepare-boundary-parks.d.ts +105 -0
- package/dist/core/runner/prepare-boundary-parks.js +169 -0
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +170 -0
- package/dist/core/runner/prepare-caps-and-workflow.js +255 -0
- package/dist/core/runner/prepare-config-doors.d.ts +2 -10
- package/dist/core/runner/prepare-context-lane.d.ts +119 -0
- package/dist/core/runner/prepare-context-lane.js +230 -0
- package/dist/core/runner/prepare-defer-classify.d.ts +86 -0
- package/dist/core/runner/prepare-defer-classify.js +107 -0
- package/dist/core/runner/prepare-delegation-surface.d.ts +104 -0
- package/dist/core/runner/prepare-delegation-surface.js +144 -0
- package/dist/core/runner/prepare-execution-env.d.ts +54 -0
- package/dist/core/runner/prepare-execution-env.js +86 -0
- package/dist/core/runner/prepare-file-history.d.ts +95 -0
- package/dist/core/runner/prepare-file-history.js +383 -0
- package/dist/core/runner/prepare-gate-stations.d.ts +177 -0
- package/dist/core/runner/prepare-gate-stations.js +290 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +9 -11
- package/dist/core/runner/prepare-hands-readface.js +1 -1
- package/dist/core/runner/prepare-inherited-gate.d.ts +268 -0
- package/dist/core/runner/prepare-inherited-gate.js +266 -0
- package/dist/core/runner/prepare-listings.d.ts +77 -0
- package/dist/core/runner/prepare-listings.js +76 -0
- package/dist/core/runner/prepare-lsp.d.ts +55 -0
- package/dist/core/runner/prepare-lsp.js +27 -0
- package/dist/core/runner/prepare-memory-engine-session.d.ts +84 -0
- package/dist/core/runner/prepare-memory-engine-session.js +233 -0
- package/dist/core/runner/prepare-memory.d.ts +1 -1
- package/dist/core/runner/prepare-offload-wrappers.d.ts +62 -0
- package/dist/core/runner/prepare-offload-wrappers.js +45 -0
- package/dist/core/runner/prepare-park-ask.d.ts +164 -0
- package/dist/core/runner/prepare-park-ask.js +377 -0
- package/dist/core/runner/prepare-policy-chain.d.ts +208 -0
- package/dist/core/runner/prepare-policy-chain.js +584 -0
- package/dist/core/runner/prepare-project-context.d.ts +119 -0
- package/dist/core/runner/prepare-project-context.js +148 -0
- package/dist/core/runner/prepare-prompt-assembly.d.ts +95 -0
- package/dist/core/runner/prepare-prompt-assembly.js +162 -0
- package/dist/core/runner/prepare-prompt-inputs.d.ts +119 -0
- package/dist/core/runner/prepare-prompt-inputs.js +141 -0
- package/dist/core/runner/prepare-protocol-tools.d.ts +91 -0
- package/dist/core/runner/prepare-protocol-tools.js +179 -0
- package/dist/core/runner/prepare-question-face.d.ts +101 -0
- package/dist/core/runner/prepare-question-face.js +84 -0
- package/dist/core/runner/prepare-run-refs.d.ts +89 -0
- package/dist/core/runner/prepare-run-refs.js +39 -0
- package/dist/core/runner/prepare-safety-scan.d.ts +3 -7
- package/dist/core/runner/prepare-safety-scan.js +1 -2
- package/dist/core/runner/prepare-suspend-saga.d.ts +170 -0
- package/dist/core/runner/prepare-suspend-saga.js +308 -0
- package/dist/core/runner/prepare-task.d.ts +14 -1976
- package/dist/core/runner/prepare-task.js +182 -5162
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +111 -0
- package/dist/core/runner/prepare-tool-disclosure-mount.js +219 -0
- package/dist/core/runner/prepare-turn-wiring.d.ts +154 -0
- package/dist/core/runner/prepare-turn-wiring.js +201 -0
- package/dist/core/runner/prepare-wiring-manifest.d.ts +192 -0
- package/dist/core/runner/prepare-wiring-manifest.js +247 -0
- package/dist/core/runner/prepare-workspace-restore.d.ts +3 -56
- package/dist/core/runner/prepare-workspace-restore.js +4 -38
- package/dist/core/runner/prompt-hash-salt.d.ts +1 -0
- package/dist/core/runner/prompt-hash-salt.js +2 -0
- package/dist/core/runner/remote-env-retry.d.ts +29 -0
- package/dist/core/runner/remote-env-retry.js +16 -0
- package/dist/core/runner/rollback-stack.d.ts +32 -0
- package/dist/core/runner/rollback-stack.js +30 -0
- package/dist/core/runner/runtask.js +1 -1
- package/dist/core/runner/workspace-path.d.ts +33 -0
- package/dist/core/runner/workspace-path.js +22 -0
- package/dist/core/session.d.ts +12 -0
- package/dist/core/session.js +3 -0
- package/dist/core/tool-policy.d.ts +16 -0
- package/dist/core/tool-policy.js +3 -0
- package/dist/core/types.d.ts +2 -2
- package/dist/core/write-protect.js +3 -2
- package/package.json +6 -2
- /package/dist/core/runner/{prepare-announce-once.d.ts → announce-once-ledger.d.ts} +0 -0
- /package/dist/core/runner/{prepare-announce-once.js → announce-once-ledger.js} +0 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { defaultTaskRegistry } from "../task-registry.js";
|
|
2
|
+
import { isRemoteExecutionEnv, isSuspendable, RemoteExecutionError } from "../remote-env.js";
|
|
3
|
+
import { constraintChainDigest, constraintChainEntryOfLayer } from "../tool-policy.js";
|
|
4
|
+
import { raceSettlementAgainstSignal } from "./abort-race.js";
|
|
5
|
+
import { placementValueOrAbsent } from "./checkpoint-scope.js";
|
|
6
|
+
import { serializeAnnouncedListings } from "./announce-once-ledger.js";
|
|
7
|
+
import { remoteEnvFailureNote, restoreWorkspaceWithRetry } from "./remote-env-retry.js";
|
|
8
|
+
const PARK_COMPENSATION_TIMEOUT_MS = 30_000;
|
|
9
|
+
export async function compensateUnparkedPause(remoteEnv, snapshotId, io) {
|
|
10
|
+
const boundCtl = new AbortController();
|
|
11
|
+
const boundTimer = setTimeout(() => boundCtl.abort(), io.boundMs);
|
|
12
|
+
const bound = boundCtl.signal;
|
|
13
|
+
let phase = "resumeVM";
|
|
14
|
+
let restoreAttempts = 1;
|
|
15
|
+
let vmRunning = false;
|
|
16
|
+
const work = (async () => {
|
|
17
|
+
const { outcome: back, attempts: backAttempts } = await restoreWorkspaceWithRetry(remoteEnv, snapshotId, { abortSignal: bound }, (attempt) => (restoreAttempts = attempt));
|
|
18
|
+
if (!back.ok) {
|
|
19
|
+
io.noteFailure(remoteEnvFailureNote("resumeVM", back.error, backAttempts));
|
|
20
|
+
io.disclose(back.error);
|
|
21
|
+
return { ok: false, reason: `resumeVM failed (${back.error.code}) while compensating an unparked pause` };
|
|
22
|
+
}
|
|
23
|
+
vmRunning = true;
|
|
24
|
+
if (bound.aborted) {
|
|
25
|
+
return { ok: false, reason: "resumeVM settled only after the decision bound" };
|
|
26
|
+
}
|
|
27
|
+
phase = "postResumeInit";
|
|
28
|
+
const init = await remoteEnv.postResumeInit({ abortSignal: bound });
|
|
29
|
+
if (!init.ok) {
|
|
30
|
+
io.noteFailure(remoteEnvFailureNote("postResumeInit", init.error, 1));
|
|
31
|
+
io.disclose(init.error);
|
|
32
|
+
return { ok: false, reason: `postResumeInit failed (${init.error.code}) while compensating an unparked pause` };
|
|
33
|
+
}
|
|
34
|
+
return { ok: true };
|
|
35
|
+
})();
|
|
36
|
+
const raced = await raceSettlementAgainstSignal(work, bound);
|
|
37
|
+
clearTimeout(boundTimer);
|
|
38
|
+
if (raced.tag === "value")
|
|
39
|
+
return raced.value;
|
|
40
|
+
if (raced.tag === "threw") {
|
|
41
|
+
io.noteFailure(remoteEnvFailureNote(phase, raced.error instanceof RemoteExecutionError
|
|
42
|
+
? raced.error
|
|
43
|
+
: new RemoteExecutionError("unknown", `the pause compensation's ${phase} call threw: ${raced.error instanceof Error ? raced.error.message : String(raced.error)}`), phase === "resumeVM" ? restoreAttempts : 1));
|
|
44
|
+
io.disclose(raced.error);
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
reason: "the pause compensation threw (adapter contract violation; the deployment's error face carries the exception)",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const timedOut = new RemoteExecutionError("timeout", `the pause compensation's ${phase} call did not settle within the ${io.boundMs}ms decision bound`);
|
|
51
|
+
io.noteFailure(remoteEnvFailureNote(phase, timedOut, phase === "resumeVM" ? restoreAttempts : 1));
|
|
52
|
+
io.disclose(timedOut);
|
|
53
|
+
const destroyOwnerlessLate = () => {
|
|
54
|
+
io.disclose(new Error(`park compensation settled after its ${io.boundMs}ms decision bound: the VM is running with no run ` +
|
|
55
|
+
`left to own it — destroying the env (an ownerless running VM must not linger; the run already took ` +
|
|
56
|
+
`the fail-closed arm when the bound fired)`));
|
|
57
|
+
void Promise.resolve()
|
|
58
|
+
.then(() => remoteEnv.destroy())
|
|
59
|
+
.catch((destroyErr) => io.disclose(destroyErr));
|
|
60
|
+
};
|
|
61
|
+
void work.then((late) => {
|
|
62
|
+
if (vmRunning) {
|
|
63
|
+
destroyOwnerlessLate();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
void late;
|
|
67
|
+
}, (lateErr) => {
|
|
68
|
+
io.disclose(lateErr);
|
|
69
|
+
if (vmRunning)
|
|
70
|
+
destroyOwnerlessLate();
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
reason: `the pause compensation did not settle within ${io.boundMs}ms — treated as failed (a late-restored VM is destroyed; a late restore failure is disclosed)`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function stampPlacementRootSessionId(placementRootResolved) {
|
|
78
|
+
return placementValueOrAbsent(placementRootResolved);
|
|
79
|
+
}
|
|
80
|
+
function autoModeLatchHealthy(d) {
|
|
81
|
+
try {
|
|
82
|
+
return d.breakerOpen() !== true && typeof d.consecutiveFailures === "function" && d.consecutiveFailures() === 0;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function prepareSuspendSaga(input) {
|
|
89
|
+
const { gateMachineryActive, abortController, harness, checkpointStore, deps, sessionId, hostTaskId, taskScope, liveSpendRef, resume, nestedStats, internals, activeTools, outputRef, readFileStateForCheckpoint, faceCheckpointSection, reminderMark, handsCwdRef, worktreeSessionRef, inheritedParentConstraints, seedInheritedGate, inheritedAncestorRules, inheritedShellGate, autoModeIntent, autoModeDecider, inheritedAdmittedOrgScopes, ownOrgVerdictRef, orgGovernedProvenance, announcedListingsRef, gitStatusRef, hookIdentity, placementRootResolved, externalContentTargetActive, remoteEnvFailures, memoryEngineSession, suspendLoopRef, ownedEnv, incompleteSuspendAdapter } = input;
|
|
90
|
+
if (!gateMachineryActive)
|
|
91
|
+
return { saga: undefined };
|
|
92
|
+
const inFlightSpendMicroUsd = () => {
|
|
93
|
+
const own = liveSpendRef.get?.().costMicroUsd ?? 0;
|
|
94
|
+
const seededNested = resume?.seed.nestedStats.costMicroUsd;
|
|
95
|
+
const nestedDelta = nestedStats.costMicroUsd - (typeof seededNested === "number" && Number.isFinite(seededNested) ? seededNested : 0);
|
|
96
|
+
return (Number.isFinite(own) ? own : 0) + Math.max(0, Number.isFinite(nestedDelta) ? nestedDelta : 0);
|
|
97
|
+
};
|
|
98
|
+
const repairBundleForCheckpoint = (parkedSpendMicroUsd) => {
|
|
99
|
+
const carried = internals?.repairBundle !== undefined
|
|
100
|
+
? structuredClone(internals.repairBundle)
|
|
101
|
+
: resume?.seed.repairBundle !== undefined
|
|
102
|
+
? structuredClone(resume.seed.repairBundle)
|
|
103
|
+
: undefined;
|
|
104
|
+
if (carried === undefined)
|
|
105
|
+
return undefined;
|
|
106
|
+
if (parkedSpendMicroUsd === undefined || !Number.isFinite(parkedSpendMicroUsd) || parkedSpendMicroUsd <= 0)
|
|
107
|
+
return carried;
|
|
108
|
+
const prior = typeof carried.spentMicroUsd === "number" && Number.isFinite(carried.spentMicroUsd) ? Math.max(0, carried.spentMicroUsd) : 0;
|
|
109
|
+
carried.spentMicroUsd = prior + parkedSpendMicroUsd;
|
|
110
|
+
return carried;
|
|
111
|
+
};
|
|
112
|
+
const screeningParkDisclosedRef = { done: false };
|
|
113
|
+
const serializeCheckpointState = (workspaceHandle, parkedSpendMicroUsd) => ({
|
|
114
|
+
activeTools: [...activeTools],
|
|
115
|
+
outputRef: { value: outputRef.value, set: outputRef.set },
|
|
116
|
+
nestedStats: { ...nestedStats },
|
|
117
|
+
consolidationNotes: undefined,
|
|
118
|
+
readFileState: readFileStateForCheckpoint ? [...readFileStateForCheckpoint.entries()] : undefined,
|
|
119
|
+
readFace: faceCheckpointSection(),
|
|
120
|
+
reminderMark,
|
|
121
|
+
repairBundle: repairBundleForCheckpoint(parkedSpendMicroUsd),
|
|
122
|
+
workspaceHandle,
|
|
123
|
+
handsCwd: handsCwdRef?.current,
|
|
124
|
+
activeWorktree: worktreeSessionRef?.current ? { ...worktreeSessionRef.current } : undefined,
|
|
125
|
+
runningBackgroundTasks: (() => {
|
|
126
|
+
const live = defaultTaskRegistry
|
|
127
|
+
.list({ owner: hostTaskId, scope: taskScope, sessionId })
|
|
128
|
+
.filter((t) => t.status === "pending" || t.status === "running")
|
|
129
|
+
.map((t) => ({ id: t.task_id, ...(t.description !== undefined ? { description: t.description } : {}) }));
|
|
130
|
+
return live.length > 0 ? live : undefined;
|
|
131
|
+
})(),
|
|
132
|
+
pendingSteer: undefined,
|
|
133
|
+
pendingSteerQueue: undefined,
|
|
134
|
+
inheritedGate: (() => {
|
|
135
|
+
const requiresParentConstraint = (inheritedParentConstraints?.length ?? 0) > 0 || seedInheritedGate?.requiresParentConstraint === true;
|
|
136
|
+
const parentConstraintCount = (inheritedParentConstraints?.length ?? 0) > 0 ? inheritedParentConstraints.length : seedInheritedGate?.parentConstraintCount;
|
|
137
|
+
const constraintChain = (inheritedParentConstraints?.length ?? 0) > 0
|
|
138
|
+
? inheritedParentConstraints.map((pc) => constraintChainEntryOfLayer(pc))
|
|
139
|
+
: seedInheritedGate?.constraintChain;
|
|
140
|
+
const constraintDigest = (inheritedParentConstraints?.length ?? 0) > 0
|
|
141
|
+
? constraintChainDigest(constraintChain)
|
|
142
|
+
: seedInheritedGate?.constraintDigest;
|
|
143
|
+
const autoModeIntentCarried = autoModeIntent && (autoModeDecider === undefined || autoModeLatchHealthy(autoModeDecider));
|
|
144
|
+
return inheritedAncestorRules !== undefined ||
|
|
145
|
+
inheritedShellGate !== undefined ||
|
|
146
|
+
autoModeIntentCarried ||
|
|
147
|
+
inheritedAdmittedOrgScopes !== undefined ||
|
|
148
|
+
ownOrgVerdictRef.current !== undefined ||
|
|
149
|
+
orgGovernedProvenance ||
|
|
150
|
+
requiresParentConstraint
|
|
151
|
+
? {
|
|
152
|
+
...(inheritedAncestorRules !== undefined ? { ancestorRules: structuredClone(inheritedAncestorRules) } : {}),
|
|
153
|
+
...(inheritedShellGate !== undefined ? { shellGate: inheritedShellGate } : {}),
|
|
154
|
+
...(autoModeIntentCarried ? { autoModeRequested: true } : {}),
|
|
155
|
+
...(inheritedAdmittedOrgScopes !== undefined ? { admittedOrgScopes: [...inheritedAdmittedOrgScopes] } : {}),
|
|
156
|
+
...(ownOrgVerdictRef.current !== undefined
|
|
157
|
+
? { ownAdmittedOrgScopes: [...ownOrgVerdictRef.current.scopes], ownAdmittedOrgWriteScope: ownOrgVerdictRef.current.writeScope }
|
|
158
|
+
: {}),
|
|
159
|
+
...(orgGovernedProvenance ? { orgAdmissionGoverned: true } : {}),
|
|
160
|
+
requiresParentConstraint,
|
|
161
|
+
...(requiresParentConstraint && parentConstraintCount !== undefined ? { parentConstraintCount } : {}),
|
|
162
|
+
...(requiresParentConstraint && constraintChain !== undefined
|
|
163
|
+
? { constraintChain: structuredClone(constraintChain), ...(constraintDigest !== undefined ? { constraintDigest } : {}) }
|
|
164
|
+
: {}),
|
|
165
|
+
}
|
|
166
|
+
: undefined;
|
|
167
|
+
})(),
|
|
168
|
+
announcedListings: serializeAnnouncedListings(announcedListingsRef),
|
|
169
|
+
gitAnnouncement: gitStatusRef.announced !== undefined ? { ...gitStatusRef.announced } : undefined,
|
|
170
|
+
delegationProvenance: internals?.delegationProvenance !== undefined ? { ...internals.delegationProvenance.ref.current } : undefined,
|
|
171
|
+
isDelegatedChild: hookIdentity.isDelegatedChild ? true : undefined,
|
|
172
|
+
placementRootSessionId: stampPlacementRootSessionId(placementRootResolved),
|
|
173
|
+
externalContentTarget: externalContentTargetActive ? true : undefined,
|
|
174
|
+
});
|
|
175
|
+
const compensatePausedVM = (remoteEnv, snapshotId) => compensateUnparkedPause(remoteEnv, snapshotId, {
|
|
176
|
+
boundMs: PARK_COMPENSATION_TIMEOUT_MS,
|
|
177
|
+
noteFailure: (note) => remoteEnvFailures.push(note),
|
|
178
|
+
disclose: (err) => {
|
|
179
|
+
try {
|
|
180
|
+
deps.onError?.(err, { phase: "config", sessionId });
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
const commitSuspendSaga = async (token, cp, remoteEnv, remoteHandle, cutSignal) => {
|
|
187
|
+
if (!checkpointStore)
|
|
188
|
+
return { tag: "absent" };
|
|
189
|
+
if (memoryEngineSession)
|
|
190
|
+
await memoryEngineSession.harvest("checkpoint");
|
|
191
|
+
if (cutSignal?.aborted) {
|
|
192
|
+
if (remoteEnv !== undefined && remoteHandle?.snapshotId !== undefined) {
|
|
193
|
+
const back = await compensatePausedVM(remoteEnv, remoteHandle.snapshotId);
|
|
194
|
+
if (!back.ok) {
|
|
195
|
+
abortController.abort();
|
|
196
|
+
void harness.abort();
|
|
197
|
+
return { tag: "compensation_failed", reason: back.reason };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { tag: "cut" };
|
|
201
|
+
}
|
|
202
|
+
const announceCommittedScreeningPark = () => {
|
|
203
|
+
const committedCount = cp.state.inheritedGate?.parentConstraintCount;
|
|
204
|
+
if (cp.state.inheritedGate?.requiresParentConstraint === true &&
|
|
205
|
+
!screeningParkDisclosedRef.done &&
|
|
206
|
+
(inheritedParentConstraints ?? []).some((pc) => pc.preToolUse !== undefined)) {
|
|
207
|
+
screeningParkDisclosedRef.done = true;
|
|
208
|
+
try {
|
|
209
|
+
deps.onError?.(new Error(`durable park under an inherited PreToolUse SCREENING constraint: this checkpoint records ` +
|
|
210
|
+
`requiresParentConstraint with parentConstraintCount=${committedCount ?? "(unrecorded)"}, and the live ` +
|
|
211
|
+
`closures cannot be persisted. A resume on THIS Runner re-supplies them automatically; a resume on a fresh ` +
|
|
212
|
+
`Runner (restart / another replica) must hand back the whole chain via resumeStream(..., internals), ` +
|
|
213
|
+
`rebuilding the screening entry with createPreToolUseConstraintPolicy(hook, env) — otherwise the row stays ` +
|
|
214
|
+
`pending. If the face only observes, declare Hooks.preToolUseObservational and it stops entering the chain.`), { phase: "degraded", sessionId, classification: "screening-constraint-in-durable-chain" });
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
const confirmPutOutcome = async (putErr) => {
|
|
221
|
+
if (putErr?.code === "checkpoint.already_exists")
|
|
222
|
+
return "absent";
|
|
223
|
+
try {
|
|
224
|
+
const row = await checkpointStore.get(token);
|
|
225
|
+
if (row == null)
|
|
226
|
+
return "absent";
|
|
227
|
+
const sameCall = (a, b) => a.kind === b.kind && (a.kind !== "tool_approval" || b.kind !== "tool_approval" || a.toolCallId === b.toolCallId);
|
|
228
|
+
const isOurs = row.scope === cp.scope && row.sessionId === cp.sessionId && row.leafId === cp.leafId && sameCall(row.pendingAction, cp.pendingAction);
|
|
229
|
+
return isOurs ? "committed" : "absent";
|
|
230
|
+
}
|
|
231
|
+
catch (readErr) {
|
|
232
|
+
deps.onError?.(readErr, { phase: "config", sessionId });
|
|
233
|
+
return "unknown";
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
try {
|
|
237
|
+
await checkpointStore.put(token, cp);
|
|
238
|
+
announceCommittedScreeningPark();
|
|
239
|
+
return { tag: "committed" };
|
|
240
|
+
}
|
|
241
|
+
catch (putErr) {
|
|
242
|
+
deps.onError?.(putErr, { phase: "config", sessionId });
|
|
243
|
+
const confirmed = await confirmPutOutcome(putErr);
|
|
244
|
+
if (confirmed === "committed") {
|
|
245
|
+
try {
|
|
246
|
+
deps.onError?.(new Error(`durable approval checkpoint: the store's write reported a failure but the row EXISTS — the commit ` +
|
|
247
|
+
`landed and its acknowledgement was lost (a network backend can complete the INSERT and drop the ` +
|
|
248
|
+
`connection before replying). The run suspends on that row rather than continuing, which would leave ` +
|
|
249
|
+
`a redeemable pending checkpoint behind. This backend's acknowledgement path is lossy — the operator ` +
|
|
250
|
+
`face carries the store's own message.`), { phase: "degraded", sessionId, classification: "checkpoint-put-confirmation-lost" });
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
}
|
|
254
|
+
announceCommittedScreeningPark();
|
|
255
|
+
return { tag: "committed" };
|
|
256
|
+
}
|
|
257
|
+
if (confirmed === "unknown") {
|
|
258
|
+
const unknownReason = "the approval checkpoint's state cannot be established (the store rejected the write and then could not be read back; a committed-but-unacknowledged row may exist) — the run is stopped rather than continued past an approval whose durable record is unknown";
|
|
259
|
+
try {
|
|
260
|
+
deps.onError?.(new Error(`durable approval checkpoint: ${unknownReason}`), {
|
|
261
|
+
phase: "degraded",
|
|
262
|
+
sessionId,
|
|
263
|
+
classification: "checkpoint-put-outcome-unknown",
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
}
|
|
268
|
+
abortController.abort();
|
|
269
|
+
void harness.abort();
|
|
270
|
+
return { tag: "unknown", reason: unknownReason };
|
|
271
|
+
}
|
|
272
|
+
const reason = "the approval checkpoint could not be persisted (the store rejected the write; the deployment's error face carries the store's own message)";
|
|
273
|
+
if (remoteEnv !== undefined && remoteHandle?.snapshotId !== undefined) {
|
|
274
|
+
const back = await compensatePausedVM(remoteEnv, remoteHandle.snapshotId);
|
|
275
|
+
if (back.ok)
|
|
276
|
+
return { tag: "absent", reason };
|
|
277
|
+
abortController.abort();
|
|
278
|
+
void harness.abort();
|
|
279
|
+
return { tag: "compensation_failed", reason };
|
|
280
|
+
}
|
|
281
|
+
return { tag: "absent", reason };
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
const suspendLoopCapHit = (count, cap, detail) => {
|
|
285
|
+
if (cap === undefined)
|
|
286
|
+
return false;
|
|
287
|
+
if (count + 1 <= cap)
|
|
288
|
+
return false;
|
|
289
|
+
suspendLoopRef.hit = true;
|
|
290
|
+
abortController.abort();
|
|
291
|
+
void harness.abort();
|
|
292
|
+
try {
|
|
293
|
+
deps.onError?.(new Error(`suspend loop: task already suspended ${count} time(s) (max ${cap}); refusing to suspend again${detail}`), { phase: "config", sessionId });
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
};
|
|
299
|
+
const suspendableEnv = ownedEnv !== undefined && isSuspendable(ownedEnv) ? ownedEnv : undefined;
|
|
300
|
+
const parkOnlyRemoteEnv = suspendableEnv === undefined && ownedEnv !== undefined && isRemoteExecutionEnv(ownedEnv) && incompleteSuspendAdapter === undefined
|
|
301
|
+
? ownedEnv
|
|
302
|
+
: undefined;
|
|
303
|
+
const parkOnlyHandle = (env) => {
|
|
304
|
+
const { snapshotId: _lineage, ...identity } = env.workspaceHandle();
|
|
305
|
+
return { ...identity, restoreMode: "park_only" };
|
|
306
|
+
};
|
|
307
|
+
return { saga: { inFlightSpendMicroUsd, serializeCheckpointState, compensatePausedVM, commitSuspendSaga, suspendLoopCapHit, suspendableEnv, parkOnlyRemoteEnv, parkOnlyHandle } };
|
|
308
|
+
}
|