@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
|
@@ -5,15 +5,11 @@
|
|
|
5
5
|
* Runs detached from parent, provides Unix socket for attach clients.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { appendFileSync
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
detectProviderStreamingModeError,
|
|
14
|
-
recoverProviderStructuredOutput,
|
|
15
|
-
supportsProviderStructuredOutputRecovery,
|
|
16
|
-
} from './provider-helper-runtime.js';
|
|
8
|
+
import { appendFileSync } from 'fs';
|
|
9
|
+
import { getTask, updateTask } from './store.js';
|
|
10
|
+
import { createCommandSpecCleanup } from './command-spec-cleanup.js';
|
|
11
|
+
import { createProviderSessionCapture } from './provider-session-capture.js';
|
|
12
|
+
import * as watcherOutputRuntime from './watcher-output-runtime.js';
|
|
17
13
|
import { createRequire } from 'module';
|
|
18
14
|
|
|
19
15
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -22,8 +18,9 @@ import { createRequire } from 'module';
|
|
|
22
18
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
23
19
|
|
|
24
20
|
const [, , taskIdArg, cwdArg, logFileArg, argsJsonArg, configJsonArg] = process.argv;
|
|
25
|
-
let
|
|
26
|
-
let
|
|
21
|
+
let commandCleanup = watcherOutputRuntime.COMMAND_CLEANUP_UNINITIALIZED;
|
|
22
|
+
let crashStarted = false;
|
|
23
|
+
let server = null;
|
|
27
24
|
|
|
28
25
|
function emergencyLog(msg) {
|
|
29
26
|
if (logFileArg) {
|
|
@@ -37,37 +34,47 @@ function emergencyLog(msg) {
|
|
|
37
34
|
}
|
|
38
35
|
}
|
|
39
36
|
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
emergencyLog(`\n[${timestamp}][CRASH] ${source}: ${errorMsg}\n`);
|
|
45
|
-
emergencyLog(`[${timestamp}][CRASH] Process terminating due to unhandled error\n`);
|
|
46
|
-
cleanupCommandSpecSync();
|
|
37
|
+
function stopAttachableProvider(exitObserved = false) {
|
|
38
|
+
return watcherOutputRuntime.terminateWatcherProvider(server, { exitObserved });
|
|
39
|
+
}
|
|
47
40
|
|
|
41
|
+
async function failWatcher(error, source) {
|
|
42
|
+
if (crashStarted) return;
|
|
43
|
+
crashStarted = true;
|
|
48
44
|
if (taskIdArg) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
emergencyLog
|
|
57
|
-
|
|
45
|
+
await watcherOutputRuntime.completeWatcherFailure({
|
|
46
|
+
taskId: taskIdArg,
|
|
47
|
+
error,
|
|
48
|
+
source,
|
|
49
|
+
commandCleanup,
|
|
50
|
+
terminateProvider: stopAttachableProvider,
|
|
51
|
+
updateTask,
|
|
52
|
+
emergencyLog,
|
|
53
|
+
terminalUpdates: { socketPath: null },
|
|
54
|
+
});
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
process.exit(1);
|
|
61
58
|
}
|
|
62
59
|
|
|
63
60
|
process.on('uncaughtException', (error) => {
|
|
64
|
-
|
|
61
|
+
void failWatcher(error, 'uncaughtException');
|
|
65
62
|
});
|
|
66
63
|
|
|
67
64
|
process.on('unhandledRejection', (reason) => {
|
|
68
|
-
|
|
65
|
+
void failWatcher(reason, 'unhandledRejection');
|
|
69
66
|
});
|
|
70
67
|
|
|
68
|
+
const persistedTask = taskIdArg ? getTask(taskIdArg) : null;
|
|
69
|
+
if (persistedTask) {
|
|
70
|
+
commandCleanup = createCommandSpecCleanup(
|
|
71
|
+
persistedTask.commandCleanup || { cleanup: [], cleanupMetadata: [] },
|
|
72
|
+
(cleanupPath, error) => {
|
|
73
|
+
emergencyLog(`[${Date.now()}][CLEANUP] Failed to delete ${cleanupPath}: ${error.message}\n`);
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
71
78
|
const require = createRequire(import.meta.url);
|
|
72
79
|
const { AttachServer } = require('../src/attach');
|
|
73
80
|
const { getTaskSocketPath } = require('../src/attach/socket-paths');
|
|
@@ -84,188 +91,57 @@ const commandSpec = config.commandSpec || {
|
|
|
84
91
|
env: config.env || {},
|
|
85
92
|
cleanup: [],
|
|
86
93
|
};
|
|
87
|
-
commandSpecCleanup = commandSpec.cleanup || [];
|
|
88
|
-
let server = null;
|
|
89
|
-
|
|
90
94
|
const socketPath = getTaskSocketPath(taskId);
|
|
91
95
|
|
|
92
96
|
function log(msg) {
|
|
93
97
|
appendFileSync(logFile, msg);
|
|
94
98
|
}
|
|
95
99
|
|
|
96
|
-
const providerName =
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
100
|
+
const { providerName, env, command, finalArgs } = watcherOutputRuntime.resolveWatcherCommand(
|
|
101
|
+
config,
|
|
102
|
+
commandSpec,
|
|
103
|
+
args,
|
|
104
|
+
normalizeProviderName
|
|
105
|
+
);
|
|
106
|
+
const providerSessionCapture = createProviderSessionCapture({
|
|
107
|
+
providerName,
|
|
108
|
+
taskId,
|
|
109
|
+
updateTask,
|
|
110
|
+
log,
|
|
111
|
+
requestedSessionId: persistedTask?.requestedResumeSessionId || null,
|
|
112
|
+
initialSessionId: persistedTask?.sessionId || null,
|
|
113
|
+
initialSessionIdConflict: persistedTask?.sessionIdConflict === true,
|
|
114
|
+
});
|
|
110
115
|
let outputBuffer = '';
|
|
111
|
-
let streamingModeError = null;
|
|
112
|
-
let fatalError = null;
|
|
113
|
-
|
|
114
|
-
function splitBufferLines(buffer, chunk) {
|
|
115
|
-
const nextBuffer = buffer + chunk;
|
|
116
|
-
const lines = nextBuffer.split('\n');
|
|
117
|
-
const remaining = lines.pop() || '';
|
|
118
|
-
return { lines, remaining };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function maybeHandleFatalError(line, timestamp) {
|
|
122
|
-
if (fatalError) {
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const detected = detectProviderFatalError(providerName, line);
|
|
127
|
-
if (!detected) {
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
fatalError = detected;
|
|
132
|
-
|
|
133
|
-
if (silentJsonMode) {
|
|
134
|
-
log(`[${timestamp}]${line}\n`);
|
|
135
|
-
}
|
|
136
|
-
log(`[${timestamp}][FATAL] ${detected}\n`);
|
|
137
116
|
|
|
117
|
+
function stopProviderAfterFatalOutput(timestamp) {
|
|
138
118
|
if (server) {
|
|
139
119
|
server.stop('SIGTERM').catch((error) => {
|
|
140
120
|
log(`[${timestamp}][FATAL] Attach server stop failed: ${error.message}\n`);
|
|
141
121
|
});
|
|
142
122
|
}
|
|
143
|
-
return true;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function captureStreamingError(line, timestamp) {
|
|
147
|
-
const detectedError = detectProviderStreamingModeError(providerName, line);
|
|
148
|
-
if (!detectedError) {
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
streamingModeError = { ...detectedError, timestamp };
|
|
153
|
-
return true;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function maybeCaptureStructuredOutput(line) {
|
|
157
|
-
try {
|
|
158
|
-
const json = JSON.parse(line);
|
|
159
|
-
if (json.structured_output) {
|
|
160
|
-
finalResultJson = line;
|
|
161
|
-
}
|
|
162
|
-
} catch {
|
|
163
|
-
// Not JSON, skip
|
|
164
|
-
}
|
|
165
123
|
}
|
|
166
124
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
maybeCaptureStructuredOutput(line);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function handleStreamingLines(lines, timestamp) {
|
|
179
|
-
for (const line of lines) {
|
|
180
|
-
maybeHandleFatalError(line, timestamp);
|
|
181
|
-
if (captureStreamingError(line, timestamp)) {
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
log(`[${timestamp}]${line}\n`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function flushOutputBuffer(timestamp) {
|
|
189
|
-
if (!outputBuffer.trim()) {
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (!enableRecovery) {
|
|
194
|
-
if (!silentJsonMode) {
|
|
195
|
-
log(`[${timestamp}]${outputBuffer}\n`);
|
|
196
|
-
}
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
maybeHandleFatalError(outputBuffer, timestamp);
|
|
201
|
-
if (captureStreamingError(outputBuffer, timestamp)) {
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (silentJsonMode) {
|
|
206
|
-
maybeCaptureStructuredOutput(outputBuffer);
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
log(`[${timestamp}]${outputBuffer}\n`);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function attemptRecovery(code, timestamp) {
|
|
214
|
-
if (!(code !== 0 && streamingModeError?.sessionId)) {
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const recovered = recoverProviderStructuredOutput(providerName, streamingModeError.sessionId);
|
|
219
|
-
if (recovered?.payload) {
|
|
220
|
-
const recoveredLine = JSON.stringify(recovered.payload);
|
|
221
|
-
if (silentJsonMode) {
|
|
222
|
-
finalResultJson = recoveredLine;
|
|
223
|
-
} else {
|
|
224
|
-
log(`[${timestamp}]${recoveredLine}\n`);
|
|
225
|
-
}
|
|
226
|
-
} else if (streamingModeError.line) {
|
|
227
|
-
if (silentJsonMode) {
|
|
228
|
-
log(streamingModeError.line + '\n');
|
|
229
|
-
} else {
|
|
230
|
-
log(`[${streamingModeError.timestamp}]${streamingModeError.line}\n`);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return recovered;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
async function cleanupCommandSpec() {
|
|
238
|
-
if (cleanupStarted) return;
|
|
239
|
-
cleanupStarted = true;
|
|
240
|
-
for (const file of commandSpecCleanup) {
|
|
241
|
-
try {
|
|
242
|
-
await unlink(file);
|
|
243
|
-
} catch (error) {
|
|
244
|
-
log(`[${Date.now()}][CLEANUP] Failed to delete ${file}: ${error.message}\n`);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function cleanupCommandSpecSync() {
|
|
250
|
-
if (cleanupStarted) return;
|
|
251
|
-
cleanupStarted = true;
|
|
252
|
-
for (const file of commandSpecCleanup) {
|
|
253
|
-
try {
|
|
254
|
-
unlinkSync(file);
|
|
255
|
-
} catch (error) {
|
|
256
|
-
emergencyLog(`[${Date.now()}][CLEANUP] Failed to delete ${file}: ${error.message}\n`);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function writeCompletionFooter(code, signal) {
|
|
262
|
-
if (config.outputFormat === 'json') {
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
125
|
+
const outputRuntime = watcherOutputRuntime.createWatcherOutputRuntime({
|
|
126
|
+
config,
|
|
127
|
+
providerName,
|
|
128
|
+
log,
|
|
129
|
+
stopProvider: stopProviderAfterFatalOutput,
|
|
130
|
+
providerSessionCapture,
|
|
131
|
+
});
|
|
265
132
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
133
|
+
if (
|
|
134
|
+
await watcherOutputRuntime.completePendingWatcherCancellation({
|
|
135
|
+
taskId,
|
|
136
|
+
getTask,
|
|
137
|
+
commandCleanup,
|
|
138
|
+
terminateProvider: () => true,
|
|
139
|
+
updateTask,
|
|
140
|
+
emergencyLog,
|
|
141
|
+
terminalUpdates: { socketPath: null },
|
|
142
|
+
})
|
|
143
|
+
) {
|
|
144
|
+
process.exit(0);
|
|
269
145
|
}
|
|
270
146
|
|
|
271
147
|
server = new AttachServer({
|
|
@@ -280,48 +156,21 @@ server = new AttachServer({
|
|
|
280
156
|
});
|
|
281
157
|
|
|
282
158
|
server.on('output', (data) => {
|
|
283
|
-
|
|
284
|
-
const timestamp = Date.now();
|
|
285
|
-
|
|
286
|
-
const { lines, remaining } = splitBufferLines(outputBuffer, chunk);
|
|
287
|
-
outputBuffer = remaining;
|
|
288
|
-
|
|
289
|
-
if (silentJsonMode) {
|
|
290
|
-
handleSilentJsonLines(lines, timestamp);
|
|
291
|
-
} else {
|
|
292
|
-
handleStreamingLines(lines, timestamp);
|
|
293
|
-
}
|
|
159
|
+
outputBuffer = outputRuntime.consumeOutput(outputBuffer, data);
|
|
294
160
|
});
|
|
295
161
|
|
|
296
162
|
server.on('exit', async ({ exitCode, signal }) => {
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
writeCompletionFooter(code, signal);
|
|
309
|
-
await cleanupCommandSpec();
|
|
310
|
-
|
|
311
|
-
const resolvedCode = fatalError ? 1 : recovered?.payload ? 0 : code;
|
|
312
|
-
const status = resolvedCode === 0 ? 'completed' : 'failed';
|
|
313
|
-
try {
|
|
314
|
-
await updateTask(taskId, {
|
|
315
|
-
status,
|
|
316
|
-
pid: null,
|
|
317
|
-
processGroupId: null,
|
|
318
|
-
exitCode: resolvedCode,
|
|
319
|
-
error: fatalError || (resolvedCode !== 0 && signal ? `Killed by ${signal}` : null),
|
|
320
|
-
socketPath: null,
|
|
321
|
-
});
|
|
322
|
-
} catch (updateError) {
|
|
323
|
-
log(`[${Date.now()}][ERROR] Failed to update task status: ${updateError.message}\n`);
|
|
324
|
-
}
|
|
163
|
+
if (crashStarted) return;
|
|
164
|
+
const completion = outputRuntime.complete({ code: exitCode, signal, outputBuffer });
|
|
165
|
+
await watcherOutputRuntime.completeWatcherTask({
|
|
166
|
+
taskId,
|
|
167
|
+
completion,
|
|
168
|
+
commandCleanup,
|
|
169
|
+
terminateProvider: () => stopAttachableProvider(true),
|
|
170
|
+
updateTask,
|
|
171
|
+
emergencyLog,
|
|
172
|
+
terminalUpdates: { socketPath: null },
|
|
173
|
+
});
|
|
325
174
|
|
|
326
175
|
setTimeout(() => {
|
|
327
176
|
process.exit(0);
|
|
@@ -329,19 +178,7 @@ server.on('exit', async ({ exitCode, signal }) => {
|
|
|
329
178
|
});
|
|
330
179
|
|
|
331
180
|
server.on('error', async (err) => {
|
|
332
|
-
|
|
333
|
-
await cleanupCommandSpec();
|
|
334
|
-
try {
|
|
335
|
-
await updateTask(taskId, {
|
|
336
|
-
status: 'failed',
|
|
337
|
-
pid: null,
|
|
338
|
-
processGroupId: null,
|
|
339
|
-
error: err.message,
|
|
340
|
-
});
|
|
341
|
-
} catch (updateError) {
|
|
342
|
-
log(`[${Date.now()}][ERROR] Failed to update task status: ${updateError.message}\n`);
|
|
343
|
-
}
|
|
344
|
-
process.exit(1);
|
|
181
|
+
await failWatcher(err, 'attach server error');
|
|
345
182
|
});
|
|
346
183
|
|
|
347
184
|
server.on('clientAttach', ({ clientId }) => {
|
|
@@ -363,22 +200,33 @@ try {
|
|
|
363
200
|
terminationStrategy: process.platform === 'win32' ? 'process-tree' : 'process-group',
|
|
364
201
|
});
|
|
365
202
|
|
|
203
|
+
if (getTask(taskId)?.cancelRequested) {
|
|
204
|
+
crashStarted = true;
|
|
205
|
+
await watcherOutputRuntime.completePendingWatcherCancellation({
|
|
206
|
+
taskId,
|
|
207
|
+
getTask,
|
|
208
|
+
commandCleanup,
|
|
209
|
+
terminateProvider: stopAttachableProvider,
|
|
210
|
+
updateTask,
|
|
211
|
+
emergencyLog,
|
|
212
|
+
terminalUpdates: { socketPath: null },
|
|
213
|
+
});
|
|
214
|
+
process.exit(0);
|
|
215
|
+
}
|
|
216
|
+
|
|
366
217
|
log(`[${Date.now()}][SYSTEM] Started with PTY (attachable)\n`);
|
|
367
218
|
log(`[${Date.now()}][SYSTEM] Socket: ${socketPath}\n`);
|
|
368
219
|
log(`[${Date.now()}][SYSTEM] PID: ${server.pid}\n`);
|
|
369
220
|
} catch (err) {
|
|
370
|
-
|
|
371
|
-
await cleanupCommandSpec();
|
|
372
|
-
updateTask(taskId, { status: 'failed', error: err.message });
|
|
373
|
-
process.exit(1);
|
|
221
|
+
await failWatcher(err, 'attach server start');
|
|
374
222
|
}
|
|
375
223
|
|
|
376
224
|
process.on('SIGTERM', async () => {
|
|
377
225
|
log(`[${Date.now()}][SYSTEM] Received SIGTERM, stopping...\n`);
|
|
378
|
-
await
|
|
226
|
+
await stopAttachableProvider();
|
|
379
227
|
});
|
|
380
228
|
|
|
381
229
|
process.on('SIGINT', async () => {
|
|
382
230
|
log(`[${Date.now()}][SYSTEM] Received SIGINT, stopping...\n`);
|
|
383
|
-
await
|
|
231
|
+
await stopAttachableProvider();
|
|
384
232
|
});
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { lstat, realpath, rm, unlink } from 'fs/promises';
|
|
2
|
+
import { lstatSync, realpathSync, rmSync, unlinkSync } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { basename, dirname, isAbsolute, resolve } from 'path';
|
|
5
|
+
import { createRequire } from 'module';
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const {
|
|
9
|
+
isCanonicalClaudeSettingsOverlayDirectory,
|
|
10
|
+
isClaudeSettingsOverlayDirectory,
|
|
11
|
+
} = require('../src/worktree-claude-config');
|
|
12
|
+
|
|
13
|
+
const CLEANUP_METADATA_KEYS = ['kind', 'path', 'provider', 'reason'];
|
|
14
|
+
const SCHEMA_DIRECTORY_PATTERN = /^zeroshot-schema-[A-Za-z0-9_-]+$/u;
|
|
15
|
+
const SCHEMA_FILE_PATTERN =
|
|
16
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.json$/u;
|
|
17
|
+
|
|
18
|
+
function assertClosedCleanupMetadata(cleanupPath, metadata) {
|
|
19
|
+
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
|
|
20
|
+
throw new Error(`Refusing cleanup without closed ownership metadata: ${cleanupPath}`);
|
|
21
|
+
}
|
|
22
|
+
const keys = Object.keys(metadata).sort();
|
|
23
|
+
if (
|
|
24
|
+
keys.length !== CLEANUP_METADATA_KEYS.length ||
|
|
25
|
+
keys.some((key, index) => key !== CLEANUP_METADATA_KEYS[index])
|
|
26
|
+
) {
|
|
27
|
+
throw new Error(`Refusing cleanup with open ownership metadata: ${cleanupPath}`);
|
|
28
|
+
}
|
|
29
|
+
if (metadata.path !== cleanupPath) {
|
|
30
|
+
throw new Error(`Refusing cleanup with mismatched ownership path: ${cleanupPath}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createCleanupPlan(commandSpec) {
|
|
35
|
+
if (!Array.isArray(commandSpec?.cleanup) || !Array.isArray(commandSpec?.cleanupMetadata)) {
|
|
36
|
+
throw new Error('Refusing cleanup with malformed cleanup collections');
|
|
37
|
+
}
|
|
38
|
+
if (commandSpec.cleanup.length !== commandSpec.cleanupMetadata.length) {
|
|
39
|
+
throw new Error('Refusing cleanup without one closed metadata receipt per path');
|
|
40
|
+
}
|
|
41
|
+
const uniquePaths = new Set(commandSpec.cleanup);
|
|
42
|
+
if (uniquePaths.size !== commandSpec.cleanup.length) {
|
|
43
|
+
throw new Error('Refusing cleanup with duplicate cleanup paths');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return commandSpec.cleanup.map((cleanupPath) => {
|
|
47
|
+
if (typeof cleanupPath !== 'string' || cleanupPath.length === 0) {
|
|
48
|
+
throw new Error('Refusing cleanup with a non-string or empty path');
|
|
49
|
+
}
|
|
50
|
+
const matches = commandSpec.cleanupMetadata.filter(
|
|
51
|
+
(metadata) => metadata?.path === cleanupPath
|
|
52
|
+
);
|
|
53
|
+
if (matches.length !== 1) {
|
|
54
|
+
throw new Error(`Refusing cleanup without exact ownership metadata: ${cleanupPath}`);
|
|
55
|
+
}
|
|
56
|
+
assertClosedCleanupMetadata(cleanupPath, matches[0]);
|
|
57
|
+
return { cleanupPath, metadata: matches[0] };
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function assertOwnedTempDirectory(cleanupPath, metadata) {
|
|
62
|
+
if (
|
|
63
|
+
metadata.provider !== 'claude' ||
|
|
64
|
+
metadata.reason !== 'settings-overlay' ||
|
|
65
|
+
!isCanonicalClaudeSettingsOverlayDirectory(cleanupPath)
|
|
66
|
+
) {
|
|
67
|
+
throw new Error(`Refusing unowned temporary directory cleanup: ${cleanupPath}`);
|
|
68
|
+
}
|
|
69
|
+
if (isClaudeSettingsOverlayDirectory(cleanupPath)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
lstatSync(cleanupPath);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error?.code === 'ENOENT') return true;
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`Refusing unowned temporary directory cleanup: ${cleanupPath}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function assertCanonicalSchemaPath(cleanupPath, metadata) {
|
|
82
|
+
if (
|
|
83
|
+
metadata.kind !== 'temp-file' ||
|
|
84
|
+
metadata.provider !== 'codex' ||
|
|
85
|
+
metadata.reason !== 'output-schema' ||
|
|
86
|
+
!isAbsolute(cleanupPath) ||
|
|
87
|
+
resolve(cleanupPath) !== cleanupPath
|
|
88
|
+
) {
|
|
89
|
+
throw new Error(`Refusing unowned output-schema cleanup: ${cleanupPath}`);
|
|
90
|
+
}
|
|
91
|
+
const schemaDirectory = dirname(cleanupPath);
|
|
92
|
+
if (
|
|
93
|
+
dirname(schemaDirectory) !== resolve(tmpdir()) ||
|
|
94
|
+
!SCHEMA_DIRECTORY_PATTERN.test(basename(schemaDirectory)) ||
|
|
95
|
+
!SCHEMA_FILE_PATTERN.test(basename(cleanupPath))
|
|
96
|
+
) {
|
|
97
|
+
throw new Error(`Refusing non-canonical output-schema cleanup: ${cleanupPath}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function assertOwnedSchemaFile(cleanupPath, metadata) {
|
|
102
|
+
assertCanonicalSchemaPath(cleanupPath, metadata);
|
|
103
|
+
const schemaDirectory = dirname(cleanupPath);
|
|
104
|
+
const [tempRoot, realSchemaDirectory, directoryStat, schemaStat] = await Promise.all([
|
|
105
|
+
realpath(tmpdir()),
|
|
106
|
+
realpath(schemaDirectory),
|
|
107
|
+
lstat(schemaDirectory),
|
|
108
|
+
lstat(cleanupPath),
|
|
109
|
+
]);
|
|
110
|
+
if (
|
|
111
|
+
directoryStat.isSymbolicLink() ||
|
|
112
|
+
!directoryStat.isDirectory() ||
|
|
113
|
+
dirname(realSchemaDirectory) !== tempRoot ||
|
|
114
|
+
basename(realSchemaDirectory) !== basename(schemaDirectory) ||
|
|
115
|
+
schemaStat.isSymbolicLink() ||
|
|
116
|
+
!schemaStat.isFile()
|
|
117
|
+
) {
|
|
118
|
+
throw new Error(`Refusing non-regular or escaped output-schema cleanup: ${cleanupPath}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function assertOwnedSchemaFileSync(cleanupPath, metadata) {
|
|
123
|
+
assertCanonicalSchemaPath(cleanupPath, metadata);
|
|
124
|
+
const schemaDirectory = dirname(cleanupPath);
|
|
125
|
+
const tempRoot = realpathSync(tmpdir());
|
|
126
|
+
const realSchemaDirectory = realpathSync(schemaDirectory);
|
|
127
|
+
const directoryStat = lstatSync(schemaDirectory);
|
|
128
|
+
const schemaStat = lstatSync(cleanupPath);
|
|
129
|
+
if (
|
|
130
|
+
directoryStat.isSymbolicLink() ||
|
|
131
|
+
!directoryStat.isDirectory() ||
|
|
132
|
+
dirname(realSchemaDirectory) !== tempRoot ||
|
|
133
|
+
basename(realSchemaDirectory) !== basename(schemaDirectory) ||
|
|
134
|
+
schemaStat.isSymbolicLink() ||
|
|
135
|
+
!schemaStat.isFile()
|
|
136
|
+
) {
|
|
137
|
+
throw new Error(`Refusing non-regular or escaped output-schema cleanup: ${cleanupPath}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function removeCleanupPath(cleanupPath, metadata) {
|
|
142
|
+
if (metadata.kind === 'temp-directory') {
|
|
143
|
+
if (assertOwnedTempDirectory(cleanupPath, metadata)) return;
|
|
144
|
+
await rm(cleanupPath, { recursive: true, force: true });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
await assertOwnedSchemaFile(cleanupPath, metadata);
|
|
148
|
+
await unlink(cleanupPath);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function removeCleanupPathSync(cleanupPath, metadata) {
|
|
152
|
+
if (metadata.kind === 'temp-directory') {
|
|
153
|
+
if (assertOwnedTempDirectory(cleanupPath, metadata)) return;
|
|
154
|
+
rmSync(cleanupPath, { recursive: true, force: true });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
assertOwnedSchemaFileSync(cleanupPath, metadata);
|
|
158
|
+
unlinkSync(cleanupPath);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Build one idempotent cleanup owner for a provider command. Callers may run it
|
|
163
|
+
* only after the persisted provider termination boundary is terminal.
|
|
164
|
+
*/
|
|
165
|
+
export function createCommandSpecCleanup(commandSpec, logFailure) {
|
|
166
|
+
let started = false;
|
|
167
|
+
let result = true;
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
async run() {
|
|
171
|
+
if (started) return result;
|
|
172
|
+
started = true;
|
|
173
|
+
let cleanupPlan;
|
|
174
|
+
try {
|
|
175
|
+
cleanupPlan = createCleanupPlan(commandSpec);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
logFailure('<command-cleanup>', error);
|
|
178
|
+
result = false;
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
let succeeded = true;
|
|
182
|
+
for (const { cleanupPath, metadata } of cleanupPlan) {
|
|
183
|
+
try {
|
|
184
|
+
await removeCleanupPath(cleanupPath, metadata);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (error?.code === 'ENOENT') continue;
|
|
187
|
+
succeeded = false;
|
|
188
|
+
logFailure(cleanupPath, error);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
result = succeeded;
|
|
192
|
+
return result;
|
|
193
|
+
},
|
|
194
|
+
runSync() {
|
|
195
|
+
if (started) return result;
|
|
196
|
+
started = true;
|
|
197
|
+
let cleanupPlan;
|
|
198
|
+
try {
|
|
199
|
+
cleanupPlan = createCleanupPlan(commandSpec);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
logFailure('<command-cleanup>', error);
|
|
202
|
+
result = false;
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
let succeeded = true;
|
|
206
|
+
for (const { cleanupPath, metadata } of cleanupPlan) {
|
|
207
|
+
try {
|
|
208
|
+
removeCleanupPathSync(cleanupPath, metadata);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (error?.code === 'ENOENT') continue;
|
|
211
|
+
succeeded = false;
|
|
212
|
+
logFailure(cleanupPath, error);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
result = succeeded;
|
|
216
|
+
return result;
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|