@the-open-engine/zeroshot 6.24.0 → 6.25.1
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/cli/index.js +135 -72
- package/lib/agent-cli-provider/adapters/omp.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/omp.js +37 -11
- package/lib/agent-cli-provider/adapters/omp.js.map +1 -1
- package/lib/agent-cli-provider/omp-rpc-driver.d.ts.map +1 -1
- package/lib/agent-cli-provider/omp-rpc-driver.js +27 -2
- package/lib/agent-cli-provider/omp-rpc-driver.js.map +1 -1
- package/lib/agent-cli-provider/omp-rpc-session.js +3 -3
- package/lib/agent-cli-provider/omp-rpc-session.js.map +1 -1
- package/lib/agent-cli-provider/provider-registry.d.ts +1 -1
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
- package/lib/agent-cli-provider/provider-registry.js +7 -1
- package/lib/agent-cli-provider/provider-registry.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +2 -0
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/package.json +1 -1
- package/src/agent/agent-lifecycle.js +66 -2
- package/src/agent/agent-task-executor.js +72 -3
- package/src/agent/provider-session.js +125 -2
- package/src/agent-cli-provider/adapters/omp.ts +41 -11
- package/src/agent-cli-provider/omp-rpc-driver.ts +31 -3
- package/src/agent-cli-provider/omp-rpc-session.ts +3 -3
- package/src/agent-cli-provider/provider-registry.ts +7 -1
- package/src/agent-cli-provider/types.ts +4 -0
- package/src/omp-blob-root.js +110 -0
- package/src/omp-config-overlay.js +9 -1
- package/src/omp-execution-fingerprint.js +62 -0
- package/src/omp-session-limits.js +41 -0
- package/src/omp-session-partition.js +424 -0
- package/src/omp-session-verifier.js +740 -0
- package/task-lib/commands/clean.js +92 -35
- package/task-lib/commands/kill.js +21 -0
- package/task-lib/commands/resume.js +62 -0
- package/task-lib/commands/run.js +80 -0
- package/task-lib/omp-session-cleanup.js +197 -0
- package/task-lib/omp-session-ownership-schema.js +268 -0
- package/task-lib/omp-session-ownership.js +367 -0
- package/task-lib/omp-storage-root.js +35 -0
- package/task-lib/rpc-watcher.js +368 -2
- package/task-lib/runner.js +243 -5
- package/task-lib/store.js +106 -76
package/task-lib/rpc-watcher.js
CHANGED
|
@@ -11,11 +11,26 @@
|
|
|
11
11
|
* The prompt itself never appears in argv (`ps` and /proc/<pid>/cmdline would expose it for as long
|
|
12
12
|
* as this watcher lives). task-lib/runner.js sends it over the private stdin pipe described in
|
|
13
13
|
* src/watcher-prompt-channel.js, and OMP is not spawned until a complete payload has arrived.
|
|
14
|
+
*
|
|
15
|
+
* OMP session ownership (issue #866) is driven from here:
|
|
16
|
+
* before spawn resume partitions are fully verified against the complete committed tuple
|
|
17
|
+
* at `ready` re-verified, then the owner-fenced CAS transfer runs *before* the prompt write
|
|
18
|
+
* at terminal the materialized session is verified and recorded/committed for the owner kind
|
|
19
|
+
* Any drift, conflict, failure, cancellation, or uncertainty marks the row cleanup-required rather
|
|
20
|
+
* than leaving a partition that a later turn could resume.
|
|
14
21
|
*/
|
|
15
22
|
|
|
16
23
|
import { appendFileSync } from 'fs';
|
|
24
|
+
import { basename, resolve as resolvePath } from 'path';
|
|
17
25
|
import { getTask, updateTask } from './store.js';
|
|
18
26
|
import { createCommandSpecCleanup } from './command-spec-cleanup.js';
|
|
27
|
+
import {
|
|
28
|
+
commitOwnership,
|
|
29
|
+
readOwnership,
|
|
30
|
+
recordVerifiedMaterialization,
|
|
31
|
+
retireOmpOwnershipAtTerminalBoundary,
|
|
32
|
+
transferOmpSessionOwnership,
|
|
33
|
+
} from './omp-session-ownership.js';
|
|
19
34
|
import {
|
|
20
35
|
completeWatcherFailure,
|
|
21
36
|
completePendingWatcherCancellation,
|
|
@@ -34,6 +49,12 @@ const {
|
|
|
34
49
|
runOmpRpcTask,
|
|
35
50
|
} = require('./provider-helper-runtime.js');
|
|
36
51
|
const { receiveWatcherPrompt } = require('../src/watcher-prompt-channel.js');
|
|
52
|
+
const {
|
|
53
|
+
checkPartitionPathReady,
|
|
54
|
+
verifyExistingOmpPartition,
|
|
55
|
+
verifyFreshMaterialization,
|
|
56
|
+
} = require('../src/omp-session-verifier.js');
|
|
57
|
+
const { computeOmpExecutionFingerprint } = require('../src/omp-execution-fingerprint.js');
|
|
37
58
|
|
|
38
59
|
// No overall task timeout: detached tasks run until the provider produces a terminal frame,
|
|
39
60
|
// matching watcher.js's unbounded child.on('close') wait for every other lane.
|
|
@@ -42,6 +63,9 @@ const NO_TIMEOUT_MS = 2_147_483_647;
|
|
|
42
63
|
const [, , taskId, cwd, logFile, , configJson] = process.argv;
|
|
43
64
|
const config = configJson ? JSON.parse(configJson) : {};
|
|
44
65
|
const commandSpec = config.commandSpec || {};
|
|
66
|
+
const ompSession = config.ompSession || { kind: 'none' };
|
|
67
|
+
const ompResumeExpectation = config.ompResumeExpectation || null;
|
|
68
|
+
const ompCanonicalWorkspace = config.ompCanonicalWorkspace || null;
|
|
45
69
|
|
|
46
70
|
function log(msg) {
|
|
47
71
|
appendFileSync(logFile, msg);
|
|
@@ -63,6 +87,8 @@ const outputRuntime = createRpcWatcherOutputRuntime({ log });
|
|
|
63
87
|
|
|
64
88
|
let crashStarted = false;
|
|
65
89
|
let spawnEvidence = null;
|
|
90
|
+
let ownershipTransferred = false;
|
|
91
|
+
let promptCheckpointPassed = false;
|
|
66
92
|
|
|
67
93
|
function terminateOwnedProviderBoundary(exitObserved = false) {
|
|
68
94
|
if (!spawnEvidence) return true;
|
|
@@ -72,9 +98,22 @@ function terminateOwnedProviderBoundary(exitObserved = false) {
|
|
|
72
98
|
);
|
|
73
99
|
}
|
|
74
100
|
|
|
101
|
+
/** Every failed/cancelled/uncertain terminal boundary in this watcher routes through the shared
|
|
102
|
+
* durable-boundary retirement, so a post-transfer failure leaves the resumed row cleanup-required
|
|
103
|
+
* rather than provisional forever. Idempotent and never throwing, so it cannot itself prevent the
|
|
104
|
+
* task from reaching a terminal status. */
|
|
105
|
+
function markOmpCleanupRequiredSafely() {
|
|
106
|
+
retireOmpOwnershipAtTerminalBoundary(taskId, (error) =>
|
|
107
|
+
emergencyLog(
|
|
108
|
+
`[${Date.now()}][OMP-OWNERSHIP] Failed to mark cleanup-required: ${error.message}\n`
|
|
109
|
+
)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
75
113
|
async function crashWithError(error, source) {
|
|
76
114
|
if (crashStarted) return;
|
|
77
115
|
crashStarted = true;
|
|
116
|
+
markOmpCleanupRequiredSafely();
|
|
78
117
|
await completeWatcherFailure({
|
|
79
118
|
taskId,
|
|
80
119
|
error,
|
|
@@ -87,6 +126,297 @@ async function crashWithError(error, source) {
|
|
|
87
126
|
process.exit(1);
|
|
88
127
|
}
|
|
89
128
|
|
|
129
|
+
function identityText(identity) {
|
|
130
|
+
return identity ? `${identity.device}:${identity.inode}` : 'none';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Compare a verified partition and OMP's reported evidence against the *complete* committed tuple
|
|
135
|
+
* the resume descriptor carries. Every field is compared exactly and in full — never a basename,
|
|
136
|
+
* never a prefix — so a returned session file that merely shares a basename with the requested one,
|
|
137
|
+
* a re-minted session ID, a substituted inode, or an execution-contract change all fail here.
|
|
138
|
+
*
|
|
139
|
+
* `evidence` is null on the pre-spawn pass, where OMP has reported nothing yet; the structural and
|
|
140
|
+
* identity halves still apply. When evidence *is* present this is the pre-prompt checkpoint, so
|
|
141
|
+
* the echoed session identity must be complete (see assertEchoedSessionMatches).
|
|
142
|
+
*/
|
|
143
|
+
function assertNoResumeDrift(verified, evidence) {
|
|
144
|
+
const expected = ompResumeExpectation;
|
|
145
|
+
const drift = [];
|
|
146
|
+
|
|
147
|
+
assertEchoedSessionMatches(evidence, drift, { requireComplete: evidence !== null });
|
|
148
|
+
|
|
149
|
+
if (verified.sessionFileName !== expected.sessionFileName) {
|
|
150
|
+
drift.push(`sessionFileName ${verified.sessionFileName} != ${expected.sessionFileName}`);
|
|
151
|
+
}
|
|
152
|
+
if (verified.sessionFilePath !== expected.sessionFilePath) {
|
|
153
|
+
drift.push(`sessionFilePath ${verified.sessionFilePath} != ${expected.sessionFilePath}`);
|
|
154
|
+
}
|
|
155
|
+
if (
|
|
156
|
+
identityText(verified.partitionIdentity) !== identityText(expected.expectedPartitionIdentity)
|
|
157
|
+
) {
|
|
158
|
+
drift.push(
|
|
159
|
+
`partitionIdentity ${identityText(verified.partitionIdentity)} != ${identityText(expected.expectedPartitionIdentity)}`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (
|
|
163
|
+
identityText(verified.sessionFileIdentity) !==
|
|
164
|
+
identityText(expected.expectedSessionFileIdentity)
|
|
165
|
+
) {
|
|
166
|
+
drift.push(
|
|
167
|
+
`sessionFileIdentity ${identityText(verified.sessionFileIdentity)} != ${identityText(expected.expectedSessionFileIdentity)}`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (verified.artifactManifestDigest !== expected.expectedArtifactManifestDigest) {
|
|
171
|
+
drift.push('artifactManifestDigest');
|
|
172
|
+
}
|
|
173
|
+
// The session ID written into the transcript header is the on-disk truth about which session
|
|
174
|
+
// this file is; a partition whose header names a different session is not the recorded one.
|
|
175
|
+
if (verified.sessionHeader.sessionId !== expected.expectedSessionId) {
|
|
176
|
+
drift.push(
|
|
177
|
+
`header sessionId ${verified.sessionHeader.sessionId} != ${expected.expectedSessionId}`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
if (
|
|
181
|
+
verified.sessionHeader.cwd !== null &&
|
|
182
|
+
verified.sessionHeader.cwd !== expected.canonicalWorkspace
|
|
183
|
+
) {
|
|
184
|
+
drift.push(`header cwd ${verified.sessionHeader.cwd} != ${expected.canonicalWorkspace}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (drift.length > 0) {
|
|
188
|
+
throw new Error(`OMP resume verification detected drift: ${drift.join('; ')}.`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The half of the resume check that stays valid for the whole turn: what OMP itself says about the
|
|
194
|
+
* session it opened, plus the execution contract it is running under.
|
|
195
|
+
*
|
|
196
|
+
* Split out from the structural half because `session_info_update` (a builtin slash-command side
|
|
197
|
+
* channel, docs/rpc.md) re-fires the driver's `ready` hook *after* the prompt, by which point the
|
|
198
|
+
* transcript has legitimately grown — re-running the manifest/inode comparison there would reject
|
|
199
|
+
* a perfectly healthy turn. The identity and fingerprint comparisons below have no such staleness,
|
|
200
|
+
* so a mid-turn switch to a different session or a changed model/thinking level is still caught.
|
|
201
|
+
*
|
|
202
|
+
* `requireComplete` is the difference between the two callers, and it is the difference between
|
|
203
|
+
* "OMP agreed it opened exactly this session" and "OMP declined to say".
|
|
204
|
+
*
|
|
205
|
+
* At the pre-prompt checkpoint of a resume it is set, and both echoed values must be present and
|
|
206
|
+
* exactly equal: the full session ID and the full absolute session file. Without it, a `get_state`
|
|
207
|
+
* that simply omits `sessionId` (or `sessionFile`) would transfer a committed lineage and receive
|
|
208
|
+
* the prompt on the strength of the *disk* alone — and disk state cannot answer the only question
|
|
209
|
+
* that matters here, which is which session the running OMP process actually attached to. A prefix
|
|
210
|
+
* is never enough either: OMP resolves `--resume` IDs by prefix (session-manager.ts), so a shorter
|
|
211
|
+
* echoed ID is precisely the ambiguity this check exists to reject.
|
|
212
|
+
*
|
|
213
|
+
* `session_info_update` passes leave it unset: those frames legitimately carry only a subset
|
|
214
|
+
* (docs/rpc.md), and the driver merges them onto evidence whose complete form this checkpoint has
|
|
215
|
+
* already proven, so a partial later frame is checked against that proof rather than replacing it.
|
|
216
|
+
*/
|
|
217
|
+
function assertEchoedSessionMatches(evidence, drift, { requireComplete = false } = {}) {
|
|
218
|
+
if (!evidence) return;
|
|
219
|
+
const expected = ompResumeExpectation;
|
|
220
|
+
|
|
221
|
+
if (requireComplete) {
|
|
222
|
+
if (!evidence.sessionId) {
|
|
223
|
+
drift.push('OMP reported no sessionId at the pre-prompt checkpoint');
|
|
224
|
+
}
|
|
225
|
+
if (!evidence.sessionFile) {
|
|
226
|
+
drift.push('OMP reported no sessionFile at the pre-prompt checkpoint');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (evidence.selectedProvider !== expected.expectedSelectedProvider) {
|
|
231
|
+
drift.push(
|
|
232
|
+
`selectedProvider ${evidence.selectedProvider} != ${expected.expectedSelectedProvider}`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
if (evidence.selectedModel !== expected.expectedSelectedModel) {
|
|
236
|
+
drift.push(`selectedModel ${evidence.selectedModel} != ${expected.expectedSelectedModel}`);
|
|
237
|
+
}
|
|
238
|
+
// Zeroshot selector / thinking / config-overlay / pinned-OMP-version drift, in one digest.
|
|
239
|
+
const fingerprint = computeOmpExecutionFingerprint({
|
|
240
|
+
expectedVersion: OMP_SUPPORTED_VERSION,
|
|
241
|
+
commandSpec,
|
|
242
|
+
evidence,
|
|
243
|
+
});
|
|
244
|
+
if (fingerprint !== expected.expectedExecutionFingerprint) {
|
|
245
|
+
drift.push('executionFingerprint');
|
|
246
|
+
}
|
|
247
|
+
// OMP echoes the session it actually opened. Require the full path and the full ID, so a
|
|
248
|
+
// different file under the same directory — or a session whose ID merely starts the same —
|
|
249
|
+
// cannot be finalized in place of the requested one.
|
|
250
|
+
if (evidence.sessionId !== null && evidence.sessionId !== expected.expectedSessionId) {
|
|
251
|
+
drift.push(`echoed sessionId ${evidence.sessionId} != ${expected.expectedSessionId}`);
|
|
252
|
+
}
|
|
253
|
+
if (
|
|
254
|
+
evidence.sessionFile !== null &&
|
|
255
|
+
resolvePath(evidence.sessionFile) !== expected.sessionFilePath
|
|
256
|
+
) {
|
|
257
|
+
drift.push(`echoed sessionFile ${evidence.sessionFile} != ${expected.sessionFilePath}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Identity/fingerprint-only re-check for `ready` hooks that fire after the prompt. */
|
|
262
|
+
function assertNoEchoedResumeDrift(evidence) {
|
|
263
|
+
const drift = [];
|
|
264
|
+
assertEchoedSessionMatches(evidence, drift);
|
|
265
|
+
if (drift.length > 0) {
|
|
266
|
+
throw new Error(`OMP resume verification detected drift: ${drift.join('; ')}.`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Full structural verification of an existing (resume) partition against the recorded tuple.
|
|
271
|
+
* Runs twice by design: once before spawn (fail fast, never start OMP against a doomed partition)
|
|
272
|
+
* and again from the onSession('ready') hook right before the prompt is sent (closes the window
|
|
273
|
+
* between spawn and OMP actually opening the file). */
|
|
274
|
+
function verifyResumeSession(evidence) {
|
|
275
|
+
const verified = verifyExistingOmpPartition(
|
|
276
|
+
ompResumeExpectation.partitionPath,
|
|
277
|
+
ompResumeExpectation.sessionFileName,
|
|
278
|
+
{ expectedPartitionIdentity: ompResumeExpectation.expectedPartitionIdentity }
|
|
279
|
+
);
|
|
280
|
+
assertNoResumeDrift(verified, evidence);
|
|
281
|
+
return verified;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function verifyOmpSessionBeforeSpawn() {
|
|
285
|
+
if (ompSession.kind === 'none') return;
|
|
286
|
+
if (ompSession.kind === 'fresh') {
|
|
287
|
+
checkPartitionPathReady(ompSession.partition.path);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
verifyResumeSession(null);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Owner-fenced ownership transfer, run from the `ready` hook after re-verification and strictly
|
|
295
|
+
* before the prompt command is written. One transaction moves the prior committed owner's lineage
|
|
296
|
+
* onto this task's provisional row and clears the prior row, so the partition never has two
|
|
297
|
+
* committed owners.
|
|
298
|
+
*
|
|
299
|
+
* From here to this turn's own success boundary the partition has *no* committed owner at all —
|
|
300
|
+
* the authoritative live claimant is this still-`provisional` row, by design, and every partition
|
|
301
|
+
* fence is written for that (see findAuthoritativeOwnersForPartition). A transfer that does not
|
|
302
|
+
* apply (the prior owner moved, this row already advanced) throws, which fails the turn closed
|
|
303
|
+
* through the same cleanup-required path as any other drift: the resumed session is never steered
|
|
304
|
+
* on an unresolved ownership claim, and a turn that dies after the transfer retires the row it now
|
|
305
|
+
* holds rather than stranding the lineage.
|
|
306
|
+
*/
|
|
307
|
+
function transferResumedOwnershipBeforePrompt() {
|
|
308
|
+
if (ownershipTransferred) return;
|
|
309
|
+
const transferred = transferOmpSessionOwnership({
|
|
310
|
+
fromTaskId: ompResumeExpectation.priorOwnerTaskId,
|
|
311
|
+
toTaskId: taskId,
|
|
312
|
+
});
|
|
313
|
+
if (!transferred) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`OMP resume: could not transfer ownership of partition ${ompResumeExpectation.partitionId} from task ${ompResumeExpectation.priorOwnerTaskId}; refusing to prompt.`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
ownershipTransferred = true;
|
|
319
|
+
log(
|
|
320
|
+
`[${Date.now()}][OMP-OWNERSHIP] Transferred partition ${ompResumeExpectation.partitionId} from ${ompResumeExpectation.priorOwnerTaskId}\n`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Terminal OMP ownership boundary. Standalone owners have no parent process left to consult — the
|
|
326
|
+
* watcher itself is the terminal boundary, so a materialized/verified session commits directly.
|
|
327
|
+
* Cluster-agent owners are different: the spawning agent (agent-lifecycle.js, a *different*
|
|
328
|
+
* process) still has to validate this turn's logical/schema output and run its onComplete hook, so
|
|
329
|
+
* the watcher only records the owner-fenced verified evidence here and leaves the state
|
|
330
|
+
* 'provisional'; commitRecordedOwnership() advances it to 'committed' from that later boundary.
|
|
331
|
+
* Every failed/cancelled/uncertain path — including a still-provisional record after a supposedly
|
|
332
|
+
* successful commit/record call — falls through to cleanup-required.
|
|
333
|
+
*/
|
|
334
|
+
function finalizeOmpOwnership(completion, result) {
|
|
335
|
+
if (ompSession.kind === 'none') return;
|
|
336
|
+
try {
|
|
337
|
+
if (
|
|
338
|
+
completion.status !== 'completed' ||
|
|
339
|
+
!result.session?.sessionId ||
|
|
340
|
+
!result.session?.sessionFile
|
|
341
|
+
) {
|
|
342
|
+
markOmpCleanupRequiredSafely();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const current = readOwnership(taskId);
|
|
346
|
+
if (!current || current.state !== 'provisional') {
|
|
347
|
+
markOmpCleanupRequiredSafely();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const reportedSessionFile = resolvePath(result.session.sessionFile);
|
|
352
|
+
const sessionFileName = basename(reportedSessionFile);
|
|
353
|
+
const expectedPartitionPath = current.partitionPath;
|
|
354
|
+
// The reported session file must live inside *this* task's partition; a session OMP wrote
|
|
355
|
+
// somewhere else is not something this owner may claim.
|
|
356
|
+
if (resolvePath(expectedPartitionPath, sessionFileName) !== reportedSessionFile) {
|
|
357
|
+
throw new Error(
|
|
358
|
+
`OMP reported session file ${reportedSessionFile}, which is not a direct child of ${expectedPartitionPath}.`
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const verified =
|
|
363
|
+
ompSession.kind === 'fresh'
|
|
364
|
+
? verifyFreshMaterialization(expectedPartitionPath, sessionFileName, {
|
|
365
|
+
expectedPartitionIdentity: current.partitionIdentity,
|
|
366
|
+
})
|
|
367
|
+
: verifyExistingOmpPartition(expectedPartitionPath, sessionFileName, {
|
|
368
|
+
expectedPartitionIdentity: current.partitionIdentity,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Descriptor/header verification for a fresh materialization: the transcript's own header must
|
|
372
|
+
// name the session OMP reported and the workspace this task was canonicalized against.
|
|
373
|
+
if (verified.sessionHeader.sessionId !== result.session.sessionId) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
`Materialized session header id ${verified.sessionHeader.sessionId} does not match the reported ${result.session.sessionId}.`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
const expectedWorkspace = ompCanonicalWorkspace || current.canonicalWorkspace;
|
|
379
|
+
if (verified.sessionHeader.cwd !== null && verified.sessionHeader.cwd !== expectedWorkspace) {
|
|
380
|
+
throw new Error(
|
|
381
|
+
`Materialized session header cwd ${verified.sessionHeader.cwd} does not match ${expectedWorkspace}.`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
if (ompSession.kind === 'resume') {
|
|
385
|
+
if (!ownershipTransferred) {
|
|
386
|
+
throw new Error('OMP resume completed without an applied ownership transfer.');
|
|
387
|
+
}
|
|
388
|
+
if (verified.sessionHeader.sessionId !== ompResumeExpectation.expectedSessionId) {
|
|
389
|
+
throw new Error('Resumed session header id drifted from the recorded owner.');
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const executionFingerprint = computeOmpExecutionFingerprint({
|
|
394
|
+
expectedVersion: OMP_SUPPORTED_VERSION,
|
|
395
|
+
commandSpec,
|
|
396
|
+
evidence: result.session,
|
|
397
|
+
});
|
|
398
|
+
const evidence = {
|
|
399
|
+
taskId,
|
|
400
|
+
sessionId: result.session.sessionId,
|
|
401
|
+
sessionFilePath: verified.sessionFilePath,
|
|
402
|
+
partitionIdentity: verified.partitionIdentity,
|
|
403
|
+
sessionFileIdentity: verified.sessionFileIdentity,
|
|
404
|
+
artifactManifestDigest: verified.artifactManifestDigest,
|
|
405
|
+
executionFingerprint,
|
|
406
|
+
selectedProvider: result.session.selectedProvider,
|
|
407
|
+
selectedModel: result.session.selectedModel,
|
|
408
|
+
};
|
|
409
|
+
const advanced =
|
|
410
|
+
current.owner.kind === 'cluster-agent'
|
|
411
|
+
? recordVerifiedMaterialization(evidence)
|
|
412
|
+
: commitOwnership(evidence);
|
|
413
|
+
if (!advanced) markOmpCleanupRequiredSafely();
|
|
414
|
+
} catch (error) {
|
|
415
|
+
emergencyLog(`[${Date.now()}][OMP-OWNERSHIP] Terminal verification failed: ${error.message}\n`);
|
|
416
|
+
markOmpCleanupRequiredSafely();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
90
420
|
process.on('uncaughtException', (error) => {
|
|
91
421
|
void crashWithError(error, 'uncaughtException');
|
|
92
422
|
});
|
|
@@ -106,6 +436,7 @@ async function run() {
|
|
|
106
436
|
emergencyLog,
|
|
107
437
|
})
|
|
108
438
|
) {
|
|
439
|
+
markOmpCleanupRequiredSafely();
|
|
109
440
|
process.exit(0);
|
|
110
441
|
return;
|
|
111
442
|
}
|
|
@@ -124,6 +455,17 @@ async function run() {
|
|
|
124
455
|
return;
|
|
125
456
|
}
|
|
126
457
|
|
|
458
|
+
// Two-phase verification, checkpoint 1 (before spawn): a resume partition/file that is missing,
|
|
459
|
+
// over bounds, non-regular, or already drifted from what the owner recorded must never see OMP
|
|
460
|
+
// spawned at all. Fresh sessions have nothing to verify yet — checkpoint 1 for them is the
|
|
461
|
+
// partition-path sanity check at the 'ready' onSession hook below.
|
|
462
|
+
try {
|
|
463
|
+
verifyOmpSessionBeforeSpawn();
|
|
464
|
+
} catch (error) {
|
|
465
|
+
await crashWithError(error, 'omp-session-verify-before-spawn');
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
127
469
|
const controller = new AbortController();
|
|
128
470
|
|
|
129
471
|
const result = await runOmpRpcTask(
|
|
@@ -131,7 +473,7 @@ async function run() {
|
|
|
131
473
|
commandSpec: { ...commandSpec, cwd: commandSpec.cwd || cwd },
|
|
132
474
|
prompt,
|
|
133
475
|
expectedVersion: OMP_SUPPORTED_VERSION,
|
|
134
|
-
session:
|
|
476
|
+
session: ompSession,
|
|
135
477
|
signal: controller.signal,
|
|
136
478
|
timeoutMs: NO_TIMEOUT_MS,
|
|
137
479
|
abortGraceMs: ABORT_GRACE_MS,
|
|
@@ -148,6 +490,7 @@ async function run() {
|
|
|
148
490
|
if (getTask(taskId)?.cancelRequested) {
|
|
149
491
|
crashStarted = true;
|
|
150
492
|
controller.abort();
|
|
493
|
+
markOmpCleanupRequiredSafely();
|
|
151
494
|
await completePendingWatcherCancellation({
|
|
152
495
|
taskId,
|
|
153
496
|
getTask,
|
|
@@ -163,13 +506,36 @@ async function run() {
|
|
|
163
506
|
outputRuntime.logEvent(event);
|
|
164
507
|
return Promise.resolve();
|
|
165
508
|
},
|
|
166
|
-
|
|
509
|
+
// Checkpoint 2 (before prompt): the driver awaits this hook before sending the `prompt`
|
|
510
|
+
// command, so throwing here (drift, an unready fresh partition, or an ownership transfer
|
|
511
|
+
// that did not apply) fails the task closed without ever steering the resumed/fresh session.
|
|
512
|
+
//
|
|
513
|
+
// This hook also re-fires later in the turn for `session_info_update` frames. Those passes
|
|
514
|
+
// re-check only what OMP itself reports (see assertNoEchoedResumeDrift); the structural
|
|
515
|
+
// manifest/inode comparison belongs to the pre-prompt pass alone, because the transcript is
|
|
516
|
+
// supposed to grow once the turn is under way.
|
|
517
|
+
onSession: (evidence) => {
|
|
518
|
+
if (evidence.phase !== 'ready') return;
|
|
519
|
+
if (ompSession.kind === 'fresh') {
|
|
520
|
+
checkPartitionPathReady(ompSession.partition.path);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (ompSession.kind !== 'resume') return;
|
|
524
|
+
if (promptCheckpointPassed) {
|
|
525
|
+
assertNoEchoedResumeDrift(evidence);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
verifyResumeSession(evidence);
|
|
529
|
+
transferResumedOwnershipBeforePrompt();
|
|
530
|
+
promptCheckpointPassed = true;
|
|
531
|
+
},
|
|
167
532
|
}
|
|
168
533
|
);
|
|
169
534
|
|
|
170
535
|
if (crashStarted) return; // Cancelled mid-spawn; the onSpawn hook already exited the process.
|
|
171
536
|
|
|
172
537
|
const completion = outputRuntime.complete(result);
|
|
538
|
+
finalizeOmpOwnership(completion, result);
|
|
173
539
|
await completeWatcherTask({
|
|
174
540
|
taskId,
|
|
175
541
|
completion,
|