@c4t4/heyamigo 0.8.7 → 0.8.8
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 +102 -0
- package/dist/ai/codex.js +224 -0
- package/dist/ai/provider.js +9 -0
- package/dist/ai/providers.js +18 -0
- package/dist/ai/spawn.js +15 -12
- package/dist/cli/setup.js +56 -1
- package/dist/config.js +5 -0
- package/dist/gateway/commands.js +2 -2
- package/dist/memory/compressed.js +5 -36
- package/dist/memory/digest.js +7 -39
- package/dist/memory/journal-nudger.js +5 -35
- package/dist/memory/journal-observer.js +6 -36
- package/dist/queue/async-tasks.js +39 -154
- package/dist/queue/worker.js +2 -2
- package/package.json +1 -1
package/dist/ai/claude.js
CHANGED
|
@@ -104,3 +104,105 @@ export async function askClaude(params) {
|
|
|
104
104
|
});
|
|
105
105
|
return result;
|
|
106
106
|
}
|
|
107
|
+
// Claude's per-mode permission + tool defaults. The caller can still override
|
|
108
|
+
// allowedTools explicitly; mode just sets the floor.
|
|
109
|
+
function permissionModeFor(mode) {
|
|
110
|
+
switch (mode) {
|
|
111
|
+
case 'read-only':
|
|
112
|
+
return 'default'; // prompts on writes; we layer allowedTools to enforce
|
|
113
|
+
case 'auto':
|
|
114
|
+
case 'full':
|
|
115
|
+
return 'acceptEdits';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function defaultAllowedToolsFor(mode) {
|
|
119
|
+
if (mode === 'read-only')
|
|
120
|
+
return ['Read', 'Grep', 'Glob', 'WebFetch'];
|
|
121
|
+
return undefined; // no restriction
|
|
122
|
+
}
|
|
123
|
+
function buildTaskArgs(params) {
|
|
124
|
+
const args = [
|
|
125
|
+
'-p',
|
|
126
|
+
'--output-format',
|
|
127
|
+
'stream-json',
|
|
128
|
+
'--verbose',
|
|
129
|
+
'--model',
|
|
130
|
+
config.claude.model,
|
|
131
|
+
'--permission-mode',
|
|
132
|
+
permissionModeFor(params.mode),
|
|
133
|
+
];
|
|
134
|
+
if (params.sessionId) {
|
|
135
|
+
args.push('--resume', params.sessionId);
|
|
136
|
+
}
|
|
137
|
+
else if (params.includeSystemPrompt) {
|
|
138
|
+
args.push('--append-system-prompt', systemPrompt());
|
|
139
|
+
}
|
|
140
|
+
// On fresh sessions, fold the configured baseline read dirs in too. On
|
|
141
|
+
// resume Claude already has them baked into the session state.
|
|
142
|
+
if (!params.sessionId && params.includeSystemPrompt) {
|
|
143
|
+
for (const dir of config.claude.addDirs) {
|
|
144
|
+
args.push('--add-dir', resolve(process.cwd(), dir));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (const dir of params.addDirs ?? []) {
|
|
148
|
+
args.push('--add-dir', resolve(process.cwd(), dir));
|
|
149
|
+
}
|
|
150
|
+
const allowedTools = params.allowedTools && params.allowedTools !== 'all'
|
|
151
|
+
? params.allowedTools
|
|
152
|
+
: defaultAllowedToolsFor(params.mode);
|
|
153
|
+
if (allowedTools && allowedTools.length > 0) {
|
|
154
|
+
args.push('--allowedTools', allowedTools.join(','));
|
|
155
|
+
}
|
|
156
|
+
return args;
|
|
157
|
+
}
|
|
158
|
+
function laneTimeoutMs(lane) {
|
|
159
|
+
return TIMEOUT_MS[lane];
|
|
160
|
+
}
|
|
161
|
+
export async function runClaudeTask(params) {
|
|
162
|
+
const args = buildTaskArgs(params);
|
|
163
|
+
const { stdout, stderr, durationMs } = await runClaude({
|
|
164
|
+
args,
|
|
165
|
+
input: params.input,
|
|
166
|
+
timeoutMs: laneTimeoutMs(params.lane),
|
|
167
|
+
caller: params.caller,
|
|
168
|
+
});
|
|
169
|
+
const startedAt = Date.now() - durationMs;
|
|
170
|
+
const parsed = parseStreamJson(stdout);
|
|
171
|
+
if (!parsed) {
|
|
172
|
+
throw new Error(`${params.caller} stream-json produced no result event: ${stdout.slice(0, 200)}`);
|
|
173
|
+
}
|
|
174
|
+
if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
|
|
175
|
+
throw new Error(`${params.caller} bad output: ${parsed.result || stdout.slice(0, 200)}`);
|
|
176
|
+
}
|
|
177
|
+
const reply = parsed.result.trim();
|
|
178
|
+
const usage = {
|
|
179
|
+
inputTokens: parsed.usage?.input_tokens ?? 0,
|
|
180
|
+
cacheReadTokens: parsed.usage?.cache_read_input_tokens ?? 0,
|
|
181
|
+
cacheCreationTokens: parsed.usage?.cache_creation_input_tokens ?? 0,
|
|
182
|
+
outputTokens: parsed.usage?.output_tokens ?? 0,
|
|
183
|
+
numTurns: parsed.numTurns ?? 0,
|
|
184
|
+
};
|
|
185
|
+
void logPrompt({
|
|
186
|
+
ts: Math.floor(startedAt / 1000),
|
|
187
|
+
caller: params.caller,
|
|
188
|
+
args,
|
|
189
|
+
input: params.input,
|
|
190
|
+
output: reply,
|
|
191
|
+
sessionId: parsed.sessionId ?? undefined,
|
|
192
|
+
usage,
|
|
193
|
+
durationMs,
|
|
194
|
+
stderr,
|
|
195
|
+
eventTypes: parsed.eventTypes,
|
|
196
|
+
});
|
|
197
|
+
return {
|
|
198
|
+
reply,
|
|
199
|
+
sessionId: parsed.sessionId ?? undefined,
|
|
200
|
+
usage,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
export const claudeProvider = {
|
|
204
|
+
name: 'claude',
|
|
205
|
+
ask: askClaude,
|
|
206
|
+
runTask: runClaudeTask,
|
|
207
|
+
reloadSystemPrompt,
|
|
208
|
+
};
|
package/dist/ai/codex.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Codex CLI provider. Maps the neutral AiProvider contract onto
|
|
2
|
+
// `codex exec --json`. Flag names match the public CLI docs as of writing
|
|
3
|
+
// (developers.openai.com/codex/cli/features); if your local Codex version
|
|
4
|
+
// uses different flags, the small surface here is the only place to adjust.
|
|
5
|
+
//
|
|
6
|
+
// What's wired:
|
|
7
|
+
// - exec mode with --json (NDJSON event stream on stdout)
|
|
8
|
+
// - --add-dir for extra writable roots
|
|
9
|
+
// - --sandbox-mode for tier (read-only / workspace-write / danger-full-access)
|
|
10
|
+
// - --resume <id> for session continuation
|
|
11
|
+
// - prompt passed on stdin (matches the spawn plumbing that already
|
|
12
|
+
// pipes input to child.stdin)
|
|
13
|
+
//
|
|
14
|
+
// What's deliberately coarse:
|
|
15
|
+
// - allowedTools is ignored on this provider. Codex has no per-tool
|
|
16
|
+
// allowlist; the sandbox mode is the only knob. The mode argument
|
|
17
|
+
// covers the practical cases (read vs. write vs. full).
|
|
18
|
+
import { readFileSync } from 'fs';
|
|
19
|
+
import { resolve } from 'path';
|
|
20
|
+
import { config } from '../config.js';
|
|
21
|
+
import { logger } from '../logger.js';
|
|
22
|
+
import { logPrompt } from '../promptlog.js';
|
|
23
|
+
import { runClaude, TIMEOUT_MS } from './spawn.js';
|
|
24
|
+
let cachedSystemPrompt = null;
|
|
25
|
+
function systemPrompt() {
|
|
26
|
+
if (cachedSystemPrompt !== null)
|
|
27
|
+
return cachedSystemPrompt;
|
|
28
|
+
const personality = readFileSync(resolve(process.cwd(), config.claude.personalityFile), 'utf-8');
|
|
29
|
+
let memoryInstructions = '';
|
|
30
|
+
try {
|
|
31
|
+
memoryInstructions = readFileSync(resolve(process.cwd(), config.memory.instructionsFile), 'utf-8');
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// memory instructions optional
|
|
35
|
+
}
|
|
36
|
+
cachedSystemPrompt = memoryInstructions
|
|
37
|
+
? `${personality}\n\n---\n\n${memoryInstructions}`
|
|
38
|
+
: personality;
|
|
39
|
+
return cachedSystemPrompt;
|
|
40
|
+
}
|
|
41
|
+
function reloadSystemPrompt() {
|
|
42
|
+
cachedSystemPrompt = null;
|
|
43
|
+
}
|
|
44
|
+
// Codex sandbox vocabulary. The CLI flag is --sandbox-mode (or --sandbox in
|
|
45
|
+
// some builds); values are: read-only, workspace-write, danger-full-access.
|
|
46
|
+
function sandboxFor(mode) {
|
|
47
|
+
switch (mode) {
|
|
48
|
+
case 'read-only':
|
|
49
|
+
return 'read-only';
|
|
50
|
+
case 'auto':
|
|
51
|
+
return 'workspace-write';
|
|
52
|
+
case 'full':
|
|
53
|
+
return 'danger-full-access';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function laneTimeoutMs(lane) {
|
|
57
|
+
return TIMEOUT_MS[lane];
|
|
58
|
+
}
|
|
59
|
+
function buildExecArgs(params) {
|
|
60
|
+
const args = ['exec', '--json'];
|
|
61
|
+
args.push('--sandbox-mode', sandboxFor(params.mode));
|
|
62
|
+
if (params.sessionId) {
|
|
63
|
+
// Resume keeps the prior conversation; system prompt and add-dirs
|
|
64
|
+
// were baked in on the original turn.
|
|
65
|
+
args.push('--resume', params.sessionId);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
for (const dir of params.addDirs ?? []) {
|
|
69
|
+
args.push('--add-dir', resolve(process.cwd(), dir));
|
|
70
|
+
}
|
|
71
|
+
if (params.includeSystemPrompt) {
|
|
72
|
+
// Codex doesn't have Claude's --append-system-prompt. The portable
|
|
73
|
+
// approach is to inline the personality at the top of the prompt.
|
|
74
|
+
// (An alternative is writing AGENTS.md into cwd; we don't do that
|
|
75
|
+
// here because it'd mutate the repo.)
|
|
76
|
+
params.prompt = `${systemPrompt()}\n\n---\n\n${params.prompt}`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Prompt as positional arg. `codex exec` reads stdin only with `-`, and
|
|
80
|
+
// passing it positionally avoids ambiguity with the spawn pipe.
|
|
81
|
+
args.push(params.prompt);
|
|
82
|
+
return args;
|
|
83
|
+
}
|
|
84
|
+
function parseCodexOutput(stdout) {
|
|
85
|
+
const events = [];
|
|
86
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
87
|
+
const trimmed = line.trim();
|
|
88
|
+
if (!trimmed)
|
|
89
|
+
continue;
|
|
90
|
+
try {
|
|
91
|
+
events.push(JSON.parse(trimmed));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Codex occasionally emits non-JSON preamble; skip it.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (events.length === 0)
|
|
98
|
+
return null;
|
|
99
|
+
// Find the final agent message. Codex labels it variously across
|
|
100
|
+
// versions — try the common shapes in order.
|
|
101
|
+
let reply = null;
|
|
102
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
103
|
+
const ev = events[i];
|
|
104
|
+
if (ev.msg?.type === 'agent_message' &&
|
|
105
|
+
typeof ev.msg.message === 'string') {
|
|
106
|
+
reply = ev.msg.message;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
if (typeof ev.message === 'string') {
|
|
110
|
+
reply = ev.message;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
if (typeof ev.text === 'string') {
|
|
114
|
+
reply = ev.text;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (reply === null)
|
|
119
|
+
return null;
|
|
120
|
+
// Session id — Codex uses different field names across versions.
|
|
121
|
+
let sessionId;
|
|
122
|
+
for (const ev of events) {
|
|
123
|
+
const id = ev.session_id ?? ev.conversation_id ?? ev.response_id;
|
|
124
|
+
if (typeof id === 'string' && id) {
|
|
125
|
+
sessionId = id;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Usage — last event with a usage object wins (final turn totals).
|
|
130
|
+
let inputTokens = 0;
|
|
131
|
+
let outputTokens = 0;
|
|
132
|
+
let cacheReadTokens = 0;
|
|
133
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
134
|
+
const u = events[i].usage;
|
|
135
|
+
if (u) {
|
|
136
|
+
inputTokens = u.input_tokens ?? 0;
|
|
137
|
+
outputTokens = u.output_tokens ?? 0;
|
|
138
|
+
cacheReadTokens = u.cached_input_tokens ?? 0;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
reply: reply.trim(),
|
|
144
|
+
sessionId,
|
|
145
|
+
usage: {
|
|
146
|
+
inputTokens,
|
|
147
|
+
cacheReadTokens,
|
|
148
|
+
cacheCreationTokens: 0,
|
|
149
|
+
outputTokens,
|
|
150
|
+
numTurns: 0,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async function runCodexTask(params) {
|
|
155
|
+
const args = buildExecArgs({
|
|
156
|
+
mode: params.mode,
|
|
157
|
+
addDirs: params.addDirs,
|
|
158
|
+
sessionId: params.sessionId,
|
|
159
|
+
includeSystemPrompt: params.includeSystemPrompt,
|
|
160
|
+
prompt: params.input,
|
|
161
|
+
});
|
|
162
|
+
logger.debug({ caller: params.caller, resume: !!params.sessionId }, 'spawning codex exec');
|
|
163
|
+
// input is empty here — the prompt rides in argv (Codex exec semantics).
|
|
164
|
+
// Empty stdin end() is harmless.
|
|
165
|
+
const { stdout, stderr, durationMs } = await runClaude({
|
|
166
|
+
args,
|
|
167
|
+
input: '',
|
|
168
|
+
timeoutMs: laneTimeoutMs(params.lane),
|
|
169
|
+
caller: params.caller,
|
|
170
|
+
bin: 'codex',
|
|
171
|
+
});
|
|
172
|
+
const startedAt = Date.now() - durationMs;
|
|
173
|
+
const parsed = parseCodexOutput(stdout);
|
|
174
|
+
if (!parsed) {
|
|
175
|
+
throw new Error(`codex produced no parseable result; stdout: ${stdout.slice(0, 500)}`);
|
|
176
|
+
}
|
|
177
|
+
void logPrompt({
|
|
178
|
+
ts: Math.floor(startedAt / 1000),
|
|
179
|
+
caller: params.caller,
|
|
180
|
+
args,
|
|
181
|
+
input: params.input,
|
|
182
|
+
output: parsed.reply,
|
|
183
|
+
sessionId: parsed.sessionId,
|
|
184
|
+
usage: parsed.usage,
|
|
185
|
+
durationMs,
|
|
186
|
+
stderr,
|
|
187
|
+
});
|
|
188
|
+
return parsed;
|
|
189
|
+
}
|
|
190
|
+
async function askCodex(params) {
|
|
191
|
+
const result = await runCodexTask({
|
|
192
|
+
input: params.input,
|
|
193
|
+
caller: 'worker',
|
|
194
|
+
mode: 'auto',
|
|
195
|
+
lane: 'main',
|
|
196
|
+
sessionId: params.sessionId,
|
|
197
|
+
includeSystemPrompt: true,
|
|
198
|
+
allowedTools: params.allowedTools,
|
|
199
|
+
addDirs: [
|
|
200
|
+
config.memory.dir,
|
|
201
|
+
config.storage.mediaDir,
|
|
202
|
+
],
|
|
203
|
+
});
|
|
204
|
+
if (!result.sessionId) {
|
|
205
|
+
throw new Error('codex ask: response missing session id');
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
reply: result.reply,
|
|
209
|
+
sessionId: result.sessionId,
|
|
210
|
+
usage: result.usage ?? {
|
|
211
|
+
inputTokens: 0,
|
|
212
|
+
cacheReadTokens: 0,
|
|
213
|
+
cacheCreationTokens: 0,
|
|
214
|
+
outputTokens: 0,
|
|
215
|
+
numTurns: 0,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
export const codexProvider = {
|
|
220
|
+
name: 'codex',
|
|
221
|
+
ask: askCodex,
|
|
222
|
+
runTask: runCodexTask,
|
|
223
|
+
reloadSystemPrompt,
|
|
224
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Provider abstraction for the user-facing chat ask path. Lets the worker
|
|
2
|
+
// route conversation turns to either Claude or Codex (or any future CLI)
|
|
3
|
+
// without knowing the wire details.
|
|
4
|
+
//
|
|
5
|
+
// Scope: covers the interactive worker call (one turn in, one turn out, with
|
|
6
|
+
// resumable session ids and per-role tool gating). Memory digests, async
|
|
7
|
+
// tasks, and the journal observers stay on `runClaude` directly — they're
|
|
8
|
+
// Claude-specific batch pipelines and not in this interface.
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { config } from '../config.js';
|
|
2
|
+
import { claudeProvider } from './claude.js';
|
|
3
|
+
import { codexProvider } from './codex.js';
|
|
4
|
+
const REGISTRY = {
|
|
5
|
+
claude: claudeProvider,
|
|
6
|
+
codex: codexProvider,
|
|
7
|
+
};
|
|
8
|
+
// Resolve the active provider. Defaults to claude if no override is set in
|
|
9
|
+
// config; pass an explicit name to force one (useful for per-role routing
|
|
10
|
+
// once access.json learns about it).
|
|
11
|
+
export function getProvider(name) {
|
|
12
|
+
const resolved = name ?? config.ai.provider;
|
|
13
|
+
return REGISTRY[resolved];
|
|
14
|
+
}
|
|
15
|
+
export function reloadAllSystemPrompts() {
|
|
16
|
+
for (const p of Object.values(REGISTRY))
|
|
17
|
+
p.reloadSystemPrompt();
|
|
18
|
+
}
|
package/dist/ai/spawn.js
CHANGED
|
@@ -100,23 +100,26 @@ function killGroup(child, signal) {
|
|
|
100
100
|
// should go through this.
|
|
101
101
|
export async function runClaude(opts) {
|
|
102
102
|
const { args, input, timeoutMs, caller } = opts;
|
|
103
|
+
const bin = opts.bin ?? 'claude';
|
|
103
104
|
const startedAt = Date.now();
|
|
104
105
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
105
|
-
const
|
|
106
|
+
const env = { ...process.env };
|
|
107
|
+
if (bin === 'claude') {
|
|
108
|
+
// ANTHROPIC_LOG=debug surfaces the SDK's HTTP layer to stderr:
|
|
109
|
+
// request URLs, status codes, retries, rate-limit notices. We
|
|
110
|
+
// capture stderr and put a truncated copy into the promptlog so
|
|
111
|
+
// we can diagnose API hangs/rate-limits post-mortem instead of
|
|
112
|
+
// staring at "Claude subprocess is idle, why?". Only meaningful
|
|
113
|
+
// for the Anthropic CLI; other providers ignore it.
|
|
114
|
+
env.ANTHROPIC_LOG = process.env.ANTHROPIC_LOG ?? 'debug';
|
|
115
|
+
}
|
|
116
|
+
const child = spawn(bin, args, {
|
|
106
117
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
107
118
|
cwd: opts.cwd ?? process.cwd(),
|
|
108
119
|
// detached:true puts the child in its own process group, so killGroup
|
|
109
120
|
// can SIGTERM the whole tree (Playwright MCP, Chromium, etc.) at once.
|
|
110
121
|
detached: true,
|
|
111
|
-
|
|
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
|
-
},
|
|
122
|
+
env,
|
|
120
123
|
});
|
|
121
124
|
let stdout = '';
|
|
122
125
|
let stderr = '';
|
|
@@ -153,7 +156,7 @@ export async function runClaude(opts) {
|
|
|
153
156
|
settled = true;
|
|
154
157
|
clearTimeout(timer);
|
|
155
158
|
logFail(`spawn failed: ${err.message}`);
|
|
156
|
-
rejectPromise(new ClaudeSpawnError(caller,
|
|
159
|
+
rejectPromise(new ClaudeSpawnError(caller, `${bin} spawn failed: ${err.message}`));
|
|
157
160
|
});
|
|
158
161
|
child.on('close', (code, signal) => {
|
|
159
162
|
if (settled)
|
|
@@ -167,7 +170,7 @@ export async function runClaude(opts) {
|
|
|
167
170
|
}
|
|
168
171
|
if (code !== 0) {
|
|
169
172
|
logFail(`exit ${code}: ${stderr.slice(0, 500)}`);
|
|
170
|
-
return rejectPromise(new ClaudeSpawnError(caller,
|
|
173
|
+
return rejectPromise(new ClaudeSpawnError(caller, `${bin} exited with code ${code}: ${stderr.slice(0, 500)}`));
|
|
171
174
|
}
|
|
172
175
|
resolvePromise({ stdout, stderr, durationMs });
|
|
173
176
|
});
|
package/dist/cli/setup.js
CHANGED
|
@@ -18,6 +18,41 @@ function which(bin) {
|
|
|
18
18
|
const r = run(`which ${bin}`);
|
|
19
19
|
return r.ok ? r.output : null;
|
|
20
20
|
}
|
|
21
|
+
// Idempotently add the Playwright MCP entry to ~/.codex/config.toml so the
|
|
22
|
+
// Codex CLI auto-launches the same MCP server Claude uses. We don't have a
|
|
23
|
+
// TOML parser available; the entry has a fixed, simple shape so a text-level
|
|
24
|
+
// presence check + append is safe enough here. Returns true if the entry was
|
|
25
|
+
// added or already present, false on write failure.
|
|
26
|
+
function addPlaywrightToCodexConfig(cdpUrl) {
|
|
27
|
+
const codexDir = resolve(homedir(), '.codex');
|
|
28
|
+
const configPath = resolve(codexDir, 'config.toml');
|
|
29
|
+
const block = [
|
|
30
|
+
'',
|
|
31
|
+
'[mcp_servers.playwright]',
|
|
32
|
+
'command = "npx"',
|
|
33
|
+
`args = ["@playwright/mcp@latest", "--cdp-endpoint", "${cdpUrl}"]`,
|
|
34
|
+
'',
|
|
35
|
+
].join('\n');
|
|
36
|
+
try {
|
|
37
|
+
mkdirSync(codexDir, { recursive: true });
|
|
38
|
+
let existing = '';
|
|
39
|
+
if (existsSync(configPath)) {
|
|
40
|
+
existing = readFileSync(configPath, 'utf-8');
|
|
41
|
+
// Match both [mcp_servers.playwright] and [mcp_servers."playwright"].
|
|
42
|
+
if (/\[mcp_servers\.(?:"?)playwright(?:"?)\]/.test(existing)) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const next = existing.endsWith('\n') || existing === ''
|
|
47
|
+
? existing + block
|
|
48
|
+
: existing + '\n' + block;
|
|
49
|
+
writeFileSync(configPath, next, 'utf-8');
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
21
56
|
function runLive(cmd) {
|
|
22
57
|
const result = spawnSync('sh', ['-c', cmd], { stdio: 'inherit' });
|
|
23
58
|
return result.status === 0;
|
|
@@ -296,16 +331,26 @@ export async function runSetup() {
|
|
|
296
331
|
if (!isLinux) {
|
|
297
332
|
p.log.warning('Automated browser setup is available on Linux only. ' +
|
|
298
333
|
'On macOS/Windows: start Chrome with --remote-debugging-port=9222 manually, ' +
|
|
299
|
-
'then
|
|
334
|
+
'then for Claude: claude mcp add playwright -- npx @playwright/mcp@latest --cdp-endpoint "http://localhost:9222"; ' +
|
|
335
|
+
'for Codex: add [mcp_servers.playwright] to ~/.codex/config.toml.');
|
|
300
336
|
}
|
|
301
337
|
else {
|
|
302
338
|
// ── Check if already running ─────────────────────────────
|
|
303
339
|
const cdpUrl = 'http://localhost:9222';
|
|
304
340
|
const alreadyRunning = run(`curl -s '${cdpUrl}/json/version'`);
|
|
305
341
|
const mcpConfigured = run('claude mcp list 2>/dev/null').output.includes('playwright');
|
|
342
|
+
const hasCodex = !!which('codex');
|
|
306
343
|
if (alreadyRunning.ok && alreadyRunning.output.includes('Browser') && mcpConfigured) {
|
|
307
344
|
p.log.success('Chrome already running (localhost:9222)');
|
|
308
345
|
p.log.success('Claude already connected to Chrome');
|
|
346
|
+
if (hasCodex) {
|
|
347
|
+
if (addPlaywrightToCodexConfig(cdpUrl)) {
|
|
348
|
+
p.log.success('Codex connected to Chrome (~/.codex/config.toml)');
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
p.log.warning('Could not write ~/.codex/config.toml — add [mcp_servers.playwright] manually');
|
|
352
|
+
}
|
|
353
|
+
}
|
|
309
354
|
p.log.info('View browser (SSH tunnel):\n' +
|
|
310
355
|
` ssh -L 6090:127.0.0.1:6090 ${process.env.USER || 'root'}@<server-ip>\n` +
|
|
311
356
|
' Then open: http://localhost:6090/vnc.html');
|
|
@@ -394,6 +439,16 @@ export async function runSetup() {
|
|
|
394
439
|
sc.stop('Connection failed');
|
|
395
440
|
p.log.warning('Run manually: claude mcp add playwright -- npx @playwright/mcp@latest --cdp-endpoint "http://localhost:9222"');
|
|
396
441
|
}
|
|
442
|
+
// Mirror the MCP entry into Codex if it's installed, so the same
|
|
443
|
+
// browser lane works when ai.provider is flipped to codex.
|
|
444
|
+
if (hasCodex) {
|
|
445
|
+
if (addPlaywrightToCodexConfig(cdpUrl)) {
|
|
446
|
+
p.log.success('Codex connected to Chrome (~/.codex/config.toml)');
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
p.log.warning('Could not write ~/.codex/config.toml — add [mcp_servers.playwright] manually');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
397
452
|
if (vncInstalled) {
|
|
398
453
|
p.log.info('Watch the browser (localhost only, via SSH tunnel):\n' +
|
|
399
454
|
` ssh -L 6090:127.0.0.1:6090 ${process.env.USER || 'root'}@<server-ip>\n` +
|
package/dist/config.js
CHANGED
|
@@ -24,6 +24,11 @@ const ConfigSchema = z.object({
|
|
|
24
24
|
status: z.array(z.string()),
|
|
25
25
|
reload: z.array(z.string()),
|
|
26
26
|
}),
|
|
27
|
+
ai: z
|
|
28
|
+
.object({
|
|
29
|
+
provider: z.enum(['claude', 'codex']).default('claude'),
|
|
30
|
+
})
|
|
31
|
+
.default({ provider: 'claude' }),
|
|
27
32
|
claude: z.object({
|
|
28
33
|
model: z.string(),
|
|
29
34
|
personalityFile: z.string(),
|
package/dist/gateway/commands.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { clearSession, getSessionInfo } from '../ai/sessions.js';
|
|
2
|
-
import {
|
|
2
|
+
import { reloadAllSystemPrompts } from '../ai/providers.js';
|
|
3
3
|
import { config } from '../config.js';
|
|
4
4
|
import { runDigestNow } from '../memory/scheduler.js';
|
|
5
5
|
import { sendText } from '../wa/sender.js';
|
|
@@ -41,7 +41,7 @@ export async function tryCommand(ctx) {
|
|
|
41
41
|
return true;
|
|
42
42
|
}
|
|
43
43
|
if (config.commands.reload.includes(cmd)) {
|
|
44
|
-
|
|
44
|
+
reloadAllSystemPrompts();
|
|
45
45
|
const existed = clearSession(ctx.jid);
|
|
46
46
|
const reply = existed
|
|
47
47
|
? 'Personality reloaded and session reset.'
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, writeFileSync, } from 'fs';
|
|
2
2
|
import { dirname, resolve } from 'path';
|
|
3
3
|
import { mkdirSync } from 'fs';
|
|
4
|
-
import {
|
|
5
|
-
import { config } from '../config.js';
|
|
4
|
+
import { getProvider } from '../ai/providers.js';
|
|
6
5
|
import { logger } from '../logger.js';
|
|
7
|
-
import { logPrompt } from '../promptlog.js';
|
|
8
6
|
import { listJournals, readEntries } from './journals.js';
|
|
9
7
|
import { memoryRoot, treeRoot, entityIndexPath } from './paths.js';
|
|
10
8
|
// The compressed view is a rolling index across all memory: people, chats,
|
|
@@ -221,42 +219,13 @@ ${raw}
|
|
|
221
219
|
Output ONLY the compressed index in the exact format above. No preamble, no explanation, no code fences.`;
|
|
222
220
|
}
|
|
223
221
|
async function spawnGenerator(prompt) {
|
|
224
|
-
const
|
|
225
|
-
'-p',
|
|
226
|
-
'--output-format',
|
|
227
|
-
'stream-json',
|
|
228
|
-
'--verbose',
|
|
229
|
-
'--model',
|
|
230
|
-
config.claude.model,
|
|
231
|
-
'--permission-mode',
|
|
232
|
-
'acceptEdits',
|
|
233
|
-
];
|
|
234
|
-
const { stdout, stderr, durationMs } = await runClaude({
|
|
235
|
-
args,
|
|
222
|
+
const { reply } = await getProvider().runTask({
|
|
236
223
|
input: prompt,
|
|
237
|
-
timeoutMs: TIMEOUT_MS.background,
|
|
238
224
|
caller: 'compressed',
|
|
225
|
+
mode: 'auto',
|
|
226
|
+
lane: 'background',
|
|
239
227
|
});
|
|
240
|
-
|
|
241
|
-
const parsed = parseStreamJson(stdout);
|
|
242
|
-
if (!parsed) {
|
|
243
|
-
throw new Error(`compressed stream-json produced no result event: ${stdout.slice(0, 200)}`);
|
|
244
|
-
}
|
|
245
|
-
if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
|
|
246
|
-
throw new Error(`compressed bad output: ${parsed.result || stdout.slice(0, 200)}`);
|
|
247
|
-
}
|
|
248
|
-
const output = parsed.result.trim();
|
|
249
|
-
void logPrompt({
|
|
250
|
-
ts: Math.floor(startedAt / 1000),
|
|
251
|
-
caller: 'compressed',
|
|
252
|
-
args,
|
|
253
|
-
input: prompt,
|
|
254
|
-
output,
|
|
255
|
-
durationMs,
|
|
256
|
-
stderr,
|
|
257
|
-
eventTypes: parsed.eventTypes,
|
|
258
|
-
});
|
|
259
|
-
return output;
|
|
228
|
+
return reply;
|
|
260
229
|
}
|
|
261
230
|
let buildInFlight = null;
|
|
262
231
|
export async function rebuildCompressed() {
|
package/dist/memory/digest.js
CHANGED
|
@@ -1,51 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getProvider } from '../ai/providers.js';
|
|
2
2
|
import { config } from '../config.js';
|
|
3
3
|
import { logger } from '../logger.js';
|
|
4
|
-
import { logPrompt } from '../promptlog.js';
|
|
5
4
|
import { readLast } from '../store/messages.js';
|
|
6
5
|
import { markCompressedDirty } from './compressed.js';
|
|
7
6
|
import { readBrief, readProfile, setLastDigestedAt, writeBrief, writeProfile, } from './store.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* Returns the new content Claude proposed.
|
|
11
|
-
*/
|
|
7
|
+
// Stateless agent call to consolidate memory. Routed through the provider
|
|
8
|
+
// abstraction so it runs on whichever CLI is configured.
|
|
12
9
|
async function spawnDigester(prompt) {
|
|
13
|
-
const
|
|
14
|
-
'-p',
|
|
15
|
-
'--output-format',
|
|
16
|
-
'stream-json',
|
|
17
|
-
'--verbose',
|
|
18
|
-
'--model',
|
|
19
|
-
config.claude.model,
|
|
20
|
-
'--permission-mode',
|
|
21
|
-
'acceptEdits',
|
|
22
|
-
];
|
|
23
|
-
const { stdout, stderr, durationMs } = await runClaude({
|
|
24
|
-
args,
|
|
10
|
+
const { reply } = await getProvider().runTask({
|
|
25
11
|
input: prompt,
|
|
26
|
-
timeoutMs: TIMEOUT_MS.background,
|
|
27
|
-
caller: 'digester',
|
|
28
|
-
});
|
|
29
|
-
const startedAt = Date.now() - durationMs;
|
|
30
|
-
const parsed = parseStreamJson(stdout);
|
|
31
|
-
if (!parsed) {
|
|
32
|
-
throw new Error(`digester stream-json produced no result event: ${stdout.slice(0, 200)}`);
|
|
33
|
-
}
|
|
34
|
-
if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
|
|
35
|
-
throw new Error(`digester bad output: ${parsed.result || stdout.slice(0, 200)}`);
|
|
36
|
-
}
|
|
37
|
-
const output = parsed.result.trim();
|
|
38
|
-
void logPrompt({
|
|
39
|
-
ts: Math.floor(startedAt / 1000),
|
|
40
12
|
caller: 'digester',
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
output,
|
|
44
|
-
durationMs,
|
|
45
|
-
stderr,
|
|
46
|
-
eventTypes: parsed.eventTypes,
|
|
13
|
+
mode: 'auto',
|
|
14
|
+
lane: 'background',
|
|
47
15
|
});
|
|
48
|
-
return
|
|
16
|
+
return reply;
|
|
49
17
|
}
|
|
50
18
|
function formatMessagesForDigest(messages) {
|
|
51
19
|
return messages
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getProvider } from '../ai/providers.js';
|
|
2
2
|
import { config } from '../config.js';
|
|
3
3
|
import { initiate } from '../gateway/outgoing.js';
|
|
4
4
|
import { logger } from '../logger.js';
|
|
5
|
-
import { logPrompt } from '../promptlog.js';
|
|
6
5
|
import { readLast } from '../store/messages.js';
|
|
7
6
|
import { canSendProactive } from '../wa/whitelist.js';
|
|
8
7
|
import { isInQuietHours, nextFireTs, parseCadence, } from './journal-cadence.js';
|
|
@@ -15,42 +14,13 @@ function defaultNudgeJid() {
|
|
|
15
14
|
return `${config.owner.number}@s.whatsapp.net`;
|
|
16
15
|
}
|
|
17
16
|
async function spawnComposer(prompt) {
|
|
18
|
-
const
|
|
19
|
-
'-p',
|
|
20
|
-
'--output-format',
|
|
21
|
-
'stream-json',
|
|
22
|
-
'--verbose',
|
|
23
|
-
'--model',
|
|
24
|
-
config.claude.model,
|
|
25
|
-
'--permission-mode',
|
|
26
|
-
'acceptEdits',
|
|
27
|
-
];
|
|
28
|
-
const { stdout, stderr, durationMs } = await runClaude({
|
|
29
|
-
args,
|
|
17
|
+
const { reply } = await getProvider().runTask({
|
|
30
18
|
input: prompt,
|
|
31
|
-
timeoutMs: TIMEOUT_MS.background,
|
|
32
|
-
caller: 'journal-nudger',
|
|
33
|
-
});
|
|
34
|
-
const startedAt = Date.now() - durationMs;
|
|
35
|
-
const parsed = parseStreamJson(stdout);
|
|
36
|
-
if (!parsed) {
|
|
37
|
-
throw new Error(`nudger stream-json produced no result event: ${stdout.slice(0, 200)}`);
|
|
38
|
-
}
|
|
39
|
-
if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
|
|
40
|
-
throw new Error(`nudger bad output: ${parsed.result || stdout.slice(0, 200)}`);
|
|
41
|
-
}
|
|
42
|
-
const output = parsed.result.trim();
|
|
43
|
-
void logPrompt({
|
|
44
|
-
ts: Math.floor(startedAt / 1000),
|
|
45
19
|
caller: 'journal-nudger',
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
output,
|
|
49
|
-
durationMs,
|
|
50
|
-
stderr,
|
|
51
|
-
eventTypes: parsed.eventTypes,
|
|
20
|
+
mode: 'auto',
|
|
21
|
+
lane: 'background',
|
|
52
22
|
});
|
|
53
|
-
return
|
|
23
|
+
return reply;
|
|
54
24
|
}
|
|
55
25
|
function formatMsg(m) {
|
|
56
26
|
const who = m.direction === 'out' ? 'assistant' : m.pushName || m.senderNumber || 'user';
|
|
@@ -1,52 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getProvider } from '../ai/providers.js';
|
|
2
2
|
import { config } from '../config.js';
|
|
3
3
|
import { logger } from '../logger.js';
|
|
4
|
-
import { logPrompt } from '../promptlog.js';
|
|
5
4
|
import { readLast } from '../store/messages.js';
|
|
6
5
|
import { appendEntry, getLastScannedTs, getJournal, readEntries, setLastScannedTs, } from './journals.js';
|
|
7
6
|
// How many recent messages to include in the scan window on each sweep.
|
|
8
7
|
// Observer runs every memory.sweepIntervalMs (default 3h), so this window
|
|
9
8
|
// must cover at least that much chat activity to avoid gaps.
|
|
10
9
|
const SCAN_WINDOW = 200;
|
|
11
|
-
// How many recent entries to show
|
|
10
|
+
// How many recent entries to show the observer for dedup context.
|
|
12
11
|
const DEDUP_WINDOW = 20;
|
|
13
12
|
async function spawnObserver(prompt) {
|
|
14
|
-
const
|
|
15
|
-
'-p',
|
|
16
|
-
'--output-format',
|
|
17
|
-
'stream-json',
|
|
18
|
-
'--verbose',
|
|
19
|
-
'--model',
|
|
20
|
-
config.claude.model,
|
|
21
|
-
'--permission-mode',
|
|
22
|
-
'acceptEdits',
|
|
23
|
-
];
|
|
24
|
-
const { stdout, stderr, durationMs } = await runClaude({
|
|
25
|
-
args,
|
|
13
|
+
const { reply } = await getProvider().runTask({
|
|
26
14
|
input: prompt,
|
|
27
|
-
timeoutMs: TIMEOUT_MS.background,
|
|
28
|
-
caller: 'journal-observer',
|
|
29
|
-
});
|
|
30
|
-
const startedAt = Date.now() - durationMs;
|
|
31
|
-
const parsed = parseStreamJson(stdout);
|
|
32
|
-
if (!parsed) {
|
|
33
|
-
throw new Error(`journal observer stream-json produced no result event: ${stdout.slice(0, 200)}`);
|
|
34
|
-
}
|
|
35
|
-
if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
|
|
36
|
-
throw new Error(`journal observer bad output: ${parsed.result || stdout.slice(0, 200)}`);
|
|
37
|
-
}
|
|
38
|
-
const output = parsed.result.trim();
|
|
39
|
-
void logPrompt({
|
|
40
|
-
ts: Math.floor(startedAt / 1000),
|
|
41
15
|
caller: 'journal-observer',
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
output,
|
|
45
|
-
durationMs,
|
|
46
|
-
stderr,
|
|
47
|
-
eventTypes: parsed.eventTypes,
|
|
16
|
+
mode: 'auto',
|
|
17
|
+
lane: 'background',
|
|
48
18
|
});
|
|
49
|
-
return
|
|
19
|
+
return reply;
|
|
50
20
|
}
|
|
51
21
|
function formatMsg(m) {
|
|
52
22
|
const date = new Date(m.timestamp * 1000)
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
2
|
import { dirname, resolve } from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { getProvider } from '../ai/providers.js';
|
|
4
4
|
import { config } from '../config.js';
|
|
5
5
|
import fastq from 'fastq';
|
|
6
6
|
import { initiate } from '../gateway/outgoing.js';
|
|
7
7
|
import { logger } from '../logger.js';
|
|
8
|
-
|
|
9
|
-
// Concurrency: how many async Claude workers can run simultaneously.
|
|
8
|
+
// Concurrency: how many async workers can run simultaneously.
|
|
10
9
|
// Start conservative — each process is expensive (Playwright, multi-minute runs).
|
|
11
10
|
// Tune via config.asyncTasks.concurrency once we have real usage data.
|
|
12
11
|
const CONCURRENCY = 3;
|
|
@@ -18,7 +17,7 @@ const inProgress = new Map();
|
|
|
18
17
|
const queue = fastq.promise(async (task) => {
|
|
19
18
|
inProgress.set(task.id, task);
|
|
20
19
|
try {
|
|
21
|
-
await
|
|
20
|
+
await executeAsyncTask(task);
|
|
22
21
|
}
|
|
23
22
|
catch (err) {
|
|
24
23
|
logger.error({ err, id: task.id, jid: task.jid }, 'async task failed unexpectedly');
|
|
@@ -48,26 +47,6 @@ export function listAsyncTasks(jid) {
|
|
|
48
47
|
return all.filter((t) => t.jid === jid);
|
|
49
48
|
}
|
|
50
49
|
// ---------- task runner ----------
|
|
51
|
-
let cachedSystemPrompt = null;
|
|
52
|
-
function systemPrompt() {
|
|
53
|
-
if (cachedSystemPrompt !== null)
|
|
54
|
-
return cachedSystemPrompt;
|
|
55
|
-
const personality = readFileSync(resolve(process.cwd(), config.claude.personalityFile), 'utf-8');
|
|
56
|
-
let memoryInstructions = '';
|
|
57
|
-
try {
|
|
58
|
-
memoryInstructions = readFileSync(resolve(process.cwd(), config.memory.instructionsFile), 'utf-8');
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
// optional
|
|
62
|
-
}
|
|
63
|
-
cachedSystemPrompt = memoryInstructions
|
|
64
|
-
? `${personality}\n\n---\n\n${memoryInstructions}`
|
|
65
|
-
: personality;
|
|
66
|
-
return cachedSystemPrompt;
|
|
67
|
-
}
|
|
68
|
-
export function reloadAsyncSystemPrompt() {
|
|
69
|
-
cachedSystemPrompt = null;
|
|
70
|
-
}
|
|
71
50
|
function buildPrompt(task) {
|
|
72
51
|
const lines = [
|
|
73
52
|
`You are a BACKGROUND WORKER doing a delayed chat reply. The chat already got an ack ("on it, will report back"). Now you do the work, and your output IS the follow-up chat reply — the full answer the owner is waiting for.`,
|
|
@@ -109,66 +88,28 @@ function buildPrompt(task) {
|
|
|
109
88
|
];
|
|
110
89
|
return lines.join('\n');
|
|
111
90
|
}
|
|
112
|
-
function
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
'--verbose',
|
|
118
|
-
'--model',
|
|
119
|
-
config.claude.model,
|
|
120
|
-
'--permission-mode',
|
|
121
|
-
'acceptEdits',
|
|
122
|
-
'--append-system-prompt',
|
|
123
|
-
systemPrompt(),
|
|
91
|
+
function asyncTaskAddDirs() {
|
|
92
|
+
return [
|
|
93
|
+
...config.claude.addDirs,
|
|
94
|
+
config.memory.dir,
|
|
95
|
+
config.storage.mediaDir,
|
|
124
96
|
];
|
|
125
|
-
for (const dir of config.claude.addDirs) {
|
|
126
|
-
args.push('--add-dir', resolve(process.cwd(), dir));
|
|
127
|
-
}
|
|
128
|
-
args.push('--add-dir', resolve(process.cwd(), config.memory.dir));
|
|
129
|
-
args.push('--add-dir', resolve(process.cwd(), config.storage.mediaDir));
|
|
130
|
-
if (task.allowedTools &&
|
|
131
|
-
task.allowedTools !== 'all' &&
|
|
132
|
-
task.allowedTools.length > 0) {
|
|
133
|
-
args.push('--allowedTools', task.allowedTools.join(','));
|
|
134
|
-
}
|
|
135
|
-
return args;
|
|
136
97
|
}
|
|
137
|
-
async function
|
|
138
|
-
const args = buildArgs(task);
|
|
139
|
-
const { stdout, stderr, durationMs } = await runClaude({
|
|
140
|
-
args,
|
|
141
|
-
input: prompt,
|
|
142
|
-
timeoutMs: TIMEOUT_MS.async,
|
|
143
|
-
caller: 'async-task',
|
|
144
|
-
});
|
|
145
|
-
const startedAt = Date.now() - durationMs;
|
|
146
|
-
const parsed = parseStreamJson(stdout);
|
|
147
|
-
if (!parsed) {
|
|
148
|
-
throw new Error(`async task stream-json produced no result event: ${stdout.slice(0, 200)}`);
|
|
149
|
-
}
|
|
150
|
-
if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
|
|
151
|
-
throw new Error(`async task bad output: ${parsed.result || stdout.slice(0, 200)}`);
|
|
152
|
-
}
|
|
153
|
-
const output = parsed.result.trim();
|
|
154
|
-
void logPrompt({
|
|
155
|
-
ts: Math.floor(startedAt / 1000),
|
|
156
|
-
caller: 'async-task',
|
|
157
|
-
args,
|
|
158
|
-
input: prompt,
|
|
159
|
-
output,
|
|
160
|
-
durationMs,
|
|
161
|
-
stderr,
|
|
162
|
-
eventTypes: parsed.eventTypes,
|
|
163
|
-
});
|
|
164
|
-
return output;
|
|
165
|
-
}
|
|
166
|
-
async function runTask(task) {
|
|
98
|
+
async function executeAsyncTask(task) {
|
|
167
99
|
const prompt = buildPrompt(task);
|
|
168
100
|
const elapsedLog = () => `${Math.round((Date.now() - task.startedAt * 1000) / 1000)}s`;
|
|
169
101
|
let output;
|
|
170
102
|
try {
|
|
171
|
-
|
|
103
|
+
const { reply } = await getProvider().runTask({
|
|
104
|
+
input: prompt,
|
|
105
|
+
caller: 'async-task',
|
|
106
|
+
mode: 'auto',
|
|
107
|
+
lane: 'async',
|
|
108
|
+
includeSystemPrompt: true,
|
|
109
|
+
addDirs: asyncTaskAddDirs(),
|
|
110
|
+
allowedTools: task.allowedTools,
|
|
111
|
+
});
|
|
112
|
+
output = reply;
|
|
172
113
|
}
|
|
173
114
|
catch (err) {
|
|
174
115
|
logger.error({ err, id: task.id, jid: task.jid, elapsed: elapsedLog() }, 'async task claude call failed');
|
|
@@ -394,89 +335,44 @@ function buildBrowserPrompt(task, isResume) {
|
|
|
394
335
|
];
|
|
395
336
|
return lines.join('\n');
|
|
396
337
|
}
|
|
397
|
-
function
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
'--verbose',
|
|
403
|
-
'--model',
|
|
404
|
-
config.claude.model,
|
|
405
|
-
'--permission-mode',
|
|
406
|
-
'acceptEdits',
|
|
338
|
+
function browserAddDirs() {
|
|
339
|
+
return [
|
|
340
|
+
...config.claude.addDirs,
|
|
341
|
+
config.memory.dir,
|
|
342
|
+
config.storage.mediaDir,
|
|
407
343
|
];
|
|
408
|
-
if (sessionId) {
|
|
409
|
-
// Resume — system prompt and memory-dirs are already baked into session
|
|
410
|
-
args.push('--resume', sessionId);
|
|
411
|
-
}
|
|
412
|
-
else {
|
|
413
|
-
// First call — bootstrap the persistent session
|
|
414
|
-
args.push('--append-system-prompt', systemPrompt());
|
|
415
|
-
for (const dir of config.claude.addDirs) {
|
|
416
|
-
args.push('--add-dir', resolve(process.cwd(), dir));
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
// Memory + media dirs re-added each call (harmless if already baked; needed
|
|
420
|
-
// on fresh bootstrap; lets the browser worker Read updated memory files
|
|
421
|
-
// between turns).
|
|
422
|
-
args.push('--add-dir', resolve(process.cwd(), config.memory.dir));
|
|
423
|
-
args.push('--add-dir', resolve(process.cwd(), config.storage.mediaDir));
|
|
424
|
-
if (task.allowedTools &&
|
|
425
|
-
task.allowedTools !== 'all' &&
|
|
426
|
-
task.allowedTools.length > 0) {
|
|
427
|
-
args.push('--allowedTools', task.allowedTools.join(','));
|
|
428
|
-
}
|
|
429
|
-
return args;
|
|
430
344
|
}
|
|
431
345
|
async function runBrowserTask(task) {
|
|
432
346
|
const session = loadBrowserSession();
|
|
433
347
|
const isResume = !!session.sessionId;
|
|
434
348
|
const prompt = buildBrowserPrompt(task, isResume);
|
|
435
|
-
const args = buildBrowserArgs(task, session.sessionId);
|
|
436
|
-
const startedAtMs = Date.now();
|
|
437
349
|
const elapsedLog = () => `${Math.round((Date.now() - task.startedAt * 1000) / 1000)}s`;
|
|
438
|
-
let
|
|
439
|
-
let
|
|
440
|
-
let durationMs;
|
|
350
|
+
let reply;
|
|
351
|
+
let returnedSessionId;
|
|
441
352
|
try {
|
|
442
|
-
const result = await
|
|
443
|
-
args,
|
|
353
|
+
const result = await getProvider().runTask({
|
|
444
354
|
input: prompt,
|
|
445
|
-
timeoutMs: TIMEOUT_MS.async,
|
|
446
355
|
caller: 'browser-task',
|
|
356
|
+
mode: 'auto',
|
|
357
|
+
lane: 'async',
|
|
358
|
+
includeSystemPrompt: !isResume,
|
|
359
|
+
addDirs: browserAddDirs(),
|
|
360
|
+
allowedTools: task.allowedTools,
|
|
361
|
+
sessionId: session.sessionId ?? undefined,
|
|
447
362
|
});
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
durationMs = result.durationMs;
|
|
363
|
+
reply = result.reply;
|
|
364
|
+
returnedSessionId = result.sessionId;
|
|
451
365
|
}
|
|
452
366
|
catch (err) {
|
|
453
|
-
logger.error({ err, id: task.id, jid: task.jid, elapsed: elapsedLog() }, 'browser task
|
|
367
|
+
logger.error({ err, id: task.id, jid: task.jid, elapsed: elapsedLog() }, 'browser task provider call failed');
|
|
454
368
|
await initiate({
|
|
455
369
|
jid: task.jid,
|
|
456
370
|
text: `Heads up: the browser task "${truncate(task.description, 80)}" failed. Ask me again and I'll retry.`,
|
|
457
371
|
});
|
|
458
372
|
return;
|
|
459
373
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
logger.error({ id: task.id }, 'browser task stream-json produced no result event');
|
|
463
|
-
await initiate({
|
|
464
|
-
jid: task.jid,
|
|
465
|
-
text: `Heads up: the browser task "${truncate(task.description, 80)}" returned an unparseable response.`,
|
|
466
|
-
});
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
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
|
-
await initiate({
|
|
472
|
-
jid: task.jid,
|
|
473
|
-
text: `Heads up: the browser task "${truncate(task.description, 80)}" returned an error.`,
|
|
474
|
-
});
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
// Persist the session id. On first call Claude returns the new sessionId;
|
|
478
|
-
// on resume it may return the same or a rotated one.
|
|
479
|
-
const returnedSessionId = parsed.sessionId;
|
|
374
|
+
// Persist the session id. On first call the provider returns the new
|
|
375
|
+
// sessionId; on resume it may return the same or a rotated one.
|
|
480
376
|
if (returnedSessionId) {
|
|
481
377
|
const now = Math.floor(Date.now() / 1000);
|
|
482
378
|
saveBrowserSession({
|
|
@@ -486,20 +382,9 @@ async function runBrowserTask(task) {
|
|
|
486
382
|
resumeCount: (session.resumeCount ?? 0) + (isResume ? 1 : 0),
|
|
487
383
|
});
|
|
488
384
|
}
|
|
489
|
-
void logPrompt({
|
|
490
|
-
ts: Math.floor(startedAtMs / 1000),
|
|
491
|
-
caller: 'browser-task',
|
|
492
|
-
args,
|
|
493
|
-
input: prompt,
|
|
494
|
-
output: parsed.result,
|
|
495
|
-
sessionId: returnedSessionId ?? undefined,
|
|
496
|
-
durationMs,
|
|
497
|
-
stderr,
|
|
498
|
-
eventTypes: parsed.eventTypes,
|
|
499
|
-
});
|
|
500
385
|
// Route markers the same way the general async lane does.
|
|
501
386
|
const { extractFlags } = await import('../memory/digest-flag.js');
|
|
502
|
-
const { clean, digest, journals, journalCreates } = extractFlags(
|
|
387
|
+
const { clean, digest, journals, journalCreates } = extractFlags(reply);
|
|
503
388
|
const { appendEntry, createJournal, getJournal, isValidSlug } = await import('../memory/journals.js');
|
|
504
389
|
for (const op of journalCreates) {
|
|
505
390
|
if (!isValidSlug(op.slug))
|
package/dist/queue/worker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getProvider } from '../ai/providers.js';
|
|
2
2
|
import { clearSession, setSession, setUsage } from '../ai/sessions.js';
|
|
3
3
|
import { config } from '../config.js';
|
|
4
4
|
import { logger } from '../logger.js';
|
|
@@ -14,7 +14,7 @@ function isStaleSessionError(err) {
|
|
|
14
14
|
async function callClaude(job) {
|
|
15
15
|
const startedAt = Date.now();
|
|
16
16
|
const wasFresh = !job.sessionId;
|
|
17
|
-
const { reply, sessionId, usage } = await
|
|
17
|
+
const { reply, sessionId, usage } = await getProvider().ask({
|
|
18
18
|
input: job.input,
|
|
19
19
|
sessionId: job.sessionId,
|
|
20
20
|
allowedTools: job.allowedTools,
|