@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.
- package/bin/zeroshot-cluster-worker.js +16 -0
- package/cli/commands/providers.js +4 -1
- package/cli/index.js +153 -24
- package/docs/openengine-cluster-protocol/v1/legacy-worker.md +117 -0
- package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/claude.js +23 -10
- 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 +4 -0
- package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
- package/lib/agent-cli-provider/adapters/common.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/common.js +2 -1
- package/lib/agent-cli-provider/adapters/common.js.map +1 -1
- package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-options.js +2 -1
- 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.map +1 -1
- package/lib/agent-cli-provider/provider-registry.d.ts +1 -1
- package/lib/agent-cli-provider/provider-registry.js +1 -1
- package/lib/agent-cli-provider/provider-registry.js.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.js +6 -2
- package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +3 -1
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/lib/cluster-worker/contracts.js +194 -0
- package/lib/cluster-worker/engine-adapter.js +300 -0
- package/lib/cluster-worker/executable.js +229 -0
- package/lib/cluster-worker/index.d.ts +227 -0
- package/lib/cluster-worker/index.js +294 -0
- package/lib/cluster-worker/object-utils.js +17 -0
- package/lib/cluster-worker/process-stdio.js +37 -0
- package/lib/cluster-worker/profiles.js +81 -0
- package/lib/cluster-worker/runtime-engine.js +50 -0
- package/lib/cluster-worker/runtime-support.js +298 -0
- package/lib/cluster-worker/state-machine.js +147 -0
- package/lib/cluster-worker/terminal-normalizer.js +95 -0
- package/lib/cluster-worker/worker-internals.js +20 -0
- package/lib/detached-startup.js +127 -11
- package/lib/process-liveness.js +26 -0
- package/lib/settings.js +1 -0
- package/lib/start-cluster.js +93 -18
- package/package.json +8 -2
- package/protocol/openengine-cluster/v1/worker.schema.json +1174 -0
- package/scripts/assert-release-published.js +65 -11
- package/scripts/run-lint-staged-no-stash.js +68 -0
- package/src/agent/agent-lifecycle.js +368 -91
- package/src/agent/agent-task-executor.js +384 -101
- package/src/agent-cli-provider/adapters/claude.ts +29 -11
- package/src/agent-cli-provider/adapters/codex.ts +4 -0
- package/src/agent-cli-provider/adapters/common.ts +2 -1
- package/src/agent-cli-provider/contract-options.ts +2 -1
- package/src/agent-cli-provider/index.ts +1 -0
- package/src/agent-cli-provider/provider-registry.ts +1 -1
- package/src/agent-cli-provider/single-agent-runtime.ts +8 -2
- package/src/agent-cli-provider/types.ts +3 -1
- package/src/agent-wrapper.js +9 -2
- package/src/config-validator.js +10 -11
- package/src/orchestrator.js +482 -67
- package/task-lib/attachable-watcher.js +10 -1
- package/task-lib/commands/kill.js +34 -9
- package/task-lib/commands/status.js +13 -3
- package/task-lib/process-termination.js +202 -0
- package/task-lib/runner.js +8 -20
- package/task-lib/store.js +28 -6
- 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, {
|
|
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 {
|
|
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
|
-
|
|
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, {
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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 {
|
|
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'
|
|
16
|
-
|
|
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
|
+
}
|
package/task-lib/runner.js
CHANGED
|
@@ -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;
|
package/task-lib/watcher.js
CHANGED
|
@@ -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, {
|
|
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, {
|
|
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
|
}
|