@covibes/zeroshot 5.2.1 → 5.4.0
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/CHANGELOG.md +174 -189
- package/README.md +226 -195
- package/cli/commands/providers.js +149 -0
- package/cli/index.js +3145 -2366
- package/cli/lib/first-run.js +40 -3
- package/cli/message-formatters-normal.js +28 -6
- package/cluster-templates/base-templates/debug-workflow.json +24 -78
- package/cluster-templates/base-templates/full-workflow.json +99 -316
- package/cluster-templates/base-templates/single-worker.json +23 -15
- package/cluster-templates/base-templates/worker-validator.json +105 -36
- package/cluster-templates/conductor-bootstrap.json +9 -7
- package/lib/docker-config.js +14 -1
- package/lib/git-remote-utils.js +165 -0
- package/lib/id-detector.js +10 -7
- package/lib/provider-defaults.js +62 -0
- package/lib/provider-detection.js +59 -0
- package/lib/provider-names.js +57 -0
- package/lib/settings/claude-auth.js +78 -0
- package/lib/settings.js +298 -15
- package/lib/stream-json-parser.js +4 -238
- package/package.json +27 -6
- package/scripts/setup-merge-queue.sh +170 -0
- package/scripts/validate-templates.js +100 -0
- package/src/agent/agent-config.js +140 -63
- package/src/agent/agent-context-builder.js +336 -165
- package/src/agent/agent-hook-executor.js +337 -67
- package/src/agent/agent-lifecycle.js +386 -287
- package/src/agent/agent-stuck-detector.js +7 -7
- package/src/agent/agent-task-executor.js +944 -683
- package/src/agent/output-extraction.js +217 -0
- package/src/agent/output-reformatter.js +175 -0
- package/src/agent/schema-utils.js +146 -0
- package/src/agent-wrapper.js +112 -31
- package/src/agents/git-pusher-template.js +285 -0
- package/src/claude-task-runner.js +145 -44
- package/src/config-router.js +13 -13
- package/src/config-validator.js +1049 -563
- package/src/input-helpers.js +65 -0
- package/src/isolation-manager.js +499 -320
- package/src/issue-providers/README.md +305 -0
- package/src/issue-providers/azure-devops-provider.js +273 -0
- package/src/issue-providers/base-provider.js +232 -0
- package/src/issue-providers/github-provider.js +179 -0
- package/src/issue-providers/gitlab-provider.js +241 -0
- package/src/issue-providers/index.js +196 -0
- package/src/issue-providers/jira-provider.js +239 -0
- package/src/ledger.js +50 -11
- package/src/lib/safe-exec.js +88 -0
- package/src/orchestrator.js +1348 -757
- package/src/preflight.js +306 -149
- package/src/process-metrics.js +98 -56
- package/src/providers/anthropic/cli-builder.js +73 -0
- package/src/providers/anthropic/index.js +204 -0
- package/src/providers/anthropic/models.js +23 -0
- package/src/providers/anthropic/output-parser.js +177 -0
- package/src/providers/base-provider.js +251 -0
- package/src/providers/capabilities.js +60 -0
- package/src/providers/google/cli-builder.js +55 -0
- package/src/providers/google/index.js +116 -0
- package/src/providers/google/models.js +24 -0
- package/src/providers/google/output-parser.js +101 -0
- package/src/providers/index.js +91 -0
- package/src/providers/openai/cli-builder.js +133 -0
- package/src/providers/openai/index.js +136 -0
- package/src/providers/openai/models.js +21 -0
- package/src/providers/openai/output-parser.js +143 -0
- package/src/providers/opencode/cli-builder.js +42 -0
- package/src/providers/opencode/index.js +103 -0
- package/src/providers/opencode/models.js +55 -0
- package/src/providers/opencode/output-parser.js +122 -0
- package/src/schemas/sub-cluster.js +68 -39
- package/src/status-footer.js +94 -48
- package/src/sub-cluster-wrapper.js +92 -36
- package/src/task-runner.js +8 -6
- package/src/template-resolver.js +12 -9
- package/src/tui/data-poller.js +123 -99
- package/src/tui/formatters.js +4 -3
- package/src/tui/keybindings.js +259 -318
- package/src/tui/layout.js +20 -3
- package/src/tui/renderer.js +11 -21
- package/task-lib/attachable-watcher.js +150 -111
- package/task-lib/claude-recovery.js +156 -0
- package/task-lib/commands/episodes.js +105 -0
- package/task-lib/commands/list.js +3 -3
- package/task-lib/commands/logs.js +231 -189
- package/task-lib/commands/resume.js +3 -2
- package/task-lib/commands/run.js +12 -3
- package/task-lib/commands/schedules.js +111 -61
- package/task-lib/config.js +0 -2
- package/task-lib/runner.js +128 -50
- package/task-lib/scheduler.js +3 -3
- package/task-lib/store.js +464 -152
- package/task-lib/tui/formatters.js +94 -45
- package/task-lib/tui/renderer.js +13 -3
- package/task-lib/tui.js +73 -32
- package/task-lib/watcher.js +157 -100
- package/src/agents/git-pusher-agent.json +0 -20
- package/src/github.js +0 -103
package/src/tui/renderer.js
CHANGED
|
@@ -37,11 +37,9 @@ class Renderer {
|
|
|
37
37
|
* @param {Array} clusters - Array of cluster objects
|
|
38
38
|
*/
|
|
39
39
|
renderClustersTable(clusters) {
|
|
40
|
-
|
|
41
|
-
clusters = [];
|
|
42
|
-
}
|
|
40
|
+
const clusterList = !clusters || !Array.isArray(clusters) ? [] : clusters;
|
|
43
41
|
|
|
44
|
-
const data =
|
|
42
|
+
const data = clusterList.map((c) => {
|
|
45
43
|
if (!c) return ['', '', '', ''];
|
|
46
44
|
|
|
47
45
|
const icon = stateIcon(c.state || 'unknown');
|
|
@@ -68,23 +66,19 @@ class Renderer {
|
|
|
68
66
|
* @param {Map} resourceStats - Map of PID -> {cpu, memory}
|
|
69
67
|
*/
|
|
70
68
|
renderSystemStats(clusters, resourceStats) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
if (!resourceStats || !(resourceStats instanceof Map)) {
|
|
75
|
-
resourceStats = new Map();
|
|
76
|
-
}
|
|
69
|
+
const clusterList = !clusters || !Array.isArray(clusters) ? [] : clusters;
|
|
70
|
+
const statsMap = !resourceStats || !(resourceStats instanceof Map) ? new Map() : resourceStats;
|
|
77
71
|
|
|
78
72
|
// Calculate aggregate stats
|
|
79
|
-
const activeClusters =
|
|
80
|
-
const totalAgents =
|
|
73
|
+
const activeClusters = clusterList.filter((c) => c && c.state === 'running').length;
|
|
74
|
+
const totalAgents = clusterList.reduce((sum, c) => sum + (c?.agentCount || 0), 0);
|
|
81
75
|
|
|
82
76
|
// Calculate average CPU and memory from resource stats
|
|
83
77
|
let totalCpu = 0;
|
|
84
78
|
let totalMemory = 0;
|
|
85
79
|
let statCount = 0;
|
|
86
80
|
|
|
87
|
-
|
|
81
|
+
statsMap.forEach((stat) => {
|
|
88
82
|
if (stat && typeof stat.cpu === 'number' && typeof stat.memory === 'number') {
|
|
89
83
|
totalCpu += stat.cpu;
|
|
90
84
|
totalMemory += stat.memory;
|
|
@@ -125,18 +119,14 @@ class Renderer {
|
|
|
125
119
|
return;
|
|
126
120
|
}
|
|
127
121
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
if (!resourceStats || !(resourceStats instanceof Map)) {
|
|
132
|
-
resourceStats = new Map();
|
|
133
|
-
}
|
|
122
|
+
const agentList = !agents || !Array.isArray(agents) ? [] : agents;
|
|
123
|
+
const statsMap = !resourceStats || !(resourceStats instanceof Map) ? new Map() : resourceStats;
|
|
134
124
|
|
|
135
|
-
const data =
|
|
125
|
+
const data = agentList.map((a) => {
|
|
136
126
|
if (!a) return ['', '', '', '', '', ''];
|
|
137
127
|
|
|
138
128
|
const pid = a.pid;
|
|
139
|
-
const stats =
|
|
129
|
+
const stats = statsMap.get(pid) || { cpu: 0, memory: 0 };
|
|
140
130
|
|
|
141
131
|
const agentId = truncate(a.id || '', 12);
|
|
142
132
|
const role = truncate(a.role || '', 12);
|
|
@@ -1,42 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Attachable Watcher - spawns
|
|
5
|
-
*
|
|
4
|
+
* Attachable Watcher - spawns a CLI process with PTY for attach/detach support
|
|
6
5
|
* Runs detached from parent, provides Unix socket for attach clients.
|
|
7
|
-
* Uses node-pty for proper terminal emulation.
|
|
8
|
-
*
|
|
9
|
-
* Key differences from legacy watcher.js:
|
|
10
|
-
* - Uses AttachServer (node-pty) instead of child_process.spawn
|
|
11
|
-
* - Creates Unix socket at ~/.zeroshot/sockets/task-<id>.sock
|
|
12
|
-
* - Supports multiple attached clients
|
|
13
|
-
* - Still writes to log file for backward compatibility
|
|
14
|
-
*
|
|
15
|
-
* CRITICAL: Global error handlers installed FIRST to catch silent crashes
|
|
16
6
|
*/
|
|
17
7
|
|
|
18
8
|
import { appendFileSync, existsSync, mkdirSync } from 'fs';
|
|
19
9
|
import { join } from 'path';
|
|
20
10
|
import { homedir } from 'os';
|
|
21
11
|
import { updateTask } from './store.js';
|
|
12
|
+
import { detectStreamingModeError, recoverStructuredOutput } from './claude-recovery.js';
|
|
13
|
+
import { createRequire } from 'module';
|
|
22
14
|
|
|
23
15
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
24
16
|
// 🔴 CRITICAL: Global error handlers - MUST be installed BEFORE any async ops
|
|
25
17
|
// Without these, uncaught errors cause SILENT process death (no logs, no status)
|
|
26
18
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
27
19
|
|
|
28
|
-
// Parse args early so we can log errors to the correct file
|
|
29
20
|
const [, , taskIdArg, cwdArg, logFileArg, argsJsonArg, configJsonArg] = process.argv;
|
|
30
21
|
|
|
31
|
-
/**
|
|
32
|
-
* Emergency logger - works even if main log function isn't ready
|
|
33
|
-
*/
|
|
34
22
|
function emergencyLog(msg) {
|
|
35
23
|
if (logFileArg) {
|
|
36
24
|
try {
|
|
37
25
|
appendFileSync(logFileArg, msg);
|
|
38
26
|
} catch {
|
|
39
|
-
// Last resort - stderr
|
|
40
27
|
process.stderr.write(msg);
|
|
41
28
|
}
|
|
42
29
|
} else {
|
|
@@ -44,9 +31,6 @@ function emergencyLog(msg) {
|
|
|
44
31
|
}
|
|
45
32
|
}
|
|
46
33
|
|
|
47
|
-
/**
|
|
48
|
-
* Mark task as failed and exit
|
|
49
|
-
*/
|
|
50
34
|
function crashWithError(error, source) {
|
|
51
35
|
const timestamp = Date.now();
|
|
52
36
|
const errorMsg = error instanceof Error ? error.stack || error.message : String(error);
|
|
@@ -54,7 +38,6 @@ function crashWithError(error, source) {
|
|
|
54
38
|
emergencyLog(`\n[${timestamp}][CRASH] ${source}: ${errorMsg}\n`);
|
|
55
39
|
emergencyLog(`[${timestamp}][CRASH] Process terminating due to unhandled error\n`);
|
|
56
40
|
|
|
57
|
-
// Try to update task status - may fail if error is in store.js itself
|
|
58
41
|
if (taskIdArg) {
|
|
59
42
|
try {
|
|
60
43
|
updateTask(taskIdArg, {
|
|
@@ -67,11 +50,9 @@ function crashWithError(error, source) {
|
|
|
67
50
|
}
|
|
68
51
|
}
|
|
69
52
|
|
|
70
|
-
// Exit with error code
|
|
71
53
|
process.exit(1);
|
|
72
54
|
}
|
|
73
55
|
|
|
74
|
-
// Install handlers IMMEDIATELY
|
|
75
56
|
process.on('uncaughtException', (error) => {
|
|
76
57
|
crashWithError(error, 'uncaughtException');
|
|
77
58
|
});
|
|
@@ -80,24 +61,19 @@ process.on('unhandledRejection', (reason) => {
|
|
|
80
61
|
crashWithError(reason, 'unhandledRejection');
|
|
81
62
|
});
|
|
82
63
|
|
|
83
|
-
// Import attach infrastructure from src package (CommonJS)
|
|
84
|
-
import { createRequire } from 'module';
|
|
85
64
|
const require = createRequire(import.meta.url);
|
|
86
65
|
const { AttachServer } = require('../src/attach');
|
|
87
|
-
const {
|
|
66
|
+
const { normalizeProviderName } = require('../lib/provider-names');
|
|
88
67
|
|
|
89
|
-
// Use the args parsed earlier (during error handler setup)
|
|
90
68
|
const taskId = taskIdArg;
|
|
91
69
|
const cwd = cwdArg;
|
|
92
70
|
const logFile = logFileArg;
|
|
93
71
|
const args = JSON.parse(argsJsonArg);
|
|
94
72
|
const config = configJsonArg ? JSON.parse(configJsonArg) : {};
|
|
95
73
|
|
|
96
|
-
// Socket path for attach
|
|
97
74
|
const SOCKET_DIR = join(homedir(), '.zeroshot', 'sockets');
|
|
98
75
|
const socketPath = join(SOCKET_DIR, `${taskId}.sock`);
|
|
99
76
|
|
|
100
|
-
// Ensure socket directory exists
|
|
101
77
|
if (!existsSync(SOCKET_DIR)) {
|
|
102
78
|
mkdirSync(SOCKET_DIR, { recursive: true });
|
|
103
79
|
}
|
|
@@ -106,33 +82,133 @@ function log(msg) {
|
|
|
106
82
|
appendFileSync(logFile, msg);
|
|
107
83
|
}
|
|
108
84
|
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
// Add model flag - priority: config.model > ANTHROPIC_MODEL env var
|
|
113
|
-
const claudeArgs = [...args];
|
|
114
|
-
const model = config.model || env.ANTHROPIC_MODEL;
|
|
115
|
-
if (model && !claudeArgs.includes('--model')) {
|
|
116
|
-
claudeArgs.unshift('--model', model);
|
|
117
|
-
}
|
|
85
|
+
const providerName = normalizeProviderName(config.provider || 'claude');
|
|
86
|
+
const enableRecovery = providerName === 'claude';
|
|
118
87
|
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
const finalArgs = [...
|
|
88
|
+
const env = { ...process.env, ...(config.env || {}) };
|
|
89
|
+
const command = config.command || 'claude';
|
|
90
|
+
const finalArgs = [...args];
|
|
122
91
|
|
|
123
|
-
// For JSON schema output with silent mode, track final result
|
|
124
92
|
const silentJsonMode =
|
|
125
|
-
config.outputFormat === 'json' && config.jsonSchema && config.silentJsonOutput;
|
|
126
|
-
let finalResultJson = null;
|
|
93
|
+
config.outputFormat === 'json' && config.jsonSchema && config.silentJsonOutput && enableRecovery;
|
|
127
94
|
|
|
128
|
-
|
|
95
|
+
let finalResultJson = null;
|
|
129
96
|
let outputBuffer = '';
|
|
97
|
+
let streamingModeError = null;
|
|
98
|
+
|
|
99
|
+
function splitBufferLines(buffer, chunk) {
|
|
100
|
+
const nextBuffer = buffer + chunk;
|
|
101
|
+
const lines = nextBuffer.split('\n');
|
|
102
|
+
const remaining = lines.pop() || '';
|
|
103
|
+
return { lines, remaining };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function captureStreamingError(line, timestamp) {
|
|
107
|
+
if (!enableRecovery) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const detectedError = detectStreamingModeError(line);
|
|
112
|
+
if (!detectedError) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
streamingModeError = { ...detectedError, timestamp };
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function maybeCaptureStructuredOutput(line) {
|
|
121
|
+
try {
|
|
122
|
+
const json = JSON.parse(line);
|
|
123
|
+
if (json.structured_output) {
|
|
124
|
+
finalResultJson = line;
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
// Not JSON, skip
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function handleSilentJsonLines(lines, timestamp) {
|
|
132
|
+
for (const line of lines) {
|
|
133
|
+
if (!line.trim()) continue;
|
|
134
|
+
if (captureStreamingError(line, timestamp)) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
maybeCaptureStructuredOutput(line);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function handleStreamingLines(lines, timestamp) {
|
|
142
|
+
for (const line of lines) {
|
|
143
|
+
if (captureStreamingError(line, timestamp)) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
log(`[${timestamp}]${line}\n`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function flushOutputBuffer(timestamp) {
|
|
151
|
+
if (!outputBuffer.trim()) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!enableRecovery) {
|
|
156
|
+
if (!silentJsonMode) {
|
|
157
|
+
log(`[${timestamp}]${outputBuffer}\n`);
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (captureStreamingError(outputBuffer, timestamp)) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (silentJsonMode) {
|
|
167
|
+
maybeCaptureStructuredOutput(outputBuffer);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
log(`[${timestamp}]${outputBuffer}\n`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function attemptRecovery(code, timestamp) {
|
|
175
|
+
if (!(enableRecovery && code !== 0 && streamingModeError?.sessionId)) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const recovered = recoverStructuredOutput(streamingModeError.sessionId);
|
|
180
|
+
if (recovered?.payload) {
|
|
181
|
+
const recoveredLine = JSON.stringify(recovered.payload);
|
|
182
|
+
if (silentJsonMode) {
|
|
183
|
+
finalResultJson = recoveredLine;
|
|
184
|
+
} else {
|
|
185
|
+
log(`[${timestamp}]${recoveredLine}\n`);
|
|
186
|
+
}
|
|
187
|
+
} else if (streamingModeError.line) {
|
|
188
|
+
if (silentJsonMode) {
|
|
189
|
+
log(streamingModeError.line + '\n');
|
|
190
|
+
} else {
|
|
191
|
+
log(`[${streamingModeError.timestamp}]${streamingModeError.line}\n`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return recovered;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function writeCompletionFooter(code, signal) {
|
|
199
|
+
if (config.outputFormat === 'json') {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
log(`\n${'='.repeat(50)}\n`);
|
|
204
|
+
log(`Finished: ${new Date().toISOString()}\n`);
|
|
205
|
+
log(`Exit code: ${code}, Signal: ${signal}\n`);
|
|
206
|
+
}
|
|
130
207
|
|
|
131
|
-
// Create AttachServer to spawn Claude with PTY
|
|
132
208
|
const server = new AttachServer({
|
|
133
209
|
id: taskId,
|
|
134
210
|
socketPath,
|
|
135
|
-
command
|
|
211
|
+
command,
|
|
136
212
|
args: finalArgs,
|
|
137
213
|
cwd,
|
|
138
214
|
env,
|
|
@@ -140,96 +216,62 @@ const server = new AttachServer({
|
|
|
140
216
|
rows: 30,
|
|
141
217
|
});
|
|
142
218
|
|
|
143
|
-
// Handle output from PTY
|
|
144
219
|
server.on('output', (data) => {
|
|
145
220
|
const chunk = data.toString();
|
|
146
221
|
const timestamp = Date.now();
|
|
147
222
|
|
|
223
|
+
const { lines, remaining } = splitBufferLines(outputBuffer, chunk);
|
|
224
|
+
outputBuffer = remaining;
|
|
225
|
+
|
|
148
226
|
if (silentJsonMode) {
|
|
149
|
-
|
|
150
|
-
outputBuffer += chunk;
|
|
151
|
-
const lines = outputBuffer.split('\n');
|
|
152
|
-
outputBuffer = lines.pop() || '';
|
|
153
|
-
|
|
154
|
-
for (const line of lines) {
|
|
155
|
-
if (!line.trim()) continue;
|
|
156
|
-
try {
|
|
157
|
-
const json = JSON.parse(line);
|
|
158
|
-
if (json.structured_output) {
|
|
159
|
-
finalResultJson = line;
|
|
160
|
-
}
|
|
161
|
-
} catch {
|
|
162
|
-
// Not JSON, skip
|
|
163
|
-
}
|
|
164
|
-
}
|
|
227
|
+
handleSilentJsonLines(lines, timestamp);
|
|
165
228
|
} else {
|
|
166
|
-
|
|
167
|
-
outputBuffer += chunk;
|
|
168
|
-
const lines = outputBuffer.split('\n');
|
|
169
|
-
outputBuffer = lines.pop() || '';
|
|
170
|
-
|
|
171
|
-
for (const line of lines) {
|
|
172
|
-
log(`[${timestamp}]${line}\n`);
|
|
173
|
-
}
|
|
229
|
+
handleStreamingLines(lines, timestamp);
|
|
174
230
|
}
|
|
175
231
|
});
|
|
176
232
|
|
|
177
|
-
|
|
178
|
-
server.on('exit', ({ exitCode, signal }) => {
|
|
233
|
+
server.on('exit', async ({ exitCode, signal }) => {
|
|
179
234
|
const timestamp = Date.now();
|
|
180
235
|
const code = exitCode;
|
|
181
236
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
const json = JSON.parse(outputBuffer);
|
|
187
|
-
if (json.structured_output) {
|
|
188
|
-
finalResultJson = outputBuffer;
|
|
189
|
-
}
|
|
190
|
-
} catch {
|
|
191
|
-
// Not valid JSON
|
|
192
|
-
}
|
|
193
|
-
} else {
|
|
194
|
-
log(`[${timestamp}]${outputBuffer}\n`);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
237
|
+
flushOutputBuffer(timestamp);
|
|
238
|
+
|
|
239
|
+
const recovered = attemptRecovery(code, timestamp);
|
|
197
240
|
|
|
198
|
-
// In silent JSON mode, log ONLY the final structured_output JSON
|
|
199
241
|
if (silentJsonMode && finalResultJson) {
|
|
200
242
|
log(finalResultJson + '\n');
|
|
201
243
|
}
|
|
202
244
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
245
|
+
writeCompletionFooter(code, signal);
|
|
246
|
+
|
|
247
|
+
const resolvedCode = recovered?.payload ? 0 : code;
|
|
248
|
+
const status = resolvedCode === 0 ? 'completed' : 'failed';
|
|
249
|
+
try {
|
|
250
|
+
await updateTask(taskId, {
|
|
251
|
+
status,
|
|
252
|
+
exitCode: resolvedCode,
|
|
253
|
+
error: resolvedCode !== 0 && signal ? `Killed by ${signal}` : null,
|
|
254
|
+
socketPath: null,
|
|
255
|
+
});
|
|
256
|
+
} catch (updateError) {
|
|
257
|
+
log(`[${Date.now()}][ERROR] Failed to update task status: ${updateError.message}\n`);
|
|
208
258
|
}
|
|
209
259
|
|
|
210
|
-
// Simple status: completed if exit 0, failed otherwise
|
|
211
|
-
const status = code === 0 ? 'completed' : 'failed';
|
|
212
|
-
updateTask(taskId, {
|
|
213
|
-
status,
|
|
214
|
-
exitCode: code,
|
|
215
|
-
error: signal ? `Killed by ${signal}` : null,
|
|
216
|
-
socketPath: null, // Clear socket path on exit
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
// Give clients time to receive exit message before exiting
|
|
220
260
|
setTimeout(() => {
|
|
221
261
|
process.exit(0);
|
|
222
262
|
}, 500);
|
|
223
263
|
});
|
|
224
264
|
|
|
225
|
-
|
|
226
|
-
server.on('error', (err) => {
|
|
265
|
+
server.on('error', async (err) => {
|
|
227
266
|
log(`\nError: ${err.message}\n`);
|
|
228
|
-
|
|
267
|
+
try {
|
|
268
|
+
await updateTask(taskId, { status: 'failed', error: err.message });
|
|
269
|
+
} catch (updateError) {
|
|
270
|
+
log(`[${Date.now()}][ERROR] Failed to update task status: ${updateError.message}\n`);
|
|
271
|
+
}
|
|
229
272
|
process.exit(1);
|
|
230
273
|
});
|
|
231
274
|
|
|
232
|
-
// Handle client attach/detach for logging
|
|
233
275
|
server.on('clientAttach', ({ clientId }) => {
|
|
234
276
|
log(`[${Date.now()}][ATTACH] Client attached: ${clientId.slice(0, 8)}...\n`);
|
|
235
277
|
});
|
|
@@ -238,11 +280,9 @@ server.on('clientDetach', ({ clientId }) => {
|
|
|
238
280
|
log(`[${Date.now()}][DETACH] Client detached: ${clientId.slice(0, 8)}...\n`);
|
|
239
281
|
});
|
|
240
282
|
|
|
241
|
-
// Start the server
|
|
242
283
|
try {
|
|
243
284
|
await server.start();
|
|
244
285
|
|
|
245
|
-
// Update task with PID and socket path
|
|
246
286
|
updateTask(taskId, {
|
|
247
287
|
pid: server.pid,
|
|
248
288
|
socketPath,
|
|
@@ -258,7 +298,6 @@ try {
|
|
|
258
298
|
process.exit(1);
|
|
259
299
|
}
|
|
260
300
|
|
|
261
|
-
// Handle process signals for cleanup
|
|
262
301
|
process.on('SIGTERM', async () => {
|
|
263
302
|
log(`[${Date.now()}][SYSTEM] Received SIGTERM, stopping...\n`);
|
|
264
303
|
await server.stop('SIGTERM');
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
|
|
5
|
+
export const STREAMING_MODE_ERROR = 'only prompt commands are supported in streaming mode';
|
|
6
|
+
|
|
7
|
+
export function detectStreamingModeError(line) {
|
|
8
|
+
const trimmed = typeof line === 'string' ? line.trim() : '';
|
|
9
|
+
if (!trimmed.startsWith('{')) return null;
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(trimmed);
|
|
13
|
+
if (
|
|
14
|
+
parsed &&
|
|
15
|
+
parsed.type === 'result' &&
|
|
16
|
+
parsed.is_error === true &&
|
|
17
|
+
Array.isArray(parsed.errors) &&
|
|
18
|
+
parsed.errors.includes(STREAMING_MODE_ERROR) &&
|
|
19
|
+
typeof parsed.session_id === 'string'
|
|
20
|
+
) {
|
|
21
|
+
return {
|
|
22
|
+
sessionId: parsed.session_id,
|
|
23
|
+
line: trimmed,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// Ignore parse errors - not JSON
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function findSessionJsonlPath(sessionId) {
|
|
34
|
+
const claudeDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
|
|
35
|
+
const projectsDir = join(claudeDir, 'projects');
|
|
36
|
+
if (!existsSync(projectsDir)) return null;
|
|
37
|
+
|
|
38
|
+
const target = `${sessionId}.jsonl`;
|
|
39
|
+
const queue = [projectsDir];
|
|
40
|
+
|
|
41
|
+
while (queue.length > 0) {
|
|
42
|
+
const dir = queue.pop();
|
|
43
|
+
if (!dir) continue;
|
|
44
|
+
|
|
45
|
+
let entries;
|
|
46
|
+
try {
|
|
47
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
48
|
+
} catch {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
if (entry.isFile() && entry.name === target) {
|
|
54
|
+
return join(dir, entry.name);
|
|
55
|
+
}
|
|
56
|
+
if (entry.isDirectory()) {
|
|
57
|
+
queue.push(join(dir, entry.name));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function recoverStructuredOutput(sessionId) {
|
|
66
|
+
const jsonlPath = findSessionJsonlPath(sessionId);
|
|
67
|
+
if (!jsonlPath) return null;
|
|
68
|
+
|
|
69
|
+
const fileContents = readJsonlFile(jsonlPath);
|
|
70
|
+
if (!fileContents) return null;
|
|
71
|
+
|
|
72
|
+
const { structuredOutput, usage } = findStructuredOutput(fileContents);
|
|
73
|
+
|
|
74
|
+
if (!structuredOutput) return null;
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
payload: buildStructuredOutputPayload(sessionId, structuredOutput, usage),
|
|
78
|
+
sourcePath: jsonlPath,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readJsonlFile(jsonlPath) {
|
|
83
|
+
try {
|
|
84
|
+
return readFileSync(jsonlPath, 'utf8');
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function findStructuredOutput(fileContents) {
|
|
91
|
+
const lines = fileContents.split('\n');
|
|
92
|
+
let structuredOutput = null;
|
|
93
|
+
let usage = null;
|
|
94
|
+
|
|
95
|
+
for (const line of lines) {
|
|
96
|
+
const entry = parseJsonLine(line);
|
|
97
|
+
if (!entry) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const extracted = extractStructuredOutputFromEntry(entry);
|
|
102
|
+
if (extracted) {
|
|
103
|
+
structuredOutput = extracted.structuredOutput;
|
|
104
|
+
usage = extracted.usage;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { structuredOutput, usage };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseJsonLine(line) {
|
|
112
|
+
if (!line.trim()) return null;
|
|
113
|
+
try {
|
|
114
|
+
return JSON.parse(line);
|
|
115
|
+
} catch {
|
|
116
|
+
// Skip invalid JSON lines
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function extractStructuredOutputFromEntry(entry) {
|
|
122
|
+
const message = entry?.message;
|
|
123
|
+
const content = message?.content;
|
|
124
|
+
if (!Array.isArray(content)) return null;
|
|
125
|
+
|
|
126
|
+
let structuredOutput = null;
|
|
127
|
+
let usage = null;
|
|
128
|
+
for (const block of content) {
|
|
129
|
+
if (block?.type === 'tool_use' && block?.name === 'StructuredOutput' && block?.input) {
|
|
130
|
+
structuredOutput = block.input;
|
|
131
|
+
usage = message?.usage && typeof message.usage === 'object' ? message.usage : null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!structuredOutput) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { structuredOutput, usage };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildStructuredOutputPayload(sessionId, structuredOutput, usage) {
|
|
143
|
+
const payload = {
|
|
144
|
+
type: 'result',
|
|
145
|
+
subtype: 'success',
|
|
146
|
+
is_error: false,
|
|
147
|
+
structured_output: structuredOutput,
|
|
148
|
+
session_id: sessionId,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
if (usage) {
|
|
152
|
+
payload.usage = usage;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return payload;
|
|
156
|
+
}
|