@the-open-engine/zeroshot 6.9.1 → 6.10.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.
Files changed (65) hide show
  1. package/cli/commands/inspect.js +1 -0
  2. package/cli/index.js +17 -1
  3. package/cluster-hooks/block-ask-user-question.py +2 -3
  4. package/cluster-hooks/block-dangerous-git.py +2 -3
  5. package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
  6. package/lib/agent-cli-provider/adapters/claude.js +57 -3
  7. package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
  8. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  9. package/lib/agent-cli-provider/adapters/codex.js +32 -2
  10. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  11. package/lib/agent-cli-provider/adapters/common.js +1 -1
  12. package/lib/agent-cli-provider/adapters/common.js.map +1 -1
  13. package/lib/agent-cli-provider/adapters/index.d.ts +1 -0
  14. package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -1
  15. package/lib/agent-cli-provider/adapters/index.js +8 -0
  16. package/lib/agent-cli-provider/adapters/index.js.map +1 -1
  17. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
  18. package/lib/agent-cli-provider/contract-options.js +3 -0
  19. package/lib/agent-cli-provider/contract-options.js.map +1 -1
  20. package/lib/agent-cli-provider/index.d.ts +1 -1
  21. package/lib/agent-cli-provider/index.d.ts.map +1 -1
  22. package/lib/agent-cli-provider/index.js +2 -1
  23. package/lib/agent-cli-provider/index.js.map +1 -1
  24. package/lib/agent-cli-provider/provider-registry.d.ts +9 -0
  25. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  26. package/lib/agent-cli-provider/provider-registry.js +3 -0
  27. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  28. package/lib/agent-cli-provider/types.d.ts +10 -2
  29. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  30. package/lib/agent-cli-provider/types.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/agent/agent-context-builder.js +90 -4
  33. package/src/agent/agent-context-sources.js +20 -2
  34. package/src/agent/agent-lifecycle.js +43 -8
  35. package/src/agent/agent-task-executor.js +557 -314
  36. package/src/agent/guidance-queue.js +29 -7
  37. package/src/agent/provider-session.js +277 -0
  38. package/src/agent-cli-provider/adapters/claude.ts +64 -4
  39. package/src/agent-cli-provider/adapters/codex.ts +47 -3
  40. package/src/agent-cli-provider/adapters/common.ts +1 -1
  41. package/src/agent-cli-provider/adapters/index.ts +10 -0
  42. package/src/agent-cli-provider/contract-options.ts +7 -0
  43. package/src/agent-cli-provider/index.ts +1 -0
  44. package/src/agent-cli-provider/provider-registry.ts +13 -1
  45. package/src/agent-cli-provider/types.ts +13 -6
  46. package/src/agent-wrapper.js +44 -8
  47. package/src/claude-task-runner.js +153 -44
  48. package/src/ledger-sequence.js +63 -0
  49. package/src/ledger.js +114 -29
  50. package/src/orchestrator.js +40 -2
  51. package/src/task-spawn-cleanup-ownership.js +82 -0
  52. package/src/worktree-claude-config.js +193 -85
  53. package/task-lib/attachable-watcher.js +101 -253
  54. package/task-lib/command-spec-cleanup.js +219 -0
  55. package/task-lib/commands/clean.js +35 -5
  56. package/task-lib/commands/get-task-id-by-spawn-token.js +10 -0
  57. package/task-lib/commands/kill.js +117 -4
  58. package/task-lib/commands/resume.js +29 -6
  59. package/task-lib/commands/status.js +4 -0
  60. package/task-lib/provider-helper-runtime.js +1 -0
  61. package/task-lib/provider-session-capture.js +138 -0
  62. package/task-lib/runner.js +70 -5
  63. package/task-lib/store.js +126 -12
  64. package/task-lib/watcher-output-runtime.js +284 -0
  65. package/task-lib/watcher.js +131 -242
package/src/ledger.js CHANGED
@@ -16,6 +16,9 @@ const {
16
16
  USER_GUIDANCE_AGENT,
17
17
  USER_GUIDANCE_CLUSTER,
18
18
  } = require('./guidance-topics');
19
+ const { messageSequenceFromSql, messageSequenceToSql } = require('./ledger-sequence');
20
+
21
+ const MESSAGE_SEQUENCE_SELECT = 'CAST(rowid AS TEXT) AS sequence';
19
22
 
20
23
  class Ledger extends EventEmitter {
21
24
  constructor(dbPath = ':memory:', options = {}) {
@@ -91,17 +94,23 @@ class Ledger extends EventEmitter {
91
94
  }
92
95
 
93
96
  _prepareStatements() {
94
- this.stmts = {
95
- insert: this.db.prepare(`
97
+ const insert = this.db.prepare(`
96
98
  INSERT INTO messages (id, timestamp, topic, sender, receiver, content_text, content_data, metadata, cluster_id)
97
99
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
98
- `),
100
+ `);
101
+ insert.safeIntegers(true);
99
102
 
100
- queryBase: `SELECT * FROM messages WHERE cluster_id = ?`,
103
+ this.stmts = {
104
+ insert,
105
+
106
+ queryBase: `SELECT ${MESSAGE_SEQUENCE_SELECT}, * FROM messages WHERE cluster_id = ?`,
101
107
 
102
108
  count: this.db.prepare(`SELECT COUNT(*) as count FROM messages WHERE cluster_id = ?`),
103
109
 
104
- getAll: this.db.prepare(`SELECT * FROM messages WHERE cluster_id = ? ORDER BY timestamp ASC`),
110
+ getAll: this.db.prepare(
111
+ `SELECT ${MESSAGE_SEQUENCE_SELECT}, * FROM messages WHERE cluster_id = ?` +
112
+ ` ORDER BY timestamp ASC, rowid ASC`
113
+ ),
105
114
  };
106
115
  }
107
116
 
@@ -145,7 +154,7 @@ class Ledger extends EventEmitter {
145
154
  };
146
155
 
147
156
  try {
148
- this.stmts.insert.run(
157
+ const insertResult = this.stmts.insert.run(
149
158
  record.id,
150
159
  record.timestamp,
151
160
  record.topic,
@@ -156,6 +165,7 @@ class Ledger extends EventEmitter {
156
165
  record.metadata,
157
166
  record.cluster_id
158
167
  );
168
+ record.sequence = messageSequenceFromSql(insertResult.lastInsertRowid);
159
169
 
160
170
  // Invalidate cache
161
171
  this.cache.clear();
@@ -219,7 +229,7 @@ class Ledger extends EventEmitter {
219
229
  cluster_id: message.cluster_id,
220
230
  };
221
231
 
222
- this.stmts.insert.run(
232
+ const insertResult = this.stmts.insert.run(
223
233
  record.id,
224
234
  record.timestamp,
225
235
  record.topic,
@@ -230,6 +240,7 @@ class Ledger extends EventEmitter {
230
240
  record.metadata,
231
241
  record.cluster_id
232
242
  );
243
+ record.sequence = messageSequenceFromSql(insertResult.lastInsertRowid);
233
244
 
234
245
  results.push(this._deserializeMessage(record));
235
246
  }
@@ -265,7 +276,19 @@ class Ledger extends EventEmitter {
265
276
  * @returns {Array} Matching messages
266
277
  */
267
278
  query(criteria) {
268
- const { cluster_id, topic, sender, receiver, since, until, limit, offset } = criteria;
279
+ const {
280
+ cluster_id,
281
+ topic,
282
+ sender,
283
+ receiver,
284
+ since,
285
+ after,
286
+ until,
287
+ afterId,
288
+ throughId,
289
+ limit,
290
+ offset,
291
+ } = criteria;
269
292
 
270
293
  if (!cluster_id) {
271
294
  throw new Error('cluster_id is required for queries');
@@ -295,18 +318,44 @@ class Ledger extends EventEmitter {
295
318
  params.push(typeof since === 'number' ? since : new Date(since).getTime());
296
319
  }
297
320
 
321
+ if (after !== undefined && after !== null) {
322
+ if (!Number.isInteger(after) || after < 0) {
323
+ throw new Error('after must be a non-negative durable ledger cursor');
324
+ }
325
+ conditions.push('timestamp > ?');
326
+ params.push(after);
327
+ }
328
+
298
329
  if (until) {
299
330
  conditions.push('timestamp <= ?');
300
331
  params.push(typeof until === 'number' ? until : new Date(until).getTime());
301
332
  }
302
333
 
334
+ if (afterId !== undefined && afterId !== null) {
335
+ conditions.push('rowid > ?');
336
+ params.push(messageSequenceToSql(afterId, 'afterId'));
337
+ }
338
+
339
+ if (throughId !== undefined && throughId !== null) {
340
+ conditions.push('rowid <= ?');
341
+ params.push(messageSequenceToSql(throughId, 'throughId'));
342
+ }
343
+
303
344
  // Defend against prototype pollution affecting default query ordering.
304
345
  // Only treat `criteria.order` as set if it's an own property.
305
346
  const orderValue = Object.prototype.hasOwnProperty.call(criteria, 'order')
306
347
  ? criteria.order
307
348
  : undefined;
308
349
  const direction = String(orderValue ?? 'asc').toLowerCase() === 'desc' ? 'DESC' : 'ASC';
309
- let sql = `SELECT * FROM messages WHERE ${conditions.join(' AND ')} ORDER BY timestamp ${direction}`;
350
+ const sequenceBounded =
351
+ (afterId !== undefined && afterId !== null) ||
352
+ (throughId !== undefined && throughId !== null);
353
+ const orderClause = sequenceBounded
354
+ ? `rowid ${direction}`
355
+ : `timestamp ${direction}, rowid ${direction}`;
356
+ let sql =
357
+ `SELECT ${MESSAGE_SEQUENCE_SELECT}, * FROM messages WHERE ${conditions.join(' AND ')}` +
358
+ ` ORDER BY ${orderClause}`;
310
359
 
311
360
  if (limit) {
312
361
  sql += ` LIMIT ?`;
@@ -325,11 +374,12 @@ class Ledger extends EventEmitter {
325
374
 
326
375
  /**
327
376
  * Query guidance mailbox for cluster-wide + agent-specific guidance
328
- * @param {Object} criteria - { cluster_id, target_agent_id, lastDeliveredAt, limit }
329
- * @returns {Array} Guidance messages ordered by timestamp ASC
377
+ * @param {Object} criteria - Timestamp compatibility filters plus afterId/throughId sequences
378
+ * @returns {Array} Guidance messages ordered by durable message sequence ASC
330
379
  */
331
380
  queryGuidanceMailbox(criteria) {
332
- const { cluster_id, target_agent_id, lastDeliveredAt, limit } = criteria || {};
381
+ const { cluster_id, target_agent_id, lastDeliveredAt, afterId, throughId, limit } =
382
+ criteria || {};
333
383
 
334
384
  if (!cluster_id) {
335
385
  throw new Error('cluster_id is required for guidance mailbox queries');
@@ -351,7 +401,8 @@ class Ledger extends EventEmitter {
351
401
  }
352
402
 
353
403
  const params = [cluster_id, USER_GUIDANCE_CLUSTER];
354
- let sql = 'SELECT * FROM messages WHERE cluster_id = ? AND (topic = ?';
404
+ let sql =
405
+ `SELECT ${MESSAGE_SEQUENCE_SELECT}, * FROM messages ` + 'WHERE cluster_id = ? AND (topic = ?';
355
406
 
356
407
  if (target_agent_id) {
357
408
  params.push(USER_GUIDANCE_AGENT, target_agent_id);
@@ -365,7 +416,20 @@ class Ledger extends EventEmitter {
365
416
  sql += ' AND timestamp > ?';
366
417
  }
367
418
 
368
- sql += ' ORDER BY timestamp ASC';
419
+ if (afterId !== undefined && afterId !== null) {
420
+ params.push(messageSequenceToSql(afterId, 'afterId'));
421
+ sql += ' AND rowid > ?';
422
+ }
423
+
424
+ if (throughId !== undefined && throughId !== null) {
425
+ params.push(messageSequenceToSql(throughId, 'throughId'));
426
+ sql += ' AND rowid <= ?';
427
+ }
428
+
429
+ sql +=
430
+ (afterId !== undefined && afterId !== null) || (throughId !== undefined && throughId !== null)
431
+ ? ' ORDER BY rowid ASC'
432
+ : ' ORDER BY timestamp ASC, rowid ASC';
369
433
 
370
434
  if (limit) {
371
435
  params.push(limit);
@@ -383,7 +447,8 @@ class Ledger extends EventEmitter {
383
447
  * @returns {Object|null} Last matching message
384
448
  */
385
449
  findLast(criteria) {
386
- const { cluster_id, topic, sender, receiver, since, until } = criteria;
450
+ const { cluster_id, topic, sender, receiver, since, until, throughId, orderBySequence } =
451
+ criteria;
387
452
 
388
453
  if (!cluster_id) {
389
454
  throw new Error('cluster_id is required for queries');
@@ -418,7 +483,16 @@ class Ledger extends EventEmitter {
418
483
  params.push(typeof until === 'number' ? until : new Date(until).getTime());
419
484
  }
420
485
 
421
- const sql = `SELECT * FROM messages WHERE ${conditions.join(' AND ')} ORDER BY timestamp DESC LIMIT 1`;
486
+ if (throughId !== undefined && throughId !== null) {
487
+ conditions.push('rowid <= ?');
488
+ params.push(messageSequenceToSql(throughId, 'throughId'));
489
+ }
490
+
491
+ const orderClause =
492
+ orderBySequence || throughId !== undefined ? 'rowid DESC' : 'timestamp DESC, rowid DESC';
493
+ const sql =
494
+ `SELECT ${MESSAGE_SEQUENCE_SELECT}, * FROM messages WHERE ${conditions.join(' AND ')}` +
495
+ ` ORDER BY ${orderClause} LIMIT 1`;
422
496
 
423
497
  const stmt = this.db.prepare(sql);
424
498
  const row = stmt.get(...params);
@@ -495,7 +569,9 @@ class Ledger extends EventEmitter {
495
569
  */
496
570
  _computeTokensByRole(cluster_id) {
497
571
  // Query all TOKEN_USAGE messages for this cluster
498
- const sql = `SELECT * FROM messages WHERE cluster_id = ? AND topic = 'TOKEN_USAGE' ORDER BY timestamp ASC`;
572
+ const sql =
573
+ `SELECT ${MESSAGE_SEQUENCE_SELECT}, * FROM messages WHERE cluster_id = ?` +
574
+ ` AND topic = 'TOKEN_USAGE' ORDER BY timestamp ASC, rowid ASC`;
499
575
  const stmt = this.db.prepare(sql);
500
576
  const rows = stmt.all(cluster_id);
501
577
 
@@ -589,7 +665,7 @@ class Ledger extends EventEmitter {
589
665
  * @returns {Function} Stop polling function
590
666
  */
591
667
  pollForMessages(clusterId, callback, intervalMs = 500, initialCount = 300) {
592
- let lastTimestamp = 0;
668
+ let lastSequence = '0';
593
669
  let lastMessageIds = new Set();
594
670
  let isFirstPoll = true;
595
671
 
@@ -601,23 +677,29 @@ class Ledger extends EventEmitter {
601
677
  // First poll: get last N messages by count
602
678
  if (clusterId) {
603
679
  sql =
604
- 'SELECT * FROM (SELECT * FROM messages WHERE cluster_id = ? ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC';
680
+ `SELECT * FROM (SELECT ${MESSAGE_SEQUENCE_SELECT}, * FROM messages ` +
681
+ 'WHERE cluster_id = ? ORDER BY rowid DESC LIMIT ?) ' +
682
+ 'ORDER BY CAST(sequence AS INTEGER) ASC';
605
683
  params = [clusterId, initialCount];
606
684
  } else {
607
685
  sql =
608
- 'SELECT * FROM (SELECT * FROM messages ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC';
686
+ `SELECT * FROM (SELECT ${MESSAGE_SEQUENCE_SELECT}, * FROM messages ` +
687
+ 'ORDER BY rowid DESC LIMIT ?) ORDER BY CAST(sequence AS INTEGER) ASC';
609
688
  params = [initialCount];
610
689
  }
611
690
  isFirstPoll = false;
612
691
  } else {
613
- // Subsequent polls: get messages since last timestamp
692
+ // Subsequent polls: get messages after the exact durable sequence.
614
693
  if (clusterId) {
615
694
  sql =
616
- 'SELECT * FROM messages WHERE cluster_id = ? AND timestamp >= ? ORDER BY timestamp ASC';
617
- params = [clusterId, lastTimestamp - 1000]; // 1s buffer for race conditions
695
+ `SELECT ${MESSAGE_SEQUENCE_SELECT}, * FROM messages ` +
696
+ 'WHERE cluster_id = ? AND rowid > ? ORDER BY rowid ASC';
697
+ params = [clusterId, messageSequenceToSql(lastSequence)];
618
698
  } else {
619
- sql = 'SELECT * FROM messages WHERE timestamp >= ? ORDER BY timestamp ASC';
620
- params = [lastTimestamp - 1000];
699
+ sql =
700
+ `SELECT ${MESSAGE_SEQUENCE_SELECT}, * FROM messages ` +
701
+ 'WHERE rowid > ? ORDER BY rowid ASC';
702
+ params = [messageSequenceToSql(lastSequence)];
621
703
  }
622
704
  }
623
705
 
@@ -632,10 +714,9 @@ class Ledger extends EventEmitter {
632
714
  const message = this._deserializeMessage(row);
633
715
  callback(message);
634
716
 
635
- // Update timestamp high-water mark
636
- if (row.timestamp > lastTimestamp) {
637
- lastTimestamp = row.timestamp;
638
- }
717
+ // Rows are delivered in SQLite rowid order, so the final row is the
718
+ // exact database-serialized high-water mark.
719
+ lastSequence = messageSequenceFromSql(row.sequence);
639
720
  }
640
721
 
641
722
  // Prune old message IDs to prevent memory leak
@@ -679,6 +760,10 @@ class Ledger extends EventEmitter {
679
760
  _deserializeMessage(row) {
680
761
  const message = {
681
762
  id: row.id,
763
+ sequence:
764
+ row.sequence === undefined
765
+ ? undefined
766
+ : messageSequenceFromSql(row.sequence, 'stored message sequence'),
682
767
  timestamp: row.timestamp,
683
768
  topic: row.topic,
684
769
  sender: row.sender,
@@ -52,6 +52,10 @@ const { isProcessRunning } = require('../lib/process-liveness');
52
52
  const { getProvider } = require('./providers');
53
53
  const StateSnapshotter = require('./state-snapshotter');
54
54
  const { resolveClusterRequiredQualityGates } = require('./quality-gates');
55
+ const {
56
+ normalizeProviderSession,
57
+ restoreAgentProviderSession,
58
+ } = require('./agent/provider-session');
55
59
  const {
56
60
  commandProofsToQualityGates,
57
61
  mergeCommandProofs,
@@ -231,6 +235,19 @@ function buildPrOptions(options, requiredQualityGates) {
231
235
  };
232
236
  }
233
237
 
238
+ function serializeWorktree(worktree) {
239
+ if (!worktree) {
240
+ return null;
241
+ }
242
+ return {
243
+ enabled: true,
244
+ path: worktree.path,
245
+ branch: worktree.branch,
246
+ repoRoot: worktree.repoRoot,
247
+ workDir: worktree.workDir,
248
+ };
249
+ }
250
+
234
251
  class Orchestrator {
235
252
  constructor(options = {}) {
236
253
  this.clusters = new Map(); // cluster_id -> cluster object
@@ -645,6 +662,15 @@ class Orchestrator {
645
662
  };
646
663
  }
647
664
 
665
+ if (clusterData.worktree?.enabled) {
666
+ agentOptions.worktree = {
667
+ enabled: true,
668
+ path: clusterData.worktree.path,
669
+ branch: clusterData.worktree.branch,
670
+ repoRoot: clusterData.worktree.repoRoot,
671
+ };
672
+ }
673
+
648
674
  return agentOptions;
649
675
  }
650
676
 
@@ -656,7 +682,7 @@ class Orchestrator {
656
682
  return new AgentWrapper(agentConfig, messageBus, clusterContext, agentOptions);
657
683
  }
658
684
 
659
- _restoreAgentState(agent, agentConfig, clusterData) {
685
+ _restoreAgentState(agent, agentConfig, clusterData, messageBus) {
660
686
  if (!clusterData.agentStates) return;
661
687
 
662
688
  const savedState = clusterData.agentStates.find((state) => state.id === agentConfig.id);
@@ -670,6 +696,13 @@ class Orchestrator {
670
696
  agent.currentTask = null;
671
697
  agent.currentTaskId = savedState.currentTaskId || null;
672
698
  agent.processPid = savedState.processPid || null;
699
+ agent.providerSession = restoreAgentProviderSession({
700
+ agent,
701
+ savedState,
702
+ messageBus,
703
+ clusterId: clusterData.id,
704
+ });
705
+ agent.lastGuidanceAppliedId = agent.providerSession?.guidanceSequence ?? null;
673
706
  }
674
707
 
675
708
  _rebuildClusterAgents(clusterContext, messageBus, isolation, isolationManager) {
@@ -699,7 +732,7 @@ class Orchestrator {
699
732
  isolationManager
700
733
  );
701
734
  const agent = this._instantiateAgent(agentConfig, messageBus, clusterContext, agentOptions);
702
- this._restoreAgentState(agent, agentConfig, clusterData);
735
+ this._restoreAgentState(agent, agentConfig, clusterData, messageBus);
703
736
  agents.push(agent);
704
737
  }
705
738
 
@@ -879,6 +912,7 @@ class Orchestrator {
879
912
  workDir: cluster.isolation.workDir, // Required for resume
880
913
  }
881
914
  : null,
915
+ worktree: serializeWorktree(cluster.worktree),
882
916
  // Persist agent runtime states for accurate status display from other processes
883
917
  agentStates: cluster.agents
884
918
  ? cluster.agents.map((a) => ({
@@ -888,6 +922,8 @@ class Orchestrator {
888
922
  currentTask: a.currentTask ? true : false,
889
923
  currentTaskId: a.currentTaskId,
890
924
  processPid: a.processPid,
925
+ providerSession: normalizeProviderSession(a.providerSession),
926
+ lastGuidanceAppliedId: a.lastGuidanceAppliedId ?? null,
891
927
  }))
892
928
  : null,
893
929
  setupLogPath: cluster.setupLogPath || null,
@@ -1585,6 +1621,8 @@ class Orchestrator {
1585
1621
  [
1586
1622
  'TASK_STARTED',
1587
1623
  'TASK_COMPLETED',
1624
+ 'TASK_FAILED',
1625
+ 'RETRY_SCHEDULED',
1588
1626
  'PROCESS_SPAWNED',
1589
1627
  'TASK_ID_ASSIGNED',
1590
1628
  'STARTED',
@@ -0,0 +1,82 @@
1
+ const { randomUUID } = require('node:crypto');
2
+
3
+ const TASK_SPAWN_OWNERSHIP_TOKEN_ENV = 'ZEROSHOT_TASK_SPAWN_OWNERSHIP_TOKEN';
4
+
5
+ const COMMAND_CLEANUP_OWNER = Object.freeze({
6
+ CALLER: 'caller',
7
+ TASK_LIFECYCLE: 'task-lifecycle',
8
+ });
9
+
10
+ function normalizeError(error) {
11
+ return error instanceof Error ? error : new Error(String(error));
12
+ }
13
+
14
+ /**
15
+ * Mark a wrapper failure after the detached task record durably accepted the
16
+ * launch ownership token. Human stdout and process spawn events are not
17
+ * ownership receipts.
18
+ */
19
+ function transferCommandCleanupOwnership(error) {
20
+ const normalized = normalizeError(error);
21
+ normalized.commandCleanupOwner = COMMAND_CLEANUP_OWNER.TASK_LIFECYCLE;
22
+ return normalized;
23
+ }
24
+
25
+ function callerOwnsCommandCleanup(error) {
26
+ return error?.commandCleanupOwner !== COMMAND_CLEANUP_OWNER.TASK_LIFECYCLE;
27
+ }
28
+
29
+ function cleanupCallerOwnedCommand(error, cleanup) {
30
+ if (callerOwnsCommandCleanup(error)) cleanup();
31
+ }
32
+
33
+ function requireTaskIdFromWrapperResult({
34
+ code,
35
+ stdout,
36
+ stderr,
37
+ parseTaskId,
38
+ persistedTaskId = null,
39
+ }) {
40
+ if (code !== 0) {
41
+ throw new Error(`zeroshot task run failed with code ${code}: ${stderr}`);
42
+ }
43
+ if (!persistedTaskId) {
44
+ const printedTaskId = parseTaskId(stdout);
45
+ throw new Error(
46
+ `Detached task ownership receipt was not persisted${
47
+ printedTaskId ? ` for wrapper output ${printedTaskId}` : ''
48
+ }.`
49
+ );
50
+ }
51
+ const printedTaskId = parseTaskId(stdout);
52
+ if (printedTaskId && printedTaskId !== persistedTaskId) {
53
+ throw new Error(
54
+ `Task ownership receipt ${persistedTaskId} did not match wrapper output ${printedTaskId}.`
55
+ );
56
+ }
57
+ return persistedTaskId;
58
+ }
59
+
60
+ function createTaskSpawnOwnershipToken() {
61
+ return randomUUID();
62
+ }
63
+
64
+ function trackTaskWrapperCleanupOwnership(findPersistedTaskId) {
65
+ return (error) => {
66
+ try {
67
+ return findPersistedTaskId() ? transferCommandCleanupOwnership(error) : error;
68
+ } catch {
69
+ return transferCommandCleanupOwnership(error);
70
+ }
71
+ };
72
+ }
73
+
74
+ module.exports = {
75
+ COMMAND_CLEANUP_OWNER,
76
+ TASK_SPAWN_OWNERSHIP_TOKEN_ENV,
77
+ cleanupCallerOwnedCommand,
78
+ callerOwnsCommandCleanup,
79
+ createTaskSpawnOwnershipToken,
80
+ requireTaskIdFromWrapperResult,
81
+ trackTaskWrapperCleanupOwnership,
82
+ };