@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/task-lib/store.js CHANGED
@@ -12,6 +12,7 @@ import { TASKS_DIR, LOGS_DIR } from './config.js';
12
12
  import { generateName } from './name-generator.js';
13
13
 
14
14
  const DB_FILE = join(TASKS_DIR, 'store.db');
15
+ export const TASK_STORE_SCHEMA_VERSION = 4;
15
16
 
16
17
  /** @type {Database.Database | null} */
17
18
  let db = null;
@@ -41,6 +42,9 @@ function getDb() {
41
42
  status TEXT NOT NULL DEFAULT 'pending',
42
43
  pid INTEGER,
43
44
  session_id TEXT,
45
+ session_id_conflict INTEGER NOT NULL DEFAULT 0,
46
+ requested_resume_session_id TEXT,
47
+ resume_identity_verified INTEGER NOT NULL DEFAULT 0,
44
48
  log_file TEXT,
45
49
  created_at TEXT NOT NULL,
46
50
  updated_at TEXT NOT NULL,
@@ -52,7 +56,10 @@ function getDb() {
52
56
  socket_path TEXT,
53
57
  attachable INTEGER DEFAULT 0,
54
58
  process_group_id INTEGER,
55
- termination_strategy TEXT
59
+ termination_strategy TEXT,
60
+ command_cleanup TEXT,
61
+ cancel_requested INTEGER DEFAULT 0,
62
+ spawn_ownership_token TEXT
56
63
  );
57
64
 
58
65
  CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
@@ -77,8 +84,7 @@ function getDb() {
77
84
  CREATE INDEX IF NOT EXISTS idx_schedules_enabled ON schedules(enabled);
78
85
  `);
79
86
 
80
- ensureTaskColumn(db, 'process_group_id', 'INTEGER');
81
- ensureTaskColumn(db, 'termination_strategy', 'TEXT');
87
+ migrateTaskStore(db);
82
88
 
83
89
  return db;
84
90
  }
@@ -90,6 +96,62 @@ function ensureTaskColumn(database, name, definition) {
90
96
  }
91
97
  }
92
98
 
99
+ function parseCommandCleanup(value) {
100
+ if (typeof value !== 'string' || value === '') return null;
101
+ try {
102
+ const parsed = JSON.parse(value);
103
+ return parsed && typeof parsed === 'object' ? parsed : null;
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ function serializeCommandCleanup(value) {
110
+ return value ? JSON.stringify(value) : null;
111
+ }
112
+
113
+ export function migrateTaskStore(database) {
114
+ ensureTaskColumn(database, 'process_group_id', 'INTEGER');
115
+ ensureTaskColumn(database, 'termination_strategy', 'TEXT');
116
+ ensureTaskColumn(database, 'command_cleanup', 'TEXT');
117
+ ensureTaskColumn(database, 'cancel_requested', 'INTEGER DEFAULT 0');
118
+ ensureTaskColumn(database, 'spawn_ownership_token', 'TEXT');
119
+ ensureTaskColumn(database, 'requested_resume_session_id', 'TEXT');
120
+ ensureTaskColumn(database, 'session_id_conflict', 'INTEGER NOT NULL DEFAULT 0');
121
+ ensureTaskColumn(database, 'resume_identity_verified', 'INTEGER NOT NULL DEFAULT 0');
122
+ database.exec(`
123
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_spawn_ownership_token
124
+ ON tasks(spawn_ownership_token)
125
+ WHERE spawn_ownership_token IS NOT NULL
126
+ `);
127
+
128
+ const version = database.pragma('user_version', { simple: true });
129
+ if (version >= TASK_STORE_SCHEMA_VERSION) return;
130
+
131
+ database.transaction(() => {
132
+ if (version < 2) {
133
+ database
134
+ .prepare(
135
+ `UPDATE tasks
136
+ SET requested_resume_session_id = COALESCE(requested_resume_session_id, session_id),
137
+ session_id = NULL`
138
+ )
139
+ .run();
140
+ }
141
+ if (version < 3) {
142
+ database.prepare('UPDATE tasks SET session_id_conflict = 0').run();
143
+ }
144
+ if (version < 4) {
145
+ database.prepare('UPDATE tasks SET resume_identity_verified = 0').run();
146
+ }
147
+ database.pragma(`user_version = ${TASK_STORE_SCHEMA_VERSION}`);
148
+ })();
149
+ }
150
+
151
+ function nullable(value) {
152
+ return value || null;
153
+ }
154
+
93
155
  export function ensureDirs() {
94
156
  if (!existsSync(TASKS_DIR)) mkdirSync(TASKS_DIR, { recursive: true });
95
157
  if (!existsSync(LOGS_DIR)) mkdirSync(LOGS_DIR, { recursive: true });
@@ -112,6 +174,9 @@ function rowToTask(row) {
112
174
  status: row.status,
113
175
  pid: row.pid,
114
176
  sessionId: row.session_id,
177
+ sessionIdConflict: Boolean(row.session_id_conflict),
178
+ requestedResumeSessionId: row.requested_resume_session_id,
179
+ resumeIdentityVerified: Boolean(row.resume_identity_verified),
115
180
  logFile: row.log_file,
116
181
  createdAt: row.created_at,
117
182
  updatedAt: row.updated_at,
@@ -124,6 +189,9 @@ function rowToTask(row) {
124
189
  attachable: Boolean(row.attachable),
125
190
  processGroupId: row.process_group_id,
126
191
  terminationStrategy: row.termination_strategy,
192
+ commandCleanup: parseCommandCleanup(row.command_cleanup),
193
+ cancelRequested: Boolean(row.cancel_requested),
194
+ spawnOwnershipToken: row.spawn_ownership_token,
127
195
  };
128
196
  }
129
197
 
@@ -149,13 +217,15 @@ export function saveTasks(tasks) {
149
217
  const database = getDb();
150
218
  const insert = database.prepare(`
151
219
  INSERT OR REPLACE INTO tasks (
152
- id, prompt, full_prompt, cwd, status, pid, session_id, log_file,
220
+ id, prompt, full_prompt, cwd, status, pid, session_id, session_id_conflict, requested_resume_session_id, resume_identity_verified, log_file,
153
221
  created_at, updated_at, exit_code, error, provider, model,
154
- schedule_id, socket_path, attachable, process_group_id, termination_strategy
222
+ schedule_id, socket_path, attachable, process_group_id, termination_strategy,
223
+ command_cleanup, cancel_requested, spawn_ownership_token
155
224
  ) VALUES (
156
- @id, @prompt, @fullPrompt, @cwd, @status, @pid, @sessionId, @logFile,
225
+ @id, @prompt, @fullPrompt, @cwd, @status, @pid, @sessionId, @sessionIdConflict, @requestedResumeSessionId, @resumeIdentityVerified, @logFile,
157
226
  @createdAt, @updatedAt, @exitCode, @error, @provider, @model,
158
- @scheduleId, @socketPath, @attachable, @processGroupId, @terminationStrategy
227
+ @scheduleId, @socketPath, @attachable, @processGroupId, @terminationStrategy,
228
+ @commandCleanup, @cancelRequested, @spawnOwnershipToken
159
229
  )
160
230
  `);
161
231
 
@@ -172,6 +242,9 @@ export function saveTasks(tasks) {
172
242
  status: task.status || 'pending',
173
243
  pid: task.pid || null,
174
244
  sessionId: task.sessionId || null,
245
+ sessionIdConflict: task.sessionIdConflict ? 1 : 0,
246
+ requestedResumeSessionId: nullable(task.requestedResumeSessionId),
247
+ resumeIdentityVerified: task.resumeIdentityVerified ? 1 : 0,
175
248
  logFile: task.logFile || null,
176
249
  createdAt: task.createdAt || new Date().toISOString(),
177
250
  updatedAt: task.updatedAt || new Date().toISOString(),
@@ -184,6 +257,9 @@ export function saveTasks(tasks) {
184
257
  attachable: task.attachable ? 1 : 0,
185
258
  processGroupId: task.processGroupId || null,
186
259
  terminationStrategy: task.terminationStrategy || null,
260
+ commandCleanup: serializeCommandCleanup(task.commandCleanup),
261
+ cancelRequested: task.cancelRequested ? 1 : 0,
262
+ spawnOwnershipToken: task.spawnOwnershipToken || null,
187
263
  });
188
264
  }
189
265
  });
@@ -214,6 +290,24 @@ export function getTask(id) {
214
290
  return rowToTask(row);
215
291
  }
216
292
 
293
+ export function getTaskBySpawnOwnershipToken(token) {
294
+ if (typeof token !== 'string' || token.length === 0) return null;
295
+ const row = getDb().prepare('SELECT * FROM tasks WHERE spawn_ownership_token = ?').get(token);
296
+ return rowToTask(row);
297
+ }
298
+
299
+ export function requestTaskCancellation(id) {
300
+ const now = new Date().toISOString();
301
+ getDb()
302
+ .prepare(
303
+ `UPDATE tasks
304
+ SET cancel_requested = 1, updated_at = ?, error = ?
305
+ WHERE id = ? AND status = 'running'`
306
+ )
307
+ .run(now, 'Cancellation requested before provider startup completed', id);
308
+ return getTask(id);
309
+ }
310
+
217
311
  /**
218
312
  * Update a task
219
313
  * @param {string} id
@@ -240,6 +334,9 @@ export function updateTask(id, updates) {
240
334
  status = @status,
241
335
  pid = @pid,
242
336
  session_id = @sessionId,
337
+ session_id_conflict = @sessionIdConflict,
338
+ requested_resume_session_id = @requestedResumeSessionId,
339
+ resume_identity_verified = @resumeIdentityVerified,
243
340
  log_file = @logFile,
244
341
  updated_at = @updatedAt,
245
342
  exit_code = @exitCode,
@@ -250,7 +347,10 @@ export function updateTask(id, updates) {
250
347
  socket_path = @socketPath,
251
348
  attachable = @attachable,
252
349
  process_group_id = @processGroupId,
253
- termination_strategy = @terminationStrategy
350
+ termination_strategy = @terminationStrategy,
351
+ command_cleanup = @commandCleanup,
352
+ cancel_requested =
353
+ CASE WHEN @hasCancelRequested = 1 THEN @cancelRequested ELSE cancel_requested END
254
354
  WHERE id = @id
255
355
  `
256
356
  )
@@ -262,6 +362,9 @@ export function updateTask(id, updates) {
262
362
  status: updated.status || 'pending',
263
363
  pid: updated.pid || null,
264
364
  sessionId: updated.sessionId || null,
365
+ sessionIdConflict: updated.sessionIdConflict ? 1 : 0,
366
+ requestedResumeSessionId: nullable(updated.requestedResumeSessionId),
367
+ resumeIdentityVerified: updated.resumeIdentityVerified ? 1 : 0,
265
368
  logFile: updated.logFile || null,
266
369
  updatedAt: updated.updatedAt,
267
370
  exitCode: updated.exitCode ?? null,
@@ -273,6 +376,9 @@ export function updateTask(id, updates) {
273
376
  attachable: updated.attachable ? 1 : 0,
274
377
  processGroupId: updated.processGroupId || null,
275
378
  terminationStrategy: updated.terminationStrategy || null,
379
+ commandCleanup: serializeCommandCleanup(updated.commandCleanup),
380
+ hasCancelRequested: Object.prototype.hasOwnProperty.call(updates, 'cancelRequested') ? 1 : 0,
381
+ cancelRequested: updated.cancelRequested ? 1 : 0,
276
382
  });
277
383
 
278
384
  return updated;
@@ -295,13 +401,15 @@ export function addTask(task) {
295
401
  .prepare(
296
402
  `
297
403
  INSERT INTO tasks (
298
- id, prompt, full_prompt, cwd, status, pid, session_id, log_file,
404
+ id, prompt, full_prompt, cwd, status, pid, session_id, session_id_conflict, requested_resume_session_id, resume_identity_verified, log_file,
299
405
  created_at, updated_at, exit_code, error, provider, model,
300
- schedule_id, socket_path, attachable, process_group_id, termination_strategy
406
+ schedule_id, socket_path, attachable, process_group_id, termination_strategy,
407
+ command_cleanup, cancel_requested, spawn_ownership_token
301
408
  ) VALUES (
302
- @id, @prompt, @fullPrompt, @cwd, @status, @pid, @sessionId, @logFile,
409
+ @id, @prompt, @fullPrompt, @cwd, @status, @pid, @sessionId, @sessionIdConflict, @requestedResumeSessionId, @resumeIdentityVerified, @logFile,
303
410
  @createdAt, @updatedAt, @exitCode, @error, @provider, @model,
304
- @scheduleId, @socketPath, @attachable, @processGroupId, @terminationStrategy
411
+ @scheduleId, @socketPath, @attachable, @processGroupId, @terminationStrategy,
412
+ @commandCleanup, @cancelRequested, @spawnOwnershipToken
305
413
  )
306
414
  `
307
415
  )
@@ -313,6 +421,9 @@ export function addTask(task) {
313
421
  status: fullTask.status || 'pending',
314
422
  pid: fullTask.pid || null,
315
423
  sessionId: fullTask.sessionId || null,
424
+ sessionIdConflict: fullTask.sessionIdConflict ? 1 : 0,
425
+ requestedResumeSessionId: nullable(fullTask.requestedResumeSessionId),
426
+ resumeIdentityVerified: fullTask.resumeIdentityVerified ? 1 : 0,
316
427
  logFile: fullTask.logFile || null,
317
428
  createdAt: fullTask.createdAt,
318
429
  updatedAt: fullTask.updatedAt,
@@ -325,6 +436,9 @@ export function addTask(task) {
325
436
  attachable: fullTask.attachable ? 1 : 0,
326
437
  processGroupId: fullTask.processGroupId || null,
327
438
  terminationStrategy: fullTask.terminationStrategy || null,
439
+ commandCleanup: serializeCommandCleanup(fullTask.commandCleanup),
440
+ cancelRequested: fullTask.cancelRequested ? 1 : 0,
441
+ spawnOwnershipToken: fullTask.spawnOwnershipToken || null,
328
442
  });
329
443
 
330
444
  return fullTask;
@@ -0,0 +1,284 @@
1
+ import { spawn } from 'child_process';
2
+ import {
3
+ detectProviderFatalError,
4
+ detectProviderStreamingModeError,
5
+ recoverProviderStructuredOutput,
6
+ supportsProviderStructuredOutputRecovery,
7
+ } from './provider-helper-runtime.js';
8
+ import { terminateProcess } from './process-termination.js';
9
+
10
+ export const COMMAND_CLEANUP_UNINITIALIZED = Symbol('command-cleanup-uninitialized');
11
+
12
+ export function spawnWatcherProvider(command, finalArgs, options) {
13
+ return spawn(command, finalArgs, {
14
+ ...options,
15
+ windowsHide: true,
16
+ });
17
+ }
18
+
19
+ export async function terminateWatcherProvider(providerProcess, options = {}) {
20
+ const pid = providerProcess?.pid;
21
+ if (!pid) return true;
22
+ const platform = options.platform || process.platform;
23
+ const terminate = options.terminateProcessFn || terminateProcess;
24
+ const terminationStrategy = platform === 'win32' ? 'process-tree' : 'process-group';
25
+ const result = await terminate(pid, {
26
+ processGroupId: platform === 'win32' ? null : pid,
27
+ terminationStrategy,
28
+ });
29
+ if (terminationStrategy === 'process-tree' && result.alreadyDead && !options.exitObserved) {
30
+ return false;
31
+ }
32
+ return result.terminated;
33
+ }
34
+
35
+ function splitBufferLines(buffer, chunk) {
36
+ const nextBuffer = buffer + chunk;
37
+ const lines = nextBuffer.split('\n');
38
+ return { lines: lines.slice(0, -1), remaining: lines.at(-1) || '' };
39
+ }
40
+
41
+ export function resolveWatcherCommand(config, commandSpec, fallbackArgs, normalizeProviderName) {
42
+ return {
43
+ providerName: normalizeProviderName(config.provider || 'claude'),
44
+ env: { ...process.env, ...(commandSpec.env || {}) },
45
+ command: commandSpec.binary,
46
+ finalArgs: [...(commandSpec.args || fallbackArgs)],
47
+ };
48
+ }
49
+
50
+ export async function completeWatcherTask({
51
+ taskId,
52
+ completion,
53
+ commandCleanup,
54
+ terminateProvider,
55
+ updateTask,
56
+ emergencyLog,
57
+ terminalUpdates = {},
58
+ }) {
59
+ let providerTerminal = false;
60
+ try {
61
+ providerTerminal = await terminateProvider();
62
+ } catch (error) {
63
+ emergencyLog(`[${Date.now()}][CLEANUP] Provider termination check failed: ${error.message}\n`);
64
+ }
65
+ if (!providerTerminal) {
66
+ emergencyLog(
67
+ `[${Date.now()}][CLEANUP] Provider termination boundary is still live; preserving command cleanup paths.\n`
68
+ );
69
+ try {
70
+ await updateTask(taskId, {
71
+ status: 'running',
72
+ error: completion.error
73
+ ? `${completion.error}; provider termination could not be confirmed`
74
+ : 'Provider termination could not be confirmed; retry and cleanup remain blocked',
75
+ });
76
+ } catch (error) {
77
+ emergencyLog(`[${Date.now()}][ERROR] Failed to preserve task ownership: ${error.message}\n`);
78
+ }
79
+ return false;
80
+ }
81
+
82
+ let cleanupSucceeded = false;
83
+ if (commandCleanup === COMMAND_CLEANUP_UNINITIALIZED) {
84
+ emergencyLog(
85
+ `[${Date.now()}][CLEANUP] Command cleanup ownership was not initialized; preserving the persisted receipt.\n`
86
+ );
87
+ } else if (commandCleanup?.run) {
88
+ try {
89
+ cleanupSucceeded = await commandCleanup.run();
90
+ } catch (error) {
91
+ emergencyLog(`[${Date.now()}][CLEANUP] Command cleanup failed: ${error.message}\n`);
92
+ }
93
+ }
94
+ try {
95
+ await updateTask(taskId, {
96
+ status: completion.status,
97
+ pid: null,
98
+ processGroupId: null,
99
+ exitCode: completion.resolvedCode,
100
+ error: completion.error,
101
+ cancelRequested: false,
102
+ ...terminalUpdates,
103
+ ...(completion.terminalUpdates || {}),
104
+ ...(cleanupSucceeded ? { commandCleanup: null } : {}),
105
+ });
106
+ } catch (error) {
107
+ emergencyLog(`[${Date.now()}][ERROR] Failed to update task status: ${error.message}\n`);
108
+ }
109
+ return true;
110
+ }
111
+
112
+ export async function completePendingWatcherCancellation({
113
+ taskId,
114
+ getTask,
115
+ ...completionOptions
116
+ }) {
117
+ if (!getTask(taskId)?.cancelRequested) return false;
118
+ await completeWatcherTask({
119
+ taskId,
120
+ ...completionOptions,
121
+ completion: {
122
+ status: 'killed',
123
+ resolvedCode: 143,
124
+ error: 'Cancellation requested before provider startup completed',
125
+ },
126
+ });
127
+ return true;
128
+ }
129
+
130
+ export function completeWatcherFailure({ error, source, ...completionOptions }) {
131
+ const errorMessage = error instanceof Error ? error.stack || error.message : String(error);
132
+ completionOptions.emergencyLog(`\n[${Date.now()}][CRASH] ${source}: ${errorMessage}\n`);
133
+ return completeWatcherTask({
134
+ ...completionOptions,
135
+ completion: {
136
+ status: 'failed',
137
+ resolvedCode: 1,
138
+ error: `${source}: ${errorMessage}`,
139
+ },
140
+ });
141
+ }
142
+
143
+ export function createWatcherOutputRuntime({
144
+ config,
145
+ providerName,
146
+ log,
147
+ stopProvider,
148
+ providerSessionCapture = null,
149
+ }) {
150
+ const enableRecovery = supportsProviderStructuredOutputRecovery(providerName);
151
+ const silentJsonMode =
152
+ config.outputFormat === 'json' &&
153
+ config.jsonSchema &&
154
+ config.silentJsonOutput &&
155
+ enableRecovery;
156
+ let finalResultJson = null;
157
+ let streamingModeError = null;
158
+ let fatalError = null;
159
+ const captureProviderSession = providerSessionCapture?.captureLine || (() => {});
160
+
161
+ function maybeHandleFatalError(line, timestamp) {
162
+ if (fatalError) return false;
163
+ const detected = detectProviderFatalError(providerName, line);
164
+ if (!detected) return false;
165
+ fatalError = detected;
166
+ if (silentJsonMode) log(`[${timestamp}]${line}\n`);
167
+ log(`[${timestamp}][FATAL] ${detected}\n`);
168
+ stopProvider(timestamp);
169
+ return true;
170
+ }
171
+
172
+ function captureStreamingError(line, timestamp) {
173
+ const detectedError = detectProviderStreamingModeError(providerName, line);
174
+ if (!detectedError) return false;
175
+ streamingModeError = { ...detectedError, timestamp };
176
+ return true;
177
+ }
178
+
179
+ function maybeCaptureStructuredOutput(line) {
180
+ try {
181
+ const json = JSON.parse(line);
182
+ if (json.structured_output) finalResultJson = line;
183
+ } catch {
184
+ // Not JSON, skip.
185
+ }
186
+ }
187
+
188
+ function handleOutputLine(line, timestamp) {
189
+ captureProviderSession(line);
190
+ if (silentJsonMode && !line.trim()) return;
191
+ maybeHandleFatalError(line, timestamp);
192
+ if (captureStreamingError(line, timestamp)) return;
193
+ if (silentJsonMode) {
194
+ maybeCaptureStructuredOutput(line);
195
+ } else {
196
+ log(`[${timestamp}]${line}\n`);
197
+ }
198
+ }
199
+
200
+ function consumeOutput(buffer, chunk) {
201
+ const timestamp = Date.now();
202
+ const { lines, remaining } = splitBufferLines(buffer, chunk.toString());
203
+ for (const line of lines) handleOutputLine(line, timestamp);
204
+ return remaining;
205
+ }
206
+
207
+ function consumeStderr(buffer, chunk) {
208
+ const timestamp = Date.now();
209
+ const { lines, remaining } = splitBufferLines(buffer, chunk.toString());
210
+ for (const line of lines) log(`[${timestamp}]${line}\n`);
211
+ return remaining;
212
+ }
213
+
214
+ function flushOutput(buffer, timestamp) {
215
+ if (!buffer.trim()) return;
216
+ captureProviderSession(buffer);
217
+ if (!enableRecovery) {
218
+ if (!silentJsonMode) log(`[${timestamp}]${buffer}\n`);
219
+ return;
220
+ }
221
+ maybeHandleFatalError(buffer, timestamp);
222
+ if (captureStreamingError(buffer, timestamp)) return;
223
+ if (silentJsonMode) {
224
+ maybeCaptureStructuredOutput(buffer);
225
+ } else {
226
+ log(`[${timestamp}]${buffer}\n`);
227
+ }
228
+ }
229
+
230
+ function flushStderr(buffer, timestamp) {
231
+ if (!buffer.trim()) return;
232
+ maybeHandleFatalError(buffer, timestamp);
233
+ log(`[${timestamp}]${buffer}\n`);
234
+ }
235
+
236
+ function attemptRecovery(code, timestamp) {
237
+ if (!(code !== 0 && streamingModeError?.sessionId)) return null;
238
+ const recovered = recoverProviderStructuredOutput(providerName, streamingModeError.sessionId);
239
+ if (recovered?.payload) {
240
+ const recoveredLine = JSON.stringify(recovered.payload);
241
+ if (silentJsonMode) {
242
+ finalResultJson = recoveredLine;
243
+ } else {
244
+ log(`[${timestamp}]${recoveredLine}\n`);
245
+ }
246
+ } else if (streamingModeError.line) {
247
+ const prefix = silentJsonMode ? '' : `[${streamingModeError.timestamp}]`;
248
+ log(`${prefix}${streamingModeError.line}\n`);
249
+ }
250
+ return recovered;
251
+ }
252
+
253
+ function complete({ code, signal, outputBuffer, stderrBuffer = null }) {
254
+ const timestamp = Date.now();
255
+ flushOutput(outputBuffer, timestamp);
256
+ if (stderrBuffer !== null) flushStderr(stderrBuffer, timestamp);
257
+ const recovered = attemptRecovery(code, timestamp);
258
+ const sessionIdentityError = providerSessionCapture?.getCompletionError() || null;
259
+ if (silentJsonMode && finalResultJson) log(`${finalResultJson}\n`);
260
+ if (config.outputFormat !== 'json') {
261
+ log(`\n${'='.repeat(50)}\n`);
262
+ log(`Finished: ${new Date().toISOString()}\n`);
263
+ log(`Exit code: ${code}, Signal: ${signal}\n`);
264
+ }
265
+ let resolvedCode = code;
266
+ if (recovered?.payload) {
267
+ resolvedCode = 0;
268
+ }
269
+ if (fatalError || sessionIdentityError) {
270
+ resolvedCode = 1;
271
+ }
272
+ return {
273
+ resolvedCode,
274
+ status: resolvedCode === 0 ? 'completed' : 'failed',
275
+ error:
276
+ fatalError ||
277
+ sessionIdentityError ||
278
+ (resolvedCode !== 0 && signal ? `Killed by ${signal}` : null),
279
+ terminalUpdates: providerSessionCapture?.getCompletionUpdate(resolvedCode) || {},
280
+ };
281
+ }
282
+
283
+ return { complete, consumeOutput, consumeStderr };
284
+ }