@c4t4/heyamigo 0.6.1 → 0.7.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/config/memory-instructions.md +30 -19
- package/dist/ai/claude.js +42 -75
- package/dist/ai/spawn.js +121 -0
- package/dist/gateway/incoming.js +20 -6
- package/dist/memory/compressed.js +27 -58
- package/dist/memory/digest.js +27 -56
- package/dist/memory/journal-nudger.js +27 -58
- package/dist/memory/journal-observer.js +27 -58
- package/dist/queue/async-tasks.js +27 -58
- package/package.json +1 -1
|
@@ -133,44 +133,55 @@ Confirm the change in your reply so the owner sees what you did:
|
|
|
133
133
|
|
|
134
134
|
## ASYNC background work
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
**ANY browser tool use goes through a background worker. No exceptions. Ever.**
|
|
137
|
+
|
|
138
|
+
The chat queue is serialized per chat. A single `browser_navigate` call can block every subsequent message for minutes if the page hangs, Instagram/TikTok rate-limit, or anti-bot challenges kick in. This happens constantly in practice. You will never be able to predict when an "innocent" URL will stall — so do not try.
|
|
139
|
+
|
|
140
|
+
Hard rule: if ANY part of fulfilling a request needs a browser tool (`browser_navigate`, `browser_click`, `browser_take_screenshot`, `browser_snapshot`, `browser_type`, `browser_evaluate`, or any `mcp__*playwright*` tool), delegate to the async lane. Even a single URL. Even "just checking quickly". Even when the user says "just".
|
|
141
|
+
|
|
142
|
+
### How to delegate
|
|
137
143
|
|
|
138
144
|
Two parts in the same reply:
|
|
139
145
|
|
|
140
|
-
1.
|
|
141
|
-
2. Append at the END:
|
|
146
|
+
1. One or two-sentence ack in the reply text. Short. No over-explaining. Examples: "On it, will report back." / "Scraping now, few minutes." / "Looking into it."
|
|
147
|
+
2. Append at the END of your reply:
|
|
142
148
|
```
|
|
143
149
|
[ASYNC: <self-sufficient task description>]
|
|
144
150
|
```
|
|
145
151
|
|
|
146
|
-
|
|
152
|
+
Full example for a single-URL Instagram check:
|
|
147
153
|
|
|
148
154
|
```
|
|
149
|
-
On it. Will send the
|
|
155
|
+
On it. Will send the bio and recent posts shortly.
|
|
150
156
|
|
|
151
|
-
[ASYNC:
|
|
157
|
+
[ASYNC: Navigate to https://instagram.com/rivoara_official using the browser tool. Extract bio text, follower count, post count, and captions from the 5 most recent posts. Output as plain text with clear sections. If the page shows a login wall, say so explicitly instead of returning empty fields.]
|
|
152
158
|
```
|
|
153
159
|
|
|
154
|
-
|
|
160
|
+
The async worker has full browser access and will do the work without blocking this chat. When done, the result lands in this chat as a new message.
|
|
155
161
|
|
|
156
|
-
|
|
157
|
-
|
|
162
|
+
### When to use ASYNC (besides browser)
|
|
163
|
+
|
|
164
|
+
Also use it for:
|
|
158
165
|
- Multi-step investigations with several tool calls
|
|
159
166
|
- Anything you expect to take more than ~30 seconds
|
|
160
167
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
-
|
|
164
|
-
-
|
|
165
|
-
-
|
|
168
|
+
### When NOT to use ASYNC
|
|
169
|
+
|
|
170
|
+
- Things answerable from your context, memory, compressed view, or recent entries — just answer
|
|
171
|
+
- Short reasoning, calculations, or explanations
|
|
172
|
+
- Immediate questions the owner needs answered RIGHT NOW in this reply
|
|
173
|
+
- Single quick non-browser tool calls (e.g. one Read, one Grep)
|
|
174
|
+
|
|
175
|
+
Browser is the hard "always async" rule. Everything else is judgment.
|
|
166
176
|
|
|
167
177
|
### Writing the task description
|
|
168
178
|
|
|
169
179
|
The async worker has NO chat history, NO session, no memory of your conversation. Its only input is the description you write. Self-sufficient means:
|
|
170
180
|
- Spell out exactly what to do.
|
|
171
|
-
- Include every constraint, exclusion, and required context.
|
|
172
|
-
- Reference
|
|
173
|
-
- Specify the expected output shape (
|
|
181
|
+
- Include every constraint, exclusion, and required context (URLs, accounts, filters).
|
|
182
|
+
- Reference any logged-in sessions the worker should use (e.g. "use the Rivoara TikTok account, already logged in").
|
|
183
|
+
- Specify the expected output shape (fields, order, format).
|
|
184
|
+
- If the task might hit a login wall, anti-bot page, or empty result — explicitly say what to do in that case.
|
|
174
185
|
|
|
175
186
|
Over-specify. A vague description produces a vague result.
|
|
176
187
|
|
|
@@ -198,6 +209,6 @@ Rules:
|
|
|
198
209
|
|
|
199
210
|
You have a Chrome browser via Playwright MCP: `browser_navigate`, `browser_take_screenshot`, `browser_snapshot`, `browser_click`, `browser_type`, `browser_evaluate`, etc.
|
|
200
211
|
|
|
201
|
-
|
|
212
|
+
**Never use them inline.** All browser work goes through the async lane — see the ASYNC section above. No exceptions for "quick checks" or "just one URL". Delegate every time.
|
|
202
213
|
|
|
203
|
-
To send a screenshot back:
|
|
214
|
+
To send a screenshot back from an async task: the async worker takes it with the browser tool (saving to `storage/temp/`), then includes `[IMAGE: /absolute/path.png]` in its result message.
|
package/dist/ai/claude.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
1
|
import { readFileSync } from 'fs';
|
|
3
2
|
import { resolve } from 'path';
|
|
4
3
|
import { config } from '../config.js';
|
|
5
4
|
import { logger } from '../logger.js';
|
|
6
5
|
import { logPrompt } from '../promptlog.js';
|
|
6
|
+
import { runClaude, TIMEOUT_MS } from './spawn.js';
|
|
7
7
|
let cachedSystemPrompt = null;
|
|
8
8
|
function systemPrompt() {
|
|
9
9
|
if (cachedSystemPrompt !== null)
|
|
@@ -56,80 +56,47 @@ function buildArgs(params) {
|
|
|
56
56
|
}
|
|
57
57
|
export async function askClaude(params) {
|
|
58
58
|
const args = buildArgs(params);
|
|
59
|
-
const startedAt = Date.now();
|
|
60
59
|
logger.debug({ resume: !!params.sessionId, inputChars: params.input.length }, 'spawning claude');
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
let stdout = '';
|
|
67
|
-
let stderr = '';
|
|
68
|
-
child.stdout.on('data', (chunk) => {
|
|
69
|
-
stdout += chunk.toString('utf-8');
|
|
70
|
-
});
|
|
71
|
-
child.stderr.on('data', (chunk) => {
|
|
72
|
-
stderr += chunk.toString('utf-8');
|
|
73
|
-
});
|
|
74
|
-
child.on('error', (err) => {
|
|
75
|
-
void logPrompt({
|
|
76
|
-
ts: Math.floor(startedAt / 1000),
|
|
77
|
-
caller: 'worker',
|
|
78
|
-
args,
|
|
79
|
-
input: params.input,
|
|
80
|
-
error: `spawn failed: ${err.message}`,
|
|
81
|
-
durationMs: Date.now() - startedAt,
|
|
82
|
-
});
|
|
83
|
-
rejectPromise(new Error(`claude spawn failed: ${err.message}`));
|
|
84
|
-
});
|
|
85
|
-
child.on('close', (code) => {
|
|
86
|
-
if (code !== 0) {
|
|
87
|
-
void logPrompt({
|
|
88
|
-
ts: Math.floor(startedAt / 1000),
|
|
89
|
-
caller: 'worker',
|
|
90
|
-
args,
|
|
91
|
-
input: params.input,
|
|
92
|
-
error: `exit ${code}: ${stderr.slice(0, 500)}`,
|
|
93
|
-
durationMs: Date.now() - startedAt,
|
|
94
|
-
});
|
|
95
|
-
return rejectPromise(new Error(`claude exited with code ${code}: ${stderr.slice(0, 500)}`));
|
|
96
|
-
}
|
|
97
|
-
try {
|
|
98
|
-
const parsed = JSON.parse(stdout);
|
|
99
|
-
if (parsed.is_error || parsed.subtype !== 'success') {
|
|
100
|
-
return rejectPromise(new Error(`claude returned error (subtype=${parsed.subtype}): ${parsed.result ?? stderr.slice(0, 200)}`));
|
|
101
|
-
}
|
|
102
|
-
if (!parsed.result || !parsed.session_id) {
|
|
103
|
-
return rejectPromise(new Error(`claude output missing result or session_id: ${stdout.slice(0, 200)}`));
|
|
104
|
-
}
|
|
105
|
-
const result = {
|
|
106
|
-
reply: parsed.result,
|
|
107
|
-
sessionId: parsed.session_id,
|
|
108
|
-
usage: {
|
|
109
|
-
inputTokens: parsed.usage?.input_tokens ?? 0,
|
|
110
|
-
cacheReadTokens: parsed.usage?.cache_read_input_tokens ?? 0,
|
|
111
|
-
cacheCreationTokens: parsed.usage?.cache_creation_input_tokens ?? 0,
|
|
112
|
-
outputTokens: parsed.usage?.output_tokens ?? 0,
|
|
113
|
-
numTurns: parsed.num_turns ?? 0,
|
|
114
|
-
},
|
|
115
|
-
};
|
|
116
|
-
void logPrompt({
|
|
117
|
-
ts: Math.floor(startedAt / 1000),
|
|
118
|
-
caller: 'worker',
|
|
119
|
-
args,
|
|
120
|
-
input: params.input,
|
|
121
|
-
output: result.reply,
|
|
122
|
-
sessionId: result.sessionId,
|
|
123
|
-
usage: result.usage,
|
|
124
|
-
durationMs: Date.now() - startedAt,
|
|
125
|
-
});
|
|
126
|
-
resolvePromise(result);
|
|
127
|
-
}
|
|
128
|
-
catch (err) {
|
|
129
|
-
rejectPromise(new Error(`failed to parse claude output: ${err.message}\nstdout: ${stdout.slice(0, 500)}`));
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
child.stdin.write(params.input);
|
|
133
|
-
child.stdin.end();
|
|
60
|
+
const { stdout, durationMs } = await runClaude({
|
|
61
|
+
args,
|
|
62
|
+
input: params.input,
|
|
63
|
+
timeoutMs: TIMEOUT_MS.main,
|
|
64
|
+
caller: 'worker',
|
|
134
65
|
});
|
|
66
|
+
const startedAt = Date.now() - durationMs;
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = JSON.parse(stdout);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
throw new Error(`failed to parse claude output: ${err.message}\nstdout: ${stdout.slice(0, 500)}`);
|
|
73
|
+
}
|
|
74
|
+
if (parsed.is_error || parsed.subtype !== 'success') {
|
|
75
|
+
throw new Error(`claude returned error (subtype=${parsed.subtype}): ${parsed.result ?? ''}`);
|
|
76
|
+
}
|
|
77
|
+
if (!parsed.result || !parsed.session_id) {
|
|
78
|
+
throw new Error(`claude output missing result or session_id: ${stdout.slice(0, 200)}`);
|
|
79
|
+
}
|
|
80
|
+
const result = {
|
|
81
|
+
reply: parsed.result,
|
|
82
|
+
sessionId: parsed.session_id,
|
|
83
|
+
usage: {
|
|
84
|
+
inputTokens: parsed.usage?.input_tokens ?? 0,
|
|
85
|
+
cacheReadTokens: parsed.usage?.cache_read_input_tokens ?? 0,
|
|
86
|
+
cacheCreationTokens: parsed.usage?.cache_creation_input_tokens ?? 0,
|
|
87
|
+
outputTokens: parsed.usage?.output_tokens ?? 0,
|
|
88
|
+
numTurns: parsed.num_turns ?? 0,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
void logPrompt({
|
|
92
|
+
ts: Math.floor(startedAt / 1000),
|
|
93
|
+
caller: 'worker',
|
|
94
|
+
args,
|
|
95
|
+
input: params.input,
|
|
96
|
+
output: result.reply,
|
|
97
|
+
sessionId: result.sessionId,
|
|
98
|
+
usage: result.usage,
|
|
99
|
+
durationMs,
|
|
100
|
+
});
|
|
101
|
+
return result;
|
|
135
102
|
}
|
package/dist/ai/spawn.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { logger } from '../logger.js';
|
|
3
|
+
import { logPrompt } from '../promptlog.js';
|
|
4
|
+
export class ClaudeTimeoutError extends Error {
|
|
5
|
+
caller;
|
|
6
|
+
durationMs;
|
|
7
|
+
timeoutMs;
|
|
8
|
+
constructor(caller, durationMs, timeoutMs) {
|
|
9
|
+
super(`${caller} timed out after ${Math.round(durationMs / 1000)}s (cap ${Math.round(timeoutMs / 1000)}s)`);
|
|
10
|
+
this.caller = caller;
|
|
11
|
+
this.durationMs = durationMs;
|
|
12
|
+
this.timeoutMs = timeoutMs;
|
|
13
|
+
this.name = 'ClaudeTimeoutError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class ClaudeSpawnError extends Error {
|
|
17
|
+
caller;
|
|
18
|
+
constructor(caller, message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.caller = caller;
|
|
21
|
+
this.name = 'ClaudeSpawnError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// Kill the process group of a detached child. Playwright MCP and any Chromium
|
|
25
|
+
// children sit under the claude subprocess; without process-group kill they
|
|
26
|
+
// linger after we SIGTERM the parent and accumulate on the host.
|
|
27
|
+
function killGroup(child, signal) {
|
|
28
|
+
if (!child.pid)
|
|
29
|
+
return;
|
|
30
|
+
try {
|
|
31
|
+
// Negative PID = target the whole process group (see kill(2)).
|
|
32
|
+
process.kill(-child.pid, signal);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
// Fallback: signal just the parent. Better than nothing.
|
|
36
|
+
try {
|
|
37
|
+
child.kill(signal);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
logger.warn({ err, pid: child.pid }, 'failed to kill claude subprocess');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Run a `claude -p ...` subprocess with a hard timeout, full-tree kill on
|
|
45
|
+
// expiry, and uniform promptlog handling. All claude spawns in the codebase
|
|
46
|
+
// should go through this.
|
|
47
|
+
export async function runClaude(opts) {
|
|
48
|
+
const { args, input, timeoutMs, caller } = opts;
|
|
49
|
+
const startedAt = Date.now();
|
|
50
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
51
|
+
const child = spawn('claude', args, {
|
|
52
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
53
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
54
|
+
// detached:true puts the child in its own process group, so killGroup
|
|
55
|
+
// can SIGTERM the whole tree (Playwright MCP, Chromium, etc.) at once.
|
|
56
|
+
detached: true,
|
|
57
|
+
});
|
|
58
|
+
let stdout = '';
|
|
59
|
+
let stderr = '';
|
|
60
|
+
let timedOut = false;
|
|
61
|
+
let settled = false;
|
|
62
|
+
const logFail = (error) => void logPrompt({
|
|
63
|
+
ts: Math.floor(startedAt / 1000),
|
|
64
|
+
caller,
|
|
65
|
+
args,
|
|
66
|
+
input,
|
|
67
|
+
error,
|
|
68
|
+
durationMs: Date.now() - startedAt,
|
|
69
|
+
});
|
|
70
|
+
const timer = setTimeout(() => {
|
|
71
|
+
timedOut = true;
|
|
72
|
+
logger.warn({ caller, pid: child.pid, timeoutMs }, 'claude subprocess timed out, killing process group');
|
|
73
|
+
killGroup(child, 'SIGTERM');
|
|
74
|
+
// Grace window, then SIGKILL if still alive.
|
|
75
|
+
setTimeout(() => {
|
|
76
|
+
if (!settled)
|
|
77
|
+
killGroup(child, 'SIGKILL');
|
|
78
|
+
}, 2000).unref();
|
|
79
|
+
}, timeoutMs);
|
|
80
|
+
timer.unref();
|
|
81
|
+
child.stdout.on('data', (chunk) => {
|
|
82
|
+
stdout += chunk.toString('utf-8');
|
|
83
|
+
});
|
|
84
|
+
child.stderr.on('data', (chunk) => {
|
|
85
|
+
stderr += chunk.toString('utf-8');
|
|
86
|
+
});
|
|
87
|
+
child.on('error', (err) => {
|
|
88
|
+
if (settled)
|
|
89
|
+
return;
|
|
90
|
+
settled = true;
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
logFail(`spawn failed: ${err.message}`);
|
|
93
|
+
rejectPromise(new ClaudeSpawnError(caller, `claude spawn failed: ${err.message}`));
|
|
94
|
+
});
|
|
95
|
+
child.on('close', (code, signal) => {
|
|
96
|
+
if (settled)
|
|
97
|
+
return;
|
|
98
|
+
settled = true;
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
const durationMs = Date.now() - startedAt;
|
|
101
|
+
if (timedOut) {
|
|
102
|
+
logFail(`timeout after ${durationMs}ms (cap ${timeoutMs}ms); signal=${signal}`);
|
|
103
|
+
return rejectPromise(new ClaudeTimeoutError(caller, durationMs, timeoutMs));
|
|
104
|
+
}
|
|
105
|
+
if (code !== 0) {
|
|
106
|
+
logFail(`exit ${code}: ${stderr.slice(0, 500)}`);
|
|
107
|
+
return rejectPromise(new ClaudeSpawnError(caller, `claude exited with code ${code}: ${stderr.slice(0, 500)}`));
|
|
108
|
+
}
|
|
109
|
+
resolvePromise({ stdout, durationMs });
|
|
110
|
+
});
|
|
111
|
+
child.stdin.write(input);
|
|
112
|
+
child.stdin.end();
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// Per-lane defaults. Individual callers can override, but these are the
|
|
116
|
+
// shipped caps. Browser-heavy work lives in the async lane.
|
|
117
|
+
export const TIMEOUT_MS = {
|
|
118
|
+
main: 5 * 60 * 1000,
|
|
119
|
+
async: 15 * 60 * 1000,
|
|
120
|
+
background: 3 * 60 * 1000,
|
|
121
|
+
};
|
package/dist/gateway/incoming.js
CHANGED
|
@@ -166,15 +166,29 @@ async function processMessages(messages, sock, ownerJid, isHistorySync = false)
|
|
|
166
166
|
clearInterval(typingHeartbeat);
|
|
167
167
|
typingHeartbeat = null;
|
|
168
168
|
};
|
|
169
|
+
// Defense-in-depth: if nothing else clears the heartbeat within 10 min
|
|
170
|
+
// (e.g. a code path forgot), force-stop. Prevents runaway "typing..."
|
|
171
|
+
// indicators when the pipeline silently fails.
|
|
172
|
+
const typingSafetyCap = setTimeout(() => {
|
|
173
|
+
if (typingHeartbeat) {
|
|
174
|
+
logger.warn({ jid: job.jid }, 'typingHeartbeat safety-cap fired, forcing clear');
|
|
175
|
+
stopTyping();
|
|
176
|
+
}
|
|
177
|
+
}, 10 * 60 * 1000);
|
|
178
|
+
typingSafetyCap.unref();
|
|
169
179
|
enqueue(job)
|
|
170
|
-
.then((result) =>
|
|
171
|
-
stopTyping();
|
|
172
|
-
return handleReply(job, result, msg);
|
|
173
|
-
})
|
|
180
|
+
.then((result) => handleReply(job, result, msg))
|
|
174
181
|
.catch((err) => {
|
|
182
|
+
const isTimeout = err instanceof Error && err.name === 'ClaudeTimeoutError';
|
|
183
|
+
logger.error({ err, jid: job.jid, isTimeout }, 'pipeline failed');
|
|
184
|
+
const replyText = isTimeout
|
|
185
|
+
? 'That request timed out. The task was cancelled, queue is moving.'
|
|
186
|
+
: config.reply.errorMessage;
|
|
187
|
+
return handleReply(job, { reply: replyText }, msg).catch((e) => logger.error({ err: e, jid: job.jid }, 'failed to send error reply'));
|
|
188
|
+
})
|
|
189
|
+
.finally(() => {
|
|
175
190
|
stopTyping();
|
|
176
|
-
|
|
177
|
-
void handleReply(job, { reply: config.reply.errorMessage }, msg).catch((e) => logger.error({ err: e, jid: job.jid }, 'failed to send error reply'));
|
|
191
|
+
clearTimeout(typingSafetyCap);
|
|
178
192
|
});
|
|
179
193
|
}
|
|
180
194
|
catch (err) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
1
|
import { existsSync, readFileSync, readdirSync, writeFileSync, } from 'fs';
|
|
3
2
|
import { dirname, resolve } from 'path';
|
|
4
3
|
import { mkdirSync } from 'fs';
|
|
4
|
+
import { 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';
|
|
@@ -230,64 +230,33 @@ async function spawnGenerator(prompt) {
|
|
|
230
230
|
'--permission-mode',
|
|
231
231
|
'acceptEdits',
|
|
232
232
|
];
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
});
|
|
259
|
-
child.on('close', (code) => {
|
|
260
|
-
if (code !== 0) {
|
|
261
|
-
logFail(`exit ${code}: ${stderr.slice(0, 300)}`);
|
|
262
|
-
return rejectPromise(new Error(`compressed generator exit ${code}`));
|
|
263
|
-
}
|
|
264
|
-
try {
|
|
265
|
-
const parsed = JSON.parse(stdout);
|
|
266
|
-
if (parsed.is_error ||
|
|
267
|
-
parsed.subtype !== 'success' ||
|
|
268
|
-
!parsed.result) {
|
|
269
|
-
logFail(`bad output: ${parsed.result ?? stderr.slice(0, 200)}`);
|
|
270
|
-
return rejectPromise(new Error('compressed generator bad output'));
|
|
271
|
-
}
|
|
272
|
-
const output = parsed.result.trim();
|
|
273
|
-
void logPrompt({
|
|
274
|
-
ts: Math.floor(startedAt / 1000),
|
|
275
|
-
caller: 'compressed',
|
|
276
|
-
args,
|
|
277
|
-
input: prompt,
|
|
278
|
-
output,
|
|
279
|
-
durationMs: Date.now() - startedAt,
|
|
280
|
-
});
|
|
281
|
-
resolvePromise(output);
|
|
282
|
-
}
|
|
283
|
-
catch (err) {
|
|
284
|
-
logFail(`parse failed: ${err.message}`);
|
|
285
|
-
rejectPromise(err);
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
child.stdin.write(prompt);
|
|
289
|
-
child.stdin.end();
|
|
233
|
+
const { stdout, durationMs } = await runClaude({
|
|
234
|
+
args,
|
|
235
|
+
input: prompt,
|
|
236
|
+
timeoutMs: TIMEOUT_MS.background,
|
|
237
|
+
caller: 'compressed',
|
|
238
|
+
});
|
|
239
|
+
const startedAt = Date.now() - durationMs;
|
|
240
|
+
let parsed;
|
|
241
|
+
try {
|
|
242
|
+
parsed = JSON.parse(stdout);
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
throw new Error(`compressed parse failed: ${err.message}`);
|
|
246
|
+
}
|
|
247
|
+
if (parsed.is_error || parsed.subtype !== 'success' || !parsed.result) {
|
|
248
|
+
throw new Error(`compressed bad output: ${parsed.result ?? stdout.slice(0, 200)}`);
|
|
249
|
+
}
|
|
250
|
+
const output = parsed.result.trim();
|
|
251
|
+
void logPrompt({
|
|
252
|
+
ts: Math.floor(startedAt / 1000),
|
|
253
|
+
caller: 'compressed',
|
|
254
|
+
args,
|
|
255
|
+
input: prompt,
|
|
256
|
+
output,
|
|
257
|
+
durationMs,
|
|
290
258
|
});
|
|
259
|
+
return output;
|
|
291
260
|
}
|
|
292
261
|
let buildInFlight = null;
|
|
293
262
|
export async function rebuildCompressed() {
|
package/dist/memory/digest.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { 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';
|
|
@@ -19,62 +19,33 @@ async function spawnDigester(prompt) {
|
|
|
19
19
|
'--permission-mode',
|
|
20
20
|
'acceptEdits',
|
|
21
21
|
];
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
});
|
|
48
|
-
child.on('close', (code) => {
|
|
49
|
-
if (code !== 0) {
|
|
50
|
-
logFail(`exit ${code}: ${stderr.slice(0, 300)}`);
|
|
51
|
-
return rejectPromise(new Error(`digester exit ${code}: ${stderr.slice(0, 300)}`));
|
|
52
|
-
}
|
|
53
|
-
try {
|
|
54
|
-
const parsed = JSON.parse(stdout);
|
|
55
|
-
if (parsed.is_error || parsed.subtype !== 'success' || !parsed.result) {
|
|
56
|
-
logFail(`bad output: ${parsed.result ?? stderr.slice(0, 200)}`);
|
|
57
|
-
return rejectPromise(new Error(`digester bad output: ${parsed.result ?? stderr.slice(0, 200)}`));
|
|
58
|
-
}
|
|
59
|
-
const output = parsed.result.trim();
|
|
60
|
-
void logPrompt({
|
|
61
|
-
ts: Math.floor(startedAt / 1000),
|
|
62
|
-
caller: 'digester',
|
|
63
|
-
args,
|
|
64
|
-
input: prompt,
|
|
65
|
-
output,
|
|
66
|
-
durationMs: Date.now() - startedAt,
|
|
67
|
-
});
|
|
68
|
-
resolvePromise(output);
|
|
69
|
-
}
|
|
70
|
-
catch (err) {
|
|
71
|
-
logFail(`parse failed: ${err.message}`);
|
|
72
|
-
rejectPromise(new Error(`digester parse failed: ${err.message}`));
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
child.stdin.write(prompt);
|
|
76
|
-
child.stdin.end();
|
|
22
|
+
const { stdout, durationMs } = await runClaude({
|
|
23
|
+
args,
|
|
24
|
+
input: prompt,
|
|
25
|
+
timeoutMs: TIMEOUT_MS.background,
|
|
26
|
+
caller: 'digester',
|
|
27
|
+
});
|
|
28
|
+
const startedAt = Date.now() - durationMs;
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = JSON.parse(stdout);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
throw new Error(`digester parse failed: ${err.message}`);
|
|
35
|
+
}
|
|
36
|
+
if (parsed.is_error || parsed.subtype !== 'success' || !parsed.result) {
|
|
37
|
+
throw new Error(`digester bad output: ${parsed.result ?? stdout.slice(0, 200)}`);
|
|
38
|
+
}
|
|
39
|
+
const output = parsed.result.trim();
|
|
40
|
+
void logPrompt({
|
|
41
|
+
ts: Math.floor(startedAt / 1000),
|
|
42
|
+
caller: 'digester',
|
|
43
|
+
args,
|
|
44
|
+
input: prompt,
|
|
45
|
+
output,
|
|
46
|
+
durationMs,
|
|
77
47
|
});
|
|
48
|
+
return output;
|
|
78
49
|
}
|
|
79
50
|
function formatMessagesForDigest(messages) {
|
|
80
51
|
return messages
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { 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';
|
|
@@ -24,64 +24,33 @@ async function spawnComposer(prompt) {
|
|
|
24
24
|
'--permission-mode',
|
|
25
25
|
'acceptEdits',
|
|
26
26
|
];
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
});
|
|
53
|
-
child.on('close', (code) => {
|
|
54
|
-
if (code !== 0) {
|
|
55
|
-
logFail(`exit ${code}: ${stderr.slice(0, 300)}`);
|
|
56
|
-
return rejectPromise(new Error(`nudger exit ${code}`));
|
|
57
|
-
}
|
|
58
|
-
try {
|
|
59
|
-
const parsed = JSON.parse(stdout);
|
|
60
|
-
if (parsed.is_error ||
|
|
61
|
-
parsed.subtype !== 'success' ||
|
|
62
|
-
!parsed.result) {
|
|
63
|
-
logFail(`bad output: ${parsed.result ?? stderr.slice(0, 200)}`);
|
|
64
|
-
return rejectPromise(new Error('nudger bad output'));
|
|
65
|
-
}
|
|
66
|
-
const output = parsed.result.trim();
|
|
67
|
-
void logPrompt({
|
|
68
|
-
ts: Math.floor(startedAt / 1000),
|
|
69
|
-
caller: 'journal-nudger',
|
|
70
|
-
args,
|
|
71
|
-
input: prompt,
|
|
72
|
-
output,
|
|
73
|
-
durationMs: Date.now() - startedAt,
|
|
74
|
-
});
|
|
75
|
-
resolvePromise(output);
|
|
76
|
-
}
|
|
77
|
-
catch (err) {
|
|
78
|
-
logFail(`parse failed: ${err.message}`);
|
|
79
|
-
rejectPromise(err);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
child.stdin.write(prompt);
|
|
83
|
-
child.stdin.end();
|
|
27
|
+
const { stdout, durationMs } = await runClaude({
|
|
28
|
+
args,
|
|
29
|
+
input: prompt,
|
|
30
|
+
timeoutMs: TIMEOUT_MS.background,
|
|
31
|
+
caller: 'journal-nudger',
|
|
32
|
+
});
|
|
33
|
+
const startedAt = Date.now() - durationMs;
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
parsed = JSON.parse(stdout);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
throw new Error(`nudger parse failed: ${err.message}`);
|
|
40
|
+
}
|
|
41
|
+
if (parsed.is_error || parsed.subtype !== 'success' || !parsed.result) {
|
|
42
|
+
throw new Error(`nudger bad output: ${parsed.result ?? stdout.slice(0, 200)}`);
|
|
43
|
+
}
|
|
44
|
+
const output = parsed.result.trim();
|
|
45
|
+
void logPrompt({
|
|
46
|
+
ts: Math.floor(startedAt / 1000),
|
|
47
|
+
caller: 'journal-nudger',
|
|
48
|
+
args,
|
|
49
|
+
input: prompt,
|
|
50
|
+
output,
|
|
51
|
+
durationMs,
|
|
84
52
|
});
|
|
53
|
+
return output;
|
|
85
54
|
}
|
|
86
55
|
function formatMsg(m) {
|
|
87
56
|
const who = m.direction === 'out' ? 'assistant' : m.pushName || m.senderNumber || 'user';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { 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';
|
|
@@ -20,64 +20,33 @@ async function spawnObserver(prompt) {
|
|
|
20
20
|
'--permission-mode',
|
|
21
21
|
'acceptEdits',
|
|
22
22
|
];
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
});
|
|
49
|
-
child.on('close', (code) => {
|
|
50
|
-
if (code !== 0) {
|
|
51
|
-
logFail(`exit ${code}: ${stderr.slice(0, 300)}`);
|
|
52
|
-
return rejectPromise(new Error(`journal observer exit ${code}: ${stderr.slice(0, 300)}`));
|
|
53
|
-
}
|
|
54
|
-
try {
|
|
55
|
-
const parsed = JSON.parse(stdout);
|
|
56
|
-
if (parsed.is_error ||
|
|
57
|
-
parsed.subtype !== 'success' ||
|
|
58
|
-
!parsed.result) {
|
|
59
|
-
logFail(`bad output: ${parsed.result ?? stderr.slice(0, 200)}`);
|
|
60
|
-
return rejectPromise(new Error(`journal observer bad output: ${parsed.result ?? stderr.slice(0, 200)}`));
|
|
61
|
-
}
|
|
62
|
-
const output = parsed.result.trim();
|
|
63
|
-
void logPrompt({
|
|
64
|
-
ts: Math.floor(startedAt / 1000),
|
|
65
|
-
caller: 'journal-observer',
|
|
66
|
-
args,
|
|
67
|
-
input: prompt,
|
|
68
|
-
output,
|
|
69
|
-
durationMs: Date.now() - startedAt,
|
|
70
|
-
});
|
|
71
|
-
resolvePromise(output);
|
|
72
|
-
}
|
|
73
|
-
catch (err) {
|
|
74
|
-
logFail(`parse failed: ${err.message}`);
|
|
75
|
-
rejectPromise(new Error(`journal observer parse failed: ${err.message}`));
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
child.stdin.write(prompt);
|
|
79
|
-
child.stdin.end();
|
|
23
|
+
const { stdout, durationMs } = await runClaude({
|
|
24
|
+
args,
|
|
25
|
+
input: prompt,
|
|
26
|
+
timeoutMs: TIMEOUT_MS.background,
|
|
27
|
+
caller: 'journal-observer',
|
|
28
|
+
});
|
|
29
|
+
const startedAt = Date.now() - durationMs;
|
|
30
|
+
let parsed;
|
|
31
|
+
try {
|
|
32
|
+
parsed = JSON.parse(stdout);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
throw new Error(`journal observer parse failed: ${err.message}`);
|
|
36
|
+
}
|
|
37
|
+
if (parsed.is_error || parsed.subtype !== 'success' || !parsed.result) {
|
|
38
|
+
throw new Error(`journal observer bad output: ${parsed.result ?? stdout.slice(0, 200)}`);
|
|
39
|
+
}
|
|
40
|
+
const output = parsed.result.trim();
|
|
41
|
+
void logPrompt({
|
|
42
|
+
ts: Math.floor(startedAt / 1000),
|
|
43
|
+
caller: 'journal-observer',
|
|
44
|
+
args,
|
|
45
|
+
input: prompt,
|
|
46
|
+
output,
|
|
47
|
+
durationMs,
|
|
80
48
|
});
|
|
49
|
+
return output;
|
|
81
50
|
}
|
|
82
51
|
function formatMsg(m) {
|
|
83
52
|
const date = new Date(m.timestamp * 1000)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
1
|
import { readFileSync } from 'fs';
|
|
3
2
|
import { resolve } from 'path';
|
|
3
|
+
import { 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';
|
|
@@ -118,64 +118,33 @@ function buildArgs(task) {
|
|
|
118
118
|
}
|
|
119
119
|
async function spawnClaudeForTask(task, prompt) {
|
|
120
120
|
const args = buildArgs(task);
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
});
|
|
147
|
-
child.on('close', (code) => {
|
|
148
|
-
if (code !== 0) {
|
|
149
|
-
logFail(`exit ${code}: ${stderr.slice(0, 300)}`);
|
|
150
|
-
return rejectPromise(new Error(`async task exit ${code}`));
|
|
151
|
-
}
|
|
152
|
-
try {
|
|
153
|
-
const parsed = JSON.parse(stdout);
|
|
154
|
-
if (parsed.is_error ||
|
|
155
|
-
parsed.subtype !== 'success' ||
|
|
156
|
-
!parsed.result) {
|
|
157
|
-
logFail(`bad output: ${parsed.result ?? stderr.slice(0, 200)}`);
|
|
158
|
-
return rejectPromise(new Error('async task bad output'));
|
|
159
|
-
}
|
|
160
|
-
const output = parsed.result.trim();
|
|
161
|
-
void logPrompt({
|
|
162
|
-
ts: Math.floor(startedAt / 1000),
|
|
163
|
-
caller: 'async-task',
|
|
164
|
-
args,
|
|
165
|
-
input: prompt,
|
|
166
|
-
output,
|
|
167
|
-
durationMs: Date.now() - startedAt,
|
|
168
|
-
});
|
|
169
|
-
resolvePromise(output);
|
|
170
|
-
}
|
|
171
|
-
catch (err) {
|
|
172
|
-
logFail(`parse failed: ${err.message}`);
|
|
173
|
-
rejectPromise(err);
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
child.stdin.write(prompt);
|
|
177
|
-
child.stdin.end();
|
|
121
|
+
const { stdout, durationMs } = await runClaude({
|
|
122
|
+
args,
|
|
123
|
+
input: prompt,
|
|
124
|
+
timeoutMs: TIMEOUT_MS.async,
|
|
125
|
+
caller: 'async-task',
|
|
126
|
+
});
|
|
127
|
+
const startedAt = Date.now() - durationMs;
|
|
128
|
+
let parsed;
|
|
129
|
+
try {
|
|
130
|
+
parsed = JSON.parse(stdout);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
throw new Error(`async task parse failed: ${err.message}`);
|
|
134
|
+
}
|
|
135
|
+
if (parsed.is_error || parsed.subtype !== 'success' || !parsed.result) {
|
|
136
|
+
throw new Error(`async task bad output: ${parsed.result ?? stdout.slice(0, 200)}`);
|
|
137
|
+
}
|
|
138
|
+
const output = parsed.result.trim();
|
|
139
|
+
void logPrompt({
|
|
140
|
+
ts: Math.floor(startedAt / 1000),
|
|
141
|
+
caller: 'async-task',
|
|
142
|
+
args,
|
|
143
|
+
input: prompt,
|
|
144
|
+
output,
|
|
145
|
+
durationMs,
|
|
178
146
|
});
|
|
147
|
+
return output;
|
|
179
148
|
}
|
|
180
149
|
async function runTask(task) {
|
|
181
150
|
const prompt = buildPrompt(task);
|