@the-open-engine/zeroshot 6.10.0 → 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 (31) hide show
  1. package/cli/index.js +16 -0
  2. package/cluster-hooks/block-ask-user-question.py +2 -3
  3. package/cluster-hooks/block-dangerous-git.py +2 -3
  4. package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
  5. package/lib/agent-cli-provider/adapters/claude.js +42 -1
  6. package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
  7. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
  8. package/lib/agent-cli-provider/contract-options.js +2 -0
  9. package/lib/agent-cli-provider/contract-options.js.map +1 -1
  10. package/lib/agent-cli-provider/types.d.ts +6 -2
  11. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  12. package/lib/agent-cli-provider/types.js.map +1 -1
  13. package/package.json +1 -1
  14. package/src/agent/agent-lifecycle.js +13 -6
  15. package/src/agent/agent-task-executor.js +526 -313
  16. package/src/agent-cli-provider/adapters/claude.ts +46 -1
  17. package/src/agent-cli-provider/contract-options.ts +6 -0
  18. package/src/agent-cli-provider/types.ts +9 -6
  19. package/src/claude-task-runner.js +153 -44
  20. package/src/task-spawn-cleanup-ownership.js +82 -0
  21. package/src/worktree-claude-config.js +193 -85
  22. package/task-lib/attachable-watcher.js +93 -267
  23. package/task-lib/command-spec-cleanup.js +219 -0
  24. package/task-lib/commands/clean.js +35 -5
  25. package/task-lib/commands/get-task-id-by-spawn-token.js +10 -0
  26. package/task-lib/commands/kill.js +117 -4
  27. package/task-lib/commands/status.js +1 -0
  28. package/task-lib/runner.js +61 -4
  29. package/task-lib/store.js +68 -6
  30. package/task-lib/watcher-output-runtime.js +284 -0
  31. package/task-lib/watcher.js +124 -257
@@ -1,4 +1,5 @@
1
1
  import { getString, isRecord, stringifyJson, tryParseJson } from '../json';
2
+ import { contractError } from '../contract-errors';
2
3
  import {
3
4
  type BuildProviderCommandOptions,
4
5
  type ClaudeCliFeatures,
@@ -66,6 +67,10 @@ function detectCliFeatures(helpText?: string | null): ClaudeCliFeatures {
66
67
  supportsVerbose: unknown ? true : /--verbose/.test(help),
67
68
  supportsModel: unknown ? true : /--model/.test(help),
68
69
  supportsEffort: unknown ? true : /--effort/.test(help),
70
+ // Settings carry mandatory safety hooks, so an unprobed or old CLI must
71
+ // fail closed before a provider process is spawned.
72
+ supportsSettings: !unknown && /--settings/.test(help),
73
+ supportsMcpConfig: !unknown && /--mcp-config/.test(help),
69
74
  supportsResume: unknown ? true : /--resume/.test(help),
70
75
  unknown,
71
76
  };
@@ -131,6 +136,40 @@ function addSessionArgs(args: string[], options: BuildProviderCommandOptions): v
131
136
  }
132
137
  }
133
138
 
139
+ function addSettingsArgs(args: string[], options: BuildProviderCommandOptions): void {
140
+ const settingsPath = options.claudeSettingsFile?.trim();
141
+ if (settingsPath) {
142
+ args.push('--settings', settingsPath);
143
+ }
144
+ }
145
+
146
+ function addMcpConfigArgs(args: string[], options: BuildProviderCommandOptions): void {
147
+ if (!options.mcpConfig?.length) return;
148
+ args.push('--mcp-config', ...options.mcpConfig);
149
+ }
150
+
151
+ function failClosedUnsupportedRunConfig(options: BuildProviderCommandOptions): void {
152
+ const features = optionFeatures(options);
153
+ if (options.claudeSettingsFile?.trim() && features.supportsSettings === false) {
154
+ throw contractError({
155
+ code: 'invalid-field',
156
+ field: 'options.cliFeatures.supportsSettings',
157
+ exitCode: 2,
158
+ message:
159
+ 'Claude CLI does not advertise --settings support required for Zeroshot safety hooks. Upgrade Claude Code before running this task.',
160
+ });
161
+ }
162
+ if (options.mcpConfig?.length && features.supportsMcpConfig === false) {
163
+ throw contractError({
164
+ code: 'invalid-field',
165
+ field: 'options.cliFeatures.supportsMcpConfig',
166
+ exitCode: 2,
167
+ message:
168
+ 'Claude CLI does not advertise --mcp-config support required to preserve repository MCP tools. Upgrade Claude Code before running this task.',
169
+ });
170
+ }
171
+ }
172
+
134
173
  function extractSessionId(line: string): string | null {
135
174
  const event = tryParseJson(line.trim());
136
175
  if (!isRecord(event)) return null;
@@ -185,15 +224,21 @@ function collectWarnings(options: BuildProviderCommandOptions): WarningMetadata[
185
224
  }
186
225
 
187
226
  function buildCommand(context: string, options: BuildProviderCommandOptions = {}): CommandSpec {
227
+ failClosedUnsupportedRunConfig(options);
188
228
  const { command, args: commandPrefix } = resolveClaudeCommand();
189
- const args: string[] = [...commandPrefix, '--print', '--input-format', 'text'];
229
+ const args: string[] = [...commandPrefix, '--print'];
190
230
  const authEnv = options.authEnv ?? {};
191
231
 
232
+ // --mcp-config is variadic. A following option terminates its values before
233
+ // the positional prompt.
234
+ addMcpConfigArgs(args, options);
235
+ args.push('--input-format', 'text');
192
236
  addOutputArgs(args, options);
193
237
  addSchemaArgs(args, options);
194
238
  addModelArgs(args, options);
195
239
  addAutoApproveArgs(args, options);
196
240
  addSessionArgs(args, options);
241
+ addSettingsArgs(args, options);
197
242
 
198
243
  args.push(context);
199
244
 
@@ -23,6 +23,7 @@ const CLI_FEATURE_FIELDS = [
23
23
  'supportsVerbose',
24
24
  'supportsModel',
25
25
  'supportsEffort',
26
+ 'supportsSettings',
26
27
  'supportsJson',
27
28
  'supportsOutputSchema',
28
29
  'supportsDir',
@@ -209,6 +210,11 @@ function normalizeBuildOptions(value: Record<string, unknown>): BuildProviderCom
209
210
  'continueSession',
210
211
  optionalBooleanValue(value.continueSession, 'options.continueSession')
211
212
  );
213
+ addDefined(
214
+ result,
215
+ 'claudeSettingsFile',
216
+ optionalStringValue(value.claudeSettingsFile, 'options.claudeSettingsFile')
217
+ );
212
218
  addDefined(result, 'cliFeatures', optionalCliFeatures(value.cliFeatures));
213
219
  addDefined(result, 'mcpConfig', optionalMcpConfig(value.mcpConfig));
214
220
  addDefined(
@@ -101,6 +101,8 @@ export interface ClaudeCliFeatures extends BaseCliFeatures {
101
101
  readonly supportsVerbose: boolean;
102
102
  readonly supportsModel: boolean;
103
103
  readonly supportsEffort: boolean;
104
+ readonly supportsSettings: boolean;
105
+ readonly supportsMcpConfig: boolean;
104
106
  readonly supportsResume: boolean;
105
107
  }
106
108
 
@@ -195,6 +197,7 @@ export interface CliFeatureOverrides {
195
197
  readonly supportsVerbose?: boolean;
196
198
  readonly supportsModel?: boolean;
197
199
  readonly supportsEffort?: boolean;
200
+ readonly supportsSettings?: boolean;
198
201
  readonly supportsJson?: boolean;
199
202
  readonly supportsOutputSchema?: boolean;
200
203
  readonly supportsDir?: boolean;
@@ -231,10 +234,10 @@ export interface CliFeatureOverrides {
231
234
  }
232
235
 
233
236
  export interface CleanupMetadata {
234
- readonly kind: 'temp-file';
237
+ readonly kind: 'temp-file' | 'temp-directory';
235
238
  readonly provider: ProviderId;
236
239
  readonly path: string;
237
- readonly reason: 'output-schema';
240
+ readonly reason: 'output-schema' | 'settings-overlay';
238
241
  }
239
242
 
240
243
  export interface WarningMetadata {
@@ -268,14 +271,14 @@ export interface BuildProviderCommandOptions {
268
271
  readonly autoApprove?: boolean;
269
272
  readonly resumeSessionId?: string;
270
273
  readonly continueSession?: boolean;
274
+ readonly claudeSettingsFile?: string;
271
275
  readonly cliFeatures?: CliFeatureOverrides;
272
276
  readonly authEnv?: Readonly<Record<string, string>>;
273
277
  readonly strictSchema?: boolean;
274
278
  readonly gateway?: GatewayBuildOptions;
275
- // MCP server configs forwarded to providers that accept an MCP config CLI flag (currently
276
- // Copilot's `--additional-mcp-config`). Each entry is an inline JSON string (the standard
277
- // `{"mcpServers": {...}}` envelope) or an `@<path>` file reference; adapters emit one flag per
278
- // entry. Providers whose adapter models no MCP flag ignore this field.
279
+ // MCP server configs forwarded to providers that accept an MCP config CLI flag. Claude accepts
280
+ // one variadic `--mcp-config` followed by file paths; adapters such as Copilot accept repeated
281
+ // flags with inline JSON so configs survive container path translation.
279
282
  readonly mcpConfig?: readonly string[];
280
283
  }
281
284
 
@@ -12,7 +12,23 @@ const { loadSettings } = require('../lib/settings');
12
12
  const { normalizeProviderName } = require('../lib/provider-names');
13
13
  const { getProvider } = require('./providers');
14
14
  const { prependWorktreeToolBinToEnv } = require('./worktree-tooling-env');
15
- const { prepareClaudeConfigDir } = require('./worktree-claude-config');
15
+ const { getTask, getTaskBySpawnOwnershipToken } = require('../task-lib/store.js');
16
+ const {
17
+ TASK_SPAWN_OWNERSHIP_TOKEN_ENV,
18
+ cleanupCallerOwnedCommand,
19
+ callerOwnsCommandCleanup,
20
+ createTaskSpawnOwnershipToken,
21
+ requireTaskIdFromWrapperResult,
22
+ trackTaskWrapperCleanupOwnership,
23
+ } = require('./task-spawn-cleanup-ownership');
24
+ const {
25
+ CLAUDE_MCP_CONFIG_ENV,
26
+ CLAUDE_SETTINGS_ENV,
27
+ cleanupClaudeSettingsOverlay,
28
+ prepareClaudeSettingsOverlay,
29
+ resolveContainerMcpConfigPath,
30
+ resolveRepoMcpConfigPath,
31
+ } = require('./worktree-claude-config');
16
32
  const {
17
33
  appendTaskRunModelArgs,
18
34
  wrapTaskRunWithIsolatedSettings,
@@ -80,6 +96,28 @@ function runCommand(command, args, options = {}, callback = null) {
80
96
  });
81
97
  }
82
98
 
99
+ const TASK_TERMINAL_STATUSES = new Set(['completed', 'failed', 'killed', 'stale', 'cancelled']);
100
+
101
+ async function cleanupPersistedTaskAfterLaunchFailure(ctPath, taskId) {
102
+ let lastError = null;
103
+ for (let attempt = 1; attempt <= 3; attempt++) {
104
+ let commandError = null;
105
+ try {
106
+ await runCommand(ctPath, ['kill', taskId], { timeout: 10000 });
107
+ } catch (error) {
108
+ commandError = error;
109
+ }
110
+ const task = getTask(taskId);
111
+ if (!task || (TASK_TERMINAL_STATUSES.has(task.status) && !task.commandCleanup)) {
112
+ return;
113
+ }
114
+ lastError =
115
+ commandError ||
116
+ new Error(`Task ${taskId} termination and command cleanup were not confirmed`);
117
+ }
118
+ throw lastError || new Error(`Task ${taskId} cleanup failed`);
119
+ }
120
+
83
121
  function runCommandSync(command, args, options = {}) {
84
122
  const timeout = options.timeout ?? 30000;
85
123
  const result = spawnSync(command, args, { ...options, timeout });
@@ -95,6 +133,17 @@ function runCommandSync(command, args, options = {}) {
95
133
  return result.stdout?.toString() || '';
96
134
  }
97
135
 
136
+ function appendIsolatedMcpConfigArgs(command, provider, options) {
137
+ if (provider !== 'claude') return;
138
+ const mcpConfigPath = resolveContainerMcpConfigPath({
139
+ cwd: options.cwd || process.cwd(),
140
+ worktreePath: options.worktreePath || null,
141
+ });
142
+ if (mcpConfigPath) {
143
+ command.push('--mcp-config', mcpConfigPath);
144
+ }
145
+ }
146
+
98
147
  class ClaudeTaskRunner extends TaskRunner {
99
148
  /**
100
149
  * @param {Object} options
@@ -191,14 +240,20 @@ class ClaudeTaskRunner extends TaskRunner {
191
240
  worktreePath,
192
241
  });
193
242
 
194
- const taskId = await this._spawnAndGetTaskId(ctPath, args, cwd, spawnEnv, agentId);
243
+ let taskId;
244
+ try {
245
+ taskId = await this._spawnAndGetTaskId(ctPath, args, cwd, spawnEnv, agentId);
246
+ } catch (error) {
247
+ cleanupCallerOwnedCommand(error, () =>
248
+ cleanupClaudeSettingsOverlay(spawnEnv[CLAUDE_SETTINGS_ENV])
249
+ );
250
+ throw error;
251
+ }
195
252
 
253
+ // Once a task ID is returned, the detached watcher owns provider
254
+ // termination and command cleanup.
196
255
  this._log(`📋 [${agentId}]: Following zeroshot logs for ${taskId}`);
197
-
198
- // Wait for task registration
199
256
  await this._waitForTaskReady(ctPath, taskId);
200
-
201
- // Follow logs until completion
202
257
  return this._followLogs(ctPath, taskId, agentId);
203
258
  }
204
259
 
@@ -325,9 +380,12 @@ class ClaudeTaskRunner extends TaskRunner {
325
380
  spawnEnv.ANTHROPIC_MODEL = resolvedModelSpec.model;
326
381
  }
327
382
  if (providerName === 'claude') {
328
- const claudeConfigDir = prepareClaudeConfigDir({ cwd, worktreePath });
329
- if (claudeConfigDir) {
330
- spawnEnv.CLAUDE_CONFIG_DIR = claudeConfigDir;
383
+ spawnEnv[CLAUDE_SETTINGS_ENV] = prepareClaudeSettingsOverlay({
384
+ includeDangerousGit: Boolean(worktreePath),
385
+ });
386
+ const mcpConfigPath = resolveRepoMcpConfigPath({ cwd, worktreePath });
387
+ if (mcpConfigPath) {
388
+ spawnEnv[CLAUDE_MCP_CONFIG_ENV] = mcpConfigPath;
331
389
  }
332
390
  }
333
391
 
@@ -345,16 +403,49 @@ class ClaudeTaskRunner extends TaskRunner {
345
403
  * @returns {Promise<string>}
346
404
  */
347
405
  _spawnAndGetTaskId(ctPath, args, cwd, spawnEnv, _agentId) {
406
+ const ownershipToken = createTaskSpawnOwnershipToken();
407
+ const findPersistedTaskId = () => getTaskBySpawnOwnershipToken(ownershipToken)?.id || null;
348
408
  return new Promise((resolve, reject) => {
349
409
  const proc = spawn(ctPath, args, {
350
410
  cwd,
351
411
  stdio: ['ignore', 'pipe', 'pipe'],
352
- env: spawnEnv,
412
+ env: { ...spawnEnv, [TASK_SPAWN_OWNERSHIP_TOKEN_ENV]: ownershipToken },
353
413
  windowsHide: true,
354
414
  });
355
415
 
356
416
  let stdout = '';
357
417
  let stderr = '';
418
+ let settled = false;
419
+ const classifyCleanupOwnership = trackTaskWrapperCleanupOwnership(findPersistedTaskId);
420
+ const rejectWithOwnership = async (error) => {
421
+ if (settled) return;
422
+ settled = true;
423
+ const classifiedError = classifyCleanupOwnership(error);
424
+ if (!callerOwnsCommandCleanup(classifiedError)) {
425
+ classifiedError.spawnOwnershipToken = ownershipToken;
426
+ let persistedTaskId = null;
427
+ let lookupError = null;
428
+ try {
429
+ persistedTaskId = findPersistedTaskId();
430
+ } catch (lookupFailure) {
431
+ lookupError = lookupFailure;
432
+ }
433
+ classifiedError.taskId = persistedTaskId;
434
+ try {
435
+ if (lookupError) throw lookupError;
436
+ if (persistedTaskId) {
437
+ await cleanupPersistedTaskAfterLaunchFailure(ctPath, persistedTaskId);
438
+ }
439
+ } catch (cleanupError) {
440
+ classifiedError.message += ` Task cleanup was not confirmed: ${cleanupError.message}`;
441
+ classifiedError.permanent = true;
442
+ classifiedError.restartExhausted = true;
443
+ classifiedError.terminationExhausted = true;
444
+ classifiedError.terminationAttempts = persistedTaskId ? 3 : 1;
445
+ }
446
+ }
447
+ reject(classifiedError);
448
+ };
358
449
 
359
450
  proc.stdout.on('data', (data) => {
360
451
  stdout += data.toString();
@@ -364,21 +455,26 @@ class ClaudeTaskRunner extends TaskRunner {
364
455
  stderr += data.toString();
365
456
  });
366
457
 
367
- proc.on('close', (code) => {
368
- if (code === 0) {
369
- const match = stdout.match(/Task spawned: ((?:task-)?[a-z]+-[a-z]+-[a-z0-9]+)/);
370
- if (match) {
371
- resolve(match[1]);
372
- } else {
373
- reject(new Error(`Could not parse task ID from output: ${stdout}`));
374
- }
375
- } else {
376
- reject(new Error(`zeroshot task run failed with code ${code}: ${stderr}`));
458
+ proc.on('close', async (code) => {
459
+ if (settled) return;
460
+ try {
461
+ const taskId = requireTaskIdFromWrapperResult({
462
+ code,
463
+ stdout,
464
+ stderr,
465
+ parseTaskId: (output) =>
466
+ output.match(/Task spawned: ((?:task-)?[a-z]+-[a-z]+-[a-z0-9]+)/)?.[1],
467
+ persistedTaskId: findPersistedTaskId(),
468
+ });
469
+ settled = true;
470
+ resolve(taskId);
471
+ } catch (error) {
472
+ await rejectWithOwnership(error);
377
473
  }
378
474
  });
379
475
 
380
- proc.on('error', (error) => {
381
- reject(error);
476
+ proc.on('error', async (error) => {
477
+ await rejectWithOwnership(error);
382
478
  });
383
479
  });
384
480
  }
@@ -424,6 +520,7 @@ class ClaudeTaskRunner extends TaskRunner {
424
520
  let statusCheckInterval = null;
425
521
  let resolved = false;
426
522
  let lineBuffer = '';
523
+ let cleanupRecoveryPending = false;
427
524
 
428
525
  // Get log file path
429
526
  try {
@@ -558,33 +655,43 @@ class ClaudeTaskRunner extends TaskRunner {
558
655
 
559
656
  statusCheckInterval = setInterval(() => {
560
657
  runCommand(ctPath, ['status', taskId], {}, (error, stdout) => {
561
- if (resolved) return;
562
-
563
- if (
564
- !error &&
565
- (stdout.includes('Status: completed') || stdout.includes('Status: failed'))
566
- ) {
567
- const success = stdout.includes('Status: completed');
658
+ if (resolved || error) return;
659
+ const terminalMatch = stdout.match(/Status:\s+(completed|failed)/i);
660
+ if (!terminalMatch) return;
661
+ if (/Cleanup:\s+pending/i.test(stdout)) {
662
+ if (!cleanupRecoveryPending) {
663
+ cleanupRecoveryPending = true;
664
+ runCommand(ctPath, ['kill', taskId], { timeout: 10000 }, (cleanupError) => {
665
+ cleanupRecoveryPending = false;
666
+ if (cleanupError) {
667
+ this._log(
668
+ `⚠️ [${agentId}]: Terminal cleanup recovery will retry: ${cleanupError.message}`
669
+ );
670
+ }
671
+ });
672
+ }
673
+ return;
674
+ }
568
675
 
569
- pollLogFile();
676
+ const success = terminalMatch[1].toLowerCase() === 'completed';
677
+ pollLogFile();
570
678
 
571
- setTimeout(() => {
572
- if (resolved) return;
573
- resolved = true;
679
+ setTimeout(() => {
680
+ if (resolved) return;
681
+ resolved = true;
574
682
 
575
- if (pollInterval) clearInterval(pollInterval);
576
- if (statusCheckInterval) clearInterval(statusCheckInterval);
683
+ clearInterval(pollInterval);
684
+ clearInterval(statusCheckInterval);
577
685
 
578
- const errorContext = extractErrorContext(success, stdout);
686
+ const errorContext = extractErrorContext(success, stdout);
579
687
 
580
- resolve({
581
- success,
582
- output,
583
- error: errorContext,
584
- taskId,
585
- });
586
- }, 500);
587
- }
688
+ resolve({
689
+ success,
690
+ output,
691
+ error: errorContext,
692
+ taskId,
693
+ });
694
+ }, 500);
588
695
  });
589
696
  }, 1000);
590
697
 
@@ -664,6 +771,8 @@ class ClaudeTaskRunner extends TaskRunner {
664
771
  command.push('--json-schema', JSON.stringify(jsonSchema));
665
772
  }
666
773
 
774
+ appendIsolatedMcpConfigArgs(command, provider, options);
775
+
667
776
  let finalContext = context;
668
777
  if (jsonSchema && desiredOutputFormat === 'json' && runOutputFormat === 'stream-json') {
669
778
  finalContext += `\n\n## Output Format (REQUIRED)\n\nReturn a JSON object that matches this schema exactly.\n\nSchema:\n\`\`\`json\n${JSON.stringify(
@@ -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
+ };