@emqo/claudebridge 0.8.0 → 0.9.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/config.yaml.example +12 -3
- package/dist/adapters/discord.d.ts +3 -0
- package/dist/adapters/discord.js +92 -20
- package/dist/adapters/telegram.d.ts +3 -0
- package/dist/adapters/telegram.js +124 -59
- package/dist/core/agent.d.ts +37 -0
- package/dist/core/agent.js +246 -232
- package/dist/core/config.d.ts +2 -70
- package/dist/core/config.js +9 -38
- package/dist/core/i18n.js +12 -4
- package/dist/core/keys.d.ts +2 -10
- package/dist/core/keys.js +5 -22
- package/dist/core/lock.d.ts +8 -4
- package/dist/core/lock.js +25 -17
- package/dist/core/logger.d.ts +11 -0
- package/dist/core/logger.js +24 -0
- package/dist/core/router.d.ts +25 -0
- package/dist/core/router.js +125 -0
- package/dist/core/schema.d.ts +166 -0
- package/dist/core/schema.js +85 -0
- package/dist/core/session.d.ts +50 -0
- package/dist/core/session.js +100 -0
- package/dist/core/store.d.ts +52 -15
- package/dist/core/store.js +95 -17
- package/dist/ctl.js +13 -1
- package/dist/index.js +30 -13
- package/dist/providers/base.d.ts +26 -0
- package/dist/providers/base.js +1 -0
- package/dist/providers/claude.d.ts +9 -0
- package/dist/providers/claude.js +53 -0
- package/dist/providers/codex.d.ts +9 -0
- package/dist/providers/codex.js +35 -0
- package/dist/providers/registry.d.ts +2 -0
- package/dist/providers/registry.js +12 -0
- package/dist/skills/bridge.d.ts +2 -0
- package/dist/skills/bridge.js +2 -0
- package/dist/webhook.js +7 -5
- package/package.json +8 -4
package/dist/core/agent.js
CHANGED
|
@@ -1,27 +1,42 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
import { mkdirSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import {
|
|
4
|
+
import { SessionLock } from "./lock.js";
|
|
5
5
|
import { AccessControl } from "./permissions.js";
|
|
6
6
|
import { EndpointRotator } from "./keys.js";
|
|
7
7
|
import { generateSkillDoc } from "../skills/bridge.js";
|
|
8
|
+
import { SessionManager } from "./session.js";
|
|
9
|
+
import { SessionRouter } from "./router.js";
|
|
10
|
+
import { log as rootLog } from "./logger.js";
|
|
11
|
+
import { getProvider } from "../providers/registry.js";
|
|
12
|
+
const log = rootLog.child("agent");
|
|
8
13
|
export class AgentEngine {
|
|
9
14
|
config;
|
|
10
15
|
store;
|
|
11
16
|
lock;
|
|
12
17
|
rotator;
|
|
18
|
+
sessionMgr;
|
|
19
|
+
router;
|
|
20
|
+
sessionExpiryTimer;
|
|
13
21
|
access;
|
|
14
22
|
constructor(config, store) {
|
|
15
23
|
this.config = config;
|
|
16
24
|
this.store = store;
|
|
17
|
-
this.lock = new
|
|
25
|
+
this.lock = new SessionLock(config.redis.enabled ? config.redis.url : undefined);
|
|
18
26
|
this.access = new AccessControl(config.access.allowed_users, config.access.allowed_groups);
|
|
19
27
|
this.rotator = new EndpointRotator(config.endpoints);
|
|
28
|
+
this.sessionMgr = new SessionManager(store, config.agent.session);
|
|
29
|
+
this.router = new SessionRouter(this.sessionMgr, this.rotator, config.agent.session);
|
|
30
|
+
// Periodic idle session expiry (every 5 min)
|
|
31
|
+
this.sessionExpiryTimer = setInterval(() => {
|
|
32
|
+
this.sessionMgr.expireIdle();
|
|
33
|
+
}, 5 * 60 * 1000);
|
|
20
34
|
}
|
|
21
35
|
reloadConfig(config) {
|
|
22
36
|
this.config = config;
|
|
23
37
|
this.access.reload(config.access.allowed_users, config.access.allowed_groups);
|
|
24
38
|
this.rotator.reload(config.endpoints);
|
|
39
|
+
// SessionManager/Router pick up config changes via reference
|
|
25
40
|
}
|
|
26
41
|
getEndpoints() {
|
|
27
42
|
return this.rotator.list();
|
|
@@ -35,6 +50,12 @@ export class AgentEngine {
|
|
|
35
50
|
getMaxParallel() {
|
|
36
51
|
return this.config.agent.max_parallel || 1;
|
|
37
52
|
}
|
|
53
|
+
getSessionManager() {
|
|
54
|
+
return this.sessionMgr;
|
|
55
|
+
}
|
|
56
|
+
getRouter() {
|
|
57
|
+
return this.router;
|
|
58
|
+
}
|
|
38
59
|
getWorkDir(userId) {
|
|
39
60
|
if (!this.config.workspace.isolation) {
|
|
40
61
|
return this.config.agent.cwd || process.cwd();
|
|
@@ -43,290 +64,283 @@ export class AgentEngine {
|
|
|
43
64
|
mkdirSync(dir, { recursive: true });
|
|
44
65
|
return dir;
|
|
45
66
|
}
|
|
67
|
+
/** @deprecated Use isSessionLocked() for multi-session mode */
|
|
46
68
|
isLocked(userId) {
|
|
47
69
|
return this.lock.isLocked(userId);
|
|
48
70
|
}
|
|
49
|
-
|
|
50
|
-
|
|
71
|
+
isSessionLocked(subSessionId) {
|
|
72
|
+
return this.lock.isLocked(subSessionId);
|
|
73
|
+
}
|
|
74
|
+
isMultiSessionEnabled() {
|
|
75
|
+
return this.config.agent.session?.enabled !== false;
|
|
76
|
+
}
|
|
77
|
+
// ─── Multi-Session Entry Point ───────────────────────────────
|
|
78
|
+
/**
|
|
79
|
+
* Main entry point for user messages in multi-session mode.
|
|
80
|
+
* Routes to the correct sub-session and executes concurrently.
|
|
81
|
+
*/
|
|
82
|
+
async handleUserMessage(userId, prompt, platform, chatId, replyToMsgId, onChunk, overrideTimeoutMs) {
|
|
83
|
+
// 1. Route
|
|
84
|
+
const decision = await this.router.route(userId, platform, chatId, prompt, replyToMsgId);
|
|
85
|
+
// 2. Create or get sub-session
|
|
86
|
+
let subSession;
|
|
87
|
+
if (decision.action === "create") {
|
|
88
|
+
if (!this.sessionMgr.canCreate(userId, platform)) {
|
|
89
|
+
// Evict the oldest idle session to make room
|
|
90
|
+
const active = this.sessionMgr.getActive(userId, platform);
|
|
91
|
+
const oldest = active.sort((a, b) => a.lastActiveAt - b.lastActiveAt)[0];
|
|
92
|
+
if (oldest) {
|
|
93
|
+
this.sessionMgr.close(oldest.id);
|
|
94
|
+
log.info("evicted oldest sub-session", { sessionId: oldest.id.slice(0, 8), userId });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
subSession = this.sessionMgr.create(userId, platform, chatId, decision.label);
|
|
98
|
+
log.info("created sub-session", { sessionId: subSession.id.slice(0, 8), label: subSession.label, userId });
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
subSession = this.sessionMgr.get(decision.subSessionId);
|
|
102
|
+
if (!subSession) {
|
|
103
|
+
// Safety fallback: session was closed/deleted between route and get
|
|
104
|
+
subSession = this.sessionMgr.create(userId, platform, chatId, prompt.slice(0, 50));
|
|
105
|
+
log.warn("routed session not found, created new", { sessionId: subSession.id.slice(0, 8), userId });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// 3. Execute in sub-session (per-session lock)
|
|
109
|
+
const res = await this._executeSubSession(subSession, prompt, platform, chatId, onChunk, overrideTimeoutMs);
|
|
110
|
+
// 4. Post-processing
|
|
111
|
+
this.store.addHistory(userId, platform, "user", prompt);
|
|
112
|
+
this.store.addHistory(userId, platform, "assistant", res.text);
|
|
113
|
+
this.store.recordUsage(userId, platform, res.cost || 0);
|
|
114
|
+
this.sessionMgr.touch(subSession.id);
|
|
115
|
+
this.sessionMgr.addCost(subSession.id, res.cost || 0);
|
|
116
|
+
// 5. Auto-label: set label on first message if empty
|
|
117
|
+
if (subSession.messageCount === 0 && !subSession.label) {
|
|
118
|
+
this.sessionMgr.updateLabel(subSession.id, prompt.slice(0, 50));
|
|
119
|
+
subSession.label = prompt.slice(0, 50);
|
|
120
|
+
}
|
|
121
|
+
// 6. Auto-summarize
|
|
122
|
+
if (this.config.agent.memory?.auto_summary)
|
|
123
|
+
this._autoSummarize(userId, prompt, res.text);
|
|
124
|
+
return { ...res, subSessionId: subSession.id, label: subSession.label };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Execute a prompt within a specific sub-session.
|
|
128
|
+
* Acquires per-session lock, resumes claude session via -r flag.
|
|
129
|
+
*/
|
|
130
|
+
async _executeSubSession(subSession, prompt, platform, chatId, onChunk, overrideTimeoutMs) {
|
|
131
|
+
const release = await this.lock.acquire(subSession.id);
|
|
51
132
|
try {
|
|
52
|
-
this.store.
|
|
53
|
-
const memories = this.config.agent.memory?.enabled ? this.store.getMemories(userId) : [];
|
|
133
|
+
const memories = this.config.agent.memory?.enabled ? this.store.getMemories(subSession.userId) : [];
|
|
54
134
|
const memoryPrompt = memories.length ? memories.map(m => `- ${m.content}`).join("\n") : "";
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
this._autoSummarize(userId, prompt, res.text);
|
|
60
|
-
return res;
|
|
135
|
+
const ep = this.rotator.count
|
|
136
|
+
? this.rotator.next()
|
|
137
|
+
: { name: "default", provider: "claude", model: "" };
|
|
138
|
+
return await this._executeWithSession(subSession, prompt, platform, chatId, ep, onChunk, memoryPrompt, overrideTimeoutMs);
|
|
61
139
|
}
|
|
62
140
|
finally {
|
|
63
141
|
release();
|
|
64
142
|
}
|
|
65
143
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
console.warn(`[agent] endpoint ${ep.name} failed (parallel), rotating`);
|
|
87
|
-
this.rotator.markFailed(ep);
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
throw err;
|
|
91
|
-
}
|
|
144
|
+
/**
|
|
145
|
+
* Core execution: spawn claude CLI with session resume for a sub-session.
|
|
146
|
+
* Thin wrapper around _spawnAgent with sub-session persistence.
|
|
147
|
+
*/
|
|
148
|
+
async _executeWithSession(subSession, prompt, platform, chatId, ep, onChunk, memoryPrompt, overrideTimeoutMs) {
|
|
149
|
+
try {
|
|
150
|
+
const res = await this._spawnAgent({
|
|
151
|
+
userId: subSession.userId, prompt, platform, chatId, ep, onChunk, memoryPrompt, overrideTimeoutMs,
|
|
152
|
+
resumeSessionId: subSession.claudeSessionId || undefined,
|
|
153
|
+
subSessionId: subSession.id,
|
|
154
|
+
subSessionLabel: subSession.label,
|
|
155
|
+
logLabel: `sub-session=${subSession.id.slice(0, 8)}`,
|
|
156
|
+
});
|
|
157
|
+
if (res.sessionId)
|
|
158
|
+
this.sessionMgr.setClaudeSessionId(subSession.id, res.sessionId);
|
|
159
|
+
return res;
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
this.sessionMgr.setClaudeSessionId(subSession.id, "");
|
|
163
|
+
throw err;
|
|
92
164
|
}
|
|
93
|
-
throw lastErr;
|
|
94
165
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const msg = String(err?.message || "");
|
|
108
|
-
if (msg.includes("429") || msg.includes("401") || msg.includes("529")) {
|
|
109
|
-
console.warn(`[agent] endpoint ${ep.name} failed, rotating`);
|
|
110
|
-
this.rotator.markFailed(ep);
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
throw err;
|
|
114
|
-
}
|
|
166
|
+
// ─── Core Spawn Infrastructure ──────────────────────────────
|
|
167
|
+
/** Build the append system prompt (memories + skill doc) */
|
|
168
|
+
_buildAppendPrompt(opts) {
|
|
169
|
+
let appendPrompt = "";
|
|
170
|
+
if (opts.memoryPrompt)
|
|
171
|
+
appendPrompt += `User memories:\n${opts.memoryPrompt}\n\n`;
|
|
172
|
+
if (this.config.agent.skill?.enabled !== false) {
|
|
173
|
+
appendPrompt += generateSkillDoc({
|
|
174
|
+
userId: opts.userId, chatId: opts.chatId, platform: opts.platform,
|
|
175
|
+
locale: this.config.locale || "en",
|
|
176
|
+
...(opts.subSessionId ? { subSessionId: opts.subSessionId, subSessionLabel: opts.subSessionLabel } : {}),
|
|
177
|
+
});
|
|
115
178
|
}
|
|
116
|
-
|
|
179
|
+
return appendPrompt.trim();
|
|
117
180
|
}
|
|
118
|
-
|
|
181
|
+
/**
|
|
182
|
+
* Unified core: spawn provider CLI, parse stream, handle timeout.
|
|
183
|
+
* All execute methods delegate here.
|
|
184
|
+
*/
|
|
185
|
+
_spawnAgent(opts) {
|
|
119
186
|
return new Promise((resolve, reject) => {
|
|
120
|
-
const
|
|
121
|
-
const cwd = this.getWorkDir(userId);
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (this.config.agent.allowed_tools?.length)
|
|
139
|
-
args.push("--allowed-tools", this.config.agent.allowed_tools.join(","));
|
|
140
|
-
if (this.config.agent.max_turns)
|
|
141
|
-
args.push("--max-turns", String(this.config.agent.max_turns));
|
|
142
|
-
if (this.config.agent.max_budget_usd)
|
|
143
|
-
args.push("--max-budget-usd", String(this.config.agent.max_budget_usd));
|
|
144
|
-
const env = { ...process.env };
|
|
145
|
-
if (ep.api_key)
|
|
146
|
-
env.ANTHROPIC_API_KEY = ep.api_key;
|
|
147
|
-
if (ep.base_url)
|
|
148
|
-
env.ANTHROPIC_BASE_URL = ep.base_url;
|
|
149
|
-
env.CLAUDEBRIDGE_DB = this.store.dbPath;
|
|
150
|
-
const child = spawn("claude", args, { cwd, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
187
|
+
const provider = getProvider(opts.ep.provider || "claude");
|
|
188
|
+
const cwd = this.getWorkDir(opts.userId);
|
|
189
|
+
const label = opts.logLabel || "";
|
|
190
|
+
const verbose = opts.verbose !== false;
|
|
191
|
+
const appendPrompt = this._buildAppendPrompt(opts);
|
|
192
|
+
const args = provider.buildArgs({
|
|
193
|
+
prompt: opts.prompt,
|
|
194
|
+
model: opts.ep.model,
|
|
195
|
+
resumeSessionId: provider.supportsSessionResume ? opts.resumeSessionId : undefined,
|
|
196
|
+
systemPrompt: this.config.agent.system_prompt || undefined,
|
|
197
|
+
appendSystemPrompt: appendPrompt || undefined,
|
|
198
|
+
allowedTools: this.config.agent.allowed_tools,
|
|
199
|
+
maxTurns: this.config.agent.max_turns,
|
|
200
|
+
maxBudgetUsd: this.config.agent.max_budget_usd,
|
|
201
|
+
permissionMode: this.config.agent.permission_mode || "acceptEdits",
|
|
202
|
+
});
|
|
203
|
+
const env = provider.buildEnv({ CLAUDEBRIDGE_DB: this.store.dbPath });
|
|
204
|
+
const child = spawn(provider.binary, args, { cwd, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
151
205
|
child.stdin.end();
|
|
152
|
-
|
|
153
|
-
const timeoutMs = overrideTimeoutMs !== undefined
|
|
206
|
+
log.info("spawned agent", { provider: opts.ep.provider || "claude", pid: child.pid, label, cwd });
|
|
207
|
+
const timeoutMs = opts.overrideTimeoutMs !== undefined
|
|
208
|
+
? opts.overrideTimeoutMs
|
|
209
|
+
: (this.config.agent.timeout_seconds || 600) * 1000;
|
|
154
210
|
const timer = timeoutMs > 0 ? setTimeout(() => { try {
|
|
155
211
|
child.kill("SIGTERM");
|
|
156
212
|
}
|
|
157
213
|
catch { } }, timeoutMs) : null;
|
|
158
214
|
let fullText = "";
|
|
159
|
-
let
|
|
215
|
+
let sessionId = opts.resumeSessionId || "";
|
|
160
216
|
let cost = 0;
|
|
161
217
|
let buffer = "";
|
|
162
218
|
child.stdout.on("data", (data) => {
|
|
163
219
|
const chunk = data.toString();
|
|
164
|
-
|
|
220
|
+
if (verbose)
|
|
221
|
+
log.debug("stdout chunk", { data: chunk.slice(0, 200) });
|
|
165
222
|
buffer += chunk;
|
|
166
223
|
const lines = buffer.split("\n");
|
|
167
224
|
buffer = lines.pop() || "";
|
|
168
225
|
for (const line of lines) {
|
|
169
226
|
if (!line.trim())
|
|
170
227
|
continue;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
if (msg.type === "result") {
|
|
186
|
-
if (msg.result)
|
|
187
|
-
fullText = msg.result;
|
|
188
|
-
if (msg.total_cost_usd)
|
|
189
|
-
cost = msg.total_cost_usd;
|
|
190
|
-
if (msg.is_error) {
|
|
191
|
-
console.error(`[agent] claude error result: subtype=${msg.subtype} message=${JSON.stringify(msg.error_message || msg.message || "unknown").slice(0, 500)}`);
|
|
228
|
+
const event = provider.parseLine(line);
|
|
229
|
+
switch (event.type) {
|
|
230
|
+
case "session_init":
|
|
231
|
+
if (event.sessionId)
|
|
232
|
+
sessionId = event.sessionId;
|
|
233
|
+
break;
|
|
234
|
+
case "text_chunk":
|
|
235
|
+
if (event.text) {
|
|
236
|
+
fullText += event.text + "\n";
|
|
237
|
+
if (opts.onChunk)
|
|
238
|
+
opts.onChunk(event.text, fullText);
|
|
192
239
|
}
|
|
193
|
-
|
|
240
|
+
break;
|
|
241
|
+
case "result":
|
|
242
|
+
if (event.text)
|
|
243
|
+
fullText = event.text;
|
|
244
|
+
if (event.cost)
|
|
245
|
+
cost = event.cost;
|
|
246
|
+
if (verbose && event.isError)
|
|
247
|
+
log.error("agent error result", { label });
|
|
248
|
+
break;
|
|
194
249
|
}
|
|
195
|
-
catch { }
|
|
196
250
|
}
|
|
197
251
|
});
|
|
198
252
|
let stderr = "";
|
|
199
253
|
child.stderr.on("data", (data) => {
|
|
200
254
|
const s = data.toString();
|
|
201
255
|
stderr += s;
|
|
202
|
-
|
|
256
|
+
if (verbose)
|
|
257
|
+
log.debug("stderr", { data: s.slice(0, 200) });
|
|
203
258
|
});
|
|
204
259
|
child.on("close", (code, signal) => {
|
|
205
260
|
if (timer)
|
|
206
261
|
clearTimeout(timer);
|
|
207
|
-
|
|
262
|
+
log.info("agent exited", { code, signal, label, textLen: fullText.length });
|
|
208
263
|
if (signal === "SIGTERM") {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
this.store.setSession(userId, newSessionId, platform);
|
|
212
|
-
resolve({ text: fullText.trim() || "(timed out)", sessionId: newSessionId, cost, timedOut: true });
|
|
264
|
+
log.warn("agent timed out", { seconds: timeoutMs / 1000 });
|
|
265
|
+
resolve({ text: fullText.trim() || "(timed out)", sessionId, cost, timedOut: true });
|
|
213
266
|
return;
|
|
214
267
|
}
|
|
215
268
|
if (code === 0 || fullText.trim()) {
|
|
216
|
-
|
|
217
|
-
this.store.setSession(userId, newSessionId, platform);
|
|
218
|
-
resolve({ text: fullText.trim() || "(no response)", sessionId: newSessionId, cost });
|
|
269
|
+
resolve({ text: fullText.trim() || "(no response)", sessionId, cost });
|
|
219
270
|
}
|
|
220
271
|
else {
|
|
221
|
-
|
|
222
|
-
this.store.clearSession(userId);
|
|
223
|
-
reject(new Error(`claude exited ${code}: ${stderr.slice(0, 500)}`));
|
|
272
|
+
reject(new Error(`agent exited ${code}: ${stderr.slice(0, 500)}`));
|
|
224
273
|
}
|
|
225
274
|
});
|
|
226
275
|
child.on("error", reject);
|
|
227
276
|
});
|
|
228
277
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
buffer = lines.pop() || "";
|
|
274
|
-
for (const line of lines) {
|
|
275
|
-
if (!line.trim())
|
|
276
|
-
continue;
|
|
277
|
-
try {
|
|
278
|
-
const msg = JSON.parse(line);
|
|
279
|
-
if (msg.type === "system" && msg.subtype === "init" && msg.session_id) {
|
|
280
|
-
sessionId = msg.session_id;
|
|
281
|
-
}
|
|
282
|
-
if (msg.type === "assistant" && msg.message?.content) {
|
|
283
|
-
for (const block of msg.message.content) {
|
|
284
|
-
if (block.type === "text" && block.text) {
|
|
285
|
-
fullText += block.text + "\n";
|
|
286
|
-
if (onChunk)
|
|
287
|
-
onChunk(block.text, fullText);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
if (msg.type === "result") {
|
|
292
|
-
if (msg.result)
|
|
293
|
-
fullText = msg.result;
|
|
294
|
-
if (msg.total_cost_usd)
|
|
295
|
-
cost = msg.total_cost_usd;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
catch { }
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
let stderr = "";
|
|
302
|
-
child.stderr.on("data", (data) => { stderr += data.toString(); });
|
|
303
|
-
child.on("close", (code, signal) => {
|
|
304
|
-
if (timer)
|
|
305
|
-
clearTimeout(timer);
|
|
306
|
-
console.log(`[agent] claude (parallel) exited code=${code} signal=${signal} text=${fullText.length}chars`);
|
|
307
|
-
if (signal === "SIGTERM") {
|
|
308
|
-
console.warn(`[agent] claude (parallel) timed out after ${timeoutMs / 1000}s`);
|
|
309
|
-
resolve({ text: fullText.trim() || "(timed out)", sessionId, cost, timedOut: true });
|
|
310
|
-
}
|
|
311
|
-
else if (code === 0 || fullText.trim()) {
|
|
312
|
-
resolve({ text: fullText.trim() || "(no response)", sessionId, cost });
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
|
-
reject(new Error(`claude exited ${code}: ${stderr.slice(0, 500)}`));
|
|
316
|
-
}
|
|
278
|
+
// ─── Legacy Single-Session Entry Points ──────────────────────
|
|
279
|
+
/** @deprecated Use handleUserMessage() for multi-session mode */
|
|
280
|
+
async runStream(userId, prompt, platform, chatId, onChunk, overrideTimeoutMs) {
|
|
281
|
+
const release = await this.lock.acquire(userId);
|
|
282
|
+
try {
|
|
283
|
+
this.store.addHistory(userId, platform, "user", prompt);
|
|
284
|
+
const memories = this.config.agent.memory?.enabled ? this.store.getMemories(userId) : [];
|
|
285
|
+
const memoryPrompt = memories.length ? memories.map(m => `- ${m.content}`).join("\n") : "";
|
|
286
|
+
const res = await this._executeWithRetry(userId, prompt, platform, chatId, onChunk, memoryPrompt, overrideTimeoutMs);
|
|
287
|
+
this.store.addHistory(userId, platform, "assistant", res.text);
|
|
288
|
+
this.store.recordUsage(userId, platform, res.cost || 0);
|
|
289
|
+
if (this.config.agent.memory?.auto_summary)
|
|
290
|
+
this._autoSummarize(userId, prompt, res.text);
|
|
291
|
+
return res;
|
|
292
|
+
}
|
|
293
|
+
finally {
|
|
294
|
+
release();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async runParallel(userId, prompt, platform, chatId, onChunk, overrideTimeoutMs) {
|
|
298
|
+
// No per-user lock — parallel tasks are independent
|
|
299
|
+
// No session resume — fresh session to prevent conflicts
|
|
300
|
+
const memories = this.config.agent.memory?.enabled ? this.store.getMemories(userId) : [];
|
|
301
|
+
const memoryPrompt = memories.length ? memories.map(m => `- ${m.content}`).join("\n") : "";
|
|
302
|
+
const ep = this.rotator.count
|
|
303
|
+
? this.rotator.next()
|
|
304
|
+
: { name: "default", provider: "claude", model: "" };
|
|
305
|
+
const res = await this._executeNoSession(userId, prompt, platform, chatId, ep, onChunk, memoryPrompt, overrideTimeoutMs);
|
|
306
|
+
this.store.recordUsage(userId, platform, res.cost || 0);
|
|
307
|
+
return res;
|
|
308
|
+
}
|
|
309
|
+
async _executeWithRetry(userId, prompt, platform, chatId, onChunk, memoryPrompt, overrideTimeoutMs) {
|
|
310
|
+
const ep = this.rotator.count
|
|
311
|
+
? this.rotator.next()
|
|
312
|
+
: { name: "default", provider: "claude", model: "" };
|
|
313
|
+
return await this._execute(userId, prompt, platform, chatId, ep, onChunk, memoryPrompt, overrideTimeoutMs);
|
|
314
|
+
}
|
|
315
|
+
/** Legacy single-session execution. Thin wrapper around _spawnAgent with store persistence. */
|
|
316
|
+
async _execute(userId, prompt, platform, chatId, ep, onChunk, memoryPrompt, overrideTimeoutMs) {
|
|
317
|
+
const resumeSessionId = this.store.getSession(userId) || undefined;
|
|
318
|
+
try {
|
|
319
|
+
const res = await this._spawnAgent({
|
|
320
|
+
userId, prompt, platform, chatId, ep, onChunk, memoryPrompt, overrideTimeoutMs,
|
|
321
|
+
resumeSessionId, logLabel: `legacy user=${userId}`,
|
|
317
322
|
});
|
|
318
|
-
|
|
323
|
+
if (res.sessionId)
|
|
324
|
+
this.store.setSession(userId, res.sessionId, platform);
|
|
325
|
+
return res;
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
this.store.clearSession(userId);
|
|
329
|
+
throw err;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/** Parallel execution without session resume. Thin wrapper around _spawnAgent. */
|
|
333
|
+
_executeNoSession(userId, prompt, platform, chatId, ep, onChunk, memoryPrompt, overrideTimeoutMs) {
|
|
334
|
+
return this._spawnAgent({
|
|
335
|
+
userId, prompt, platform, chatId, ep, onChunk, memoryPrompt, overrideTimeoutMs,
|
|
336
|
+
logLabel: "parallel", verbose: false,
|
|
319
337
|
});
|
|
320
338
|
}
|
|
321
339
|
_autoSummarize(userId, prompt, response) {
|
|
322
340
|
const ep = this.rotator.count
|
|
323
341
|
? this.rotator.next()
|
|
324
|
-
: { name: "
|
|
342
|
+
: { name: "default", provider: "claude", model: "" };
|
|
325
343
|
const env = { ...process.env };
|
|
326
|
-
if (ep.api_key)
|
|
327
|
-
env.ANTHROPIC_API_KEY = ep.api_key;
|
|
328
|
-
if (ep.base_url)
|
|
329
|
-
env.ANTHROPIC_BASE_URL = ep.base_url;
|
|
330
344
|
const summaryPrompt = `Extract 1-3 key facts worth remembering about the user from this exchange. Output only bullet points, no preamble. If nothing worth remembering, output "NONE".\n\nUser: ${prompt.slice(0, 500)}\nAssistant: ${response.slice(0, 1000)}`;
|
|
331
345
|
const args = ["-p", summaryPrompt, "--verbose", "--output-format", "stream-json", "--max-turns", "1", "--max-budget-usd", "0.05"];
|
|
332
346
|
if (ep.model)
|
|
@@ -337,7 +351,7 @@ export class AgentEngine {
|
|
|
337
351
|
child.kill("SIGTERM");
|
|
338
352
|
}
|
|
339
353
|
catch { } }, 60000);
|
|
340
|
-
|
|
354
|
+
log.info("auto-summary spawned", { pid: child.pid, userId });
|
|
341
355
|
let result = "";
|
|
342
356
|
let cost = 0;
|
|
343
357
|
let buffer = "";
|
|
@@ -365,24 +379,24 @@ export class AgentEngine {
|
|
|
365
379
|
child.on("close", (code) => {
|
|
366
380
|
clearTimeout(killTimer);
|
|
367
381
|
if (code !== 0) {
|
|
368
|
-
|
|
382
|
+
log.warn("auto-summary failed", { code, stderr: stderr.slice(0, 200), userId });
|
|
369
383
|
}
|
|
370
384
|
if (cost > 0) {
|
|
371
385
|
this.store.recordUsage(userId, "auto-summary", cost);
|
|
372
|
-
|
|
386
|
+
log.info("auto-summary cost", { cost: cost.toFixed(4), userId });
|
|
373
387
|
}
|
|
374
388
|
if (result && !result.includes("NONE")) {
|
|
375
389
|
const saved = this.store.addMemory(userId, result.trim(), "auto");
|
|
376
390
|
this.store.trimMemories(userId, this.config.agent.memory?.max_memories || 50);
|
|
377
391
|
if (saved)
|
|
378
|
-
|
|
392
|
+
log.info("auto-summary saved", { userId });
|
|
379
393
|
else
|
|
380
|
-
|
|
394
|
+
log.info("auto-summary skipped (duplicate)", { userId });
|
|
381
395
|
}
|
|
382
396
|
else {
|
|
383
|
-
|
|
397
|
+
log.info("auto-summary result=NONE", { userId });
|
|
384
398
|
}
|
|
385
399
|
});
|
|
386
|
-
child.on("error", (err) => {
|
|
400
|
+
child.on("error", (err) => { log.warn("auto-summary spawn error", { error: err.message }); });
|
|
387
401
|
}
|
|
388
402
|
}
|