@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
@@ -101,6 +101,9 @@ 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;
106
+ readonly supportsResume: boolean;
104
107
  }
105
108
 
106
109
  export interface CodexCliFeatures extends BaseCliFeatures {
@@ -112,6 +115,7 @@ export interface CodexCliFeatures extends BaseCliFeatures {
112
115
  readonly supportsConfigOverride: boolean;
113
116
  readonly supportsModel: boolean;
114
117
  readonly supportsSkipGitRepoCheck: boolean;
118
+ readonly supportsResume: boolean;
115
119
  }
116
120
 
117
121
  export interface GeminiCliFeatures extends BaseCliFeatures {
@@ -193,6 +197,7 @@ export interface CliFeatureOverrides {
193
197
  readonly supportsVerbose?: boolean;
194
198
  readonly supportsModel?: boolean;
195
199
  readonly supportsEffort?: boolean;
200
+ readonly supportsSettings?: boolean;
196
201
  readonly supportsJson?: boolean;
197
202
  readonly supportsOutputSchema?: boolean;
198
203
  readonly supportsDir?: boolean;
@@ -212,6 +217,7 @@ export interface CliFeatureOverrides {
212
217
  readonly supportsNoAskUser?: boolean;
213
218
  readonly supportsAddDir?: boolean;
214
219
  readonly supportsMcpConfig?: boolean;
220
+ readonly supportsResume?: boolean;
215
221
  readonly supportsBundledRunner?: boolean;
216
222
  readonly supportsAcpStdio?: boolean;
217
223
  readonly supportsPromptImages?: boolean;
@@ -228,10 +234,10 @@ export interface CliFeatureOverrides {
228
234
  }
229
235
 
230
236
  export interface CleanupMetadata {
231
- readonly kind: 'temp-file';
237
+ readonly kind: 'temp-file' | 'temp-directory';
232
238
  readonly provider: ProviderId;
233
239
  readonly path: string;
234
- readonly reason: 'output-schema';
240
+ readonly reason: 'output-schema' | 'settings-overlay';
235
241
  }
236
242
 
237
243
  export interface WarningMetadata {
@@ -265,14 +271,14 @@ export interface BuildProviderCommandOptions {
265
271
  readonly autoApprove?: boolean;
266
272
  readonly resumeSessionId?: string;
267
273
  readonly continueSession?: boolean;
274
+ readonly claudeSettingsFile?: string;
268
275
  readonly cliFeatures?: CliFeatureOverrides;
269
276
  readonly authEnv?: Readonly<Record<string, string>>;
270
277
  readonly strictSchema?: boolean;
271
278
  readonly gateway?: GatewayBuildOptions;
272
- // MCP server configs forwarded to providers that accept an MCP config CLI flag (currently
273
- // Copilot's `--additional-mcp-config`). Each entry is an inline JSON string (the standard
274
- // `{"mcpServers": {...}}` envelope) or an `@<path>` file reference; adapters emit one flag per
275
- // 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.
276
282
  readonly mcpConfig?: readonly string[];
277
283
  }
278
284
 
@@ -368,6 +374,7 @@ export interface ProviderAdapter {
368
374
  readonly defaultMinLevel: ModelLevel;
369
375
  detectCliFeatures(helpText?: string | null): ProviderCliFeatures;
370
376
  buildCommand(context: string, options?: BuildProviderCommandOptions): CommandSpec;
377
+ extractSessionId?(line: string): string | null;
371
378
  parseEvent(line: string, state: ProviderParserState): ProviderParseResult;
372
379
  createParserState(): ProviderParserState;
373
380
  resolveModelSpec(level: ModelLevel, overrides?: LevelOverrides): ResolvedModelSpec;
@@ -17,6 +17,11 @@ const { normalizeProviderName } = require('../lib/provider-names');
17
17
  const { getProvider } = require('./providers');
18
18
  const { buildContext } = require('./agent/agent-context-builder');
19
19
  const { collectQueuedGuidance } = require('./agent/guidance-queue');
20
+ const {
21
+ promptIdentity,
22
+ resolveAgentProviderSession,
23
+ updateAgentProviderSession,
24
+ } = require('./agent/provider-session');
20
25
  const { findMatchingTrigger, evaluateTrigger } = require('./agent/agent-trigger-evaluator');
21
26
  const { executeHook } = require('./agent/agent-hook-executor');
22
27
  const { injectInput: injectAgentInput } = require('./agent/agent-input-injector');
@@ -66,6 +71,11 @@ class AgentWrapper {
66
71
  this.currentTask = null;
67
72
  /** @type {string | null} */
68
73
  this.currentTaskId = null; // Track spawned task ID for resume capability
74
+ /** @type {{provider: string, sessionId: string, agentId: string, taskId: string, generation: number, cwd: string, worktreePath: string|null, contextSequence: string, guidanceSequence: string|null, promptIdentity: string|null} | null} */
75
+ this.providerSession = null; // Provider continuation state, owned by this logical agent only
76
+ this.currentContextSequence = '0';
77
+ this.currentGuidanceSequence = null;
78
+ this.currentPromptIdentity = null;
69
79
  /** @type {number | null} */
70
80
  this.processPid = null; // Track process PID for resource monitoring
71
81
  this.running = false;
@@ -76,7 +86,7 @@ class AgentWrapper {
76
86
  /** @type {number | null} */
77
87
  this.lastAgentStartTime = null; // Track when agent last began executing (for context filtering)
78
88
  /** @type {number | null} */
79
- this.lastGuidanceAppliedAt = null; // Track last queued guidance applied to prompt
89
+ this.lastGuidanceAppliedId = null; // Track last queued guidance sequence applied to prompt
80
90
 
81
91
  // LIVENESS DETECTION - Track output freshness to detect stuck agents
82
92
  /** @type {number | null} */
@@ -119,6 +129,7 @@ class AgentWrapper {
119
129
 
120
130
  this.testMode = options.testMode || false;
121
131
  this.quiet = options.quiet || false;
132
+ this.providerCliFeatures = options.providerCliFeatures || null;
122
133
 
123
134
  // ISOLATION SUPPORT - Run tasks inside Docker container
124
135
  this.isolation = options.isolation || null;
@@ -435,12 +446,29 @@ class AgentWrapper {
435
446
  */
436
447
  _buildContext(triggeringMessage) {
437
448
  const previousAgentStart = this.lastAgentStartTime;
449
+ const providerName = this._resolveProvider();
450
+ let providerSession = resolveAgentProviderSession(this, providerName);
451
+ if (providerSession && !this._providerSupportsSessionResume(providerName)) {
452
+ updateAgentProviderSession(this, null);
453
+ providerSession = null;
454
+ }
455
+ const contextMode = providerSession ? 'continuation' : 'full';
456
+ const selectedPrompt = this._selectPrompt();
457
+ const latestMessage = this.messageBus.findLast({
458
+ cluster_id: this.cluster.id,
459
+ orderBySequence: true,
460
+ });
461
+ this.currentContextSequence = latestMessage?.sequence || '0';
438
462
  const queuedGuidance = collectQueuedGuidance({
439
463
  messageBus: this.messageBus,
440
464
  clusterId: this.cluster.id,
441
465
  agentId: this.id,
442
- lastDeliveredAt: this.lastGuidanceAppliedAt,
466
+ afterId: this.lastGuidanceAppliedId,
467
+ throughId: this.currentContextSequence,
443
468
  });
469
+ this.currentGuidanceSequence =
470
+ queuedGuidance.latestSequence ?? this.lastGuidanceAppliedId ?? null;
471
+ this.currentPromptIdentity = promptIdentity(selectedPrompt);
444
472
  const context = buildContext({
445
473
  id: this.id,
446
474
  role: this.role,
@@ -451,27 +479,35 @@ class AgentWrapper {
451
479
  lastTaskEndTime: this.lastTaskEndTime,
452
480
  lastAgentStartTime: previousAgentStart,
453
481
  triggeringMessage,
454
- selectedPrompt: this._selectPrompt(),
482
+ selectedPrompt,
455
483
  queuedGuidance: queuedGuidance.guidanceBlock,
484
+ mode: contextMode,
485
+ continuationSequence: providerSession?.contextSequence,
486
+ contextThroughId: this.currentContextSequence,
487
+ previousPromptIdentity: providerSession?.promptIdentity,
488
+ currentPromptIdentity: this.currentPromptIdentity,
456
489
  // Pass isolation state for conditional git restriction
457
490
  worktree: this.worktree,
458
491
  isolation: this.isolation,
459
492
  });
460
493
 
461
494
  // Record when this iteration started so future "since: last_agent_start" filters work.
462
- const latestMessage = this.messageBus.findLast({ cluster_id: this.cluster.id });
463
495
  const latestTimestamp = latestMessage?.timestamp;
464
496
  const now = Date.now();
465
497
  this.lastAgentStartTime =
466
498
  typeof latestTimestamp === 'number' ? Math.max(now, latestTimestamp + 1) : now;
467
499
 
468
- if (queuedGuidance.latestTimestamp !== null) {
469
- this.lastGuidanceAppliedAt = queuedGuidance.latestTimestamp;
470
- }
471
-
472
500
  return context;
473
501
  }
474
502
 
503
+ _providerSupportsSessionResume(providerName) {
504
+ const override = this.providerCliFeatures?.[providerName];
505
+ if (override && Object.hasOwn(override, 'supportsResume')) {
506
+ return override.supportsResume === true;
507
+ }
508
+ return getProvider(providerName).getCliFeatures().supportsResume === true;
509
+ }
510
+
475
511
  /**
476
512
  * Spawn claude-zeroshots process and stream output via message bus
477
513
  * @private
@@ -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,63 @@
1
+ const MAX_SQLITE_ROWID = 9223372036854775807n;
2
+ const CANONICAL_SEQUENCE = /^(0|[1-9][0-9]*)$/;
3
+
4
+ function canonicalMessageSequence(value, name = 'message sequence') {
5
+ let sequence;
6
+ if (typeof value === 'string' && CANONICAL_SEQUENCE.test(value)) {
7
+ sequence = value;
8
+ } else if (Number.isSafeInteger(value) && value >= 0) {
9
+ // Accept legacy JSON state while normalizing every new boundary to a
10
+ // JSON-safe decimal string.
11
+ sequence = String(value);
12
+ } else {
13
+ throw new TypeError(`${name} must be a canonical non-negative decimal string`);
14
+ }
15
+
16
+ if (BigInt(sequence) > MAX_SQLITE_ROWID) {
17
+ throw new RangeError(`${name} exceeds the SQLite rowid range`);
18
+ }
19
+ return sequence;
20
+ }
21
+
22
+ function messageSequenceFromSql(value, name = 'message sequence') {
23
+ if (typeof value === 'bigint') {
24
+ if (value < 0n || value > MAX_SQLITE_ROWID) {
25
+ throw new RangeError(`${name} is outside the SQLite rowid range`);
26
+ }
27
+ return value.toString();
28
+ }
29
+ return canonicalMessageSequence(value, name);
30
+ }
31
+
32
+ function messageSequenceToSql(value, name = 'message sequence') {
33
+ return BigInt(canonicalMessageSequence(value, name));
34
+ }
35
+
36
+ function compareMessageSequences(left, right) {
37
+ const leftValue = messageSequenceToSql(left, 'left message sequence');
38
+ const rightValue = messageSequenceToSql(right, 'right message sequence');
39
+ if (leftValue < rightValue) {
40
+ return -1;
41
+ }
42
+ if (leftValue > rightValue) {
43
+ return 1;
44
+ }
45
+ return 0;
46
+ }
47
+
48
+ function tryCanonicalMessageSequence(value) {
49
+ try {
50
+ return canonicalMessageSequence(value);
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ module.exports = {
57
+ MAX_SQLITE_ROWID,
58
+ canonicalMessageSequence,
59
+ compareMessageSequences,
60
+ messageSequenceFromSql,
61
+ messageSequenceToSql,
62
+ tryCanonicalMessageSequence,
63
+ };