@c4t4/heyamigo 0.8.0 → 0.8.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/dist/ai/claude.js +17 -14
- package/dist/ai/spawn.js +64 -1
- package/dist/memory/compressed.js +10 -11
- package/dist/memory/digest.js +10 -11
- package/dist/memory/journal-nudger.js +10 -11
- package/dist/memory/journal-observer.js +10 -11
- package/dist/promptlog.js +26 -1
- package/dist/queue/async-tasks.js +21 -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,14 @@ 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',
|
|
32
36
|
'--model',
|
|
33
37
|
config.claude.model,
|
|
34
38
|
'--permission-mode',
|
|
@@ -57,35 +61,32 @@ function buildArgs(params) {
|
|
|
57
61
|
export async function askClaude(params) {
|
|
58
62
|
const args = buildArgs(params);
|
|
59
63
|
logger.debug({ resume: !!params.sessionId, inputChars: params.input.length }, 'spawning claude');
|
|
60
|
-
const { stdout, durationMs } = await runClaude({
|
|
64
|
+
const { stdout, stderr, durationMs } = await runClaude({
|
|
61
65
|
args,
|
|
62
66
|
input: params.input,
|
|
63
67
|
timeoutMs: TIMEOUT_MS.main,
|
|
64
68
|
caller: 'worker',
|
|
65
69
|
});
|
|
66
70
|
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)}`);
|
|
71
|
+
const parsed = parseStreamJson(stdout);
|
|
72
|
+
if (!parsed) {
|
|
73
|
+
throw new Error(`claude stream-json produced no result event; stdout: ${stdout.slice(0, 500)}`);
|
|
73
74
|
}
|
|
74
|
-
if (parsed.
|
|
75
|
-
throw new Error(`claude returned error (subtype=${parsed.subtype}): ${parsed.result
|
|
75
|
+
if (parsed.isError || parsed.subtype !== 'success') {
|
|
76
|
+
throw new Error(`claude returned error (subtype=${parsed.subtype}): ${parsed.result}`);
|
|
76
77
|
}
|
|
77
|
-
if (!parsed.result || !parsed.
|
|
78
|
+
if (!parsed.result || !parsed.sessionId) {
|
|
78
79
|
throw new Error(`claude output missing result or session_id: ${stdout.slice(0, 200)}`);
|
|
79
80
|
}
|
|
80
81
|
const result = {
|
|
81
82
|
reply: parsed.result,
|
|
82
|
-
sessionId: parsed.
|
|
83
|
+
sessionId: parsed.sessionId,
|
|
83
84
|
usage: {
|
|
84
85
|
inputTokens: parsed.usage?.input_tokens ?? 0,
|
|
85
86
|
cacheReadTokens: parsed.usage?.cache_read_input_tokens ?? 0,
|
|
86
87
|
cacheCreationTokens: parsed.usage?.cache_creation_input_tokens ?? 0,
|
|
87
88
|
outputTokens: parsed.usage?.output_tokens ?? 0,
|
|
88
|
-
numTurns: parsed.
|
|
89
|
+
numTurns: parsed.numTurns ?? 0,
|
|
89
90
|
},
|
|
90
91
|
};
|
|
91
92
|
void logPrompt({
|
|
@@ -97,6 +98,8 @@ export async function askClaude(params) {
|
|
|
97
98
|
sessionId: result.sessionId,
|
|
98
99
|
usage: result.usage,
|
|
99
100
|
durationMs,
|
|
101
|
+
stderr,
|
|
102
|
+
eventTypes: parsed.eventTypes,
|
|
100
103
|
});
|
|
101
104
|
return result;
|
|
102
105
|
}
|
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,25 @@ async function spawnGenerator(prompt) {
|
|
|
224
224
|
const args = [
|
|
225
225
|
'-p',
|
|
226
226
|
'--output-format',
|
|
227
|
-
'json',
|
|
227
|
+
'stream-json',
|
|
228
228
|
'--model',
|
|
229
229
|
config.claude.model,
|
|
230
230
|
'--permission-mode',
|
|
231
231
|
'acceptEdits',
|
|
232
232
|
];
|
|
233
|
-
const { stdout, durationMs } = await runClaude({
|
|
233
|
+
const { stdout, stderr, durationMs } = await runClaude({
|
|
234
234
|
args,
|
|
235
235
|
input: prompt,
|
|
236
236
|
timeoutMs: TIMEOUT_MS.background,
|
|
237
237
|
caller: 'compressed',
|
|
238
238
|
});
|
|
239
239
|
const startedAt = Date.now() - durationMs;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
catch (err) {
|
|
245
|
-
throw new Error(`compressed parse failed: ${err.message}`);
|
|
240
|
+
const parsed = parseStreamJson(stdout);
|
|
241
|
+
if (!parsed) {
|
|
242
|
+
throw new Error(`compressed stream-json produced no result event: ${stdout.slice(0, 200)}`);
|
|
246
243
|
}
|
|
247
|
-
if (parsed.
|
|
248
|
-
throw new Error(`compressed bad output: ${parsed.result
|
|
244
|
+
if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
|
|
245
|
+
throw new Error(`compressed bad output: ${parsed.result || stdout.slice(0, 200)}`);
|
|
249
246
|
}
|
|
250
247
|
const output = parsed.result.trim();
|
|
251
248
|
void logPrompt({
|
|
@@ -255,6 +252,8 @@ async function spawnGenerator(prompt) {
|
|
|
255
252
|
input: prompt,
|
|
256
253
|
output,
|
|
257
254
|
durationMs,
|
|
255
|
+
stderr,
|
|
256
|
+
eventTypes: parsed.eventTypes,
|
|
258
257
|
});
|
|
259
258
|
return output;
|
|
260
259
|
}
|
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,25 @@ async function spawnDigester(prompt) {
|
|
|
13
13
|
const args = [
|
|
14
14
|
'-p',
|
|
15
15
|
'--output-format',
|
|
16
|
-
'json',
|
|
16
|
+
'stream-json',
|
|
17
17
|
'--model',
|
|
18
18
|
config.claude.model,
|
|
19
19
|
'--permission-mode',
|
|
20
20
|
'acceptEdits',
|
|
21
21
|
];
|
|
22
|
-
const { stdout, durationMs } = await runClaude({
|
|
22
|
+
const { stdout, stderr, durationMs } = await runClaude({
|
|
23
23
|
args,
|
|
24
24
|
input: prompt,
|
|
25
25
|
timeoutMs: TIMEOUT_MS.background,
|
|
26
26
|
caller: 'digester',
|
|
27
27
|
});
|
|
28
28
|
const startedAt = Date.now() - durationMs;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
catch (err) {
|
|
34
|
-
throw new Error(`digester parse failed: ${err.message}`);
|
|
29
|
+
const parsed = parseStreamJson(stdout);
|
|
30
|
+
if (!parsed) {
|
|
31
|
+
throw new Error(`digester stream-json produced no result event: ${stdout.slice(0, 200)}`);
|
|
35
32
|
}
|
|
36
|
-
if (parsed.
|
|
37
|
-
throw new Error(`digester bad output: ${parsed.result
|
|
33
|
+
if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
|
|
34
|
+
throw new Error(`digester bad output: ${parsed.result || stdout.slice(0, 200)}`);
|
|
38
35
|
}
|
|
39
36
|
const output = parsed.result.trim();
|
|
40
37
|
void logPrompt({
|
|
@@ -44,6 +41,8 @@ async function spawnDigester(prompt) {
|
|
|
44
41
|
input: prompt,
|
|
45
42
|
output,
|
|
46
43
|
durationMs,
|
|
44
|
+
stderr,
|
|
45
|
+
eventTypes: parsed.eventTypes,
|
|
47
46
|
});
|
|
48
47
|
return output;
|
|
49
48
|
}
|
|
@@ -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,25 @@ async function spawnComposer(prompt) {
|
|
|
18
18
|
const args = [
|
|
19
19
|
'-p',
|
|
20
20
|
'--output-format',
|
|
21
|
-
'json',
|
|
21
|
+
'stream-json',
|
|
22
22
|
'--model',
|
|
23
23
|
config.claude.model,
|
|
24
24
|
'--permission-mode',
|
|
25
25
|
'acceptEdits',
|
|
26
26
|
];
|
|
27
|
-
const { stdout, durationMs } = await runClaude({
|
|
27
|
+
const { stdout, stderr, durationMs } = await runClaude({
|
|
28
28
|
args,
|
|
29
29
|
input: prompt,
|
|
30
30
|
timeoutMs: TIMEOUT_MS.background,
|
|
31
31
|
caller: 'journal-nudger',
|
|
32
32
|
});
|
|
33
33
|
const startedAt = Date.now() - durationMs;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
catch (err) {
|
|
39
|
-
throw new Error(`nudger parse failed: ${err.message}`);
|
|
34
|
+
const parsed = parseStreamJson(stdout);
|
|
35
|
+
if (!parsed) {
|
|
36
|
+
throw new Error(`nudger stream-json produced no result event: ${stdout.slice(0, 200)}`);
|
|
40
37
|
}
|
|
41
|
-
if (parsed.
|
|
42
|
-
throw new Error(`nudger bad output: ${parsed.result
|
|
38
|
+
if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
|
|
39
|
+
throw new Error(`nudger bad output: ${parsed.result || stdout.slice(0, 200)}`);
|
|
43
40
|
}
|
|
44
41
|
const output = parsed.result.trim();
|
|
45
42
|
void logPrompt({
|
|
@@ -49,6 +46,8 @@ async function spawnComposer(prompt) {
|
|
|
49
46
|
input: prompt,
|
|
50
47
|
output,
|
|
51
48
|
durationMs,
|
|
49
|
+
stderr,
|
|
50
|
+
eventTypes: parsed.eventTypes,
|
|
52
51
|
});
|
|
53
52
|
return output;
|
|
54
53
|
}
|
|
@@ -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,25 @@ async function spawnObserver(prompt) {
|
|
|
14
14
|
const args = [
|
|
15
15
|
'-p',
|
|
16
16
|
'--output-format',
|
|
17
|
-
'json',
|
|
17
|
+
'stream-json',
|
|
18
18
|
'--model',
|
|
19
19
|
config.claude.model,
|
|
20
20
|
'--permission-mode',
|
|
21
21
|
'acceptEdits',
|
|
22
22
|
];
|
|
23
|
-
const { stdout, durationMs } = await runClaude({
|
|
23
|
+
const { stdout, stderr, durationMs } = await runClaude({
|
|
24
24
|
args,
|
|
25
25
|
input: prompt,
|
|
26
26
|
timeoutMs: TIMEOUT_MS.background,
|
|
27
27
|
caller: 'journal-observer',
|
|
28
28
|
});
|
|
29
29
|
const startedAt = Date.now() - durationMs;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
catch (err) {
|
|
35
|
-
throw new Error(`journal observer parse failed: ${err.message}`);
|
|
30
|
+
const parsed = parseStreamJson(stdout);
|
|
31
|
+
if (!parsed) {
|
|
32
|
+
throw new Error(`journal observer stream-json produced no result event: ${stdout.slice(0, 200)}`);
|
|
36
33
|
}
|
|
37
|
-
if (parsed.
|
|
38
|
-
throw new Error(`journal observer bad output: ${parsed.result
|
|
34
|
+
if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
|
|
35
|
+
throw new Error(`journal observer bad output: ${parsed.result || stdout.slice(0, 200)}`);
|
|
39
36
|
}
|
|
40
37
|
const output = parsed.result.trim();
|
|
41
38
|
void logPrompt({
|
|
@@ -45,6 +42,8 @@ async function spawnObserver(prompt) {
|
|
|
45
42
|
input: prompt,
|
|
46
43
|
output,
|
|
47
44
|
durationMs,
|
|
45
|
+
stderr,
|
|
46
|
+
eventTypes: parsed.eventTypes,
|
|
48
47
|
});
|
|
49
48
|
return output;
|
|
50
49
|
}
|
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,7 @@ function buildArgs(task) {
|
|
|
113
113
|
const args = [
|
|
114
114
|
'-p',
|
|
115
115
|
'--output-format',
|
|
116
|
-
'json',
|
|
116
|
+
'stream-json',
|
|
117
117
|
'--model',
|
|
118
118
|
config.claude.model,
|
|
119
119
|
'--permission-mode',
|
|
@@ -135,22 +135,19 @@ function buildArgs(task) {
|
|
|
135
135
|
}
|
|
136
136
|
async function spawnClaudeForTask(task, prompt) {
|
|
137
137
|
const args = buildArgs(task);
|
|
138
|
-
const { stdout, durationMs } = await runClaude({
|
|
138
|
+
const { stdout, stderr, durationMs } = await runClaude({
|
|
139
139
|
args,
|
|
140
140
|
input: prompt,
|
|
141
141
|
timeoutMs: TIMEOUT_MS.async,
|
|
142
142
|
caller: 'async-task',
|
|
143
143
|
});
|
|
144
144
|
const startedAt = Date.now() - durationMs;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
catch (err) {
|
|
150
|
-
throw new Error(`async task parse failed: ${err.message}`);
|
|
145
|
+
const parsed = parseStreamJson(stdout);
|
|
146
|
+
if (!parsed) {
|
|
147
|
+
throw new Error(`async task stream-json produced no result event: ${stdout.slice(0, 200)}`);
|
|
151
148
|
}
|
|
152
|
-
if (parsed.
|
|
153
|
-
throw new Error(`async task bad output: ${parsed.result
|
|
149
|
+
if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
|
|
150
|
+
throw new Error(`async task bad output: ${parsed.result || stdout.slice(0, 200)}`);
|
|
154
151
|
}
|
|
155
152
|
const output = parsed.result.trim();
|
|
156
153
|
void logPrompt({
|
|
@@ -160,6 +157,8 @@ async function spawnClaudeForTask(task, prompt) {
|
|
|
160
157
|
input: prompt,
|
|
161
158
|
output,
|
|
162
159
|
durationMs,
|
|
160
|
+
stderr,
|
|
161
|
+
eventTypes: parsed.eventTypes,
|
|
163
162
|
});
|
|
164
163
|
return output;
|
|
165
164
|
}
|
|
@@ -398,7 +397,7 @@ function buildBrowserArgs(task, sessionId) {
|
|
|
398
397
|
const args = [
|
|
399
398
|
'-p',
|
|
400
399
|
'--output-format',
|
|
401
|
-
'json',
|
|
400
|
+
'stream-json',
|
|
402
401
|
'--model',
|
|
403
402
|
config.claude.model,
|
|
404
403
|
'--permission-mode',
|
|
@@ -435,6 +434,7 @@ async function runBrowserTask(task) {
|
|
|
435
434
|
const startedAtMs = Date.now();
|
|
436
435
|
const elapsedLog = () => `${Math.round((Date.now() - task.startedAt * 1000) / 1000)}s`;
|
|
437
436
|
let stdout;
|
|
437
|
+
let stderr;
|
|
438
438
|
let durationMs;
|
|
439
439
|
try {
|
|
440
440
|
const result = await runClaude({
|
|
@@ -444,6 +444,7 @@ async function runBrowserTask(task) {
|
|
|
444
444
|
caller: 'browser-task',
|
|
445
445
|
});
|
|
446
446
|
stdout = result.stdout;
|
|
447
|
+
stderr = result.stderr;
|
|
447
448
|
durationMs = result.durationMs;
|
|
448
449
|
}
|
|
449
450
|
catch (err) {
|
|
@@ -454,20 +455,17 @@ async function runBrowserTask(task) {
|
|
|
454
455
|
});
|
|
455
456
|
return;
|
|
456
457
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
}
|
|
461
|
-
catch (err) {
|
|
462
|
-
logger.error({ err, id: task.id }, 'browser task: failed to parse claude output');
|
|
458
|
+
const parsed = parseStreamJson(stdout);
|
|
459
|
+
if (!parsed) {
|
|
460
|
+
logger.error({ id: task.id }, 'browser task stream-json produced no result event');
|
|
463
461
|
await initiate({
|
|
464
462
|
jid: task.jid,
|
|
465
463
|
text: `Heads up: the browser task "${truncate(task.description, 80)}" returned an unparseable response.`,
|
|
466
464
|
});
|
|
467
465
|
return;
|
|
468
466
|
}
|
|
469
|
-
if (parsed.
|
|
470
|
-
logger.error({
|
|
467
|
+
if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
|
|
468
|
+
logger.error({ id: task.id, subtype: parsed.subtype, isError: parsed.isError }, 'browser task bad output');
|
|
471
469
|
await initiate({
|
|
472
470
|
jid: task.jid,
|
|
473
471
|
text: `Heads up: the browser task "${truncate(task.description, 80)}" returned an error.`,
|
|
@@ -476,7 +474,7 @@ async function runBrowserTask(task) {
|
|
|
476
474
|
}
|
|
477
475
|
// Persist the session id. On first call Claude returns the new sessionId;
|
|
478
476
|
// on resume it may return the same or a rotated one.
|
|
479
|
-
const returnedSessionId = parsed.
|
|
477
|
+
const returnedSessionId = parsed.sessionId;
|
|
480
478
|
if (returnedSessionId) {
|
|
481
479
|
const now = Math.floor(Date.now() / 1000);
|
|
482
480
|
saveBrowserSession({
|
|
@@ -494,6 +492,8 @@ async function runBrowserTask(task) {
|
|
|
494
492
|
output: parsed.result,
|
|
495
493
|
sessionId: returnedSessionId ?? undefined,
|
|
496
494
|
durationMs,
|
|
495
|
+
stderr,
|
|
496
|
+
eventTypes: parsed.eventTypes,
|
|
497
497
|
});
|
|
498
498
|
// Route markers the same way the general async lane does.
|
|
499
499
|
const { extractFlags } = await import('../memory/digest-flag.js');
|