@the-open-engine/zeroshot 6.23.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.
Files changed (51) hide show
  1. package/cli/index.js +37 -2
  2. package/docker/zeroshot-cluster/Dockerfile +7 -0
  3. package/lib/agent-cli-provider/adapters/omp.d.ts.map +1 -1
  4. package/lib/agent-cli-provider/adapters/omp.js +37 -11
  5. package/lib/agent-cli-provider/adapters/omp.js.map +1 -1
  6. package/lib/agent-cli-provider/omp-release.d.ts +3 -0
  7. package/lib/agent-cli-provider/omp-release.d.ts.map +1 -1
  8. package/lib/agent-cli-provider/omp-release.js +20 -1
  9. package/lib/agent-cli-provider/omp-release.js.map +1 -1
  10. package/lib/agent-cli-provider/omp-rpc-driver.d.ts.map +1 -1
  11. package/lib/agent-cli-provider/omp-rpc-driver.js +27 -2
  12. package/lib/agent-cli-provider/omp-rpc-driver.js.map +1 -1
  13. package/lib/agent-cli-provider/omp-rpc-session.js +3 -3
  14. package/lib/agent-cli-provider/omp-rpc-session.js.map +1 -1
  15. package/lib/agent-cli-provider/provider-registry.d.ts +20 -8
  16. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  17. package/lib/agent-cli-provider/provider-registry.js +56 -9
  18. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  19. package/lib/agent-cli-provider/types.d.ts +2 -0
  20. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  21. package/lib/agent-cli-provider/types.js.map +1 -1
  22. package/lib/docker-config.js +122 -5
  23. package/package.json +2 -2
  24. package/src/agent/agent-lifecycle.js +78 -3
  25. package/src/agent/agent-task-executor.js +72 -3
  26. package/src/agent/provider-session.js +112 -2
  27. package/src/agent-cli-provider/adapters/omp.ts +41 -11
  28. package/src/agent-cli-provider/omp-release.ts +26 -0
  29. package/src/agent-cli-provider/omp-rpc-driver.ts +31 -3
  30. package/src/agent-cli-provider/omp-rpc-session.ts +3 -3
  31. package/src/agent-cli-provider/provider-registry.ts +102 -20
  32. package/src/agent-cli-provider/types.ts +4 -0
  33. package/src/isolation-manager.js +535 -89
  34. package/src/omp-blob-root.js +110 -0
  35. package/src/omp-config-overlay.js +9 -1
  36. package/src/omp-execution-fingerprint.js +62 -0
  37. package/src/omp-session-limits.js +17 -0
  38. package/src/omp-session-partition.js +297 -0
  39. package/src/omp-session-verifier.js +576 -0
  40. package/src/orchestrator.js +11 -1
  41. package/src/preflight.js +15 -2
  42. package/task-lib/commands/clean.js +23 -0
  43. package/task-lib/commands/resume.js +42 -0
  44. package/task-lib/commands/run.js +65 -0
  45. package/task-lib/omp-session-cleanup.js +160 -0
  46. package/task-lib/omp-session-ownership-schema.js +262 -0
  47. package/task-lib/omp-session-ownership.js +332 -0
  48. package/task-lib/omp-storage-root.js +35 -0
  49. package/task-lib/rpc-watcher.js +332 -2
  50. package/task-lib/runner.js +195 -4
  51. package/task-lib/store.js +42 -7
@@ -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: { kind: 'none' },
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
- onSession: async () => {},
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,