@the-open-engine/zeroshot 6.24.0 → 6.25.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/cli/index.js +37 -2
- 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 +111 -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 +17 -0
- package/src/omp-session-partition.js +297 -0
- package/src/omp-session-verifier.js +576 -0
- package/task-lib/commands/clean.js +23 -0
- package/task-lib/commands/resume.js +42 -0
- package/task-lib/commands/run.js +65 -0
- package/task-lib/omp-session-cleanup.js +160 -0
- package/task-lib/omp-session-ownership-schema.js +262 -0
- package/task-lib/omp-session-ownership.js +332 -0
- package/task-lib/omp-storage-root.js +35 -0
- package/task-lib/rpc-watcher.js +332 -2
- package/task-lib/runner.js +195 -4
- package/task-lib/store.js +42 -7
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
|
+
markCleanupRequired,
|
|
30
|
+
readOwnership,
|
|
31
|
+
recordVerifiedMaterialization,
|
|
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,20 @@ function terminateOwnedProviderBoundary(exitObserved = false) {
|
|
|
72
98
|
);
|
|
73
99
|
}
|
|
74
100
|
|
|
101
|
+
function markOmpCleanupRequiredSafely() {
|
|
102
|
+
try {
|
|
103
|
+
markCleanupRequired(taskId);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
emergencyLog(
|
|
106
|
+
`[${Date.now()}][OMP-OWNERSHIP] Failed to mark cleanup-required: ${error.message}\n`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
75
111
|
async function crashWithError(error, source) {
|
|
76
112
|
if (crashStarted) return;
|
|
77
113
|
crashStarted = true;
|
|
114
|
+
markOmpCleanupRequiredSafely();
|
|
78
115
|
await completeWatcherFailure({
|
|
79
116
|
taskId,
|
|
80
117
|
error,
|
|
@@ -87,6 +124,263 @@ async function crashWithError(error, source) {
|
|
|
87
124
|
process.exit(1);
|
|
88
125
|
}
|
|
89
126
|
|
|
127
|
+
function identityText(identity) {
|
|
128
|
+
return identity ? `${identity.device}:${identity.inode}` : 'none';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Compare a verified partition and OMP's reported evidence against the *complete* committed tuple
|
|
133
|
+
* the resume descriptor carries. Every field is compared exactly and in full — never a basename,
|
|
134
|
+
* never a prefix — so a returned session file that merely shares a basename with the requested one,
|
|
135
|
+
* a re-minted session ID, a substituted inode, or an execution-contract change all fail here.
|
|
136
|
+
*
|
|
137
|
+
* `evidence` is null on the pre-spawn pass, where OMP has reported nothing yet; the structural and
|
|
138
|
+
* identity halves still apply.
|
|
139
|
+
*/
|
|
140
|
+
function assertNoResumeDrift(verified, evidence) {
|
|
141
|
+
const expected = ompResumeExpectation;
|
|
142
|
+
const drift = [];
|
|
143
|
+
|
|
144
|
+
assertEchoedSessionMatches(evidence, drift);
|
|
145
|
+
|
|
146
|
+
if (verified.sessionFileName !== expected.sessionFileName) {
|
|
147
|
+
drift.push(`sessionFileName ${verified.sessionFileName} != ${expected.sessionFileName}`);
|
|
148
|
+
}
|
|
149
|
+
if (verified.sessionFilePath !== expected.sessionFilePath) {
|
|
150
|
+
drift.push(`sessionFilePath ${verified.sessionFilePath} != ${expected.sessionFilePath}`);
|
|
151
|
+
}
|
|
152
|
+
if (identityText(verified.partitionIdentity) !== identityText(expected.expectedPartitionIdentity)) {
|
|
153
|
+
drift.push(
|
|
154
|
+
`partitionIdentity ${identityText(verified.partitionIdentity)} != ${identityText(expected.expectedPartitionIdentity)}`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (
|
|
158
|
+
identityText(verified.sessionFileIdentity) !== identityText(expected.expectedSessionFileIdentity)
|
|
159
|
+
) {
|
|
160
|
+
drift.push(
|
|
161
|
+
`sessionFileIdentity ${identityText(verified.sessionFileIdentity)} != ${identityText(expected.expectedSessionFileIdentity)}`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (verified.artifactManifestDigest !== expected.expectedArtifactManifestDigest) {
|
|
165
|
+
drift.push('artifactManifestDigest');
|
|
166
|
+
}
|
|
167
|
+
// The session ID written into the transcript header is the on-disk truth about which session
|
|
168
|
+
// this file is; a partition whose header names a different session is not the recorded one.
|
|
169
|
+
if (verified.sessionHeader.sessionId !== expected.expectedSessionId) {
|
|
170
|
+
drift.push(
|
|
171
|
+
`header sessionId ${verified.sessionHeader.sessionId} != ${expected.expectedSessionId}`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (
|
|
175
|
+
verified.sessionHeader.cwd !== null &&
|
|
176
|
+
verified.sessionHeader.cwd !== expected.canonicalWorkspace
|
|
177
|
+
) {
|
|
178
|
+
drift.push(`header cwd ${verified.sessionHeader.cwd} != ${expected.canonicalWorkspace}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (drift.length > 0) {
|
|
182
|
+
throw new Error(`OMP resume verification detected drift: ${drift.join('; ')}.`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The half of the resume check that stays valid for the whole turn: what OMP itself says about the
|
|
188
|
+
* session it opened, plus the execution contract it is running under.
|
|
189
|
+
*
|
|
190
|
+
* Split out from the structural half because `session_info_update` (a builtin slash-command side
|
|
191
|
+
* channel, docs/rpc.md) re-fires the driver's `ready` hook *after* the prompt, by which point the
|
|
192
|
+
* transcript has legitimately grown — re-running the manifest/inode comparison there would reject
|
|
193
|
+
* a perfectly healthy turn. The identity and fingerprint comparisons below have no such staleness,
|
|
194
|
+
* so a mid-turn switch to a different session or a changed model/thinking level is still caught.
|
|
195
|
+
*/
|
|
196
|
+
function assertEchoedSessionMatches(evidence, drift) {
|
|
197
|
+
if (!evidence) return;
|
|
198
|
+
const expected = ompResumeExpectation;
|
|
199
|
+
|
|
200
|
+
if (evidence.selectedProvider !== expected.expectedSelectedProvider) {
|
|
201
|
+
drift.push(
|
|
202
|
+
`selectedProvider ${evidence.selectedProvider} != ${expected.expectedSelectedProvider}`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if (evidence.selectedModel !== expected.expectedSelectedModel) {
|
|
206
|
+
drift.push(`selectedModel ${evidence.selectedModel} != ${expected.expectedSelectedModel}`);
|
|
207
|
+
}
|
|
208
|
+
// Zeroshot selector / thinking / config-overlay / pinned-OMP-version drift, in one digest.
|
|
209
|
+
const fingerprint = computeOmpExecutionFingerprint({
|
|
210
|
+
expectedVersion: OMP_SUPPORTED_VERSION,
|
|
211
|
+
commandSpec,
|
|
212
|
+
evidence,
|
|
213
|
+
});
|
|
214
|
+
if (fingerprint !== expected.expectedExecutionFingerprint) {
|
|
215
|
+
drift.push('executionFingerprint');
|
|
216
|
+
}
|
|
217
|
+
// OMP echoes the session it actually opened. Require the full path and the full ID, so a
|
|
218
|
+
// different file under the same directory — or a session whose ID merely starts the same —
|
|
219
|
+
// cannot be finalized in place of the requested one.
|
|
220
|
+
if (evidence.sessionId !== null && evidence.sessionId !== expected.expectedSessionId) {
|
|
221
|
+
drift.push(`echoed sessionId ${evidence.sessionId} != ${expected.expectedSessionId}`);
|
|
222
|
+
}
|
|
223
|
+
if (
|
|
224
|
+
evidence.sessionFile !== null &&
|
|
225
|
+
resolvePath(evidence.sessionFile) !== expected.sessionFilePath
|
|
226
|
+
) {
|
|
227
|
+
drift.push(`echoed sessionFile ${evidence.sessionFile} != ${expected.sessionFilePath}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Identity/fingerprint-only re-check for `ready` hooks that fire after the prompt. */
|
|
232
|
+
function assertNoEchoedResumeDrift(evidence) {
|
|
233
|
+
const drift = [];
|
|
234
|
+
assertEchoedSessionMatches(evidence, drift);
|
|
235
|
+
if (drift.length > 0) {
|
|
236
|
+
throw new Error(`OMP resume verification detected drift: ${drift.join('; ')}.`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Full structural verification of an existing (resume) partition against the recorded tuple.
|
|
241
|
+
* Runs twice by design: once before spawn (fail fast, never start OMP against a doomed partition)
|
|
242
|
+
* and again from the onSession('ready') hook right before the prompt is sent (closes the window
|
|
243
|
+
* between spawn and OMP actually opening the file). */
|
|
244
|
+
function verifyResumeSession(evidence) {
|
|
245
|
+
const verified = verifyExistingOmpPartition(
|
|
246
|
+
ompResumeExpectation.partitionPath,
|
|
247
|
+
ompResumeExpectation.sessionFileName,
|
|
248
|
+
{ expectedPartitionIdentity: ompResumeExpectation.expectedPartitionIdentity }
|
|
249
|
+
);
|
|
250
|
+
assertNoResumeDrift(verified, evidence);
|
|
251
|
+
return verified;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function verifyOmpSessionBeforeSpawn() {
|
|
255
|
+
if (ompSession.kind === 'none') return;
|
|
256
|
+
if (ompSession.kind === 'fresh') {
|
|
257
|
+
checkPartitionPathReady(ompSession.partition.path);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
verifyResumeSession(null);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Owner-fenced ownership transfer, run from the `ready` hook after re-verification and strictly
|
|
265
|
+
* before the prompt command is written. One transaction moves the prior committed owner's lineage
|
|
266
|
+
* onto this task's provisional row and clears the prior row, so the partition is never owned by
|
|
267
|
+
* two rows and never owned by none. A transfer that does not apply (the prior owner moved, this
|
|
268
|
+
* row already advanced) throws, which fails the turn closed through the same cleanup-required path
|
|
269
|
+
* as any other drift — the resumed session is never steered on an unresolved ownership claim.
|
|
270
|
+
*/
|
|
271
|
+
function transferResumedOwnershipBeforePrompt() {
|
|
272
|
+
if (ownershipTransferred) return;
|
|
273
|
+
const transferred = transferOmpSessionOwnership({
|
|
274
|
+
fromTaskId: ompResumeExpectation.priorOwnerTaskId,
|
|
275
|
+
toTaskId: taskId,
|
|
276
|
+
});
|
|
277
|
+
if (!transferred) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
`OMP resume: could not transfer ownership of partition ${ompResumeExpectation.partitionId} from task ${ompResumeExpectation.priorOwnerTaskId}; refusing to prompt.`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
ownershipTransferred = true;
|
|
283
|
+
log(
|
|
284
|
+
`[${Date.now()}][OMP-OWNERSHIP] Transferred partition ${ompResumeExpectation.partitionId} from ${ompResumeExpectation.priorOwnerTaskId}\n`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Terminal OMP ownership boundary. Standalone owners have no parent process left to consult — the
|
|
290
|
+
* watcher itself is the terminal boundary, so a materialized/verified session commits directly.
|
|
291
|
+
* Cluster-agent owners are different: the spawning agent (agent-lifecycle.js, a *different*
|
|
292
|
+
* process) still has to validate this turn's logical/schema output and run its onComplete hook, so
|
|
293
|
+
* the watcher only records the owner-fenced verified evidence here and leaves the state
|
|
294
|
+
* 'provisional'; commitRecordedOwnership() advances it to 'committed' from that later boundary.
|
|
295
|
+
* Every failed/cancelled/uncertain path — including a still-provisional record after a supposedly
|
|
296
|
+
* successful commit/record call — falls through to cleanup-required.
|
|
297
|
+
*/
|
|
298
|
+
function finalizeOmpOwnership(completion, result) {
|
|
299
|
+
if (ompSession.kind === 'none') return;
|
|
300
|
+
try {
|
|
301
|
+
if (
|
|
302
|
+
completion.status !== 'completed' ||
|
|
303
|
+
!result.session?.sessionId ||
|
|
304
|
+
!result.session?.sessionFile
|
|
305
|
+
) {
|
|
306
|
+
markOmpCleanupRequiredSafely();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const current = readOwnership(taskId);
|
|
310
|
+
if (!current || current.state !== 'provisional') {
|
|
311
|
+
markOmpCleanupRequiredSafely();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const reportedSessionFile = resolvePath(result.session.sessionFile);
|
|
316
|
+
const sessionFileName = basename(reportedSessionFile);
|
|
317
|
+
const expectedPartitionPath = current.partitionPath;
|
|
318
|
+
// The reported session file must live inside *this* task's partition; a session OMP wrote
|
|
319
|
+
// somewhere else is not something this owner may claim.
|
|
320
|
+
if (resolvePath(expectedPartitionPath, sessionFileName) !== reportedSessionFile) {
|
|
321
|
+
throw new Error(
|
|
322
|
+
`OMP reported session file ${reportedSessionFile}, which is not a direct child of ${expectedPartitionPath}.`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const verified =
|
|
327
|
+
ompSession.kind === 'fresh'
|
|
328
|
+
? verifyFreshMaterialization(expectedPartitionPath, sessionFileName, {
|
|
329
|
+
expectedPartitionIdentity: current.partitionIdentity,
|
|
330
|
+
})
|
|
331
|
+
: verifyExistingOmpPartition(expectedPartitionPath, sessionFileName, {
|
|
332
|
+
expectedPartitionIdentity: current.partitionIdentity,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Descriptor/header verification for a fresh materialization: the transcript's own header must
|
|
336
|
+
// name the session OMP reported and the workspace this task was canonicalized against.
|
|
337
|
+
if (verified.sessionHeader.sessionId !== result.session.sessionId) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
`Materialized session header id ${verified.sessionHeader.sessionId} does not match the reported ${result.session.sessionId}.`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
const expectedWorkspace = ompCanonicalWorkspace || current.canonicalWorkspace;
|
|
343
|
+
if (verified.sessionHeader.cwd !== null && verified.sessionHeader.cwd !== expectedWorkspace) {
|
|
344
|
+
throw new Error(
|
|
345
|
+
`Materialized session header cwd ${verified.sessionHeader.cwd} does not match ${expectedWorkspace}.`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
if (ompSession.kind === 'resume') {
|
|
349
|
+
if (!ownershipTransferred) {
|
|
350
|
+
throw new Error('OMP resume completed without an applied ownership transfer.');
|
|
351
|
+
}
|
|
352
|
+
if (verified.sessionHeader.sessionId !== ompResumeExpectation.expectedSessionId) {
|
|
353
|
+
throw new Error('Resumed session header id drifted from the recorded owner.');
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const executionFingerprint = computeOmpExecutionFingerprint({
|
|
358
|
+
expectedVersion: OMP_SUPPORTED_VERSION,
|
|
359
|
+
commandSpec,
|
|
360
|
+
evidence: result.session,
|
|
361
|
+
});
|
|
362
|
+
const evidence = {
|
|
363
|
+
taskId,
|
|
364
|
+
sessionId: result.session.sessionId,
|
|
365
|
+
sessionFilePath: verified.sessionFilePath,
|
|
366
|
+
partitionIdentity: verified.partitionIdentity,
|
|
367
|
+
sessionFileIdentity: verified.sessionFileIdentity,
|
|
368
|
+
artifactManifestDigest: verified.artifactManifestDigest,
|
|
369
|
+
executionFingerprint,
|
|
370
|
+
selectedProvider: result.session.selectedProvider,
|
|
371
|
+
selectedModel: result.session.selectedModel,
|
|
372
|
+
};
|
|
373
|
+
const advanced =
|
|
374
|
+
current.owner.kind === 'cluster-agent'
|
|
375
|
+
? recordVerifiedMaterialization(evidence)
|
|
376
|
+
: commitOwnership(evidence);
|
|
377
|
+
if (!advanced) markOmpCleanupRequiredSafely();
|
|
378
|
+
} catch (error) {
|
|
379
|
+
emergencyLog(`[${Date.now()}][OMP-OWNERSHIP] Terminal verification failed: ${error.message}\n`);
|
|
380
|
+
markOmpCleanupRequiredSafely();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
90
384
|
process.on('uncaughtException', (error) => {
|
|
91
385
|
void crashWithError(error, 'uncaughtException');
|
|
92
386
|
});
|
|
@@ -106,6 +400,7 @@ async function run() {
|
|
|
106
400
|
emergencyLog,
|
|
107
401
|
})
|
|
108
402
|
) {
|
|
403
|
+
markOmpCleanupRequiredSafely();
|
|
109
404
|
process.exit(0);
|
|
110
405
|
return;
|
|
111
406
|
}
|
|
@@ -124,6 +419,17 @@ async function run() {
|
|
|
124
419
|
return;
|
|
125
420
|
}
|
|
126
421
|
|
|
422
|
+
// Two-phase verification, checkpoint 1 (before spawn): a resume partition/file that is missing,
|
|
423
|
+
// over bounds, non-regular, or already drifted from what the owner recorded must never see OMP
|
|
424
|
+
// spawned at all. Fresh sessions have nothing to verify yet — checkpoint 1 for them is the
|
|
425
|
+
// partition-path sanity check at the 'ready' onSession hook below.
|
|
426
|
+
try {
|
|
427
|
+
verifyOmpSessionBeforeSpawn();
|
|
428
|
+
} catch (error) {
|
|
429
|
+
await crashWithError(error, 'omp-session-verify-before-spawn');
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
127
433
|
const controller = new AbortController();
|
|
128
434
|
|
|
129
435
|
const result = await runOmpRpcTask(
|
|
@@ -131,7 +437,7 @@ async function run() {
|
|
|
131
437
|
commandSpec: { ...commandSpec, cwd: commandSpec.cwd || cwd },
|
|
132
438
|
prompt,
|
|
133
439
|
expectedVersion: OMP_SUPPORTED_VERSION,
|
|
134
|
-
session:
|
|
440
|
+
session: ompSession,
|
|
135
441
|
signal: controller.signal,
|
|
136
442
|
timeoutMs: NO_TIMEOUT_MS,
|
|
137
443
|
abortGraceMs: ABORT_GRACE_MS,
|
|
@@ -148,6 +454,7 @@ async function run() {
|
|
|
148
454
|
if (getTask(taskId)?.cancelRequested) {
|
|
149
455
|
crashStarted = true;
|
|
150
456
|
controller.abort();
|
|
457
|
+
markOmpCleanupRequiredSafely();
|
|
151
458
|
await completePendingWatcherCancellation({
|
|
152
459
|
taskId,
|
|
153
460
|
getTask,
|
|
@@ -163,13 +470,36 @@ async function run() {
|
|
|
163
470
|
outputRuntime.logEvent(event);
|
|
164
471
|
return Promise.resolve();
|
|
165
472
|
},
|
|
166
|
-
|
|
473
|
+
// Checkpoint 2 (before prompt): the driver awaits this hook before sending the `prompt`
|
|
474
|
+
// command, so throwing here (drift, an unready fresh partition, or an ownership transfer
|
|
475
|
+
// that did not apply) fails the task closed without ever steering the resumed/fresh session.
|
|
476
|
+
//
|
|
477
|
+
// This hook also re-fires later in the turn for `session_info_update` frames. Those passes
|
|
478
|
+
// re-check only what OMP itself reports (see assertNoEchoedResumeDrift); the structural
|
|
479
|
+
// manifest/inode comparison belongs to the pre-prompt pass alone, because the transcript is
|
|
480
|
+
// supposed to grow once the turn is under way.
|
|
481
|
+
onSession: (evidence) => {
|
|
482
|
+
if (evidence.phase !== 'ready') return;
|
|
483
|
+
if (ompSession.kind === 'fresh') {
|
|
484
|
+
checkPartitionPathReady(ompSession.partition.path);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (ompSession.kind !== 'resume') return;
|
|
488
|
+
if (promptCheckpointPassed) {
|
|
489
|
+
assertNoEchoedResumeDrift(evidence);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
verifyResumeSession(evidence);
|
|
493
|
+
transferResumedOwnershipBeforePrompt();
|
|
494
|
+
promptCheckpointPassed = true;
|
|
495
|
+
},
|
|
167
496
|
}
|
|
168
497
|
);
|
|
169
498
|
|
|
170
499
|
if (crashStarted) return; // Cancelled mid-spawn; the onSpawn hook already exited the process.
|
|
171
500
|
|
|
172
501
|
const completion = outputRuntime.complete(result);
|
|
502
|
+
finalizeOmpOwnership(completion, result);
|
|
173
503
|
await completeWatcherTask({
|
|
174
504
|
taskId,
|
|
175
505
|
completion,
|