@c4t4/heyamigo 0.8.0 → 0.8.2
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/dist/ai/claude.js +18 -14
- package/dist/ai/spawn.js +64 -1
- package/dist/memory/compressed.js +11 -11
- package/dist/memory/digest.js +11 -11
- package/dist/memory/journal-nudger.js +11 -11
- package/dist/memory/journal-observer.js +11 -11
- package/dist/promptlog.js +26 -1
- package/dist/queue/async-tasks.js +23 -21
- package/package.json +1 -1
package/dist/ai/claude.js
CHANGED
|
@@ -3,7 +3,7 @@ import { resolve } from 'path';
|
|
|
3
3
|
import { config } from '../config.js';
|
|
4
4
|
import { logger } from '../logger.js';
|
|
5
5
|
import { logPrompt } from '../promptlog.js';
|
|
6
|
-
import { runClaude, TIMEOUT_MS } from './spawn.js';
|
|
6
|
+
import { parseStreamJson, runClaude, TIMEOUT_MS } from './spawn.js';
|
|
7
7
|
let cachedSystemPrompt = null;
|
|
8
8
|
function systemPrompt() {
|
|
9
9
|
if (cachedSystemPrompt !== null)
|
|
@@ -25,10 +25,15 @@ export function reloadSystemPrompt() {
|
|
|
25
25
|
cachedSystemPrompt = null;
|
|
26
26
|
}
|
|
27
27
|
function buildArgs(params) {
|
|
28
|
+
// stream-json gives per-event visibility into the agent loop (system init,
|
|
29
|
+
// assistant messages, tool_use, tool_result, final result). We parse the
|
|
30
|
+
// final 'result' event for the return shape, and log event types for
|
|
31
|
+
// diagnostic purposes.
|
|
28
32
|
const args = [
|
|
29
33
|
'-p',
|
|
30
34
|
'--output-format',
|
|
31
|
-
|
|
35
|
+
'stream-json',
|
|
36
|
+
'--verbose',
|
|
32
37
|
'--model',
|
|
33
38
|
config.claude.model,
|
|
34
39
|
'--permission-mode',
|
|
@@ -57,35 +62,32 @@ function buildArgs(params) {
|
|
|
57
62
|
export async function askClaude(params) {
|
|
58
63
|
const args = buildArgs(params);
|
|
59
64
|
logger.debug({ resume: !!params.sessionId, inputChars: params.input.length }, 'spawning claude');
|
|
60
|
-
const { stdout, durationMs } = await runClaude({
|
|
65
|
+
const { stdout, stderr, durationMs } = await runClaude({
|
|
61
66
|
args,
|
|
62
67
|
input: params.input,
|
|
63
68
|
timeoutMs: TIMEOUT_MS.main,
|
|
64
69
|
caller: 'worker',
|
|
65
70
|
});
|
|
66
71
|
const startedAt = Date.now() - durationMs;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
catch (err) {
|
|
72
|
-
throw new Error(`failed to parse claude output: ${err.message}\nstdout: ${stdout.slice(0, 500)}`);
|
|
72
|
+
const parsed = parseStreamJson(stdout);
|
|
73
|
+
if (!parsed) {
|
|
74
|
+
throw new Error(`claude stream-json produced no result event; stdout: ${stdout.slice(0, 500)}`);
|
|
73
75
|
}
|
|
74
|
-
if (parsed.
|
|
75
|
-
throw new Error(`claude returned error (subtype=${parsed.subtype}): ${parsed.result
|
|
76
|
+
if (parsed.isError || parsed.subtype !== 'success') {
|
|
77
|
+
throw new Error(`claude returned error (subtype=${parsed.subtype}): ${parsed.result}`);
|
|
76
78
|
}
|
|
77
|
-
if (!parsed.result || !parsed.
|
|
79
|
+
if (!parsed.result || !parsed.sessionId) {
|
|
78
80
|
throw new Error(`claude output missing result or session_id: ${stdout.slice(0, 200)}`);
|
|
79
81
|
}
|
|
80
82
|
const result = {
|
|
81
83
|
reply: parsed.result,
|
|
82
|
-
sessionId: parsed.
|
|
84
|
+
sessionId: parsed.sessionId,
|
|
83
85
|
usage: {
|
|
84
86
|
inputTokens: parsed.usage?.input_tokens ?? 0,
|
|
85
87
|
cacheReadTokens: parsed.usage?.cache_read_input_tokens ?? 0,
|
|
86
88
|
cacheCreationTokens: parsed.usage?.cache_creation_input_tokens ?? 0,
|
|
87
89
|
outputTokens: parsed.usage?.output_tokens ?? 0,
|
|
88
|
-
numTurns: parsed.
|
|
90
|
+
numTurns: parsed.numTurns ?? 0,
|
|
89
91
|
},
|
|
90
92
|
};
|
|
91
93
|
void logPrompt({
|
|
@@ -97,6 +99,8 @@ export async function askClaude(params) {
|
|
|
97
99
|
sessionId: result.sessionId,
|
|
98
100
|
usage: result.usage,
|
|
99
101
|
durationMs,
|
|
102
|
+
stderr,
|
|
103
|
+
eventTypes: parsed.eventTypes,
|
|
100
104
|
});
|
|
101
105
|
return result;
|
|
102
106
|
}
|
package/dist/ai/spawn.js
CHANGED
|
@@ -21,6 +21,60 @@ export class ClaudeSpawnError extends Error {
|
|
|
21
21
|
this.name = 'ClaudeSpawnError';
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
// Parse Claude CLI's --output-format stream-json output. Each line is a JSON
|
|
25
|
+
// event; the final event with type === 'result' carries the completion
|
|
26
|
+
// summary (same shape as the old single-json output format). Returns null if
|
|
27
|
+
// no result event is found — caller should treat that as an error.
|
|
28
|
+
export function parseStreamJson(stdout) {
|
|
29
|
+
const events = [];
|
|
30
|
+
const eventTypes = [];
|
|
31
|
+
const lines = stdout.split(/\r?\n/);
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
const trimmed = line.trim();
|
|
34
|
+
if (!trimmed)
|
|
35
|
+
continue;
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(trimmed);
|
|
38
|
+
events.push(parsed);
|
|
39
|
+
if (typeof parsed.type === 'string')
|
|
40
|
+
eventTypes.push(parsed.type);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Ignore malformed lines — Claude CLI occasionally emits preamble or
|
|
44
|
+
// debug lines that aren't JSON; the structured events we need are
|
|
45
|
+
// always well-formed.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Find the final result event. Walk from end to handle any stray events
|
|
49
|
+
// after 'result' (shouldn't happen but be defensive).
|
|
50
|
+
let resultEvent = null;
|
|
51
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
52
|
+
if (events[i].type === 'result') {
|
|
53
|
+
resultEvent = events[i];
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!resultEvent)
|
|
58
|
+
return null;
|
|
59
|
+
return {
|
|
60
|
+
result: typeof resultEvent.result === 'string' ? resultEvent.result : '',
|
|
61
|
+
sessionId: typeof resultEvent.session_id === 'string'
|
|
62
|
+
? resultEvent.session_id
|
|
63
|
+
: null,
|
|
64
|
+
usage: resultEvent.usage && typeof resultEvent.usage === 'object'
|
|
65
|
+
? resultEvent.usage
|
|
66
|
+
: undefined,
|
|
67
|
+
isError: !!resultEvent.is_error,
|
|
68
|
+
subtype: typeof resultEvent.subtype === 'string'
|
|
69
|
+
? resultEvent.subtype
|
|
70
|
+
: undefined,
|
|
71
|
+
numTurns: typeof resultEvent.num_turns === 'number'
|
|
72
|
+
? resultEvent.num_turns
|
|
73
|
+
: undefined,
|
|
74
|
+
eventTypes,
|
|
75
|
+
events,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
24
78
|
// Kill the process group of a detached child. Playwright MCP and any Chromium
|
|
25
79
|
// children sit under the claude subprocess; without process-group kill they
|
|
26
80
|
// linger after we SIGTERM the parent and accumulate on the host.
|
|
@@ -54,6 +108,15 @@ export async function runClaude(opts) {
|
|
|
54
108
|
// detached:true puts the child in its own process group, so killGroup
|
|
55
109
|
// can SIGTERM the whole tree (Playwright MCP, Chromium, etc.) at once.
|
|
56
110
|
detached: true,
|
|
111
|
+
// ANTHROPIC_LOG=debug surfaces the SDK's HTTP layer to stderr:
|
|
112
|
+
// request URLs, status codes, retries, rate-limit notices. We
|
|
113
|
+
// capture stderr and put a truncated copy into the promptlog so
|
|
114
|
+
// we can diagnose API hangs/rate-limits post-mortem instead of
|
|
115
|
+
// staring at "Claude subprocess is idle, why?".
|
|
116
|
+
env: {
|
|
117
|
+
...process.env,
|
|
118
|
+
ANTHROPIC_LOG: process.env.ANTHROPIC_LOG ?? 'debug',
|
|
119
|
+
},
|
|
57
120
|
});
|
|
58
121
|
let stdout = '';
|
|
59
122
|
let stderr = '';
|
|
@@ -106,7 +169,7 @@ export async function runClaude(opts) {
|
|
|
106
169
|
logFail(`exit ${code}: ${stderr.slice(0, 500)}`);
|
|
107
170
|
return rejectPromise(new ClaudeSpawnError(caller, `claude exited with code ${code}: ${stderr.slice(0, 500)}`));
|
|
108
171
|
}
|
|
109
|
-
resolvePromise({ stdout, durationMs });
|
|
172
|
+
resolvePromise({ stdout, stderr, durationMs });
|
|
110
173
|
});
|
|
111
174
|
child.stdin.write(input);
|
|
112
175
|
child.stdin.end();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, writeFileSync, } from 'fs';
|
|
2
2
|
import { dirname, resolve } from 'path';
|
|
3
3
|
import { mkdirSync } from 'fs';
|
|
4
|
-
import { runClaude, TIMEOUT_MS } from '../ai/spawn.js';
|
|
4
|
+
import { parseStreamJson, runClaude, TIMEOUT_MS } from '../ai/spawn.js';
|
|
5
5
|
import { config } from '../config.js';
|
|
6
6
|
import { logger } from '../logger.js';
|
|
7
7
|
import { logPrompt } from '../promptlog.js';
|
|
@@ -224,28 +224,26 @@ async function spawnGenerator(prompt) {
|
|
|
224
224
|
const args = [
|
|
225
225
|
'-p',
|
|
226
226
|
'--output-format',
|
|
227
|
-
'json',
|
|
227
|
+
'stream-json',
|
|
228
|
+
'--verbose',
|
|
228
229
|
'--model',
|
|
229
230
|
config.claude.model,
|
|
230
231
|
'--permission-mode',
|
|
231
232
|
'acceptEdits',
|
|
232
233
|
];
|
|
233
|
-
const { stdout, durationMs } = await runClaude({
|
|
234
|
+
const { stdout, stderr, durationMs } = await runClaude({
|
|
234
235
|
args,
|
|
235
236
|
input: prompt,
|
|
236
237
|
timeoutMs: TIMEOUT_MS.background,
|
|
237
238
|
caller: 'compressed',
|
|
238
239
|
});
|
|
239
240
|
const startedAt = Date.now() - durationMs;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
catch (err) {
|
|
245
|
-
throw new Error(`compressed parse failed: ${err.message}`);
|
|
241
|
+
const parsed = parseStreamJson(stdout);
|
|
242
|
+
if (!parsed) {
|
|
243
|
+
throw new Error(`compressed stream-json produced no result event: ${stdout.slice(0, 200)}`);
|
|
246
244
|
}
|
|
247
|
-
if (parsed.
|
|
248
|
-
throw new Error(`compressed bad output: ${parsed.result
|
|
245
|
+
if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
|
|
246
|
+
throw new Error(`compressed bad output: ${parsed.result || stdout.slice(0, 200)}`);
|
|
249
247
|
}
|
|
250
248
|
const output = parsed.result.trim();
|
|
251
249
|
void logPrompt({
|
|
@@ -255,6 +253,8 @@ async function spawnGenerator(prompt) {
|
|
|
255
253
|
input: prompt,
|
|
256
254
|
output,
|
|
257
255
|
durationMs,
|
|
256
|
+
stderr,
|
|
257
|
+
eventTypes: parsed.eventTypes,
|
|
258
258
|
});
|
|
259
259
|
return output;
|
|
260
260
|
}
|
package/dist/memory/digest.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { runClaude, TIMEOUT_MS } from '../ai/spawn.js';
|
|
1
|
+
import { parseStreamJson, runClaude, TIMEOUT_MS } from '../ai/spawn.js';
|
|
2
2
|
import { config } from '../config.js';
|
|
3
3
|
import { logger } from '../logger.js';
|
|
4
4
|
import { logPrompt } from '../promptlog.js';
|
|
@@ -13,28 +13,26 @@ async function spawnDigester(prompt) {
|
|
|
13
13
|
const args = [
|
|
14
14
|
'-p',
|
|
15
15
|
'--output-format',
|
|
16
|
-
'json',
|
|
16
|
+
'stream-json',
|
|
17
|
+
'--verbose',
|
|
17
18
|
'--model',
|
|
18
19
|
config.claude.model,
|
|
19
20
|
'--permission-mode',
|
|
20
21
|
'acceptEdits',
|
|
21
22
|
];
|
|
22
|
-
const { stdout, durationMs } = await runClaude({
|
|
23
|
+
const { stdout, stderr, durationMs } = await runClaude({
|
|
23
24
|
args,
|
|
24
25
|
input: prompt,
|
|
25
26
|
timeoutMs: TIMEOUT_MS.background,
|
|
26
27
|
caller: 'digester',
|
|
27
28
|
});
|
|
28
29
|
const startedAt = Date.now() - durationMs;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
catch (err) {
|
|
34
|
-
throw new Error(`digester parse failed: ${err.message}`);
|
|
30
|
+
const parsed = parseStreamJson(stdout);
|
|
31
|
+
if (!parsed) {
|
|
32
|
+
throw new Error(`digester stream-json produced no result event: ${stdout.slice(0, 200)}`);
|
|
35
33
|
}
|
|
36
|
-
if (parsed.
|
|
37
|
-
throw new Error(`digester bad output: ${parsed.result
|
|
34
|
+
if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
|
|
35
|
+
throw new Error(`digester bad output: ${parsed.result || stdout.slice(0, 200)}`);
|
|
38
36
|
}
|
|
39
37
|
const output = parsed.result.trim();
|
|
40
38
|
void logPrompt({
|
|
@@ -44,6 +42,8 @@ async function spawnDigester(prompt) {
|
|
|
44
42
|
input: prompt,
|
|
45
43
|
output,
|
|
46
44
|
durationMs,
|
|
45
|
+
stderr,
|
|
46
|
+
eventTypes: parsed.eventTypes,
|
|
47
47
|
});
|
|
48
48
|
return output;
|
|
49
49
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { runClaude, TIMEOUT_MS } from '../ai/spawn.js';
|
|
1
|
+
import { parseStreamJson, runClaude, TIMEOUT_MS } from '../ai/spawn.js';
|
|
2
2
|
import { config } from '../config.js';
|
|
3
3
|
import { initiate } from '../gateway/outgoing.js';
|
|
4
4
|
import { logger } from '../logger.js';
|
|
@@ -18,28 +18,26 @@ async function spawnComposer(prompt) {
|
|
|
18
18
|
const args = [
|
|
19
19
|
'-p',
|
|
20
20
|
'--output-format',
|
|
21
|
-
'json',
|
|
21
|
+
'stream-json',
|
|
22
|
+
'--verbose',
|
|
22
23
|
'--model',
|
|
23
24
|
config.claude.model,
|
|
24
25
|
'--permission-mode',
|
|
25
26
|
'acceptEdits',
|
|
26
27
|
];
|
|
27
|
-
const { stdout, durationMs } = await runClaude({
|
|
28
|
+
const { stdout, stderr, durationMs } = await runClaude({
|
|
28
29
|
args,
|
|
29
30
|
input: prompt,
|
|
30
31
|
timeoutMs: TIMEOUT_MS.background,
|
|
31
32
|
caller: 'journal-nudger',
|
|
32
33
|
});
|
|
33
34
|
const startedAt = Date.now() - durationMs;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
catch (err) {
|
|
39
|
-
throw new Error(`nudger parse failed: ${err.message}`);
|
|
35
|
+
const parsed = parseStreamJson(stdout);
|
|
36
|
+
if (!parsed) {
|
|
37
|
+
throw new Error(`nudger stream-json produced no result event: ${stdout.slice(0, 200)}`);
|
|
40
38
|
}
|
|
41
|
-
if (parsed.
|
|
42
|
-
throw new Error(`nudger bad output: ${parsed.result
|
|
39
|
+
if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
|
|
40
|
+
throw new Error(`nudger bad output: ${parsed.result || stdout.slice(0, 200)}`);
|
|
43
41
|
}
|
|
44
42
|
const output = parsed.result.trim();
|
|
45
43
|
void logPrompt({
|
|
@@ -49,6 +47,8 @@ async function spawnComposer(prompt) {
|
|
|
49
47
|
input: prompt,
|
|
50
48
|
output,
|
|
51
49
|
durationMs,
|
|
50
|
+
stderr,
|
|
51
|
+
eventTypes: parsed.eventTypes,
|
|
52
52
|
});
|
|
53
53
|
return output;
|
|
54
54
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { runClaude, TIMEOUT_MS } from '../ai/spawn.js';
|
|
1
|
+
import { parseStreamJson, runClaude, TIMEOUT_MS } from '../ai/spawn.js';
|
|
2
2
|
import { config } from '../config.js';
|
|
3
3
|
import { logger } from '../logger.js';
|
|
4
4
|
import { logPrompt } from '../promptlog.js';
|
|
@@ -14,28 +14,26 @@ async function spawnObserver(prompt) {
|
|
|
14
14
|
const args = [
|
|
15
15
|
'-p',
|
|
16
16
|
'--output-format',
|
|
17
|
-
'json',
|
|
17
|
+
'stream-json',
|
|
18
|
+
'--verbose',
|
|
18
19
|
'--model',
|
|
19
20
|
config.claude.model,
|
|
20
21
|
'--permission-mode',
|
|
21
22
|
'acceptEdits',
|
|
22
23
|
];
|
|
23
|
-
const { stdout, durationMs } = await runClaude({
|
|
24
|
+
const { stdout, stderr, durationMs } = await runClaude({
|
|
24
25
|
args,
|
|
25
26
|
input: prompt,
|
|
26
27
|
timeoutMs: TIMEOUT_MS.background,
|
|
27
28
|
caller: 'journal-observer',
|
|
28
29
|
});
|
|
29
30
|
const startedAt = Date.now() - durationMs;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
catch (err) {
|
|
35
|
-
throw new Error(`journal observer parse failed: ${err.message}`);
|
|
31
|
+
const parsed = parseStreamJson(stdout);
|
|
32
|
+
if (!parsed) {
|
|
33
|
+
throw new Error(`journal observer stream-json produced no result event: ${stdout.slice(0, 200)}`);
|
|
36
34
|
}
|
|
37
|
-
if (parsed.
|
|
38
|
-
throw new Error(`journal observer bad output: ${parsed.result
|
|
35
|
+
if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
|
|
36
|
+
throw new Error(`journal observer bad output: ${parsed.result || stdout.slice(0, 200)}`);
|
|
39
37
|
}
|
|
40
38
|
const output = parsed.result.trim();
|
|
41
39
|
void logPrompt({
|
|
@@ -45,6 +43,8 @@ async function spawnObserver(prompt) {
|
|
|
45
43
|
input: prompt,
|
|
46
44
|
output,
|
|
47
45
|
durationMs,
|
|
46
|
+
stderr,
|
|
47
|
+
eventTypes: parsed.eventTypes,
|
|
48
48
|
});
|
|
49
49
|
return output;
|
|
50
50
|
}
|
package/dist/promptlog.js
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { appendFile, mkdir, readdir, unlink } from 'fs/promises';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
3
|
import { config } from './config.js';
|
|
4
|
+
// Hard caps on fields that can grow unbounded. Prevents promptlog entries
|
|
5
|
+
// from exploding when a run produces huge stdout/stderr. prunePrompts
|
|
6
|
+
// still handles multi-day retention separately.
|
|
7
|
+
const STDOUT_MAX_BYTES = 100_000;
|
|
8
|
+
const STDERR_MAX_BYTES = 50_000;
|
|
9
|
+
const INPUT_MAX_BYTES = 200_000;
|
|
10
|
+
function truncateWithMarker(s, maxBytes) {
|
|
11
|
+
if (!s)
|
|
12
|
+
return s;
|
|
13
|
+
// Rough byte size via length — fine for mostly-ASCII prompt payloads.
|
|
14
|
+
if (s.length <= maxBytes)
|
|
15
|
+
return s;
|
|
16
|
+
const extra = s.length - maxBytes;
|
|
17
|
+
return s.slice(0, maxBytes) + `\n… [truncated ${extra} bytes]`;
|
|
18
|
+
}
|
|
4
19
|
let dirReady = false;
|
|
5
20
|
function promptsDir() {
|
|
6
21
|
return resolve(process.cwd(), 'storage/prompts');
|
|
@@ -18,7 +33,17 @@ function logFilePath() {
|
|
|
18
33
|
export async function logPrompt(entry) {
|
|
19
34
|
try {
|
|
20
35
|
await ensureDir();
|
|
21
|
-
|
|
36
|
+
const capped = {
|
|
37
|
+
...entry,
|
|
38
|
+
input: truncateWithMarker(entry.input, INPUT_MAX_BYTES),
|
|
39
|
+
};
|
|
40
|
+
if (entry.output !== undefined) {
|
|
41
|
+
capped.output = truncateWithMarker(entry.output, STDOUT_MAX_BYTES);
|
|
42
|
+
}
|
|
43
|
+
if (entry.stderr !== undefined) {
|
|
44
|
+
capped.stderr = truncateWithMarker(entry.stderr, STDERR_MAX_BYTES);
|
|
45
|
+
}
|
|
46
|
+
await appendFile(logFilePath(), JSON.stringify(capped) + '\n', 'utf-8');
|
|
22
47
|
}
|
|
23
48
|
catch {
|
|
24
49
|
// logging must never break the main flow
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
2
|
import { dirname, resolve } from 'path';
|
|
3
|
-
import { runClaude, TIMEOUT_MS } from '../ai/spawn.js';
|
|
3
|
+
import { parseStreamJson, runClaude, TIMEOUT_MS } from '../ai/spawn.js';
|
|
4
4
|
import { config } from '../config.js';
|
|
5
5
|
import fastq from 'fastq';
|
|
6
6
|
import { initiate } from '../gateway/outgoing.js';
|
|
@@ -113,7 +113,8 @@ function buildArgs(task) {
|
|
|
113
113
|
const args = [
|
|
114
114
|
'-p',
|
|
115
115
|
'--output-format',
|
|
116
|
-
'json',
|
|
116
|
+
'stream-json',
|
|
117
|
+
'--verbose',
|
|
117
118
|
'--model',
|
|
118
119
|
config.claude.model,
|
|
119
120
|
'--permission-mode',
|
|
@@ -135,22 +136,19 @@ function buildArgs(task) {
|
|
|
135
136
|
}
|
|
136
137
|
async function spawnClaudeForTask(task, prompt) {
|
|
137
138
|
const args = buildArgs(task);
|
|
138
|
-
const { stdout, durationMs } = await runClaude({
|
|
139
|
+
const { stdout, stderr, durationMs } = await runClaude({
|
|
139
140
|
args,
|
|
140
141
|
input: prompt,
|
|
141
142
|
timeoutMs: TIMEOUT_MS.async,
|
|
142
143
|
caller: 'async-task',
|
|
143
144
|
});
|
|
144
145
|
const startedAt = Date.now() - durationMs;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
catch (err) {
|
|
150
|
-
throw new Error(`async task parse failed: ${err.message}`);
|
|
146
|
+
const parsed = parseStreamJson(stdout);
|
|
147
|
+
if (!parsed) {
|
|
148
|
+
throw new Error(`async task stream-json produced no result event: ${stdout.slice(0, 200)}`);
|
|
151
149
|
}
|
|
152
|
-
if (parsed.
|
|
153
|
-
throw new Error(`async task bad output: ${parsed.result
|
|
150
|
+
if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
|
|
151
|
+
throw new Error(`async task bad output: ${parsed.result || stdout.slice(0, 200)}`);
|
|
154
152
|
}
|
|
155
153
|
const output = parsed.result.trim();
|
|
156
154
|
void logPrompt({
|
|
@@ -160,6 +158,8 @@ async function spawnClaudeForTask(task, prompt) {
|
|
|
160
158
|
input: prompt,
|
|
161
159
|
output,
|
|
162
160
|
durationMs,
|
|
161
|
+
stderr,
|
|
162
|
+
eventTypes: parsed.eventTypes,
|
|
163
163
|
});
|
|
164
164
|
return output;
|
|
165
165
|
}
|
|
@@ -398,7 +398,8 @@ function buildBrowserArgs(task, sessionId) {
|
|
|
398
398
|
const args = [
|
|
399
399
|
'-p',
|
|
400
400
|
'--output-format',
|
|
401
|
-
'json',
|
|
401
|
+
'stream-json',
|
|
402
|
+
'--verbose',
|
|
402
403
|
'--model',
|
|
403
404
|
config.claude.model,
|
|
404
405
|
'--permission-mode',
|
|
@@ -435,6 +436,7 @@ async function runBrowserTask(task) {
|
|
|
435
436
|
const startedAtMs = Date.now();
|
|
436
437
|
const elapsedLog = () => `${Math.round((Date.now() - task.startedAt * 1000) / 1000)}s`;
|
|
437
438
|
let stdout;
|
|
439
|
+
let stderr;
|
|
438
440
|
let durationMs;
|
|
439
441
|
try {
|
|
440
442
|
const result = await runClaude({
|
|
@@ -444,6 +446,7 @@ async function runBrowserTask(task) {
|
|
|
444
446
|
caller: 'browser-task',
|
|
445
447
|
});
|
|
446
448
|
stdout = result.stdout;
|
|
449
|
+
stderr = result.stderr;
|
|
447
450
|
durationMs = result.durationMs;
|
|
448
451
|
}
|
|
449
452
|
catch (err) {
|
|
@@ -454,20 +457,17 @@ async function runBrowserTask(task) {
|
|
|
454
457
|
});
|
|
455
458
|
return;
|
|
456
459
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
}
|
|
461
|
-
catch (err) {
|
|
462
|
-
logger.error({ err, id: task.id }, 'browser task: failed to parse claude output');
|
|
460
|
+
const parsed = parseStreamJson(stdout);
|
|
461
|
+
if (!parsed) {
|
|
462
|
+
logger.error({ id: task.id }, 'browser task stream-json produced no result event');
|
|
463
463
|
await initiate({
|
|
464
464
|
jid: task.jid,
|
|
465
465
|
text: `Heads up: the browser task "${truncate(task.description, 80)}" returned an unparseable response.`,
|
|
466
466
|
});
|
|
467
467
|
return;
|
|
468
468
|
}
|
|
469
|
-
if (parsed.
|
|
470
|
-
logger.error({
|
|
469
|
+
if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
|
|
470
|
+
logger.error({ id: task.id, subtype: parsed.subtype, isError: parsed.isError }, 'browser task bad output');
|
|
471
471
|
await initiate({
|
|
472
472
|
jid: task.jid,
|
|
473
473
|
text: `Heads up: the browser task "${truncate(task.description, 80)}" returned an error.`,
|
|
@@ -476,7 +476,7 @@ async function runBrowserTask(task) {
|
|
|
476
476
|
}
|
|
477
477
|
// Persist the session id. On first call Claude returns the new sessionId;
|
|
478
478
|
// on resume it may return the same or a rotated one.
|
|
479
|
-
const returnedSessionId = parsed.
|
|
479
|
+
const returnedSessionId = parsed.sessionId;
|
|
480
480
|
if (returnedSessionId) {
|
|
481
481
|
const now = Math.floor(Date.now() / 1000);
|
|
482
482
|
saveBrowserSession({
|
|
@@ -494,6 +494,8 @@ async function runBrowserTask(task) {
|
|
|
494
494
|
output: parsed.result,
|
|
495
495
|
sessionId: returnedSessionId ?? undefined,
|
|
496
496
|
durationMs,
|
|
497
|
+
stderr,
|
|
498
|
+
eventTypes: parsed.eventTypes,
|
|
497
499
|
});
|
|
498
500
|
// Route markers the same way the general async lane does.
|
|
499
501
|
const { extractFlags } = await import('../memory/digest-flag.js');
|