@the-open-engine/zeroshot 6.6.0 → 6.7.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 (68) hide show
  1. package/bin/zeroshot-cluster-worker.js +16 -0
  2. package/cli/commands/providers.js +4 -1
  3. package/cli/index.js +153 -24
  4. package/docs/openengine-cluster-protocol/v1/legacy-worker.md +117 -0
  5. package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
  6. package/lib/agent-cli-provider/adapters/claude.js +23 -10
  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 +4 -0
  10. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  11. package/lib/agent-cli-provider/adapters/common.d.ts.map +1 -1
  12. package/lib/agent-cli-provider/adapters/common.js +2 -1
  13. package/lib/agent-cli-provider/adapters/common.js.map +1 -1
  14. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
  15. package/lib/agent-cli-provider/contract-options.js +2 -1
  16. package/lib/agent-cli-provider/contract-options.js.map +1 -1
  17. package/lib/agent-cli-provider/index.d.ts +1 -1
  18. package/lib/agent-cli-provider/index.d.ts.map +1 -1
  19. package/lib/agent-cli-provider/index.js.map +1 -1
  20. package/lib/agent-cli-provider/provider-registry.d.ts +1 -1
  21. package/lib/agent-cli-provider/provider-registry.js +1 -1
  22. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  23. package/lib/agent-cli-provider/single-agent-runtime.js +6 -2
  24. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  25. package/lib/agent-cli-provider/types.d.ts +3 -1
  26. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  27. package/lib/agent-cli-provider/types.js.map +1 -1
  28. package/lib/cluster-worker/contracts.js +194 -0
  29. package/lib/cluster-worker/engine-adapter.js +300 -0
  30. package/lib/cluster-worker/executable.js +229 -0
  31. package/lib/cluster-worker/index.d.ts +227 -0
  32. package/lib/cluster-worker/index.js +294 -0
  33. package/lib/cluster-worker/object-utils.js +17 -0
  34. package/lib/cluster-worker/process-stdio.js +37 -0
  35. package/lib/cluster-worker/profiles.js +81 -0
  36. package/lib/cluster-worker/runtime-engine.js +50 -0
  37. package/lib/cluster-worker/runtime-support.js +298 -0
  38. package/lib/cluster-worker/state-machine.js +147 -0
  39. package/lib/cluster-worker/terminal-normalizer.js +95 -0
  40. package/lib/cluster-worker/worker-internals.js +20 -0
  41. package/lib/detached-startup.js +127 -11
  42. package/lib/process-liveness.js +26 -0
  43. package/lib/settings.js +1 -0
  44. package/lib/start-cluster.js +93 -18
  45. package/package.json +8 -2
  46. package/protocol/openengine-cluster/v1/worker.schema.json +1174 -0
  47. package/scripts/assert-release-published.js +65 -11
  48. package/scripts/run-lint-staged-no-stash.js +68 -0
  49. package/src/agent/agent-lifecycle.js +368 -91
  50. package/src/agent/agent-task-executor.js +384 -101
  51. package/src/agent-cli-provider/adapters/claude.ts +29 -11
  52. package/src/agent-cli-provider/adapters/codex.ts +4 -0
  53. package/src/agent-cli-provider/adapters/common.ts +2 -1
  54. package/src/agent-cli-provider/contract-options.ts +2 -1
  55. package/src/agent-cli-provider/index.ts +1 -0
  56. package/src/agent-cli-provider/provider-registry.ts +1 -1
  57. package/src/agent-cli-provider/single-agent-runtime.ts +8 -2
  58. package/src/agent-cli-provider/types.ts +3 -1
  59. package/src/agent-wrapper.js +9 -2
  60. package/src/config-validator.js +10 -11
  61. package/src/orchestrator.js +482 -67
  62. package/task-lib/attachable-watcher.js +10 -1
  63. package/task-lib/commands/kill.js +34 -9
  64. package/task-lib/commands/status.js +13 -3
  65. package/task-lib/process-termination.js +202 -0
  66. package/task-lib/runner.js +8 -20
  67. package/task-lib/store.js +28 -6
  68. package/task-lib/watcher.js +14 -2
@@ -319,6 +319,8 @@ server.on('exit', async ({ exitCode, signal }) => {
319
319
  try {
320
320
  await updateTask(taskId, {
321
321
  status,
322
+ pid: null,
323
+ processGroupId: null,
322
324
  exitCode: resolvedCode,
323
325
  error: fatalError || (resolvedCode !== 0 && signal ? `Killed by ${signal}` : null),
324
326
  socketPath: null,
@@ -336,7 +338,12 @@ server.on('error', async (err) => {
336
338
  log(`\nError: ${err.message}\n`);
337
339
  await cleanupCommandSpec();
338
340
  try {
339
- await updateTask(taskId, { status: 'failed', error: err.message });
341
+ await updateTask(taskId, {
342
+ status: 'failed',
343
+ pid: null,
344
+ processGroupId: null,
345
+ error: err.message,
346
+ });
340
347
  } catch (updateError) {
341
348
  log(`[${Date.now()}][ERROR] Failed to update task status: ${updateError.message}\n`);
342
349
  }
@@ -358,6 +365,8 @@ try {
358
365
  pid: server.pid,
359
366
  socketPath,
360
367
  attachable: true,
368
+ processGroupId: process.platform === 'win32' ? null : server.pid,
369
+ terminationStrategy: process.platform === 'win32' ? 'process-tree' : 'process-group',
361
370
  });
362
371
 
363
372
  log(`[${Date.now()}][SYSTEM] Started with PTY (attachable)\n`);
@@ -1,8 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import { getTask, updateTask } from '../store.js';
3
- import { killTask as killProcess, isProcessRunning } from '../runner.js';
3
+ import { terminateProcess } from '../runner.js';
4
4
 
5
- export function killTaskCommand(taskId) {
5
+ export async function killTaskCommand(taskId, options = {}) {
6
6
  const task = getTask(taskId);
7
7
 
8
8
  if (!task) {
@@ -15,18 +15,43 @@ export function killTaskCommand(taskId) {
15
15
  return;
16
16
  }
17
17
 
18
- if (!isProcessRunning(task.pid)) {
18
+ const terminationOptions = {
19
+ ...options,
20
+ processGroupId: task.processGroupId,
21
+ terminationStrategy: task.terminationStrategy || 'process',
22
+ };
23
+
24
+ const result = await terminateProcess(task.pid, terminationOptions);
25
+
26
+ if (result.terminated && result.alreadyDead) {
19
27
  console.log(chalk.yellow('Process already dead, updating status...'));
20
- updateTask(taskId, { status: 'stale', error: 'Process died unexpectedly' });
28
+ updateTask(taskId, {
29
+ status: 'stale',
30
+ pid: null,
31
+ processGroupId: null,
32
+ error: 'Process died unexpectedly',
33
+ });
21
34
  return;
22
35
  }
23
36
 
24
- const killed = killProcess(task.pid);
25
-
26
- if (killed) {
27
- console.log(chalk.green(`✓ Sent SIGTERM to task ${taskId} (PID: ${task.pid})`));
28
- updateTask(taskId, { status: 'killed', error: 'Killed by user' });
37
+ if (result.terminated) {
38
+ const suffix = result.escalated ? ' after SIGKILL escalation' : ' with SIGTERM';
39
+ console.log(chalk.green(`✓ Killed task ${taskId} (PID: ${task.pid})${suffix}`));
40
+ if (result.degraded) {
41
+ console.log(chalk.yellow(`Warning: ${result.degradedReason}`));
42
+ }
43
+ updateTask(taskId, {
44
+ status: 'killed',
45
+ pid: null,
46
+ processGroupId: null,
47
+ exitCode: result.escalated ? 137 : 143,
48
+ error: result.escalated ? 'Killed by user after SIGKILL escalation' : 'Killed by user',
49
+ });
29
50
  } else {
30
51
  console.log(chalk.red(`Failed to kill task ${taskId}`));
52
+ updateTask(taskId, {
53
+ error: result.error || 'Process termination failed',
54
+ });
55
+ process.exitCode = 1;
31
56
  }
32
57
  }
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { getTask } from '../store.js';
3
- import { isProcessRunning } from '../runner.js';
3
+ import { isOwnedProcessTreeRunning } from '../runner.js';
4
4
 
5
5
  export function showStatus(taskId) {
6
6
  const task = getTask(taskId);
@@ -12,8 +12,18 @@ export function showStatus(taskId) {
12
12
 
13
13
  // Verify running status
14
14
  let status = task.status;
15
- if (status === 'running' && !isProcessRunning(task.pid)) {
16
- status = 'stale (process died)';
15
+ if (status === 'running') {
16
+ try {
17
+ const running = isOwnedProcessTreeRunning(task.pid, {
18
+ processGroupId: task.processGroupId,
19
+ terminationStrategy: task.terminationStrategy || 'process',
20
+ });
21
+ if (!running) {
22
+ status = 'stale (process died)';
23
+ }
24
+ } catch (error) {
25
+ status = `stale (invalid process ownership: ${error.message})`;
26
+ }
17
27
  }
18
28
 
19
29
  const statusColor =
@@ -0,0 +1,202 @@
1
+ import { execFile } from 'child_process';
2
+ import { promisify } from 'util';
3
+
4
+ const execFileAsync = promisify(execFile);
5
+
6
+ export function isProcessRunning(pid) {
7
+ if (!pid) return false;
8
+ try {
9
+ process.kill(pid, 0);
10
+ return true;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ export function killTask(pid) {
17
+ if (!pid) return false;
18
+ try {
19
+ process.kill(pid, 'SIGTERM');
20
+ return true;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ function sleep(ms) {
27
+ return new Promise((resolve) => setTimeout(resolve, ms));
28
+ }
29
+
30
+ function normalizeTerminationOwnership(pid, options) {
31
+ const terminationStrategy = options.terminationStrategy || 'process';
32
+ const processGroupId = Number(options.processGroupId) || null;
33
+
34
+ if (terminationStrategy === 'process-group') {
35
+ if (process.platform === 'win32') {
36
+ throw new Error(
37
+ 'Process-group termination is unavailable on Windows; use terminationStrategy "process-tree"'
38
+ );
39
+ }
40
+ if (!processGroupId || processGroupId !== pid) {
41
+ throw new Error(
42
+ `Refusing process-group termination for PID ${pid}: owned processGroupId must equal the provider root PID`
43
+ );
44
+ }
45
+ }
46
+
47
+ if (terminationStrategy === 'process-tree' && process.platform !== 'win32') {
48
+ throw new Error(
49
+ `Process-tree termination is only supported on Windows; use terminationStrategy "process-group" on ${process.platform}`
50
+ );
51
+ }
52
+
53
+ if (!['process', 'process-group', 'process-tree'].includes(terminationStrategy)) {
54
+ throw new Error(`Unsupported termination strategy: ${terminationStrategy}`);
55
+ }
56
+
57
+ return { terminationStrategy, processGroupId };
58
+ }
59
+
60
+ function isProcessGroupRunning(processGroupId) {
61
+ try {
62
+ process.kill(-processGroupId, 0);
63
+ return true;
64
+ } catch (error) {
65
+ return error?.code === 'EPERM';
66
+ }
67
+ }
68
+
69
+ export function isOwnedProcessTreeRunning(pid, options = {}) {
70
+ if (!pid) return false;
71
+ const ownership = normalizeTerminationOwnership(pid, options);
72
+ if (ownership.terminationStrategy === 'process-group') {
73
+ return isProcessGroupRunning(ownership.processGroupId);
74
+ }
75
+ return isProcessRunning(pid);
76
+ }
77
+
78
+ async function signalWindowsProcessTree(pid, force) {
79
+ const args = ['/PID', String(pid), '/T'];
80
+ if (force) args.push('/F');
81
+ await execFileAsync('taskkill', args, { windowsHide: true });
82
+ }
83
+
84
+ async function signalOwnedProcessTree(pid, signal, ownership) {
85
+ if (ownership.terminationStrategy === 'process-group') {
86
+ process.kill(-ownership.processGroupId, signal);
87
+ return;
88
+ }
89
+ if (ownership.terminationStrategy === 'process-tree') {
90
+ await signalWindowsProcessTree(pid, signal === 'SIGKILL');
91
+ return;
92
+ }
93
+ process.kill(pid, signal);
94
+ }
95
+
96
+ async function waitForOwnedProcessTreeExit(pid, ownership, timeoutMs, pollMs) {
97
+ const deadline = Date.now() + timeoutMs;
98
+ const options = {
99
+ terminationStrategy: ownership.terminationStrategy,
100
+ processGroupId: ownership.processGroupId,
101
+ };
102
+ while (Date.now() < deadline) {
103
+ if (!isOwnedProcessTreeRunning(pid, options)) return true;
104
+ await sleep(pollMs);
105
+ }
106
+ return !isOwnedProcessTreeRunning(pid, options);
107
+ }
108
+
109
+ function terminationResult(ownership, overrides = {}) {
110
+ const degraded = ownership.terminationStrategy === 'process';
111
+ return {
112
+ terminated: false,
113
+ alreadyDead: false,
114
+ escalated: false,
115
+ signal: null,
116
+ scope: ownership.terminationStrategy,
117
+ degraded,
118
+ degradedReason: degraded
119
+ ? 'Task has no process-tree ownership metadata; only the provider root PID can be terminated'
120
+ : null,
121
+ ...overrides,
122
+ };
123
+ }
124
+
125
+ async function signalAndWait(pid, ownership, signal, timeoutMs, pollMs) {
126
+ const escalated = signal === 'SIGKILL';
127
+ try {
128
+ await signalOwnedProcessTree(pid, signal, ownership);
129
+ } catch (error) {
130
+ if (
131
+ !isOwnedProcessTreeRunning(pid, {
132
+ terminationStrategy: ownership.terminationStrategy,
133
+ processGroupId: ownership.processGroupId,
134
+ })
135
+ ) {
136
+ return terminationResult(ownership, {
137
+ terminated: true,
138
+ alreadyDead: signal === 'SIGTERM',
139
+ escalated,
140
+ signal: escalated ? signal : null,
141
+ });
142
+ }
143
+ return terminationResult(ownership, {
144
+ escalated,
145
+ signal,
146
+ error: error.message,
147
+ });
148
+ }
149
+
150
+ const terminated = await waitForOwnedProcessTreeExit(pid, ownership, timeoutMs, pollMs);
151
+ return terminationResult(ownership, {
152
+ terminated,
153
+ escalated,
154
+ signal,
155
+ error:
156
+ terminated || !escalated
157
+ ? null
158
+ : `Owned ${ownership.terminationStrategy} for PID ${pid} survived ${signal}`,
159
+ });
160
+ }
161
+
162
+ /**
163
+ * Terminate an owned provider process tree. Watchers create a dedicated process
164
+ * group on POSIX and persist that ownership boundary; Windows uses taskkill /T.
165
+ * Legacy tasks without ownership metadata fall back to root-only termination
166
+ * and report the degraded scope explicitly.
167
+ */
168
+ export async function terminateProcess(pid, options = {}) {
169
+ let ownership;
170
+ try {
171
+ ownership = normalizeTerminationOwnership(pid, options);
172
+ } catch (error) {
173
+ return {
174
+ terminated: false,
175
+ alreadyDead: false,
176
+ escalated: false,
177
+ signal: null,
178
+ error: error.message,
179
+ };
180
+ }
181
+
182
+ if (!isOwnedProcessTreeRunning(pid, options)) {
183
+ return terminationResult(ownership, { terminated: true, alreadyDead: true });
184
+ }
185
+
186
+ const graceful = await signalAndWait(
187
+ pid,
188
+ ownership,
189
+ 'SIGTERM',
190
+ options.graceMs ?? 5000,
191
+ options.pollMs ?? 50
192
+ );
193
+ if (graceful.terminated || graceful.error) return graceful;
194
+
195
+ return signalAndWait(
196
+ pid,
197
+ ownership,
198
+ 'SIGKILL',
199
+ options.hardKillWaitMs ?? 1000,
200
+ options.pollMs ?? 50
201
+ );
202
+ }
@@ -7,6 +7,12 @@ import { createRequire } from 'module';
7
7
 
8
8
  const require = createRequire(import.meta.url);
9
9
  const { prepareSingleAgentProviderCommand } = require('./provider-helper-runtime.js');
10
+ export {
11
+ isOwnedProcessTreeRunning,
12
+ isProcessRunning,
13
+ killTask,
14
+ terminateProcess,
15
+ } from './process-termination.js';
10
16
 
11
17
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
18
 
@@ -136,6 +142,8 @@ function buildTaskRecord({ id, prompt, cwd, options, logFile, providerName, mode
136
142
  // Attach support
137
143
  socketPath: null,
138
144
  attachable: false,
145
+ processGroupId: null,
146
+ terminationStrategy: null,
139
147
  };
140
148
  }
141
149
 
@@ -176,23 +184,3 @@ function spawnWatcher({ watcherScript, id, cwd, logFile, finalArgs, watcherConfi
176
184
  watcher.unref();
177
185
  watcher.disconnect(); // Close IPC channel so parent can exit
178
186
  }
179
-
180
- export function isProcessRunning(pid) {
181
- if (!pid) return false;
182
- try {
183
- process.kill(pid, 0);
184
- return true;
185
- } catch {
186
- return false;
187
- }
188
- }
189
-
190
- export function killTask(pid) {
191
- if (!pid) return false;
192
- try {
193
- process.kill(pid, 'SIGTERM');
194
- return true;
195
- } catch {
196
- return false;
197
- }
198
- }
package/task-lib/store.js CHANGED
@@ -50,7 +50,9 @@ function getDb() {
50
50
  model TEXT,
51
51
  schedule_id TEXT,
52
52
  socket_path TEXT,
53
- attachable INTEGER DEFAULT 0
53
+ attachable INTEGER DEFAULT 0,
54
+ process_group_id INTEGER,
55
+ termination_strategy TEXT
54
56
  );
55
57
 
56
58
  CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
@@ -75,9 +77,19 @@ function getDb() {
75
77
  CREATE INDEX IF NOT EXISTS idx_schedules_enabled ON schedules(enabled);
76
78
  `);
77
79
 
80
+ ensureTaskColumn(db, 'process_group_id', 'INTEGER');
81
+ ensureTaskColumn(db, 'termination_strategy', 'TEXT');
82
+
78
83
  return db;
79
84
  }
80
85
 
86
+ function ensureTaskColumn(database, name, definition) {
87
+ const columns = database.pragma('table_info(tasks)');
88
+ if (!columns.some((column) => column.name === name)) {
89
+ database.exec(`ALTER TABLE tasks ADD COLUMN ${name} ${definition}`);
90
+ }
91
+ }
92
+
81
93
  export function ensureDirs() {
82
94
  if (!existsSync(TASKS_DIR)) mkdirSync(TASKS_DIR, { recursive: true });
83
95
  if (!existsSync(LOGS_DIR)) mkdirSync(LOGS_DIR, { recursive: true });
@@ -110,6 +122,8 @@ function rowToTask(row) {
110
122
  scheduleId: row.schedule_id,
111
123
  socketPath: row.socket_path,
112
124
  attachable: Boolean(row.attachable),
125
+ processGroupId: row.process_group_id,
126
+ terminationStrategy: row.termination_strategy,
113
127
  };
114
128
  }
115
129
 
@@ -137,11 +151,11 @@ export function saveTasks(tasks) {
137
151
  INSERT OR REPLACE INTO tasks (
138
152
  id, prompt, full_prompt, cwd, status, pid, session_id, log_file,
139
153
  created_at, updated_at, exit_code, error, provider, model,
140
- schedule_id, socket_path, attachable
154
+ schedule_id, socket_path, attachable, process_group_id, termination_strategy
141
155
  ) VALUES (
142
156
  @id, @prompt, @fullPrompt, @cwd, @status, @pid, @sessionId, @logFile,
143
157
  @createdAt, @updatedAt, @exitCode, @error, @provider, @model,
144
- @scheduleId, @socketPath, @attachable
158
+ @scheduleId, @socketPath, @attachable, @processGroupId, @terminationStrategy
145
159
  )
146
160
  `);
147
161
 
@@ -168,6 +182,8 @@ export function saveTasks(tasks) {
168
182
  scheduleId: task.scheduleId || null,
169
183
  socketPath: task.socketPath || null,
170
184
  attachable: task.attachable ? 1 : 0,
185
+ processGroupId: task.processGroupId || null,
186
+ terminationStrategy: task.terminationStrategy || null,
171
187
  });
172
188
  }
173
189
  });
@@ -232,7 +248,9 @@ export function updateTask(id, updates) {
232
248
  model = @model,
233
249
  schedule_id = @scheduleId,
234
250
  socket_path = @socketPath,
235
- attachable = @attachable
251
+ attachable = @attachable,
252
+ process_group_id = @processGroupId,
253
+ termination_strategy = @terminationStrategy
236
254
  WHERE id = @id
237
255
  `
238
256
  )
@@ -253,6 +271,8 @@ export function updateTask(id, updates) {
253
271
  scheduleId: updated.scheduleId || null,
254
272
  socketPath: updated.socketPath || null,
255
273
  attachable: updated.attachable ? 1 : 0,
274
+ processGroupId: updated.processGroupId || null,
275
+ terminationStrategy: updated.terminationStrategy || null,
256
276
  });
257
277
 
258
278
  return updated;
@@ -277,11 +297,11 @@ export function addTask(task) {
277
297
  INSERT INTO tasks (
278
298
  id, prompt, full_prompt, cwd, status, pid, session_id, log_file,
279
299
  created_at, updated_at, exit_code, error, provider, model,
280
- schedule_id, socket_path, attachable
300
+ schedule_id, socket_path, attachable, process_group_id, termination_strategy
281
301
  ) VALUES (
282
302
  @id, @prompt, @fullPrompt, @cwd, @status, @pid, @sessionId, @logFile,
283
303
  @createdAt, @updatedAt, @exitCode, @error, @provider, @model,
284
- @scheduleId, @socketPath, @attachable
304
+ @scheduleId, @socketPath, @attachable, @processGroupId, @terminationStrategy
285
305
  )
286
306
  `
287
307
  )
@@ -303,6 +323,8 @@ export function addTask(task) {
303
323
  scheduleId: fullTask.scheduleId || null,
304
324
  socketPath: fullTask.socketPath || null,
305
325
  attachable: fullTask.attachable ? 1 : 0,
326
+ processGroupId: fullTask.processGroupId || null,
327
+ terminationStrategy: fullTask.terminationStrategy || null,
306
328
  });
307
329
 
308
330
  return fullTask;
@@ -45,10 +45,15 @@ const child = spawn(command, finalArgs, {
45
45
  cwd: commandSpec.cwd || cwd,
46
46
  env,
47
47
  stdio: ['ignore', 'pipe', 'pipe'],
48
+ detached: process.platform !== 'win32',
48
49
  windowsHide: true,
49
50
  });
50
51
 
51
- updateTask(taskId, { pid: child.pid });
52
+ updateTask(taskId, {
53
+ pid: child.pid,
54
+ processGroupId: process.platform === 'win32' ? null : child.pid,
55
+ terminationStrategy: process.platform === 'win32' ? 'process-tree' : 'process-group',
56
+ });
52
57
 
53
58
  const silentJsonMode =
54
59
  config.outputFormat === 'json' &&
@@ -285,6 +290,8 @@ child.on('close', async (code, signal) => {
285
290
  try {
286
291
  await updateTask(taskId, {
287
292
  status,
293
+ pid: null,
294
+ processGroupId: null,
288
295
  exitCode: resolvedCode,
289
296
  error: fatalError || (resolvedCode !== 0 && signal ? `Killed by ${signal}` : null),
290
297
  });
@@ -298,7 +305,12 @@ child.on('error', async (err) => {
298
305
  log(`\nError: ${err.message}\n`);
299
306
  cleanupCommandSpecSync();
300
307
  try {
301
- await updateTask(taskId, { status: 'failed', error: err.message });
308
+ await updateTask(taskId, {
309
+ status: 'failed',
310
+ pid: null,
311
+ processGroupId: null,
312
+ error: err.message,
313
+ });
302
314
  } catch (updateError) {
303
315
  log(`[${Date.now()}][ERROR] Failed to update task status: ${updateError.message}\n`);
304
316
  }