@sema-agent/core 7.5.1 → 7.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/core/protocol-table.d.ts +5 -0
- package/dist/core/protocol-table.js +1 -0
- package/dist/core/runner/abort-race.d.ts +41 -0
- package/dist/core/runner/abort-race.js +38 -0
- package/dist/core/runner/checkpoint-scope.d.ts +15 -3
- package/dist/core/runner/checkpoint-scope.js +3 -0
- package/dist/core/runner/compaction-call-options.d.ts +1 -1
- package/dist/core/runner/content-ask-bindings.d.ts +27 -0
- package/dist/core/runner/content-ask-bindings.js +1 -0
- package/dist/core/runner/contracts.d.ts +118 -6
- package/dist/core/runner/denial-limit-arms.d.ts +23 -0
- package/dist/core/runner/denial-limit-arms.js +21 -0
- package/dist/core/runner/inherited-ask-grants.d.ts +46 -0
- package/dist/core/runner/inherited-ask-grants.js +29 -0
- package/dist/core/runner/park-commit.d.ts +108 -0
- package/dist/core/runner/park-commit.js +32 -0
- package/dist/core/runner/{prepare-permission-rules.d.ts → permission-rule-lanes.d.ts} +109 -3
- package/dist/core/runner/{prepare-permission-rules.js → permission-rule-lanes.js} +47 -1
- package/dist/core/runner/prepare-ask-lane.d.ts +110 -0
- package/dist/core/runner/prepare-ask-lane.js +133 -0
- package/dist/core/runner/prepare-boundary-parks.d.ts +105 -0
- package/dist/core/runner/prepare-boundary-parks.js +169 -0
- package/dist/core/runner/prepare-context-lane.d.ts +119 -0
- package/dist/core/runner/prepare-context-lane.js +230 -0
- package/dist/core/runner/prepare-gate-stations.d.ts +177 -0
- package/dist/core/runner/prepare-gate-stations.js +290 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +4 -4
- package/dist/core/runner/prepare-inherited-gate.d.ts +4 -4
- package/dist/core/runner/prepare-memory-engine-session.d.ts +84 -0
- package/dist/core/runner/prepare-memory-engine-session.js +233 -0
- package/dist/core/runner/prepare-park-ask.d.ts +164 -0
- package/dist/core/runner/prepare-park-ask.js +377 -0
- package/dist/core/runner/prepare-policy-chain.d.ts +208 -0
- package/dist/core/runner/prepare-policy-chain.js +584 -0
- package/dist/core/runner/prepare-project-context.d.ts +1 -13
- package/dist/core/runner/prepare-project-context.js +1 -3
- package/dist/core/runner/prepare-prompt-assembly.d.ts +95 -0
- package/dist/core/runner/prepare-prompt-assembly.js +162 -0
- package/dist/core/runner/prepare-prompt-inputs.d.ts +1 -20
- package/dist/core/runner/prepare-protocol-tools.d.ts +3 -3
- package/dist/core/runner/prepare-protocol-tools.js +0 -3
- package/dist/core/runner/prepare-question-face.d.ts +3 -21
- package/dist/core/runner/prepare-question-face.js +2 -1
- package/dist/core/runner/prepare-safety-scan.d.ts +0 -5
- package/dist/core/runner/prepare-safety-scan.js +1 -2
- package/dist/core/runner/prepare-suspend-saga.d.ts +170 -0
- package/dist/core/runner/prepare-suspend-saga.js +308 -0
- package/dist/core/runner/prepare-task.d.ts +9 -136
- package/dist/core/runner/prepare-task.js +44 -2741
- package/dist/core/runner/prepare-turn-wiring.d.ts +154 -0
- package/dist/core/runner/prepare-turn-wiring.js +201 -0
- package/dist/core/runner/prepare-wiring-manifest.d.ts +11 -3
- package/dist/core/runner/prepare-wiring-manifest.js +9 -2
- package/dist/core/runner/prepare-workspace-restore.d.ts +2 -29
- package/dist/core/runner/prepare-workspace-restore.js +3 -16
- package/dist/core/runner/prompt-hash-salt.d.ts +1 -0
- package/dist/core/runner/prompt-hash-salt.js +2 -0
- package/dist/core/runner/remote-env-retry.d.ts +29 -0
- package/dist/core/runner/remote-env-retry.js +16 -0
- package/dist/core/runner/runtask.js +1 -1
- package/dist/core/session.d.ts +12 -0
- package/dist/core/session.js +3 -0
- package/package.json +1 -1
- /package/dist/core/runner/{prepare-announce-once.d.ts → announce-once-ledger.d.ts} +0 -0
- /package/dist/core/runner/{prepare-announce-once.js → announce-once-ledger.js} +0 -0
|
@@ -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
|
+
}
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import type { SessionStore } from "../session.js";
|
|
2
|
-
export {
|
|
2
|
+
export { runGuardChain, type GuardChainArgs, type GuardChainOutcome } from "./prepare-context-lane.js";
|
|
3
|
+
export { gatedCallIdOf, USAGE_WINDOW_REAP_MARGIN_MS } from "./park-commit.js";
|
|
4
|
+
export { batchContextAt } from "./prepare-park-ask.js";
|
|
5
|
+
export { compensateUnparkedPause } from "./prepare-suspend-saga.js";
|
|
6
|
+
export { DEFAULT_IRREVERSIBLE_SCOPE, checkpointScopeOf, placementValueOrAbsent } from "./checkpoint-scope.js";
|
|
3
7
|
export { mcpManifestEntries } from "./prepare-wiring-manifest.js";
|
|
4
8
|
export { __resetMaterializeEnvAnnouncements } from "./prepare-tool-disclosure-mount.js";
|
|
5
9
|
export { fileHistoryFilesystemIdentity, resolveFileHistoryScope } from "./prepare-file-history.js";
|
|
6
|
-
import { type OccurrenceIndex } from "../context-edit.js";
|
|
7
|
-
import type { RemoteExecutionEnv, SnapshotId } from "../remote-env.js";
|
|
8
10
|
import type { Runner } from "./runtask.js";
|
|
9
|
-
import type { Prepared,
|
|
11
|
+
import type { Prepared, PrepareResume, RunInternals } from "./contracts.js";
|
|
10
12
|
export type { FileHistoryBoundarySeat, InheritedGate, Prepared, PreparedMicroCompact, PrepareResume, ResolvedWorkspace, RunInternals, UsageGovernance } from "./contracts.js";
|
|
11
|
-
import type {
|
|
12
|
-
import type {
|
|
13
|
+
import type { ExecutionEnv } from "../../internal/harness.js";
|
|
14
|
+
import type { RunnerDeps, TaskSpec } from "../types.js";
|
|
13
15
|
/** Test seam (mirrors `__resetMaterializeEnvAnnouncements`): never called by production code.
|
|
14
16
|
* Re-arms BOTH arms (a WeakMap has no clear — it is re-minted). Deliberately UNLIKE the read-face
|
|
15
17
|
* seam below (console latch only): tests here legitimately reuse ONE sink across prepares to pin
|
|
@@ -36,15 +38,6 @@ export { __resetReadFaceClampAnnouncement } from "./prepare-hands-readface.js";
|
|
|
36
38
|
* checkpoint-cost calibration in one place.
|
|
37
39
|
*/
|
|
38
40
|
export declare const ENV_LIFETIME_SUSPEND_MARGIN_MS = 60000;
|
|
39
|
-
/**
|
|
40
|
-
* design/164 件五 — how long AFTER a governance window frees a `usage_window` checkpoint stays reapable-free.
|
|
41
|
-
* The retention TTL answers "was this abandoned?"; a usage-window suspend is not abandoned while the window
|
|
42
|
-
* it waits on is still full, so its deadline is pushed to `retryAfterMs + this`. One hour gives a host's
|
|
43
|
-
* scheduler a realistic chance to pick the resume up (a cron tick, a queue drain) before the row is
|
|
44
|
-
* treated as garbage — small next to the 30-day retention it is compared against, and large next to any
|
|
45
|
-
* polling interval a driver would sanely use.
|
|
46
|
-
*/
|
|
47
|
-
export declare const USAGE_WINDOW_REAP_MARGIN_MS: number;
|
|
48
41
|
/**
|
|
49
42
|
* design/164 件四 — resolve the moment an env's lifetime EXPIRES (epoch ms), or `undefined` when the env
|
|
50
43
|
* declares none / cannot be aged. Split out as a pure function so the anchor rules are testable and
|
|
@@ -71,71 +64,7 @@ export declare function resolveEnvLifetimeExpiry(env: ExecutionEnv, observedAt:
|
|
|
71
64
|
export { resolveCheckpointStore } from "../checkpoint-store.js";
|
|
72
65
|
export { isFableFamilyModelId, resolveAttachmentsConfig, resolveModelPromptTraits, resolveTaskLimits } from "./prepare-config-doors.js";
|
|
73
66
|
export { rebaseWorkspacePath, rebaseWorkspacePathAcross } from "./workspace-path.js";
|
|
74
|
-
/**
|
|
75
|
-
export interface GuardChainArgs {
|
|
76
|
-
/** The post-frontier view the chain starts from. */
|
|
77
|
-
edited: AgentMessage[];
|
|
78
|
-
/** The pre-cap counterpart view (arm A's clearSource — RB-212 durable composition seat). */
|
|
79
|
-
replayed: AgentMessage[];
|
|
80
|
-
index: OccurrenceIndex;
|
|
81
|
-
microCompact: PreparedMicroCompact;
|
|
82
|
-
guardAt: number;
|
|
83
|
-
charsPerToken: number;
|
|
84
|
-
offloadStore: import("../tool-result-store.js").ToolResultStore | undefined;
|
|
85
|
-
sessionId: string;
|
|
86
|
-
taskId: string;
|
|
87
|
-
deps: RunnerDeps;
|
|
88
|
-
trimPressureRef: {
|
|
89
|
-
droppedMessages: boolean;
|
|
90
|
-
};
|
|
91
|
-
recheck: boolean | undefined;
|
|
92
|
-
signal: AbortSignal | undefined;
|
|
93
|
-
/** The r7 per-request observation buffer — the chain DROPS it on its early returns (a discarded
|
|
94
|
-
* provisional projection must not narrate itself). */
|
|
95
|
-
pendingProjectionObservations: Array<() => void>;
|
|
96
|
-
}
|
|
97
|
-
export type GuardChainOutcome = {
|
|
98
|
-
earlyReturn: {
|
|
99
|
-
messages: AgentMessage[];
|
|
100
|
-
adoptSessionRebuild?: boolean;
|
|
101
|
-
};
|
|
102
|
-
} | {
|
|
103
|
-
working: AgentMessage[];
|
|
104
|
-
trimmed: AgentMessage[];
|
|
105
|
-
trimDroppedMessages: boolean;
|
|
106
|
-
};
|
|
107
|
-
/**
|
|
108
|
-
* design/374 slice 3 — the GUARD CHAIN over one request build (D-7 case-ii transfer table),
|
|
109
|
-
* module-level per the design/238 body-span bank. design/123 D3: the chain reads the anchored
|
|
110
|
-
* coordinate re-estimated on the EDITED array (pre-anchor clears don't lower it — deliberately;
|
|
111
|
-
* trimToBudget doc). The opt-out machine = the pre-374 backstop order byte-identical
|
|
112
|
-
* (trim as the ordinary second line); otherwise arm A (blocking machine run, only when the
|
|
113
|
-
* frontier pass is off) → arm B (in-turn forced compaction behind the adopt seam) → arm C (trim,
|
|
114
|
-
* the disaster-only last resort and this chain's sole `context.trim` emitter). Returns either the
|
|
115
|
-
* post-chain view for the pipeline tail, or an EARLY hook result the context handler must return
|
|
116
|
-
* verbatim (arm-B adoption / the r8 dead-turn stand-down).
|
|
117
|
-
*/
|
|
118
|
-
export declare function runGuardChain(args: GuardChainArgs): Promise<GuardChainOutcome>;
|
|
119
|
-
/**
|
|
120
|
-
* WHICH tool call a committed durable park is holding this run — `undefined` when nothing parked, or
|
|
121
|
-
* when the park that did commit holds no call (a resource slice, a plan review).
|
|
122
|
-
*
|
|
123
|
-
* SINGLE derivation on purpose. Two consumers need this answer: the abort-classification seam, which
|
|
124
|
-
* stamps it into the `details` of the results the loop mints for the contaminated siblings, and the
|
|
125
|
-
* `tool_end` projection, which puts it on the wire. Reading it from one function keeps the two from
|
|
126
|
-
* disagreeing about WHICH HOLDER WINS or about what a holder with no call means. They are not otherwise
|
|
127
|
-
* interchangeable: the frame face applies strictly narrower conditions on top of this answer (see
|
|
128
|
-
* `tool_end.gatedCallId`), so a frame may omit an id this function returns — never the reverse.
|
|
129
|
-
*
|
|
130
|
-
* The frame side must not read the id back out of a tool RESULT even though the marker is there: a
|
|
131
|
-
* result's `details` is written by the tool (and replaceable by post-tool hooks), so lifting a
|
|
132
|
-
* cross-call attribution from it would let any failing tool name an arbitrary call and put a phantom
|
|
133
|
-
* approval wait on someone else's frame. Same rule, same reason, as `settledBy`.
|
|
134
|
-
*
|
|
135
|
-
* The two holders are never both set (the commit-side discriminant writes exactly one); they are read
|
|
136
|
-
* here in assemble-result's slot order so the winner is the same one the terminal status is built from.
|
|
137
|
-
*/
|
|
138
|
-
export declare function gatedCallIdOf(p: Pick<Prepared, "suspendRef" | "reviewRef">): string | undefined;
|
|
67
|
+
/** Everything the run loop needs, built once by {@link prepareTask} (task setup, isolated from the loop). */
|
|
139
68
|
/**
|
|
140
69
|
* RB-330 + 5.38 r2 件2 — the SINGLE effective-delegation derivation for a leg, minted once per prepare
|
|
141
70
|
* and read by EVERY consumer face; two facets, one source:
|
|
@@ -165,62 +94,6 @@ export declare function effectiveDelegationFacts(internals: Pick<RunInternals, "
|
|
|
165
94
|
isDelegatedChild: boolean;
|
|
166
95
|
isNonForkChild: boolean;
|
|
167
96
|
};
|
|
168
|
-
/**
|
|
169
|
-
* From the resumed/active transcript, the batch position of `currentId` (design/45 §4.ter): the tool-call
|
|
170
|
-
* ids of the assistant message that issued it (the batch), and the subset already resolved (executed
|
|
171
|
-
* before the suspend — #1..k-1). ID-based, not positional (council Question #1): immune to reordering.
|
|
172
|
-
*/
|
|
173
|
-
export declare function batchContextAt(messages: AgentMessage[], currentId: string): {
|
|
174
|
-
batchToolCallIds: string[];
|
|
175
|
-
completedCallIds: string[];
|
|
176
|
-
};
|
|
177
|
-
/**
|
|
178
|
-
* design/384 slice 2 — the ONE compensation for a paused-but-unparked VM, shared by the fence's
|
|
179
|
-
* post-pause checkpoints (③ in the park closure, ④ in the saga) and the saga's put-failure absent
|
|
180
|
-
* arm (previously inline there: same two hops, one implementation now, so the three sites cannot
|
|
181
|
-
* drift). Restores the workspace (bounded transient retry) then re-establishes consistency
|
|
182
|
-
* (`postResumeInit`), both hops under one INDEPENDENT `AbortSignal.timeout(io.boundMs)` —
|
|
183
|
-
* deliberately NOT the run/cut signal: the remote contract answers an already-aborted signal
|
|
184
|
-
* `{ok:false,"aborted"}`, so gating the restore on the very signal whose firing caused the
|
|
185
|
-
* compensation made it die instantly and strand the paused VM on exactly the run-abort arm that
|
|
186
|
-
* needs it most. The adapter signal is best-effort (a deaf adapter ignores it), so the caller's
|
|
187
|
-
* await is ALSO raced against the same bound — bounded decision, three outcomes:
|
|
188
|
-
* · settled ok ⇒ the VM is running again ({ok:true});
|
|
189
|
-
* · settled not-ok / threw ⇒ recorded + disclosed, {ok:false} — the caller takes its fatal
|
|
190
|
-
* fail-closed arm (abort the run; never continue on a paused VM);
|
|
191
|
-
* · bound fires with the adapter still in flight ⇒ {ok:false} NOW, and the in-flight call
|
|
192
|
-
* continues DETACHED + swallow-guarded with the late-settlement split:
|
|
193
|
-
* – late SUCCESS: a running VM now exists with no run and no committed row to own it — the
|
|
194
|
-
* detached continuation compensates the compensation with `destroy()` (the adapter's own
|
|
195
|
-
* best-effort contract, swallow-guarded, disclosed);
|
|
196
|
-
* – late FAILURE: disclosed; the VM is most likely still paused — provider-side TTL/GC
|
|
197
|
-
* territory, and the disclosure is the end of this engine's obligation (a deaf adapter's
|
|
198
|
-
* stranded VM is that adapter's own defect surface).
|
|
199
|
-
* Decide-then-disclose throughout: `io.disclose`/`io.noteFailure` must be swallow-guarded by the
|
|
200
|
-
* caller's binding, and nothing they do can change the returned verdict. Never throws. Exported for
|
|
201
|
-
* direct unit pinning (the bounded/deaf/late arms need a small bound; production binds the
|
|
202
|
-
* `PARK_COMPENSATION_TIMEOUT_MS` constant at the one call-site closure).
|
|
203
|
-
*/
|
|
204
|
-
export declare function compensateUnparkedPause(remoteEnv: RemoteExecutionEnv, snapshotId: SnapshotId, io: {
|
|
205
|
-
boundMs: number;
|
|
206
|
-
noteFailure: (note: RemoteEnvFailureNote) => void;
|
|
207
|
-
disclose: (err: unknown) => void;
|
|
208
|
-
}): Promise<{
|
|
209
|
-
ok: true;
|
|
210
|
-
} | {
|
|
211
|
-
ok: false;
|
|
212
|
-
reason: string;
|
|
213
|
-
}>;
|
|
214
|
-
/**
|
|
215
|
-
* rescan C5 — the ONE spelling of the placement fields' empty-string discipline: `""` is absence
|
|
216
|
-
* wearing clothes (the resume entry's principal-rung posture), and every placement read that must
|
|
217
|
-
* treat it so — the resume rung's two sides (runtask), the restore fold's seed and the suspend
|
|
218
|
-
* stamp — goes through THIS helper, so three sites cannot drift into three readings. Deliberately
|
|
219
|
-
* NOT applied to the live `internals.placementRoot`/`rootSessionId` reads of the RESOLUTION fold:
|
|
220
|
-
* an empty supplied claim there is an assembly error the execution-env phase's `mintPlacementRootSessionId` refuses
|
|
221
|
-
* loudly on the factory path, and normalizing it away would silently repair what should be loud.
|
|
222
|
-
*/
|
|
223
|
-
export declare function placementValueOrAbsent(value: string | undefined): string | undefined;
|
|
224
97
|
/**
|
|
225
98
|
* Build everything a task run needs (council design/34 ②: a free function with EXPLICIT deps, not a
|
|
226
99
|
* Runner method — testable and decoupled). Resolves the model/role/thinking, acquires + reconciles the
|