@c4t4/heyamigo 0.6.1 → 0.7.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/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
- return new Promise((resolvePromise, rejectPromise) => {
62
- const child = spawn('claude', args, {
63
- stdio: ['pipe', 'pipe', 'pipe'],
64
- cwd: process.cwd(),
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
  }
@@ -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
+ };
@@ -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
- logger.error({ err, jid: job.jid }, 'pipeline failed');
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 startedAt = Date.now();
234
- return new Promise((resolvePromise, rejectPromise) => {
235
- const child = spawn('claude', args, {
236
- stdio: ['pipe', 'pipe', 'pipe'],
237
- cwd: process.cwd(),
238
- });
239
- let stdout = '';
240
- let stderr = '';
241
- child.stdout.on('data', (c) => {
242
- stdout += c.toString('utf-8');
243
- });
244
- child.stderr.on('data', (c) => {
245
- stderr += c.toString('utf-8');
246
- });
247
- const logFail = (error) => void logPrompt({
248
- ts: Math.floor(startedAt / 1000),
249
- caller: 'compressed',
250
- args,
251
- input: prompt,
252
- error,
253
- durationMs: Date.now() - startedAt,
254
- });
255
- child.on('error', (err) => {
256
- logFail(`spawn failed: ${err.message}`);
257
- rejectPromise(err);
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() {
@@ -1,4 +1,4 @@
1
- import { spawn } from 'child_process';
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 startedAt = Date.now();
23
- return new Promise((resolvePromise, rejectPromise) => {
24
- const child = spawn('claude', args, {
25
- stdio: ['pipe', 'pipe', 'pipe'],
26
- cwd: process.cwd(),
27
- });
28
- let stdout = '';
29
- let stderr = '';
30
- child.stdout.on('data', (chunk) => {
31
- stdout += chunk.toString('utf-8');
32
- });
33
- child.stderr.on('data', (chunk) => {
34
- stderr += chunk.toString('utf-8');
35
- });
36
- const logFail = (error) => void logPrompt({
37
- ts: Math.floor(startedAt / 1000),
38
- caller: 'digester',
39
- args,
40
- input: prompt,
41
- error,
42
- durationMs: Date.now() - startedAt,
43
- });
44
- child.on('error', (err) => {
45
- logFail(`spawn failed: ${err.message}`);
46
- rejectPromise(new Error(`digester spawn failed: ${err.message}`));
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 { spawn } from 'child_process';
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 startedAt = Date.now();
28
- return new Promise((resolvePromise, rejectPromise) => {
29
- const child = spawn('claude', args, {
30
- stdio: ['pipe', 'pipe', 'pipe'],
31
- cwd: process.cwd(),
32
- });
33
- let stdout = '';
34
- let stderr = '';
35
- child.stdout.on('data', (c) => {
36
- stdout += c.toString('utf-8');
37
- });
38
- child.stderr.on('data', (c) => {
39
- stderr += c.toString('utf-8');
40
- });
41
- const logFail = (error) => void logPrompt({
42
- ts: Math.floor(startedAt / 1000),
43
- caller: 'journal-nudger',
44
- args,
45
- input: prompt,
46
- error,
47
- durationMs: Date.now() - startedAt,
48
- });
49
- child.on('error', (err) => {
50
- logFail(`spawn failed: ${err.message}`);
51
- rejectPromise(err);
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 { spawn } from 'child_process';
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 startedAt = Date.now();
24
- return new Promise((resolvePromise, rejectPromise) => {
25
- const child = spawn('claude', args, {
26
- stdio: ['pipe', 'pipe', 'pipe'],
27
- cwd: process.cwd(),
28
- });
29
- let stdout = '';
30
- let stderr = '';
31
- child.stdout.on('data', (chunk) => {
32
- stdout += chunk.toString('utf-8');
33
- });
34
- child.stderr.on('data', (chunk) => {
35
- stderr += chunk.toString('utf-8');
36
- });
37
- const logFail = (error) => void logPrompt({
38
- ts: Math.floor(startedAt / 1000),
39
- caller: 'journal-observer',
40
- args,
41
- input: prompt,
42
- error,
43
- durationMs: Date.now() - startedAt,
44
- });
45
- child.on('error', (err) => {
46
- logFail(`spawn failed: ${err.message}`);
47
- rejectPromise(new Error(`journal observer spawn failed: ${err.message}`));
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 startedAt = Date.now();
122
- return new Promise((resolvePromise, rejectPromise) => {
123
- const child = spawn('claude', args, {
124
- stdio: ['pipe', 'pipe', 'pipe'],
125
- cwd: process.cwd(),
126
- });
127
- let stdout = '';
128
- let stderr = '';
129
- child.stdout.on('data', (c) => {
130
- stdout += c.toString('utf-8');
131
- });
132
- child.stderr.on('data', (c) => {
133
- stderr += c.toString('utf-8');
134
- });
135
- const logFail = (error) => void logPrompt({
136
- ts: Math.floor(startedAt / 1000),
137
- caller: 'async-task',
138
- args,
139
- input: prompt,
140
- error,
141
- durationMs: Date.now() - startedAt,
142
- });
143
- child.on('error', (err) => {
144
- logFail(`spawn failed: ${err.message}`);
145
- rejectPromise(err);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c4t4/heyamigo",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "WhatsApp AI bot powered by Claude with long-term memory, browser control, and role-based access",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",