@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.
- package/cli/commands/inspect.js +1 -0
- package/cli/index.js +17 -1
- package/cluster-hooks/block-ask-user-question.py +2 -3
- package/cluster-hooks/block-dangerous-git.py +2 -3
- package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/claude.js +57 -3
- package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.js +32 -2
- package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
- package/lib/agent-cli-provider/adapters/common.js +1 -1
- package/lib/agent-cli-provider/adapters/common.js.map +1 -1
- package/lib/agent-cli-provider/adapters/index.d.ts +1 -0
- package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/index.js +8 -0
- package/lib/agent-cli-provider/adapters/index.js.map +1 -1
- package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-options.js +3 -0
- package/lib/agent-cli-provider/contract-options.js.map +1 -1
- package/lib/agent-cli-provider/index.d.ts +1 -1
- package/lib/agent-cli-provider/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/index.js +2 -1
- package/lib/agent-cli-provider/index.js.map +1 -1
- package/lib/agent-cli-provider/provider-registry.d.ts +9 -0
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
- package/lib/agent-cli-provider/provider-registry.js +3 -0
- package/lib/agent-cli-provider/provider-registry.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +10 -2
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/package.json +1 -1
- package/src/agent/agent-context-builder.js +90 -4
- package/src/agent/agent-context-sources.js +20 -2
- package/src/agent/agent-lifecycle.js +43 -8
- package/src/agent/agent-task-executor.js +557 -314
- package/src/agent/guidance-queue.js +29 -7
- package/src/agent/provider-session.js +277 -0
- package/src/agent-cli-provider/adapters/claude.ts +64 -4
- package/src/agent-cli-provider/adapters/codex.ts +47 -3
- package/src/agent-cli-provider/adapters/common.ts +1 -1
- package/src/agent-cli-provider/adapters/index.ts +10 -0
- package/src/agent-cli-provider/contract-options.ts +7 -0
- package/src/agent-cli-provider/index.ts +1 -0
- package/src/agent-cli-provider/provider-registry.ts +13 -1
- package/src/agent-cli-provider/types.ts +13 -6
- package/src/agent-wrapper.js +44 -8
- package/src/claude-task-runner.js +153 -44
- package/src/ledger-sequence.js +63 -0
- package/src/ledger.js +114 -29
- package/src/orchestrator.js +40 -2
- package/src/task-spawn-cleanup-ownership.js +82 -0
- package/src/worktree-claude-config.js +193 -85
- package/task-lib/attachable-watcher.js +101 -253
- package/task-lib/command-spec-cleanup.js +219 -0
- package/task-lib/commands/clean.js +35 -5
- package/task-lib/commands/get-task-id-by-spawn-token.js +10 -0
- package/task-lib/commands/kill.js +117 -4
- package/task-lib/commands/resume.js +29 -6
- package/task-lib/commands/status.js +4 -0
- package/task-lib/provider-helper-runtime.js +1 -0
- package/task-lib/provider-session-capture.js +138 -0
- package/task-lib/runner.js +70 -5
- package/task-lib/store.js +126 -12
- package/task-lib/watcher-output-runtime.js +284 -0
- 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
|
-
|
|
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
|
+
}
|