@the-open-engine/zeroshot 6.9.1 → 6.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/commands/inspect.js +1 -0
- package/cli/index.js +17 -1
- package/cluster-hooks/block-ask-user-question.py +2 -3
- package/cluster-hooks/block-dangerous-git.py +2 -3
- package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/claude.js +57 -3
- package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.js +32 -2
- package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
- package/lib/agent-cli-provider/adapters/common.js +1 -1
- package/lib/agent-cli-provider/adapters/common.js.map +1 -1
- package/lib/agent-cli-provider/adapters/index.d.ts +1 -0
- package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/index.js +8 -0
- package/lib/agent-cli-provider/adapters/index.js.map +1 -1
- package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-options.js +3 -0
- package/lib/agent-cli-provider/contract-options.js.map +1 -1
- package/lib/agent-cli-provider/index.d.ts +1 -1
- package/lib/agent-cli-provider/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/index.js +2 -1
- package/lib/agent-cli-provider/index.js.map +1 -1
- package/lib/agent-cli-provider/provider-registry.d.ts +9 -0
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
- package/lib/agent-cli-provider/provider-registry.js +3 -0
- package/lib/agent-cli-provider/provider-registry.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +10 -2
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/package.json +1 -1
- package/src/agent/agent-context-builder.js +90 -4
- package/src/agent/agent-context-sources.js +20 -2
- package/src/agent/agent-lifecycle.js +43 -8
- package/src/agent/agent-task-executor.js +557 -314
- package/src/agent/guidance-queue.js +29 -7
- package/src/agent/provider-session.js +277 -0
- package/src/agent-cli-provider/adapters/claude.ts +64 -4
- package/src/agent-cli-provider/adapters/codex.ts +47 -3
- package/src/agent-cli-provider/adapters/common.ts +1 -1
- package/src/agent-cli-provider/adapters/index.ts +10 -0
- package/src/agent-cli-provider/contract-options.ts +7 -0
- package/src/agent-cli-provider/index.ts +1 -0
- package/src/agent-cli-provider/provider-registry.ts +13 -1
- package/src/agent-cli-provider/types.ts +13 -6
- package/src/agent-wrapper.js +44 -8
- package/src/claude-task-runner.js +153 -44
- package/src/ledger-sequence.js +63 -0
- package/src/ledger.js +114 -29
- package/src/orchestrator.js +40 -2
- package/src/task-spawn-cleanup-ownership.js +82 -0
- package/src/worktree-claude-config.js +193 -85
- package/task-lib/attachable-watcher.js +101 -253
- package/task-lib/command-spec-cleanup.js +219 -0
- package/task-lib/commands/clean.js +35 -5
- package/task-lib/commands/get-task-id-by-spawn-token.js +10 -0
- package/task-lib/commands/kill.js +117 -4
- package/task-lib/commands/resume.js +29 -6
- package/task-lib/commands/status.js +4 -0
- package/task-lib/provider-helper-runtime.js +1 -0
- package/task-lib/provider-session-capture.js +138 -0
- package/task-lib/runner.js +70 -5
- package/task-lib/store.js +126 -12
- package/task-lib/watcher-output-runtime.js +284 -0
- package/task-lib/watcher.js +131 -242
|
@@ -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, {
|
|
@@ -1,7 +1,35 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { createRequire } from 'module';
|
|
2
3
|
import { getTask } from '../store.js';
|
|
3
4
|
import { spawnTask } from '../runner.js';
|
|
4
5
|
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const { providerSupportsCapability } = require('../../lib/provider-names.js');
|
|
8
|
+
|
|
9
|
+
export function buildResumeTaskOptions(task) {
|
|
10
|
+
if (!providerSupportsCapability(task.provider, 'sessionResume')) {
|
|
11
|
+
throw new Error(`Provider ${task.provider} does not support safe session resume.`);
|
|
12
|
+
}
|
|
13
|
+
if (
|
|
14
|
+
task.requestedResumeSessionId &&
|
|
15
|
+
(task.status !== 'completed' || task.resumeIdentityVerified !== true)
|
|
16
|
+
) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Task ${task.id} did not durably verify its requested provider session; refusing resume.`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
if (!task.sessionId) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Task ${task.id} has no captured provider session ID; refusing cwd-wide continuation.`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
cwd: task.cwd,
|
|
28
|
+
resume: task.sessionId,
|
|
29
|
+
provider: task.provider,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
5
33
|
export async function resumeTask(taskId, newPrompt) {
|
|
6
34
|
const task = getTask(taskId);
|
|
7
35
|
|
|
@@ -23,12 +51,7 @@ export async function resumeTask(taskId, newPrompt) {
|
|
|
23
51
|
console.log(chalk.dim(`Original prompt: ${task.prompt}`));
|
|
24
52
|
console.log(chalk.dim(`Resume prompt: ${prompt}`));
|
|
25
53
|
|
|
26
|
-
const newTask = await spawnTask(prompt,
|
|
27
|
-
cwd: task.cwd,
|
|
28
|
-
continue: true, // Use --continue to load most recent session in that directory
|
|
29
|
-
sessionId: task.sessionId,
|
|
30
|
-
provider: task.provider,
|
|
31
|
-
});
|
|
54
|
+
const newTask = await spawnTask(prompt, buildResumeTaskOptions(task));
|
|
32
55
|
|
|
33
56
|
console.log(chalk.green(`\n✓ Resumed as new task: ${chalk.cyan(newTask.id)}`));
|
|
34
57
|
console.log(chalk.dim(` PID: ${newTask.pid}`));
|
|
@@ -41,6 +41,10 @@ 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'}`);
|
|
45
|
+
if (task.requestedResumeSessionId) {
|
|
46
|
+
console.log(`${chalk.dim('Requested:')} ${task.requestedResumeSessionId}`);
|
|
47
|
+
}
|
|
44
48
|
console.log(`${chalk.dim('Log File:')} ${task.logFile}`);
|
|
45
49
|
|
|
46
50
|
console.log(`\n${chalk.dim('Prompt:')}`);
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { extractProviderSessionId } from './provider-helper-runtime.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Capture a provider-owned session ID from one complete output line.
|
|
5
|
+
*
|
|
6
|
+
* The caller owns persistence so watcher variants can share parsing without
|
|
7
|
+
* importing each other's process lifecycle. Repeated IDs are idempotent; any
|
|
8
|
+
* differing ID makes the capture permanently ambiguous.
|
|
9
|
+
*/
|
|
10
|
+
export function captureProviderSessionLine({
|
|
11
|
+
providerName,
|
|
12
|
+
line,
|
|
13
|
+
currentSessionId = null,
|
|
14
|
+
observedSessionIds = new Set(currentSessionId ? [currentSessionId] : []),
|
|
15
|
+
sessionIdConflict = false,
|
|
16
|
+
onCapture = () => {},
|
|
17
|
+
onConflict = () => {},
|
|
18
|
+
}) {
|
|
19
|
+
const sessionId = extractProviderSessionId(providerName, line);
|
|
20
|
+
if (!sessionId) {
|
|
21
|
+
return { currentSessionId, sessionIdConflict };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
observedSessionIds.add(sessionId);
|
|
25
|
+
if (sessionIdConflict || observedSessionIds.size > 1) {
|
|
26
|
+
if (!sessionIdConflict) {
|
|
27
|
+
onConflict([...observedSessionIds]);
|
|
28
|
+
}
|
|
29
|
+
return { currentSessionId: null, sessionIdConflict: true };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (sessionId === currentSessionId) {
|
|
33
|
+
return { currentSessionId, sessionIdConflict: false };
|
|
34
|
+
}
|
|
35
|
+
onCapture(sessionId);
|
|
36
|
+
return { currentSessionId: sessionId, sessionIdConflict: false };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function providerSessionCompletionError({
|
|
40
|
+
requestedSessionId,
|
|
41
|
+
currentSessionId,
|
|
42
|
+
sessionIdConflict,
|
|
43
|
+
persistenceError,
|
|
44
|
+
}) {
|
|
45
|
+
if (persistenceError) {
|
|
46
|
+
return `Provider session identity could not be persisted: ${persistenceError.message}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const requested = requestedSessionId?.trim() || null;
|
|
50
|
+
if (!requested) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
if (sessionIdConflict) {
|
|
54
|
+
return 'Provider continuation emitted conflicting session identities';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const captured = currentSessionId?.trim() || null;
|
|
58
|
+
if (captured === requested) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return captured
|
|
62
|
+
? 'Provider continuation returned a different session identity'
|
|
63
|
+
: 'Provider continuation did not confirm the requested session identity';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createProviderSessionCapture({
|
|
67
|
+
providerName,
|
|
68
|
+
taskId,
|
|
69
|
+
updateTask,
|
|
70
|
+
log,
|
|
71
|
+
requestedSessionId = null,
|
|
72
|
+
initialSessionId = null,
|
|
73
|
+
initialSessionIdConflict = false,
|
|
74
|
+
}) {
|
|
75
|
+
let currentSessionId = initialSessionId;
|
|
76
|
+
let sessionIdConflict = initialSessionIdConflict;
|
|
77
|
+
let persistenceError = null;
|
|
78
|
+
const observedSessionIds = new Set(initialSessionId ? [initialSessionId] : []);
|
|
79
|
+
|
|
80
|
+
function persist(update) {
|
|
81
|
+
try {
|
|
82
|
+
updateTask(taskId, update);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
persistenceError ||= error;
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function captureLine(line) {
|
|
90
|
+
try {
|
|
91
|
+
({ currentSessionId, sessionIdConflict } = captureProviderSessionLine({
|
|
92
|
+
providerName,
|
|
93
|
+
line,
|
|
94
|
+
currentSessionId,
|
|
95
|
+
observedSessionIds,
|
|
96
|
+
sessionIdConflict,
|
|
97
|
+
onCapture: (sessionId) => {
|
|
98
|
+
// Advance memory before persistence so a failed write can never leave
|
|
99
|
+
// this watcher believing the earlier identity is still trustworthy.
|
|
100
|
+
currentSessionId = sessionId;
|
|
101
|
+
persist({ sessionId });
|
|
102
|
+
},
|
|
103
|
+
onConflict: () => {
|
|
104
|
+
currentSessionId = null;
|
|
105
|
+
sessionIdConflict = true;
|
|
106
|
+
persist({
|
|
107
|
+
sessionId: null,
|
|
108
|
+
sessionIdConflict: true,
|
|
109
|
+
resumeIdentityVerified: false,
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
}));
|
|
113
|
+
} catch (error) {
|
|
114
|
+
log(`[${Date.now()}][SESSION] Failed to persist provider session: ${error.message}\n`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getCompletionError() {
|
|
119
|
+
return providerSessionCompletionError({
|
|
120
|
+
requestedSessionId,
|
|
121
|
+
currentSessionId,
|
|
122
|
+
sessionIdConflict,
|
|
123
|
+
persistenceError,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
captureLine,
|
|
129
|
+
getCompletionError,
|
|
130
|
+
getCompletionUpdate: (resolvedCode) => {
|
|
131
|
+
const error = getCompletionError();
|
|
132
|
+
return {
|
|
133
|
+
resumeIdentityVerified: resolvedCode === 0 && !error,
|
|
134
|
+
...(error ? { sessionId: null, sessionIdConflict: true } : {}),
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
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
|
-
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 ? '...' : ''),
|
|
@@ -163,7 +210,15 @@ function buildTaskRecord({ id, prompt, cwd, options, logFile, providerName, mode
|
|
|
163
210
|
cwd,
|
|
164
211
|
status: 'running',
|
|
165
212
|
pid: null,
|
|
166
|
-
|
|
213
|
+
// Only watcher-observed provider output may populate sessionId. A requested
|
|
214
|
+
// resume ID is diagnostic input, not proof the resumed provider emitted or
|
|
215
|
+
// accepted that session identity.
|
|
216
|
+
sessionId: null,
|
|
217
|
+
sessionIdConflict: false,
|
|
218
|
+
requestedResumeSessionId: options.resume || null,
|
|
219
|
+
// Resumed tasks start fail-closed. Only the watcher terminal transaction
|
|
220
|
+
// may prove that the requested identity completed without conflict.
|
|
221
|
+
resumeIdentityVerified: !options.resume,
|
|
167
222
|
logFile,
|
|
168
223
|
createdAt: new Date().toISOString(),
|
|
169
224
|
updatedAt: new Date().toISOString(),
|
|
@@ -178,6 +233,15 @@ function buildTaskRecord({ id, prompt, cwd, options, logFile, providerName, mode
|
|
|
178
233
|
attachable: false,
|
|
179
234
|
processGroupId: null,
|
|
180
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,
|
|
181
245
|
};
|
|
182
246
|
}
|
|
183
247
|
|
|
@@ -236,6 +300,7 @@ function spawnWatcher({ watcherScript, id, cwd, logFile, finalArgs, watcherConfi
|
|
|
236
300
|
export function buildWatcherEnv(sourceEnv = process.env) {
|
|
237
301
|
const watcherEnv = { ...sourceEnv };
|
|
238
302
|
delete watcherEnv[LEGACY_ISOLATED_PROVIDER_SETTINGS_ENV];
|
|
303
|
+
delete watcherEnv[TASK_SPAWN_OWNERSHIP_TOKEN_ENV];
|
|
239
304
|
if (watcherEnv[ISOLATED_SETTINGS_FILE_MARKER] === '1') {
|
|
240
305
|
delete watcherEnv[ISOLATED_SETTINGS_FILE_ENV];
|
|
241
306
|
delete watcherEnv[ISOLATED_SETTINGS_FILE_MARKER];
|