@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.
Files changed (31) hide show
  1. package/cli/index.js +16 -0
  2. package/cluster-hooks/block-ask-user-question.py +2 -3
  3. package/cluster-hooks/block-dangerous-git.py +2 -3
  4. package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
  5. package/lib/agent-cli-provider/adapters/claude.js +42 -1
  6. package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
  7. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
  8. package/lib/agent-cli-provider/contract-options.js +2 -0
  9. package/lib/agent-cli-provider/contract-options.js.map +1 -1
  10. package/lib/agent-cli-provider/types.d.ts +6 -2
  11. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  12. package/lib/agent-cli-provider/types.js.map +1 -1
  13. package/package.json +1 -1
  14. package/src/agent/agent-lifecycle.js +13 -6
  15. package/src/agent/agent-task-executor.js +526 -313
  16. package/src/agent-cli-provider/adapters/claude.ts +46 -1
  17. package/src/agent-cli-provider/contract-options.ts +6 -0
  18. package/src/agent-cli-provider/types.ts +9 -6
  19. package/src/claude-task-runner.js +153 -44
  20. package/src/task-spawn-cleanup-ownership.js +82 -0
  21. package/src/worktree-claude-config.js +193 -85
  22. package/task-lib/attachable-watcher.js +93 -267
  23. package/task-lib/command-spec-cleanup.js +219 -0
  24. package/task-lib/commands/clean.js +35 -5
  25. package/task-lib/commands/get-task-id-by-spawn-token.js +10 -0
  26. package/task-lib/commands/kill.js +117 -4
  27. package/task-lib/commands/status.js +1 -0
  28. package/task-lib/runner.js +61 -4
  29. package/task-lib/store.js +68 -6
  30. package/task-lib/watcher-output-runtime.js +284 -0
  31. 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
- // Delete log file
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 ${toRemove.length} task(s)`));
78
+ console.log(chalk.green(`\n✓ Cleaned ${removedCount} task(s)`));
79
+ if (cleanupFailed) process.exitCode = 1;
50
80
  }
@@ -0,0 +1,10 @@
1
+ import { getTaskBySpawnOwnershipToken } from '../store.js';
2
+
3
+ export function getTaskIdBySpawnToken(token) {
4
+ const task = getTaskBySpawnOwnershipToken(token);
5
+ if (!task) {
6
+ process.exitCode = 2;
7
+ return;
8
+ }
9
+ process.stdout.write(`${task.id}\n`);
10
+ }
@@ -1,6 +1,75 @@
1
1
  import chalk from 'chalk';
2
- import { getTask, updateTask } from '../store.js';
3
- import { terminateProcess } from '../runner.js';
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
- ...options,
106
+ ...processOptions,
20
107
  processGroupId: task.processGroupId,
21
108
  terminationStrategy: task.terminationStrategy || 'process',
22
109
  };
23
110
 
24
- const result = await terminateProcess(task.pid, terminationOptions);
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
  }
@@ -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
- if (!Array.isArray(entries) || entries.length === 0) return {};
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 buildTaskRecord({ id, prompt, cwd, options, logFile, providerName, modelSpec }) {
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;