@the-open-engine/zeroshot 6.10.0 → 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/index.js +16 -0
- 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 +42 -1
- package/lib/agent-cli-provider/adapters/claude.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 -0
- package/lib/agent-cli-provider/contract-options.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +6 -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-lifecycle.js +13 -6
- package/src/agent/agent-task-executor.js +526 -313
- package/src/agent-cli-provider/adapters/claude.ts +46 -1
- package/src/agent-cli-provider/contract-options.ts +6 -0
- package/src/agent-cli-provider/types.ts +9 -6
- package/src/claude-task-runner.js +153 -44
- package/src/task-spawn-cleanup-ownership.js +82 -0
- package/src/worktree-claude-config.js +193 -85
- package/task-lib/attachable-watcher.js +93 -267
- 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/status.js +1 -0
- package/task-lib/runner.js +61 -4
- package/task-lib/store.js +68 -6
- package/task-lib/watcher-output-runtime.js +284 -0
- package/task-lib/watcher.js +124 -257
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { unlinkSync, existsSync } from 'fs';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { loadTasks, saveTasks } from '../store.js';
|
|
4
|
+
import { createCommandSpecCleanup } from '../command-spec-cleanup.js';
|
|
4
5
|
|
|
5
6
|
export function cleanTasks(options = {}) {
|
|
6
7
|
const tasks = loadTasks();
|
|
@@ -11,6 +12,8 @@ export function cleanTasks(options = {}) {
|
|
|
11
12
|
return;
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
let cleanupFailed = false;
|
|
16
|
+
let removedCount = 0;
|
|
14
17
|
const toRemove = [];
|
|
15
18
|
|
|
16
19
|
for (const task of taskList) {
|
|
@@ -33,18 +36,45 @@ export function cleanTasks(options = {}) {
|
|
|
33
36
|
console.log(chalk.dim(`Removing ${toRemove.length} task(s)...\n`));
|
|
34
37
|
|
|
35
38
|
for (const task of toRemove) {
|
|
36
|
-
|
|
39
|
+
if (task.commandCleanup) {
|
|
40
|
+
if (task.status === 'running') {
|
|
41
|
+
cleanupFailed = true;
|
|
42
|
+
console.log(
|
|
43
|
+
chalk.yellow(` Retained: ${task.id} [running] (live command cleanup ownership)`)
|
|
44
|
+
);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
let recovered = false;
|
|
48
|
+
try {
|
|
49
|
+
const cleanup = createCommandSpecCleanup(task.commandCleanup, (cleanupPath, error) => {
|
|
50
|
+
console.log(chalk.yellow(`Warning: failed to clean up ${cleanupPath}: ${error.message}`));
|
|
51
|
+
});
|
|
52
|
+
recovered = cleanup.runSync();
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.log(
|
|
55
|
+
chalk.yellow(`Warning: failed to validate cleanup for task ${task.id}: ${error.message}`)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
if (!recovered) {
|
|
59
|
+
cleanupFailed = true;
|
|
60
|
+
console.log(
|
|
61
|
+
chalk.yellow(` Retained: ${task.id} [${task.status}] (command cleanup pending)`)
|
|
62
|
+
);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
task.commandCleanup = null;
|
|
66
|
+
}
|
|
37
67
|
if (task.logFile && existsSync(task.logFile)) {
|
|
38
68
|
unlinkSync(task.logFile);
|
|
39
69
|
}
|
|
40
70
|
|
|
41
|
-
// Remove from tasks
|
|
42
|
-
delete tasks[task.id];
|
|
43
|
-
|
|
44
71
|
console.log(chalk.dim(` Removed: ${task.id} [${task.status}]`));
|
|
72
|
+
delete tasks[task.id];
|
|
73
|
+
removedCount++;
|
|
45
74
|
}
|
|
46
75
|
|
|
47
76
|
saveTasks(tasks);
|
|
48
77
|
|
|
49
|
-
console.log(chalk.green(`\n✓ Cleaned ${
|
|
78
|
+
console.log(chalk.green(`\n✓ Cleaned ${removedCount} task(s)`));
|
|
79
|
+
if (cleanupFailed) process.exitCode = 1;
|
|
50
80
|
}
|
|
@@ -1,6 +1,75 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { getTask, updateTask } from '../store.js';
|
|
3
|
-
import {
|
|
2
|
+
import { getTask, requestTaskCancellation, updateTask } from '../store.js';
|
|
3
|
+
import { createCommandSpecCleanup } from '../command-spec-cleanup.js';
|
|
4
|
+
import { terminateProcess } from '../process-termination.js';
|
|
5
|
+
|
|
6
|
+
async function cleanupTerminatedTask(task) {
|
|
7
|
+
if (!task.commandCleanup) return {};
|
|
8
|
+
try {
|
|
9
|
+
const cleanup = createCommandSpecCleanup(task.commandCleanup, (cleanupPath, error) => {
|
|
10
|
+
console.log(chalk.yellow(`Warning: failed to clean up ${cleanupPath}: ${error.message}`));
|
|
11
|
+
});
|
|
12
|
+
return (await cleanup.run()) ? { commandCleanup: null } : {};
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.log(
|
|
15
|
+
chalk.yellow(`Warning: failed to validate persisted command cleanup: ${error.message}`)
|
|
16
|
+
);
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function retryTerminalTaskCleanup(taskId, task) {
|
|
22
|
+
if (!task.commandCleanup) return true;
|
|
23
|
+
const cleanupUpdate = await cleanupTerminatedTask(task);
|
|
24
|
+
if (cleanupUpdate.commandCleanup === null) {
|
|
25
|
+
updateTask(taskId, cleanupUpdate);
|
|
26
|
+
console.log(chalk.green(`✓ Recovered pending command cleanup for task ${taskId}`));
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
console.log(chalk.yellow(`Warning: command cleanup remains pending for task ${taskId}`));
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const TERMINAL_STATUSES = new Set(['completed', 'failed', 'killed', 'stale']);
|
|
35
|
+
|
|
36
|
+
function sleep(ms) {
|
|
37
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function waitForStartupCancellation(taskId, options) {
|
|
41
|
+
const timeoutMs = options.startupCancelTimeoutMs ?? 8000;
|
|
42
|
+
const pollMs = options.startupCancelPollMs ?? options.pollMs ?? 25;
|
|
43
|
+
const deadline = Date.now() + timeoutMs;
|
|
44
|
+
|
|
45
|
+
while (Date.now() <= deadline) {
|
|
46
|
+
const current = getTask(taskId);
|
|
47
|
+
if (!current) break;
|
|
48
|
+
if (TERMINAL_STATUSES.has(current.status)) {
|
|
49
|
+
await retryTerminalTaskCleanup(taskId, current);
|
|
50
|
+
if (!getTask(taskId)?.commandCleanup) {
|
|
51
|
+
console.log(chalk.green(`✓ Cancelled task ${taskId} before provider startup completed`));
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (Number.isInteger(current.pid) && current.pid > 0) {
|
|
56
|
+
await killTaskCommand(taskId, options);
|
|
57
|
+
const terminal = getTask(taskId);
|
|
58
|
+
return Boolean(
|
|
59
|
+
terminal && TERMINAL_STATUSES.has(terminal.status) && !terminal.commandCleanup
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
await sleep(pollMs);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(
|
|
66
|
+
chalk.yellow(
|
|
67
|
+
`Cancellation for task ${taskId} remains pending; provider termination and cleanup were not confirmed`
|
|
68
|
+
)
|
|
69
|
+
);
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
4
73
|
|
|
5
74
|
export async function killTaskCommand(taskId, options = {}) {
|
|
6
75
|
const task = getTask(taskId);
|
|
@@ -11,30 +80,68 @@ export async function killTaskCommand(taskId, options = {}) {
|
|
|
11
80
|
}
|
|
12
81
|
|
|
13
82
|
if (task.status !== 'running') {
|
|
83
|
+
await retryTerminalTaskCleanup(taskId, task);
|
|
14
84
|
console.log(chalk.yellow(`Task is not running (status: ${task.status})`));
|
|
15
85
|
return;
|
|
16
86
|
}
|
|
17
87
|
|
|
88
|
+
if (!Number.isInteger(task.pid) || task.pid <= 0) {
|
|
89
|
+
requestTaskCancellation(taskId);
|
|
90
|
+
console.log(
|
|
91
|
+
chalk.yellow(
|
|
92
|
+
`Task ${taskId} has not published a provider PID; persisted cancellation is pending`
|
|
93
|
+
)
|
|
94
|
+
);
|
|
95
|
+
await waitForStartupCancellation(taskId, options);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const platform = options.platform || process.platform;
|
|
100
|
+
const terminate = options.terminateProcessFn || terminateProcess;
|
|
101
|
+
const processOptions = { ...options };
|
|
102
|
+
delete processOptions.platform;
|
|
103
|
+
delete processOptions.terminateProcessFn;
|
|
104
|
+
|
|
18
105
|
const terminationOptions = {
|
|
19
|
-
...
|
|
106
|
+
...processOptions,
|
|
20
107
|
processGroupId: task.processGroupId,
|
|
21
108
|
terminationStrategy: task.terminationStrategy || 'process',
|
|
22
109
|
};
|
|
23
110
|
|
|
24
|
-
const result = await
|
|
111
|
+
const result = await terminate(task.pid, terminationOptions);
|
|
25
112
|
|
|
26
113
|
if (result.terminated && result.alreadyDead) {
|
|
114
|
+
if (platform === 'win32' && task.terminationStrategy === 'process-tree') {
|
|
115
|
+
console.log(
|
|
116
|
+
chalk.yellow(
|
|
117
|
+
`Warning: Windows task root ${task.pid} is gone but descendant termination is unverified; preserving cleanup ownership`
|
|
118
|
+
)
|
|
119
|
+
);
|
|
120
|
+
updateTask(taskId, {
|
|
121
|
+
error: 'Windows process-tree termination could not be confirmed after root exit',
|
|
122
|
+
});
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
27
126
|
console.log(chalk.yellow('Process already dead, updating status...'));
|
|
127
|
+
const cleanupUpdate = await cleanupTerminatedTask(task);
|
|
28
128
|
updateTask(taskId, {
|
|
29
129
|
status: 'stale',
|
|
30
130
|
pid: null,
|
|
31
131
|
processGroupId: null,
|
|
32
132
|
error: 'Process died unexpectedly',
|
|
133
|
+
cancelRequested: false,
|
|
134
|
+
...cleanupUpdate,
|
|
33
135
|
});
|
|
136
|
+
if (getTask(taskId)?.commandCleanup) {
|
|
137
|
+
console.log(chalk.yellow(`Warning: command cleanup remains pending for task ${taskId}`));
|
|
138
|
+
process.exitCode = 1;
|
|
139
|
+
}
|
|
34
140
|
return;
|
|
35
141
|
}
|
|
36
142
|
|
|
37
143
|
if (result.terminated) {
|
|
144
|
+
const cleanupUpdate = await cleanupTerminatedTask(task);
|
|
38
145
|
const suffix = result.escalated ? ' after SIGKILL escalation' : ' with SIGTERM';
|
|
39
146
|
console.log(chalk.green(`✓ Killed task ${taskId} (PID: ${task.pid})${suffix}`));
|
|
40
147
|
if (result.degraded) {
|
|
@@ -46,7 +153,13 @@ export async function killTaskCommand(taskId, options = {}) {
|
|
|
46
153
|
processGroupId: null,
|
|
47
154
|
exitCode: result.escalated ? 137 : 143,
|
|
48
155
|
error: result.escalated ? 'Killed by user after SIGKILL escalation' : 'Killed by user',
|
|
156
|
+
cancelRequested: false,
|
|
157
|
+
...cleanupUpdate,
|
|
49
158
|
});
|
|
159
|
+
if (getTask(taskId)?.commandCleanup) {
|
|
160
|
+
console.log(chalk.yellow(`Warning: command cleanup remains pending for task ${taskId}`));
|
|
161
|
+
process.exitCode = 1;
|
|
162
|
+
}
|
|
50
163
|
} else {
|
|
51
164
|
console.log(chalk.red(`Failed to kill task ${taskId}`));
|
|
52
165
|
updateTask(taskId, {
|
|
@@ -41,6 +41,7 @@ export function showStatus(taskId) {
|
|
|
41
41
|
console.log(`${chalk.dim('PID:')} ${task.pid || 'N/A'}`);
|
|
42
42
|
console.log(`${chalk.dim('Exit Code:')} ${task.exitCode ?? 'N/A'}`);
|
|
43
43
|
console.log(`${chalk.dim('Session:')} ${task.sessionId || 'N/A'}`);
|
|
44
|
+
console.log(`${chalk.dim('Cleanup:')} ${task.commandCleanup ? 'pending' : 'complete'}`);
|
|
44
45
|
if (task.requestedResumeSessionId) {
|
|
45
46
|
console.log(`${chalk.dim('Requested:')} ${task.requestedResumeSessionId}`);
|
|
46
47
|
}
|
package/task-lib/runner.js
CHANGED
|
@@ -12,6 +12,12 @@ const {
|
|
|
12
12
|
ISOLATED_SETTINGS_FILE_MARKER,
|
|
13
13
|
LEGACY_ISOLATED_PROVIDER_SETTINGS_ENV,
|
|
14
14
|
} = require('../src/task-run-model-args.js');
|
|
15
|
+
const {
|
|
16
|
+
CLAUDE_MCP_CONFIG_ENV,
|
|
17
|
+
CLAUDE_SETTINGS_ENV,
|
|
18
|
+
isClaudeSettingsOverlayPath,
|
|
19
|
+
} = require('../src/worktree-claude-config');
|
|
20
|
+
const { TASK_SPAWN_OWNERSHIP_TOKEN_ENV } = require('../src/task-spawn-cleanup-ownership');
|
|
15
21
|
export {
|
|
16
22
|
isOwnedProcessTreeRunning,
|
|
17
23
|
isProcessRunning,
|
|
@@ -37,7 +43,7 @@ export function spawnTask(prompt, options = {}) {
|
|
|
37
43
|
});
|
|
38
44
|
const providerName = prepared.adapter.id;
|
|
39
45
|
const modelSpec = prepared.options.modelSpec;
|
|
40
|
-
const commandSpec = prepared.commandSpec;
|
|
46
|
+
const commandSpec = attachClaudeOverlayCleanup(prepared.commandSpec, providerName);
|
|
41
47
|
|
|
42
48
|
const task = buildTaskRecord({
|
|
43
49
|
id,
|
|
@@ -47,6 +53,7 @@ export function spawnTask(prompt, options = {}) {
|
|
|
47
53
|
logFile,
|
|
48
54
|
providerName,
|
|
49
55
|
modelSpec,
|
|
56
|
+
commandSpec,
|
|
50
57
|
});
|
|
51
58
|
|
|
52
59
|
addTask(task);
|
|
@@ -116,14 +123,24 @@ function buildProviderOptions(options, runtime, modelSelection) {
|
|
|
116
123
|
autoApprove: true,
|
|
117
124
|
...(modelSelection === undefined ? {} : { modelSpec: modelSelection.modelSpec }),
|
|
118
125
|
...mcpConfigOption(options),
|
|
126
|
+
...claudeSettingsFileOption(),
|
|
119
127
|
...(options.resume ? { resumeSessionId: options.resume } : {}),
|
|
120
128
|
...(options.continue ? { continueSession: true } : {}),
|
|
121
129
|
};
|
|
122
130
|
}
|
|
123
131
|
|
|
132
|
+
function claudeSettingsFileOption() {
|
|
133
|
+
const settingsPath = process.env[CLAUDE_SETTINGS_ENV]?.trim();
|
|
134
|
+
return settingsPath ? { claudeSettingsFile: settingsPath } : {};
|
|
135
|
+
}
|
|
136
|
+
|
|
124
137
|
function mcpConfigOption(options) {
|
|
125
|
-
const entries = options.mcpConfig;
|
|
126
|
-
|
|
138
|
+
const entries = Array.isArray(options.mcpConfig) ? [...options.mcpConfig] : [];
|
|
139
|
+
const claudeMcpConfigPath = process.env[CLAUDE_MCP_CONFIG_ENV]?.trim();
|
|
140
|
+
if (claudeMcpConfigPath && !entries.includes(claudeMcpConfigPath)) {
|
|
141
|
+
entries.push(claudeMcpConfigPath);
|
|
142
|
+
}
|
|
143
|
+
if (entries.length === 0) return {};
|
|
127
144
|
return { mcpConfig: entries };
|
|
128
145
|
}
|
|
129
146
|
|
|
@@ -155,7 +172,37 @@ function providerLevelSelection(options) {
|
|
|
155
172
|
return { modelSpec };
|
|
156
173
|
}
|
|
157
174
|
|
|
158
|
-
export function
|
|
175
|
+
export function attachClaudeOverlayCleanup(commandSpec, providerName) {
|
|
176
|
+
if (providerName !== 'claude') return commandSpec;
|
|
177
|
+
const settingsPath = process.env[CLAUDE_SETTINGS_ENV]?.trim();
|
|
178
|
+
if (!isClaudeSettingsOverlayPath(settingsPath)) return commandSpec;
|
|
179
|
+
|
|
180
|
+
const overlayDir = dirname(settingsPath);
|
|
181
|
+
return {
|
|
182
|
+
...commandSpec,
|
|
183
|
+
cleanup: [...(commandSpec.cleanup || []), overlayDir],
|
|
184
|
+
cleanupMetadata: [
|
|
185
|
+
...(commandSpec.cleanupMetadata || []),
|
|
186
|
+
{
|
|
187
|
+
kind: 'temp-directory',
|
|
188
|
+
provider: 'claude',
|
|
189
|
+
path: overlayDir,
|
|
190
|
+
reason: 'settings-overlay',
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function buildTaskRecord({
|
|
197
|
+
id,
|
|
198
|
+
prompt,
|
|
199
|
+
cwd,
|
|
200
|
+
options,
|
|
201
|
+
logFile,
|
|
202
|
+
providerName,
|
|
203
|
+
modelSpec,
|
|
204
|
+
commandSpec = {},
|
|
205
|
+
}) {
|
|
159
206
|
return {
|
|
160
207
|
id,
|
|
161
208
|
prompt: prompt.slice(0, 200) + (prompt.length > 200 ? '...' : ''),
|
|
@@ -186,6 +233,15 @@ export function buildTaskRecord({ id, prompt, cwd, options, logFile, providerNam
|
|
|
186
233
|
attachable: false,
|
|
187
234
|
processGroupId: null,
|
|
188
235
|
terminationStrategy: null,
|
|
236
|
+
cancelRequested: false,
|
|
237
|
+
spawnOwnershipToken: process.env[TASK_SPAWN_OWNERSHIP_TOKEN_ENV] || null,
|
|
238
|
+
commandCleanup:
|
|
239
|
+
commandSpec.cleanup?.length > 0
|
|
240
|
+
? {
|
|
241
|
+
cleanup: commandSpec.cleanup,
|
|
242
|
+
cleanupMetadata: commandSpec.cleanupMetadata || [],
|
|
243
|
+
}
|
|
244
|
+
: null,
|
|
189
245
|
};
|
|
190
246
|
}
|
|
191
247
|
|
|
@@ -244,6 +300,7 @@ function spawnWatcher({ watcherScript, id, cwd, logFile, finalArgs, watcherConfi
|
|
|
244
300
|
export function buildWatcherEnv(sourceEnv = process.env) {
|
|
245
301
|
const watcherEnv = { ...sourceEnv };
|
|
246
302
|
delete watcherEnv[LEGACY_ISOLATED_PROVIDER_SETTINGS_ENV];
|
|
303
|
+
delete watcherEnv[TASK_SPAWN_OWNERSHIP_TOKEN_ENV];
|
|
247
304
|
if (watcherEnv[ISOLATED_SETTINGS_FILE_MARKER] === '1') {
|
|
248
305
|
delete watcherEnv[ISOLATED_SETTINGS_FILE_ENV];
|
|
249
306
|
delete watcherEnv[ISOLATED_SETTINGS_FILE_MARKER];
|
package/task-lib/store.js
CHANGED
|
@@ -56,7 +56,10 @@ function getDb() {
|
|
|
56
56
|
socket_path TEXT,
|
|
57
57
|
attachable INTEGER DEFAULT 0,
|
|
58
58
|
process_group_id INTEGER,
|
|
59
|
-
termination_strategy TEXT
|
|
59
|
+
termination_strategy TEXT,
|
|
60
|
+
command_cleanup TEXT,
|
|
61
|
+
cancel_requested INTEGER DEFAULT 0,
|
|
62
|
+
spawn_ownership_token TEXT
|
|
60
63
|
);
|
|
61
64
|
|
|
62
65
|
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
@@ -93,12 +96,34 @@ function ensureTaskColumn(database, name, definition) {
|
|
|
93
96
|
}
|
|
94
97
|
}
|
|
95
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
|
+
|
|
96
113
|
export function migrateTaskStore(database) {
|
|
97
114
|
ensureTaskColumn(database, 'process_group_id', 'INTEGER');
|
|
98
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');
|
|
99
119
|
ensureTaskColumn(database, 'requested_resume_session_id', 'TEXT');
|
|
100
120
|
ensureTaskColumn(database, 'session_id_conflict', 'INTEGER NOT NULL DEFAULT 0');
|
|
101
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
|
+
`);
|
|
102
127
|
|
|
103
128
|
const version = database.pragma('user_version', { simple: true });
|
|
104
129
|
if (version >= TASK_STORE_SCHEMA_VERSION) return;
|
|
@@ -164,6 +189,9 @@ function rowToTask(row) {
|
|
|
164
189
|
attachable: Boolean(row.attachable),
|
|
165
190
|
processGroupId: row.process_group_id,
|
|
166
191
|
terminationStrategy: row.termination_strategy,
|
|
192
|
+
commandCleanup: parseCommandCleanup(row.command_cleanup),
|
|
193
|
+
cancelRequested: Boolean(row.cancel_requested),
|
|
194
|
+
spawnOwnershipToken: row.spawn_ownership_token,
|
|
167
195
|
};
|
|
168
196
|
}
|
|
169
197
|
|
|
@@ -191,11 +219,13 @@ export function saveTasks(tasks) {
|
|
|
191
219
|
INSERT OR REPLACE INTO tasks (
|
|
192
220
|
id, prompt, full_prompt, cwd, status, pid, session_id, session_id_conflict, requested_resume_session_id, resume_identity_verified, log_file,
|
|
193
221
|
created_at, updated_at, exit_code, error, provider, model,
|
|
194
|
-
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
|
|
195
224
|
) VALUES (
|
|
196
225
|
@id, @prompt, @fullPrompt, @cwd, @status, @pid, @sessionId, @sessionIdConflict, @requestedResumeSessionId, @resumeIdentityVerified, @logFile,
|
|
197
226
|
@createdAt, @updatedAt, @exitCode, @error, @provider, @model,
|
|
198
|
-
@scheduleId, @socketPath, @attachable, @processGroupId, @terminationStrategy
|
|
227
|
+
@scheduleId, @socketPath, @attachable, @processGroupId, @terminationStrategy,
|
|
228
|
+
@commandCleanup, @cancelRequested, @spawnOwnershipToken
|
|
199
229
|
)
|
|
200
230
|
`);
|
|
201
231
|
|
|
@@ -227,6 +257,9 @@ export function saveTasks(tasks) {
|
|
|
227
257
|
attachable: task.attachable ? 1 : 0,
|
|
228
258
|
processGroupId: task.processGroupId || null,
|
|
229
259
|
terminationStrategy: task.terminationStrategy || null,
|
|
260
|
+
commandCleanup: serializeCommandCleanup(task.commandCleanup),
|
|
261
|
+
cancelRequested: task.cancelRequested ? 1 : 0,
|
|
262
|
+
spawnOwnershipToken: task.spawnOwnershipToken || null,
|
|
230
263
|
});
|
|
231
264
|
}
|
|
232
265
|
});
|
|
@@ -257,6 +290,24 @@ export function getTask(id) {
|
|
|
257
290
|
return rowToTask(row);
|
|
258
291
|
}
|
|
259
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
|
+
|
|
260
311
|
/**
|
|
261
312
|
* Update a task
|
|
262
313
|
* @param {string} id
|
|
@@ -296,7 +347,10 @@ export function updateTask(id, updates) {
|
|
|
296
347
|
socket_path = @socketPath,
|
|
297
348
|
attachable = @attachable,
|
|
298
349
|
process_group_id = @processGroupId,
|
|
299
|
-
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
|
|
300
354
|
WHERE id = @id
|
|
301
355
|
`
|
|
302
356
|
)
|
|
@@ -322,6 +376,9 @@ export function updateTask(id, updates) {
|
|
|
322
376
|
attachable: updated.attachable ? 1 : 0,
|
|
323
377
|
processGroupId: updated.processGroupId || null,
|
|
324
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,
|
|
325
382
|
});
|
|
326
383
|
|
|
327
384
|
return updated;
|
|
@@ -346,11 +403,13 @@ export function addTask(task) {
|
|
|
346
403
|
INSERT INTO tasks (
|
|
347
404
|
id, prompt, full_prompt, cwd, status, pid, session_id, session_id_conflict, requested_resume_session_id, resume_identity_verified, log_file,
|
|
348
405
|
created_at, updated_at, exit_code, error, provider, model,
|
|
349
|
-
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
|
|
350
408
|
) VALUES (
|
|
351
409
|
@id, @prompt, @fullPrompt, @cwd, @status, @pid, @sessionId, @sessionIdConflict, @requestedResumeSessionId, @resumeIdentityVerified, @logFile,
|
|
352
410
|
@createdAt, @updatedAt, @exitCode, @error, @provider, @model,
|
|
353
|
-
@scheduleId, @socketPath, @attachable, @processGroupId, @terminationStrategy
|
|
411
|
+
@scheduleId, @socketPath, @attachable, @processGroupId, @terminationStrategy,
|
|
412
|
+
@commandCleanup, @cancelRequested, @spawnOwnershipToken
|
|
354
413
|
)
|
|
355
414
|
`
|
|
356
415
|
)
|
|
@@ -377,6 +436,9 @@ export function addTask(task) {
|
|
|
377
436
|
attachable: fullTask.attachable ? 1 : 0,
|
|
378
437
|
processGroupId: fullTask.processGroupId || null,
|
|
379
438
|
terminationStrategy: fullTask.terminationStrategy || null,
|
|
439
|
+
commandCleanup: serializeCommandCleanup(fullTask.commandCleanup),
|
|
440
|
+
cancelRequested: fullTask.cancelRequested ? 1 : 0,
|
|
441
|
+
spawnOwnershipToken: fullTask.spawnOwnershipToken || null,
|
|
380
442
|
});
|
|
381
443
|
|
|
382
444
|
return fullTask;
|