@rubytech/create-realagent 1.0.869 → 1.0.871
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/package.json +1 -1
- package/payload/platform/plugins/admin/mcp/dist/__tests__/public-hostname.test.js +83 -91
- package/payload/platform/plugins/admin/mcp/dist/__tests__/public-hostname.test.js.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/index.js +7 -10
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/lib/public-hostname.d.ts +5 -11
- package/payload/platform/plugins/admin/mcp/dist/lib/public-hostname.d.ts.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/lib/public-hostname.js +66 -47
- package/payload/platform/plugins/admin/mcp/dist/lib/public-hostname.js.map +1 -1
- package/payload/platform/plugins/admin/skills/publish-site/SKILL.md +2 -2
- package/payload/platform/plugins/docs/references/internals.md +1 -1
- package/payload/platform/scripts/__tests__/admin-persist-audit.test.ts +182 -0
- package/payload/platform/scripts/admin-persist-audit.ts +43 -17
- package/payload/server/chunk-5U36PKG4.js +11326 -0
- package/payload/server/chunk-NDEQBCVI.js +1160 -0
- package/payload/server/client-pool-XAEDMS5D.js +34 -0
- package/payload/server/maxy-edge.js +2 -2
- package/payload/server/public/assets/{Checkbox-B9hff9s8.js → Checkbox-CDffo5el.js} +1 -1
- package/payload/server/public/assets/{admin-Cpi6L_g7.js → admin-BSdV45P5.js} +2 -2
- package/payload/server/public/assets/data-vFVtOwuC.js +1 -0
- package/payload/server/public/assets/{graph-labels-ChinGFwI.js → graph-labels-C-KsUF_B.js} +1 -1
- package/payload/server/public/assets/graph-q802cxLY.js +1 -0
- package/payload/server/public/assets/{jsx-runtime-CVA1ZrPS.css → jsx-runtime-C1hGBzVx.css} +1 -1
- package/payload/server/public/assets/{page-OVrxtgOZ.js → page-B5b7tyz-.js} +1 -1
- package/payload/server/public/assets/{page-DqPf65sS.js → page-DsW7P98i.js} +1 -1
- package/payload/server/public/assets/{public-CJN5KAiK.js → public-BkNXx-3G.js} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-DyVx7e7a.js → useVoiceRecorder-DCVSlfUk.js} +1 -1
- package/payload/server/public/data.html +5 -5
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +8 -8
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +38 -18
- package/payload/server/public/assets/data-Da6iYRW1.js +0 -1
- package/payload/server/public/assets/graph-BHq-JYwV.js +0 -1
- /package/payload/server/public/assets/{jsx-runtime-nxP_2eNo.js → jsx-runtime-DFrHsKhm.js} +0 -0
|
@@ -0,0 +1,1160 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createNewAdminConversation,
|
|
3
|
+
ensureConversation,
|
|
4
|
+
isMessageUseful,
|
|
5
|
+
persistMessage,
|
|
6
|
+
setConversationAgentSessionId,
|
|
7
|
+
setSessionStoreRef
|
|
8
|
+
} from "./chunk-FHNFKJZN.js";
|
|
9
|
+
|
|
10
|
+
// app/lib/claude-agent/client-pool.ts
|
|
11
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
12
|
+
import { appendFileSync as appendFileSync2 } from "fs";
|
|
13
|
+
import { resolve as resolvePath } from "path";
|
|
14
|
+
|
|
15
|
+
// app/lib/claude-agent/logging.ts
|
|
16
|
+
import { spawnSync } from "child_process";
|
|
17
|
+
import { resolve } from "path";
|
|
18
|
+
import { platform as osPlatform } from "os";
|
|
19
|
+
import { readFileSync, readdirSync, mkdirSync, createWriteStream, statSync, unlinkSync, appendFileSync } from "fs";
|
|
20
|
+
import { lookup as dnsLookup } from "dns/promises";
|
|
21
|
+
import { createConnection as netConnect } from "net";
|
|
22
|
+
import { StringDecoder } from "string_decoder";
|
|
23
|
+
var LOG_RETENTION_DAYS = 7;
|
|
24
|
+
var isoTs = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
25
|
+
var BROWSER_TOOL_PREFIXES = [
|
|
26
|
+
"mcp__plugin_playwright_playwright__",
|
|
27
|
+
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__"
|
|
28
|
+
];
|
|
29
|
+
function isBrowserTool(name) {
|
|
30
|
+
return BROWSER_TOOL_PREFIXES.some((p) => name.startsWith(p));
|
|
31
|
+
}
|
|
32
|
+
var DIAG_HARD_CAP_MS = 5e3;
|
|
33
|
+
var DIAG_DNS_TIMEOUT_MS = 2e3;
|
|
34
|
+
var DIAG_TCP_TIMEOUT_MS = 3e3;
|
|
35
|
+
var DIAG_HTTP_TIMEOUT_MS = 4e3;
|
|
36
|
+
function quoteDiag(value) {
|
|
37
|
+
return JSON.stringify(value);
|
|
38
|
+
}
|
|
39
|
+
function extractUrl(toolName, input) {
|
|
40
|
+
if (input === null || typeof input !== "object") return void 0;
|
|
41
|
+
const obj = input;
|
|
42
|
+
if (toolName === "WebFetch" && typeof obj.url === "string") return obj.url;
|
|
43
|
+
if (isBrowserTool(toolName) && typeof obj.url === "string") return obj.url;
|
|
44
|
+
if (typeof obj.url === "string" && /^https?:\/\//.test(obj.url)) return obj.url;
|
|
45
|
+
return void 0;
|
|
46
|
+
}
|
|
47
|
+
var FULL_REDACT_ENV_VARS = /* @__PURE__ */ new Set(["HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY"]);
|
|
48
|
+
function redactEnvField(name) {
|
|
49
|
+
const value = process.env[name];
|
|
50
|
+
if (!value) return `${name.toLowerCase()}=absent`;
|
|
51
|
+
if (FULL_REDACT_ENV_VARS.has(name)) {
|
|
52
|
+
return `${name.toLowerCase()}=present`;
|
|
53
|
+
}
|
|
54
|
+
const suffix = value.length > 40 ? value.slice(-40) : value;
|
|
55
|
+
return `${name.toLowerCase()}=present suffix=${quoteDiag(suffix)}`;
|
|
56
|
+
}
|
|
57
|
+
async function probeDns(host, family) {
|
|
58
|
+
const label = family === 4 ? "dns_a" : "dns_aaaa";
|
|
59
|
+
const start = Date.now();
|
|
60
|
+
let timer;
|
|
61
|
+
try {
|
|
62
|
+
const result = await Promise.race([
|
|
63
|
+
dnsLookup(host, { family, verbatim: true }),
|
|
64
|
+
new Promise((_, reject) => {
|
|
65
|
+
timer = setTimeout(() => reject(new Error("timeout")), DIAG_DNS_TIMEOUT_MS);
|
|
66
|
+
})
|
|
67
|
+
]);
|
|
68
|
+
if (timer) clearTimeout(timer);
|
|
69
|
+
const ms = Date.now() - start;
|
|
70
|
+
return `${label}=${result.address} ${label}_ms=${ms}`;
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (timer) clearTimeout(timer);
|
|
73
|
+
const ms = Date.now() - start;
|
|
74
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
75
|
+
return `${label}=err ${label}_err=${quoteDiag(msg.slice(0, 60))} ${label}_ms=${ms}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function probeTcp(host, port) {
|
|
79
|
+
const start = Date.now();
|
|
80
|
+
return new Promise((resolvePromise) => {
|
|
81
|
+
let settled = false;
|
|
82
|
+
const sock = netConnect({ host, port, family: 0 });
|
|
83
|
+
const finish = (result) => {
|
|
84
|
+
if (settled) return;
|
|
85
|
+
settled = true;
|
|
86
|
+
try {
|
|
87
|
+
sock.destroy();
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
resolvePromise(result);
|
|
91
|
+
};
|
|
92
|
+
const timer = setTimeout(() => finish(`tcp=timeout tcp_ms=${Date.now() - start}`), DIAG_TCP_TIMEOUT_MS);
|
|
93
|
+
sock.once("connect", () => {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
finish(`tcp=ok tcp_ms=${Date.now() - start}`);
|
|
96
|
+
});
|
|
97
|
+
sock.once("error", (err) => {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
100
|
+
finish(`tcp=err tcp_err=${quoteDiag(msg.slice(0, 60))} tcp_ms=${Date.now() - start}`);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
async function probeHttp(url) {
|
|
105
|
+
const start = Date.now();
|
|
106
|
+
const controller = new AbortController();
|
|
107
|
+
const timer = setTimeout(() => controller.abort(), DIAG_HTTP_TIMEOUT_MS);
|
|
108
|
+
try {
|
|
109
|
+
const res = await fetch(url, { method: "HEAD", redirect: "manual", signal: controller.signal });
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
return `http_status=${res.status} http_ms=${Date.now() - start}`;
|
|
112
|
+
} catch (err) {
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
115
|
+
return `http_status=err http_err=${quoteDiag(msg.slice(0, 60))} http_ms=${Date.now() - start}`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function runFailureDiagnostic(toolName, toolInput) {
|
|
119
|
+
const inputKeys = toolInput !== null && typeof toolInput === "object" ? Object.keys(toolInput).join(",") : "";
|
|
120
|
+
const envFields = [
|
|
121
|
+
redactEnvField("HTTPS_PROXY"),
|
|
122
|
+
redactEnvField("HTTP_PROXY"),
|
|
123
|
+
redactEnvField("NO_PROXY"),
|
|
124
|
+
redactEnvField("NODE_OPTIONS")
|
|
125
|
+
].join(" ");
|
|
126
|
+
const url = extractUrl(toolName, toolInput);
|
|
127
|
+
if (!url) {
|
|
128
|
+
return `diag_url=none input_keys=[${inputKeys}] ${envFields}`;
|
|
129
|
+
}
|
|
130
|
+
let host;
|
|
131
|
+
let port;
|
|
132
|
+
try {
|
|
133
|
+
const parsed = new URL(url);
|
|
134
|
+
host = parsed.hostname;
|
|
135
|
+
port = parsed.port ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80;
|
|
136
|
+
} catch {
|
|
137
|
+
return `diag_url=unparseable input_keys=[${inputKeys}] ${envFields}`;
|
|
138
|
+
}
|
|
139
|
+
const probes = Promise.allSettled([
|
|
140
|
+
probeDns(host, 4),
|
|
141
|
+
probeDns(host, 6),
|
|
142
|
+
probeTcp(host, port),
|
|
143
|
+
probeHttp(url)
|
|
144
|
+
]);
|
|
145
|
+
let capTimer;
|
|
146
|
+
const capped = await Promise.race([
|
|
147
|
+
probes,
|
|
148
|
+
new Promise((resolvePromise) => {
|
|
149
|
+
capTimer = setTimeout(() => resolvePromise("__diag_timeout__"), DIAG_HARD_CAP_MS);
|
|
150
|
+
})
|
|
151
|
+
]);
|
|
152
|
+
if (capTimer) clearTimeout(capTimer);
|
|
153
|
+
if (capped === "__diag_timeout__") {
|
|
154
|
+
return `diag_host=${host} diag_port=${port} diag_timeout=true input_keys=[${inputKeys}] ${envFields}`;
|
|
155
|
+
}
|
|
156
|
+
const fields = capped.map((r) => r.status === "fulfilled" ? r.value : `probe_err=${quoteDiag(String(r.reason).slice(0, 40))}`).join(" ");
|
|
157
|
+
return `diag_host=${host} diag_port=${port} ${fields} input_keys=[${inputKeys}] ${envFields}`;
|
|
158
|
+
}
|
|
159
|
+
function agentLogStream(name, accountDir, conversationId) {
|
|
160
|
+
if (!conversationId) {
|
|
161
|
+
throw new Error(`agentLogStream: conversationId is required (name=${name}) \u2014 use preConversationLogStream for pre-session events`);
|
|
162
|
+
}
|
|
163
|
+
const logDir = resolve(accountDir, "logs");
|
|
164
|
+
mkdirSync(logDir, { recursive: true });
|
|
165
|
+
purgeOldLogs(logDir, `${name}-`);
|
|
166
|
+
const logPath = resolve(logDir, `${name}-${conversationId}.log`);
|
|
167
|
+
const stream = createWriteStream(logPath, { flags: "a" });
|
|
168
|
+
registerStreamLog(stream, { path: logPath, conversationId, name });
|
|
169
|
+
return stream;
|
|
170
|
+
}
|
|
171
|
+
function preConversationLogStream(name, accountDir) {
|
|
172
|
+
const logDir = resolve(accountDir, "logs");
|
|
173
|
+
mkdirSync(logDir, { recursive: true });
|
|
174
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
175
|
+
purgeOldLogs(logDir, `preconversation-${name}-`);
|
|
176
|
+
const logPath = resolve(logDir, `preconversation-${name}-${date}.log`);
|
|
177
|
+
const stream = createWriteStream(logPath, { flags: "a" });
|
|
178
|
+
registerStreamLog(stream, { path: logPath, conversationId: null, name: `preconversation-${name}` });
|
|
179
|
+
return stream;
|
|
180
|
+
}
|
|
181
|
+
var openStreamLogs = /* @__PURE__ */ new Map();
|
|
182
|
+
function registerStreamLog(stream, entry) {
|
|
183
|
+
openStreamLogs.set(stream, entry);
|
|
184
|
+
stream.once("close", () => {
|
|
185
|
+
openStreamLogs.delete(stream);
|
|
186
|
+
if (entry.conversationId) {
|
|
187
|
+
logTeeLog(`[log-tee] deregister conversationId=${entry.conversationId.slice(0, 8)} path=${JSON.stringify(entry.path)}`);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
if (entry.conversationId) {
|
|
191
|
+
logTeeLog(`[log-tee] register conversationId=${entry.conversationId.slice(0, 8)} path=${JSON.stringify(entry.path)}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
var LOG_TEE_TAG_RE = /^\[[a-zA-Z][a-zA-Z0-9:_\-]*\]/;
|
|
195
|
+
var originalConsoleError = null;
|
|
196
|
+
var originalConsoleLog = null;
|
|
197
|
+
var logTeeInstalled = false;
|
|
198
|
+
var logTeeCycleTimer = null;
|
|
199
|
+
var logTeeLinesEmitted = 0;
|
|
200
|
+
var logTeeLinesRouted = 0;
|
|
201
|
+
var logTeeBytesRouted = 0;
|
|
202
|
+
var logTeeFailCount = 0;
|
|
203
|
+
function logTeeLog(line) {
|
|
204
|
+
(originalConsoleError ?? console.error.bind(console))(line);
|
|
205
|
+
}
|
|
206
|
+
function appendToActiveStreams(line) {
|
|
207
|
+
if (openStreamLogs.size === 0) return;
|
|
208
|
+
const ts = isoTs();
|
|
209
|
+
const teeLine = `[${ts}] ${line.replace(/\n$/, "")}
|
|
210
|
+
`;
|
|
211
|
+
let routed = 0;
|
|
212
|
+
for (const entry of openStreamLogs.values()) {
|
|
213
|
+
if (!entry.conversationId) continue;
|
|
214
|
+
try {
|
|
215
|
+
appendFileSync(entry.path, teeLine);
|
|
216
|
+
routed++;
|
|
217
|
+
logTeeBytesRouted += teeLine.length;
|
|
218
|
+
} catch (err) {
|
|
219
|
+
logTeeFailCount++;
|
|
220
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
221
|
+
logTeeLog(`[log-tee] FAIL emit reason=${JSON.stringify(msg.slice(0, 80))} path=${JSON.stringify(entry.path)}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (routed > 0) {
|
|
225
|
+
logTeeLinesRouted += routed;
|
|
226
|
+
logTeeLinesEmitted++;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function installLogTee() {
|
|
230
|
+
if (logTeeInstalled) return;
|
|
231
|
+
logTeeInstalled = true;
|
|
232
|
+
originalConsoleError = console.error.bind(console);
|
|
233
|
+
originalConsoleLog = console.log.bind(console);
|
|
234
|
+
const wrap = (orig) => {
|
|
235
|
+
return (...args) => {
|
|
236
|
+
orig(...args);
|
|
237
|
+
const rendered = args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ");
|
|
238
|
+
if (LOG_TEE_TAG_RE.test(rendered)) {
|
|
239
|
+
appendToActiveStreams(rendered);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
console.error = wrap(originalConsoleError);
|
|
244
|
+
console.log = wrap(originalConsoleLog);
|
|
245
|
+
logTeeCycleTimer = setInterval(() => {
|
|
246
|
+
let active = 0;
|
|
247
|
+
for (const e of openStreamLogs.values()) if (e.conversationId) active++;
|
|
248
|
+
logTeeLog(
|
|
249
|
+
`[log-tee] cycle activeConversations=${active} linesEmitted=${logTeeLinesEmitted} linesRouted=${logTeeLinesRouted} bytesRouted=${logTeeBytesRouted} failCount=${logTeeFailCount}`
|
|
250
|
+
);
|
|
251
|
+
}, 3e4);
|
|
252
|
+
logTeeCycleTimer.unref?.();
|
|
253
|
+
logTeeLog(`[log-tee] installed pid=${process.pid}`);
|
|
254
|
+
}
|
|
255
|
+
function safeStringify(value) {
|
|
256
|
+
try {
|
|
257
|
+
if (value instanceof Error) return value.stack ?? value.message;
|
|
258
|
+
return JSON.stringify(value);
|
|
259
|
+
} catch {
|
|
260
|
+
return String(value);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (process.env.NODE_ENV !== "test") {
|
|
264
|
+
installLogTee();
|
|
265
|
+
}
|
|
266
|
+
function sigtermFlushStreamLogs(reason, source) {
|
|
267
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
268
|
+
for (const entry of openStreamLogs.values()) {
|
|
269
|
+
const convPart = entry.conversationId ? ` conversationId=${entry.conversationId}` : "";
|
|
270
|
+
const line = `[${ts}] [server-sigterm] reason=${reason}${convPart} name=${entry.name} source=${source}
|
|
271
|
+
`;
|
|
272
|
+
try {
|
|
273
|
+
appendFileSync(entry.path, line);
|
|
274
|
+
} catch (err) {
|
|
275
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
276
|
+
console.error(`[server-sigterm-flush-err] path=${entry.path} reason=${msg}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function purgeOldLogs(logDir, prefix) {
|
|
281
|
+
const cutoff = Date.now() - LOG_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
|
|
282
|
+
let entries;
|
|
283
|
+
try {
|
|
284
|
+
entries = readdirSync(logDir);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
287
|
+
console.error(`[log-purge-err] readdir dir=${logDir} prefix=${prefix} reason=${msg}`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
for (const file of entries) {
|
|
291
|
+
if (!file.startsWith(prefix)) continue;
|
|
292
|
+
const filePath = resolve(logDir, file);
|
|
293
|
+
try {
|
|
294
|
+
if (statSync(filePath).mtimeMs < cutoff) unlinkSync(filePath);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
297
|
+
console.error(`[log-purge-err] file=${file} reason=${msg}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// app/lib/claude-agent/session-store.ts
|
|
303
|
+
function findFirstSubstantiveUserMessage(turns) {
|
|
304
|
+
for (const t of turns) {
|
|
305
|
+
if (t.role !== "user") continue;
|
|
306
|
+
if (isMessageUseful(t.content)) return t.content;
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
var sessionStore = /* @__PURE__ */ new Map();
|
|
311
|
+
function getSession(sessionKey) {
|
|
312
|
+
return sessionStore.get(sessionKey);
|
|
313
|
+
}
|
|
314
|
+
setSessionStoreRef(sessionStore);
|
|
315
|
+
function registerSession(sessionKey, agentType, accountId, agentName, userId, userName, role) {
|
|
316
|
+
const existing = sessionStore.get(sessionKey);
|
|
317
|
+
if (existing) {
|
|
318
|
+
existing.agentType = agentType;
|
|
319
|
+
existing.accountId = accountId;
|
|
320
|
+
existing.agentName = agentName ?? existing.agentName;
|
|
321
|
+
existing.userId = userId ?? existing.userId;
|
|
322
|
+
existing.userName = userName ?? existing.userName;
|
|
323
|
+
existing.role = role ?? existing.role;
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
sessionStore.set(sessionKey, { createdAt: Date.now(), agentType, accountId, agentName, userId, userName, role });
|
|
327
|
+
}
|
|
328
|
+
function registerResumedSession(sessionKey, accountId, agentName, conversationId, messages) {
|
|
329
|
+
const messageHistory = messages.map((m) => ({
|
|
330
|
+
role: m.role,
|
|
331
|
+
content: m.content,
|
|
332
|
+
timestamp: m.timestamp ?? Date.now()
|
|
333
|
+
}));
|
|
334
|
+
sessionStore.set(sessionKey, {
|
|
335
|
+
createdAt: Date.now(),
|
|
336
|
+
agentType: "public",
|
|
337
|
+
accountId,
|
|
338
|
+
agentName,
|
|
339
|
+
conversationId,
|
|
340
|
+
messageHistory
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
function getSessionMessages(sessionKey) {
|
|
344
|
+
return sessionStore.get(sessionKey)?.messageHistory;
|
|
345
|
+
}
|
|
346
|
+
function clearSessionHistory(sessionKey) {
|
|
347
|
+
const session = sessionStore.get(sessionKey);
|
|
348
|
+
if (!session) return void 0;
|
|
349
|
+
const previousConversationId = session.conversationId;
|
|
350
|
+
session.agentSessionId = void 0;
|
|
351
|
+
session.pendingCompactionSummary = void 0;
|
|
352
|
+
session.stalledSubagents = void 0;
|
|
353
|
+
session.pendingTrimmedMessages = void 0;
|
|
354
|
+
session.pendingCommitmentOffers = void 0;
|
|
355
|
+
session.pendingTurns = void 0;
|
|
356
|
+
session.stallResume = void 0;
|
|
357
|
+
session.assistantTurnCount = void 0;
|
|
358
|
+
return previousConversationId;
|
|
359
|
+
}
|
|
360
|
+
function bufferPendingTurn(sessionKey, turn) {
|
|
361
|
+
const session = sessionStore.get(sessionKey);
|
|
362
|
+
if (!session) {
|
|
363
|
+
console.error(`[conversation-gate] bufferPendingTurn: session not found sessionKey=${sessionKey.slice(0, 8)}\u2026`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (!session.pendingTurns) session.pendingTurns = [];
|
|
367
|
+
session.pendingTurns.push(turn);
|
|
368
|
+
console.log(`[conversation-gate] ${(/* @__PURE__ */ new Date()).toISOString()} buffered sessionKey=${sessionKey.slice(0, 8)} role=${turn.role} turnCount=${session.pendingTurns.filter((t) => t.role === "user").length}`);
|
|
369
|
+
}
|
|
370
|
+
function getPendingTurnCount(sessionKey) {
|
|
371
|
+
const buf = sessionStore.get(sessionKey)?.pendingTurns;
|
|
372
|
+
if (!buf) return 0;
|
|
373
|
+
let n = 0;
|
|
374
|
+
for (const t of buf) if (t.role === "user") n++;
|
|
375
|
+
return n;
|
|
376
|
+
}
|
|
377
|
+
function drainPendingTurns(sessionKey) {
|
|
378
|
+
const session = sessionStore.get(sessionKey);
|
|
379
|
+
if (!session?.pendingTurns || session.pendingTurns.length === 0) return void 0;
|
|
380
|
+
const drained = session.pendingTurns;
|
|
381
|
+
session.pendingTurns = void 0;
|
|
382
|
+
return drained;
|
|
383
|
+
}
|
|
384
|
+
function isFlushError(o) {
|
|
385
|
+
return o !== null && "error" in o;
|
|
386
|
+
}
|
|
387
|
+
async function maybeFlushConversationBuffer(sessionKey, agentType, accountId) {
|
|
388
|
+
const sk8 = sessionKey.slice(0, 8);
|
|
389
|
+
const session = sessionStore.get(sessionKey);
|
|
390
|
+
if (!session) {
|
|
391
|
+
console.log(`[admin/conversation-flush] sessionKey=${sk8} agentType=${agentType} result=missing-session`);
|
|
392
|
+
return { error: "missing-session" };
|
|
393
|
+
}
|
|
394
|
+
if (session.conversationId) {
|
|
395
|
+
console.log(`[admin/conversation-flush] sessionKey=${sk8} agentType=${agentType} result=already-flushed conversationId=${session.conversationId.slice(0, 8)}`);
|
|
396
|
+
return { conversationId: session.conversationId, buffered: [] };
|
|
397
|
+
}
|
|
398
|
+
const bufferedCount = session.pendingTurns?.length ?? 0;
|
|
399
|
+
if (bufferedCount === 0) {
|
|
400
|
+
console.log(`[admin/conversation-flush] sessionKey=${sk8} agentType=${agentType} result=empty-buffer`);
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
if (session.flushInFlight) return session.flushInFlight;
|
|
404
|
+
const attempt = (async () => {
|
|
405
|
+
let conversationId = null;
|
|
406
|
+
if (agentType === "admin") {
|
|
407
|
+
const userId = session.userId;
|
|
408
|
+
if (!userId) {
|
|
409
|
+
console.error(`[admin/conversation-flush] sessionKey=${sk8} agentType=admin result=missing-userId bufferedCount=${bufferedCount}`);
|
|
410
|
+
return { error: "missing-userId" };
|
|
411
|
+
}
|
|
412
|
+
conversationId = await createNewAdminConversation(userId, accountId, sessionKey, session.userName);
|
|
413
|
+
} else {
|
|
414
|
+
conversationId = (await ensureConversation(accountId, "public", sessionKey, void 0, session.agentName, void 0)).conversationId;
|
|
415
|
+
}
|
|
416
|
+
if (!conversationId) {
|
|
417
|
+
console.error(`[admin/conversation-flush] sessionKey=${sk8} agentType=${agentType} result=writer-failed bufferedCount=${bufferedCount}`);
|
|
418
|
+
return { error: "writer-failed" };
|
|
419
|
+
}
|
|
420
|
+
session.conversationId = conversationId;
|
|
421
|
+
if (agentType === "admin" && session.agentSessionId) {
|
|
422
|
+
setConversationAgentSessionId(conversationId, session.agentSessionId).catch(() => {
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
const buffered = drainPendingTurns(sessionKey) ?? [];
|
|
426
|
+
for (const turn of buffered) {
|
|
427
|
+
persistMessage(conversationId, turn.role, turn.content, accountId, turn.tokens, turn.timestamp, turn.sender, turn.components, turn.attachments).catch((err) => {
|
|
428
|
+
console.error(`[admin/conversation-flush] replay persistMessage failed role=${turn.role}: ${err instanceof Error ? err.message : String(err)}`);
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
console.log(`[admin/conversation-flush] sessionKey=${sk8} agentType=${agentType} result=ok conversationId=${conversationId.slice(0, 8)} bufferedMessages=${buffered.length}`);
|
|
432
|
+
return { conversationId, buffered };
|
|
433
|
+
})();
|
|
434
|
+
session.flushInFlight = attempt;
|
|
435
|
+
try {
|
|
436
|
+
return await attempt;
|
|
437
|
+
} finally {
|
|
438
|
+
if (session.flushInFlight === attempt) session.flushInFlight = void 0;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function isDmChannelSessionKey(sessionKey) {
|
|
442
|
+
return sessionKey.startsWith("whatsapp:") || sessionKey.startsWith("telegram:");
|
|
443
|
+
}
|
|
444
|
+
function getAgentNameForSession(sessionKey) {
|
|
445
|
+
return sessionStore.get(sessionKey)?.agentName;
|
|
446
|
+
}
|
|
447
|
+
function listAdminSessionsInProgress(accountId, userId) {
|
|
448
|
+
const rows = [];
|
|
449
|
+
for (const [sessionKey, session] of Array.from(sessionStore.entries())) {
|
|
450
|
+
if (session.agentType !== "admin") continue;
|
|
451
|
+
if (session.accountId !== accountId) continue;
|
|
452
|
+
if (session.userId !== userId) continue;
|
|
453
|
+
if (session.conversationId) continue;
|
|
454
|
+
rows.push({ sessionKey, createdAt: session.createdAt });
|
|
455
|
+
}
|
|
456
|
+
rows.sort((a, b) => b.createdAt - a.createdAt);
|
|
457
|
+
return rows;
|
|
458
|
+
}
|
|
459
|
+
function validateSession(sessionKey, agentType) {
|
|
460
|
+
const session = sessionStore.get(sessionKey);
|
|
461
|
+
if (!session) return { ok: false, reason: "session-not-registered" };
|
|
462
|
+
if (session.agentType !== agentType) return { ok: false, reason: "agent-type-mismatch" };
|
|
463
|
+
if (Date.now() - session.createdAt > 24 * 60 * 60 * 1e3) {
|
|
464
|
+
sessionStore.delete(sessionKey);
|
|
465
|
+
return { ok: false, reason: "session-expired-age" };
|
|
466
|
+
}
|
|
467
|
+
if (session.grantExpiresAt && Date.now() > session.grantExpiresAt) {
|
|
468
|
+
sessionStore.delete(sessionKey);
|
|
469
|
+
return { ok: false, reason: "grant-expired" };
|
|
470
|
+
}
|
|
471
|
+
return { ok: true };
|
|
472
|
+
}
|
|
473
|
+
function storeAgentSessionId(sessionKey, agentSessionId) {
|
|
474
|
+
const session = sessionStore.get(sessionKey);
|
|
475
|
+
if (session) {
|
|
476
|
+
session.agentSessionId = agentSessionId;
|
|
477
|
+
console.error(`[session-store] storeAgentSessionId sessionKey=${sessionKey.slice(0, 12)}\u2026 sessionId=${agentSessionId.slice(0, 8)}\u2026`);
|
|
478
|
+
if (session.agentType === "admin" && session.conversationId) {
|
|
479
|
+
setConversationAgentSessionId(session.conversationId, agentSessionId).catch(() => {
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
} else {
|
|
483
|
+
console.error(`[session-store] storeAgentSessionId SKIPPED \u2014 no session entry sessionKey=${sessionKey.slice(0, 12)}\u2026 sessionId=${agentSessionId.slice(0, 8)}\u2026`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function getAgentSessionId(sessionKey) {
|
|
487
|
+
return sessionStore.get(sessionKey)?.agentSessionId;
|
|
488
|
+
}
|
|
489
|
+
function setAgentSessionId(sessionKey, agentSessionId) {
|
|
490
|
+
const session = sessionStore.get(sessionKey);
|
|
491
|
+
if (!session) return false;
|
|
492
|
+
session.agentSessionId = agentSessionId;
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
function storePendingCompactionSummary(sessionKey, summary) {
|
|
496
|
+
const session = sessionStore.get(sessionKey);
|
|
497
|
+
if (session) session.pendingCompactionSummary = summary;
|
|
498
|
+
}
|
|
499
|
+
function consumePendingCompactionSummary(sessionKey) {
|
|
500
|
+
const session = sessionStore.get(sessionKey);
|
|
501
|
+
if (!session) return void 0;
|
|
502
|
+
const summary = session.pendingCompactionSummary;
|
|
503
|
+
delete session.pendingCompactionSummary;
|
|
504
|
+
return summary;
|
|
505
|
+
}
|
|
506
|
+
function getAccountIdForSession(sessionKey) {
|
|
507
|
+
return sessionStore.get(sessionKey)?.accountId;
|
|
508
|
+
}
|
|
509
|
+
function getUserIdForSession(sessionKey) {
|
|
510
|
+
return sessionStore.get(sessionKey)?.userId;
|
|
511
|
+
}
|
|
512
|
+
function getUserNameForSession(sessionKey) {
|
|
513
|
+
return sessionStore.get(sessionKey)?.userName;
|
|
514
|
+
}
|
|
515
|
+
function getRoleForSession(sessionKey) {
|
|
516
|
+
return sessionStore.get(sessionKey)?.role;
|
|
517
|
+
}
|
|
518
|
+
function getConversationIdForSession(sessionKey) {
|
|
519
|
+
return sessionStore.get(sessionKey)?.conversationId;
|
|
520
|
+
}
|
|
521
|
+
function setConversationIdForSession(sessionKey, conversationId) {
|
|
522
|
+
const session = sessionStore.get(sessionKey);
|
|
523
|
+
if (!session) return false;
|
|
524
|
+
session.conversationId = conversationId;
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
function unregisterSession(sessionKey) {
|
|
528
|
+
return sessionStore.delete(sessionKey);
|
|
529
|
+
}
|
|
530
|
+
function getGroupSlugForSession(sessionKey) {
|
|
531
|
+
return sessionStore.get(sessionKey)?.groupSlug;
|
|
532
|
+
}
|
|
533
|
+
function getVisitorIdForSession(sessionKey) {
|
|
534
|
+
return sessionStore.get(sessionKey)?.visitorId;
|
|
535
|
+
}
|
|
536
|
+
function setGroupContextForSession(sessionKey, context) {
|
|
537
|
+
const session = sessionStore.get(sessionKey);
|
|
538
|
+
if (!session) return false;
|
|
539
|
+
session.groupSlug = context.groupSlug;
|
|
540
|
+
session.groupName = context.groupName;
|
|
541
|
+
session.conversationId = context.conversationId;
|
|
542
|
+
session.visitorId = context.visitorId;
|
|
543
|
+
session.senderDisplayName = context.senderDisplayName;
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
function registerGrantSession(sessionKey, accountId, agentName, opts) {
|
|
547
|
+
sessionStore.set(sessionKey, {
|
|
548
|
+
createdAt: Date.now(),
|
|
549
|
+
agentType: "public",
|
|
550
|
+
accountId,
|
|
551
|
+
agentName,
|
|
552
|
+
grantId: opts.grantId,
|
|
553
|
+
grantExpiresAt: opts.grantExpiresAt,
|
|
554
|
+
grantStatus: opts.grantStatus,
|
|
555
|
+
grantDisplayName: opts.grantDisplayName,
|
|
556
|
+
grantContactValue: opts.grantContactValue,
|
|
557
|
+
setupRequired: opts.setupRequired
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
function getGrantForSession(sessionKey) {
|
|
561
|
+
const session = sessionStore.get(sessionKey);
|
|
562
|
+
if (!session?.grantId) return void 0;
|
|
563
|
+
return {
|
|
564
|
+
grantId: session.grantId,
|
|
565
|
+
grantExpiresAt: session.grantExpiresAt,
|
|
566
|
+
grantStatus: session.grantStatus,
|
|
567
|
+
grantDisplayName: session.grantDisplayName,
|
|
568
|
+
grantContactValue: session.grantContactValue,
|
|
569
|
+
setupRequired: session.setupRequired
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function completeGrantSetup(sessionKey) {
|
|
573
|
+
const session = sessionStore.get(sessionKey);
|
|
574
|
+
if (session) {
|
|
575
|
+
session.setupRequired = false;
|
|
576
|
+
session.grantStatus = "active";
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function getMessageHistory(sessionKey) {
|
|
580
|
+
const session = sessionStore.get(sessionKey);
|
|
581
|
+
if (!session) return [];
|
|
582
|
+
if (!session.messageHistory) session.messageHistory = [];
|
|
583
|
+
return session.messageHistory;
|
|
584
|
+
}
|
|
585
|
+
function appendMessage(sessionKey, role, content, timestamp) {
|
|
586
|
+
if (!content) return;
|
|
587
|
+
const session = sessionStore.get(sessionKey);
|
|
588
|
+
if (!session) {
|
|
589
|
+
console.error(`[managed] appendMessage: session not found for key ${sessionKey.slice(0, 8)}\u2026 (store size: ${sessionStore.size})`);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (!session.messageHistory) session.messageHistory = [];
|
|
593
|
+
session.messageHistory.push({ role, content, timestamp: timestamp ?? Date.now() });
|
|
594
|
+
}
|
|
595
|
+
function storePendingTrimmedMessages(sessionKey, messages) {
|
|
596
|
+
const session = sessionStore.get(sessionKey);
|
|
597
|
+
if (session) session.pendingTrimmedMessages = messages;
|
|
598
|
+
}
|
|
599
|
+
function consumePendingTrimmedMessages(sessionKey) {
|
|
600
|
+
const session = sessionStore.get(sessionKey);
|
|
601
|
+
if (!session) return void 0;
|
|
602
|
+
const messages = session.pendingTrimmedMessages;
|
|
603
|
+
delete session.pendingTrimmedMessages;
|
|
604
|
+
return messages;
|
|
605
|
+
}
|
|
606
|
+
function storeStalledSubagent(sessionKey, info) {
|
|
607
|
+
const session = sessionStore.get(sessionKey);
|
|
608
|
+
if (!session) {
|
|
609
|
+
console.error(`[stall-recovery] storeStalledSubagent: session not found for key ${sessionKey.slice(0, 8)}\u2026 \u2014 stall context lost`);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (!session.stalledSubagents) session.stalledSubagents = [];
|
|
613
|
+
session.stalledSubagents.push(info);
|
|
614
|
+
}
|
|
615
|
+
function consumeStalledSubagents(sessionKey) {
|
|
616
|
+
const session = sessionStore.get(sessionKey);
|
|
617
|
+
if (!session) return void 0;
|
|
618
|
+
const stalls = session.stalledSubagents;
|
|
619
|
+
delete session.stalledSubagents;
|
|
620
|
+
return stalls && stalls.length > 0 ? stalls : void 0;
|
|
621
|
+
}
|
|
622
|
+
function setPendingCommitmentOffers(sessionKey, offers) {
|
|
623
|
+
const session = sessionStore.get(sessionKey);
|
|
624
|
+
if (session) session.pendingCommitmentOffers = offers;
|
|
625
|
+
}
|
|
626
|
+
function getAgentTypeForSession(sessionKey) {
|
|
627
|
+
return sessionStore.get(sessionKey)?.agentType;
|
|
628
|
+
}
|
|
629
|
+
function clearMessageHistory(sessionKey) {
|
|
630
|
+
const session = sessionStore.get(sessionKey);
|
|
631
|
+
if (session) session.messageHistory = [];
|
|
632
|
+
}
|
|
633
|
+
var clearAgentSessionIdHandlers = [];
|
|
634
|
+
function onClearAgentSessionId(handler) {
|
|
635
|
+
clearAgentSessionIdHandlers.push(handler);
|
|
636
|
+
}
|
|
637
|
+
var evictPoolHandlers = [];
|
|
638
|
+
function onEvictPool(handler) {
|
|
639
|
+
evictPoolHandlers.push(handler);
|
|
640
|
+
}
|
|
641
|
+
function clearAgentSessionId(sessionKey, reason) {
|
|
642
|
+
const session = sessionStore.get(sessionKey);
|
|
643
|
+
if (session) {
|
|
644
|
+
session.agentSessionId = void 0;
|
|
645
|
+
session.lastClearReason = reason;
|
|
646
|
+
console.error(`[session-store] clearAgentSessionId sessionKey=${sessionKey.slice(0, 12)}\u2026 reason=${reason}`);
|
|
647
|
+
} else {
|
|
648
|
+
console.error(`[session-store] clearAgentSessionId SKIPPED \u2014 no session entry sessionKey=${sessionKey.slice(0, 12)}\u2026 reason=${reason}`);
|
|
649
|
+
}
|
|
650
|
+
for (const handler of clearAgentSessionIdHandlers) {
|
|
651
|
+
try {
|
|
652
|
+
handler(sessionKey, reason);
|
|
653
|
+
} catch (err) {
|
|
654
|
+
console.error(`[session-store] clearAgentSessionId handler threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function evictPool(sessionKey, reason) {
|
|
659
|
+
for (const handler of evictPoolHandlers) {
|
|
660
|
+
try {
|
|
661
|
+
handler(sessionKey, reason);
|
|
662
|
+
} catch (err) {
|
|
663
|
+
console.error(`[session-store] evictPool handler threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
function storeRecoveryHandoff(sessionKey, info) {
|
|
668
|
+
const session = sessionStore.get(sessionKey);
|
|
669
|
+
if (!session) {
|
|
670
|
+
console.error(`[recovery-handoff] storeRecoveryHandoff: session not found for key ${sessionKey.slice(0, 8)}\u2026 \u2014 handoff context lost reason=${info.reason}`);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
session.recoveryHandoff = { reason: info.reason, summary: info.summary, createdAt: Date.now() };
|
|
674
|
+
}
|
|
675
|
+
function consumeRecoveryHandoff(sessionKey) {
|
|
676
|
+
const session = sessionStore.get(sessionKey);
|
|
677
|
+
if (!session?.recoveryHandoff) return void 0;
|
|
678
|
+
const handoff = session.recoveryHandoff;
|
|
679
|
+
delete session.recoveryHandoff;
|
|
680
|
+
delete session.lastClearReason;
|
|
681
|
+
return handoff;
|
|
682
|
+
}
|
|
683
|
+
function getLastClearReason(sessionKey) {
|
|
684
|
+
return sessionStore.get(sessionKey)?.lastClearReason;
|
|
685
|
+
}
|
|
686
|
+
function clearLastClearReason(sessionKey) {
|
|
687
|
+
const session = sessionStore.get(sessionKey);
|
|
688
|
+
if (session) delete session.lastClearReason;
|
|
689
|
+
}
|
|
690
|
+
function storeStallResume(sessionKey, info) {
|
|
691
|
+
const session = sessionStore.get(sessionKey);
|
|
692
|
+
if (!session) {
|
|
693
|
+
console.error(`[stall-resume] storeStallResume: session not found for key ${sessionKey.slice(0, 8)}\u2026 \u2014 resume context lost kind=${info.kind}`);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
session.stallResume = { ...info, createdAt: Date.now() };
|
|
697
|
+
}
|
|
698
|
+
function consumeStallResume(sessionKey) {
|
|
699
|
+
const session = sessionStore.get(sessionKey);
|
|
700
|
+
if (!session?.stallResume) return void 0;
|
|
701
|
+
const payload = session.stallResume;
|
|
702
|
+
delete session.stallResume;
|
|
703
|
+
return payload;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// app/lib/claude-agent/client-pool.ts
|
|
707
|
+
var AsyncQueue = class {
|
|
708
|
+
buffer = [];
|
|
709
|
+
resolvers = [];
|
|
710
|
+
closed = false;
|
|
711
|
+
push(item) {
|
|
712
|
+
if (this.closed) return;
|
|
713
|
+
const resolver = this.resolvers.shift();
|
|
714
|
+
if (resolver) resolver({ value: item, done: false });
|
|
715
|
+
else this.buffer.push(item);
|
|
716
|
+
}
|
|
717
|
+
close() {
|
|
718
|
+
if (this.closed) return;
|
|
719
|
+
this.closed = true;
|
|
720
|
+
while (this.resolvers.length > 0) {
|
|
721
|
+
const r = this.resolvers.shift();
|
|
722
|
+
r({ value: void 0, done: true });
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
[Symbol.asyncIterator]() {
|
|
726
|
+
return {
|
|
727
|
+
next: () => new Promise((resolve2) => {
|
|
728
|
+
if (this.buffer.length > 0) {
|
|
729
|
+
resolve2({ value: this.buffer.shift(), done: false });
|
|
730
|
+
} else if (this.closed) {
|
|
731
|
+
resolve2({ value: void 0, done: true });
|
|
732
|
+
} else {
|
|
733
|
+
this.resolvers.push(resolve2);
|
|
734
|
+
}
|
|
735
|
+
}),
|
|
736
|
+
return: async () => {
|
|
737
|
+
this.close();
|
|
738
|
+
return { value: void 0, done: true };
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
var clientPool = /* @__PURE__ */ new Map();
|
|
744
|
+
var DEFAULT_IDLE_MS = 30 * 60 * 1e3;
|
|
745
|
+
var idleEvictMs = (() => {
|
|
746
|
+
const v = Number(process.env.CLAUDE_CLIENT_IDLE_MS);
|
|
747
|
+
return Number.isFinite(v) && v > 0 ? v : DEFAULT_IDLE_MS;
|
|
748
|
+
})();
|
|
749
|
+
var ABORT_SDK_TIMEOUT_MS = 2e3;
|
|
750
|
+
function acquireClient(sessionKey, opts, streamLog) {
|
|
751
|
+
const existing = clientPool.get(sessionKey);
|
|
752
|
+
if (existing) {
|
|
753
|
+
existing.lastUsedAt = Date.now();
|
|
754
|
+
existing.turnsServed += 1;
|
|
755
|
+
safeWrite(
|
|
756
|
+
streamLog,
|
|
757
|
+
`[${isoTs()}] [client-warm-reuse] sessionKey=${sk(sessionKey)} ageMs=${Date.now() - existing.createdAt} turnsServed=${existing.turnsServed} cachedTokens=${existing.cachedTokensLastTurn}
|
|
758
|
+
`
|
|
759
|
+
);
|
|
760
|
+
return { entry: existing, isCold: false };
|
|
761
|
+
}
|
|
762
|
+
const userQueue = new AsyncQueue();
|
|
763
|
+
const abortController = new AbortController();
|
|
764
|
+
let q;
|
|
765
|
+
try {
|
|
766
|
+
const sdkOptions = opts.buildSdkOptions();
|
|
767
|
+
q = query({ prompt: userQueue, options: { ...sdkOptions, abortController } });
|
|
768
|
+
} catch (err) {
|
|
769
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
770
|
+
safeWrite(
|
|
771
|
+
streamLog,
|
|
772
|
+
`[${isoTs()}] [client-spawn-error] sessionKey=${sk(sessionKey)} reason=${JSON.stringify(reason.slice(0, 200))}
|
|
773
|
+
`
|
|
774
|
+
);
|
|
775
|
+
throw err;
|
|
776
|
+
}
|
|
777
|
+
const entry = {
|
|
778
|
+
query: q,
|
|
779
|
+
userQueue,
|
|
780
|
+
abortController,
|
|
781
|
+
inflight: null,
|
|
782
|
+
createdAt: Date.now(),
|
|
783
|
+
lastUsedAt: Date.now(),
|
|
784
|
+
turnsServed: 1,
|
|
785
|
+
resumedFromSessionId: opts.resumedFromSessionId,
|
|
786
|
+
cachedTokensLastTurn: -1,
|
|
787
|
+
accountId: opts.accountId,
|
|
788
|
+
accountDir: opts.accountDir,
|
|
789
|
+
logKey: opts.logKey
|
|
790
|
+
};
|
|
791
|
+
clientPool.set(sessionKey, entry);
|
|
792
|
+
safeWrite(
|
|
793
|
+
streamLog,
|
|
794
|
+
`[${isoTs()}] [client-cold-create] sessionKey=${sk(sessionKey)} resumedFrom=${opts.resumedFromSessionId ?? "none"} createdAtMs=${entry.createdAt}
|
|
795
|
+
`
|
|
796
|
+
);
|
|
797
|
+
safeWrite(
|
|
798
|
+
streamLog,
|
|
799
|
+
`[${isoTs()}] [client-sdk-argv] sessionKey=${sk(sessionKey)} cwd=${opts.accountDir} resume=${opts.resumedFromSessionId ?? "none"} configDir=${process.env.CLAUDE_CONFIG_DIR ?? "<unset>"}
|
|
800
|
+
`
|
|
801
|
+
);
|
|
802
|
+
return { entry, isCold: true };
|
|
803
|
+
}
|
|
804
|
+
function pushUserMessage(entry, content) {
|
|
805
|
+
const msg = {
|
|
806
|
+
type: "user",
|
|
807
|
+
message: { role: "user", content },
|
|
808
|
+
parent_tool_use_id: null
|
|
809
|
+
};
|
|
810
|
+
entry.userQueue.push(msg);
|
|
811
|
+
}
|
|
812
|
+
function pushUserToolResult(entry, toolUseId, content, isError = false) {
|
|
813
|
+
const msg = {
|
|
814
|
+
type: "user",
|
|
815
|
+
message: {
|
|
816
|
+
role: "user",
|
|
817
|
+
content: [{ type: "tool_result", tool_use_id: toolUseId, content, is_error: isError }]
|
|
818
|
+
},
|
|
819
|
+
parent_tool_use_id: null
|
|
820
|
+
};
|
|
821
|
+
entry.userQueue.push(msg);
|
|
822
|
+
}
|
|
823
|
+
function setInflight(entry, promise) {
|
|
824
|
+
entry.inflight = promise;
|
|
825
|
+
promise.finally(() => {
|
|
826
|
+
if (entry.inflight === promise) entry.inflight = null;
|
|
827
|
+
}).catch(() => {
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
function recordCachedTokens(entry, cachedTokens) {
|
|
831
|
+
entry.cachedTokensLastTurn = cachedTokens;
|
|
832
|
+
}
|
|
833
|
+
function getActiveClient(sessionKey) {
|
|
834
|
+
return clientPool.get(sessionKey);
|
|
835
|
+
}
|
|
836
|
+
function appendAbortLine(entry, line) {
|
|
837
|
+
const path = resolvePath(entry.accountDir, "logs", `claude-agent-stream-${entry.logKey}.log`);
|
|
838
|
+
try {
|
|
839
|
+
appendFileSync2(path, line);
|
|
840
|
+
} catch {
|
|
841
|
+
}
|
|
842
|
+
console.error(line.trimEnd());
|
|
843
|
+
}
|
|
844
|
+
async function interruptClient(sessionKey, _streamLog) {
|
|
845
|
+
void _streamLog;
|
|
846
|
+
const entry = clientPool.get(sessionKey);
|
|
847
|
+
if (!entry) return;
|
|
848
|
+
const startedAt = Date.now();
|
|
849
|
+
let resolved = false;
|
|
850
|
+
const timeout = new Promise(
|
|
851
|
+
(resolve2) => setTimeout(() => resolve2("timeout"), ABORT_SDK_TIMEOUT_MS)
|
|
852
|
+
);
|
|
853
|
+
const sdkAbort = entry.query.interrupt().then(() => {
|
|
854
|
+
resolved = true;
|
|
855
|
+
return "ok";
|
|
856
|
+
}).catch((err) => {
|
|
857
|
+
resolved = true;
|
|
858
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
859
|
+
});
|
|
860
|
+
const result = await Promise.race([sdkAbort, timeout]);
|
|
861
|
+
const durationMs = Date.now() - startedAt;
|
|
862
|
+
if (result === "timeout" && !resolved) {
|
|
863
|
+
appendAbortLine(
|
|
864
|
+
entry,
|
|
865
|
+
`[${isoTs()}] [client-abort] sessionKey=${sk(sessionKey)} method=signal durationMs=${durationMs}
|
|
866
|
+
`
|
|
867
|
+
);
|
|
868
|
+
evictClient(sessionKey, "abort-signal-fallback", null);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (typeof result === "object" && "error" in result) {
|
|
872
|
+
appendAbortLine(
|
|
873
|
+
entry,
|
|
874
|
+
`[${isoTs()}] [client-abort] sessionKey=${sk(sessionKey)} method=sdk durationMs=${durationMs} error=${JSON.stringify(result.error.slice(0, 120))}
|
|
875
|
+
`
|
|
876
|
+
);
|
|
877
|
+
evictClient(sessionKey, "abort-error", null);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
appendAbortLine(
|
|
881
|
+
entry,
|
|
882
|
+
`[${isoTs()}] [client-abort] sessionKey=${sk(sessionKey)} method=sdk durationMs=${durationMs}
|
|
883
|
+
`
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
function evictClient(sessionKey, reason, streamLog) {
|
|
887
|
+
const entry = clientPool.get(sessionKey);
|
|
888
|
+
if (!entry) return false;
|
|
889
|
+
const ageMs = Date.now() - entry.createdAt;
|
|
890
|
+
clientPool.delete(sessionKey);
|
|
891
|
+
try {
|
|
892
|
+
entry.userQueue.close();
|
|
893
|
+
} catch {
|
|
894
|
+
}
|
|
895
|
+
try {
|
|
896
|
+
entry.query.close();
|
|
897
|
+
} catch {
|
|
898
|
+
}
|
|
899
|
+
const line = `[${isoTs()}] [client-evict] reason=${reason} sessionKey=${sk(sessionKey)} ageMs=${ageMs} turnsServed=${entry.turnsServed}
|
|
900
|
+
`;
|
|
901
|
+
if (streamLog) {
|
|
902
|
+
safeWrite(streamLog, line);
|
|
903
|
+
} else {
|
|
904
|
+
appendAbortLine(entry, line);
|
|
905
|
+
}
|
|
906
|
+
return true;
|
|
907
|
+
}
|
|
908
|
+
function acquireOneShotClient(sessionKey, opts, streamLog) {
|
|
909
|
+
const userQueue = new AsyncQueue();
|
|
910
|
+
const abortController = new AbortController();
|
|
911
|
+
let q;
|
|
912
|
+
try {
|
|
913
|
+
const sdkOptions = opts.buildSdkOptions();
|
|
914
|
+
q = query({ prompt: userQueue, options: { ...sdkOptions, abortController } });
|
|
915
|
+
} catch (err) {
|
|
916
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
917
|
+
safeWrite(
|
|
918
|
+
streamLog,
|
|
919
|
+
`[${isoTs()}] [client-spawn-error] sessionKey=${sk(sessionKey)} site=compaction-one-shot reason=${JSON.stringify(reason.slice(0, 200))}
|
|
920
|
+
`
|
|
921
|
+
);
|
|
922
|
+
throw err;
|
|
923
|
+
}
|
|
924
|
+
const entry = {
|
|
925
|
+
query: q,
|
|
926
|
+
userQueue,
|
|
927
|
+
abortController,
|
|
928
|
+
inflight: null,
|
|
929
|
+
createdAt: Date.now(),
|
|
930
|
+
lastUsedAt: Date.now(),
|
|
931
|
+
turnsServed: 1,
|
|
932
|
+
resumedFromSessionId: opts.resumedFromSessionId,
|
|
933
|
+
cachedTokensLastTurn: -1,
|
|
934
|
+
accountId: opts.accountId,
|
|
935
|
+
accountDir: opts.accountDir,
|
|
936
|
+
logKey: opts.logKey
|
|
937
|
+
};
|
|
938
|
+
safeWrite(
|
|
939
|
+
streamLog,
|
|
940
|
+
`[${isoTs()}] [client-cold-create] reason=compaction-one-shot sessionKey=${sk(sessionKey)} createdAtMs=${entry.createdAt}
|
|
941
|
+
`
|
|
942
|
+
);
|
|
943
|
+
let evicted = false;
|
|
944
|
+
const evict = (reason) => {
|
|
945
|
+
if (evicted) return;
|
|
946
|
+
evicted = true;
|
|
947
|
+
const ageMs = Date.now() - entry.createdAt;
|
|
948
|
+
try {
|
|
949
|
+
entry.userQueue.close();
|
|
950
|
+
} catch {
|
|
951
|
+
}
|
|
952
|
+
try {
|
|
953
|
+
entry.query.close();
|
|
954
|
+
} catch {
|
|
955
|
+
}
|
|
956
|
+
safeWrite(
|
|
957
|
+
streamLog,
|
|
958
|
+
`[${isoTs()}] [client-evict] reason=${reason} sessionKey=${sk(sessionKey)} ageMs=${ageMs}
|
|
959
|
+
`
|
|
960
|
+
);
|
|
961
|
+
};
|
|
962
|
+
return { entry, evict };
|
|
963
|
+
}
|
|
964
|
+
function recordCrash(sessionKey, reason, streamLog) {
|
|
965
|
+
const entry = clientPool.get(sessionKey);
|
|
966
|
+
if (!entry) return;
|
|
967
|
+
const tail = JSON.stringify(reason.slice(-512));
|
|
968
|
+
clientPool.delete(sessionKey);
|
|
969
|
+
try {
|
|
970
|
+
entry.userQueue.close();
|
|
971
|
+
} catch {
|
|
972
|
+
}
|
|
973
|
+
try {
|
|
974
|
+
entry.query.close();
|
|
975
|
+
} catch {
|
|
976
|
+
}
|
|
977
|
+
safeWrite(
|
|
978
|
+
streamLog,
|
|
979
|
+
`[${isoTs()}] [client-crash] sessionKey=${sk(sessionKey)} reason=${JSON.stringify(reason.slice(0, 80))} tail=${tail}
|
|
980
|
+
`
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
var IDLE_TICK_MS = 6e4;
|
|
984
|
+
var idleTickHandle = null;
|
|
985
|
+
function evictIdleTick() {
|
|
986
|
+
const now = Date.now();
|
|
987
|
+
for (const [sessionKey, entry] of Array.from(clientPool.entries())) {
|
|
988
|
+
if (entry.inflight !== null) continue;
|
|
989
|
+
if (now - entry.lastUsedAt < idleEvictMs) continue;
|
|
990
|
+
const ageMs = now - entry.createdAt;
|
|
991
|
+
clientPool.delete(sessionKey);
|
|
992
|
+
try {
|
|
993
|
+
entry.userQueue.close();
|
|
994
|
+
} catch {
|
|
995
|
+
}
|
|
996
|
+
try {
|
|
997
|
+
entry.query.close();
|
|
998
|
+
} catch {
|
|
999
|
+
}
|
|
1000
|
+
console.error(
|
|
1001
|
+
`[${isoTs()}] [client-evict] reason=idle sessionKey=${sk(sessionKey)} ageMs=${ageMs} idleMs=${now - entry.lastUsedAt} turnsServed=${entry.turnsServed}`
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function startIdleEvictTick() {
|
|
1006
|
+
if (idleTickHandle) return;
|
|
1007
|
+
idleTickHandle = setInterval(evictIdleTick, IDLE_TICK_MS);
|
|
1008
|
+
if (idleTickHandle.unref) idleTickHandle.unref();
|
|
1009
|
+
}
|
|
1010
|
+
function stopIdleEvictTick() {
|
|
1011
|
+
if (idleTickHandle) {
|
|
1012
|
+
clearInterval(idleTickHandle);
|
|
1013
|
+
idleTickHandle = null;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
startIdleEvictTick();
|
|
1017
|
+
onClearAgentSessionId((sessionKey, reason) => {
|
|
1018
|
+
const entry = clientPool.get(sessionKey);
|
|
1019
|
+
if (!entry) return;
|
|
1020
|
+
const ageMs = Date.now() - entry.createdAt;
|
|
1021
|
+
clientPool.delete(sessionKey);
|
|
1022
|
+
try {
|
|
1023
|
+
entry.userQueue.close();
|
|
1024
|
+
} catch {
|
|
1025
|
+
}
|
|
1026
|
+
try {
|
|
1027
|
+
entry.query.close();
|
|
1028
|
+
} catch {
|
|
1029
|
+
}
|
|
1030
|
+
console.error(
|
|
1031
|
+
`[${isoTs()}] [client-evict] reason=clearAgentSessionId-${reason} sessionKey=${sk(sessionKey)} ageMs=${ageMs} turnsServed=${entry.turnsServed}`
|
|
1032
|
+
);
|
|
1033
|
+
});
|
|
1034
|
+
onEvictPool((sessionKey, reason) => {
|
|
1035
|
+
const entry = clientPool.get(sessionKey);
|
|
1036
|
+
if (!entry) return;
|
|
1037
|
+
const ageMs = Date.now() - entry.createdAt;
|
|
1038
|
+
clientPool.delete(sessionKey);
|
|
1039
|
+
try {
|
|
1040
|
+
entry.userQueue.close();
|
|
1041
|
+
} catch {
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
entry.query.close();
|
|
1045
|
+
} catch {
|
|
1046
|
+
}
|
|
1047
|
+
console.error(
|
|
1048
|
+
`[${isoTs()}] [client-evict] reason=evictPool-${reason} sessionKey=${sk(sessionKey)} ageMs=${ageMs} turnsServed=${entry.turnsServed}`
|
|
1049
|
+
);
|
|
1050
|
+
});
|
|
1051
|
+
function sk(sessionKey) {
|
|
1052
|
+
return sessionKey.length > 12 ? sessionKey.slice(0, 12) + "\u2026" : sessionKey;
|
|
1053
|
+
}
|
|
1054
|
+
function safeWrite(stream, line) {
|
|
1055
|
+
if (!stream || stream.destroyed) return;
|
|
1056
|
+
if (stream.writableEnded) return;
|
|
1057
|
+
try {
|
|
1058
|
+
stream.write(line);
|
|
1059
|
+
} catch {
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
function _poolSnapshotForTest() {
|
|
1063
|
+
const now = Date.now();
|
|
1064
|
+
return Array.from(clientPool.entries()).map(([sessionKey, entry]) => ({
|
|
1065
|
+
sessionKey,
|
|
1066
|
+
ageMs: now - entry.createdAt,
|
|
1067
|
+
idleMs: now - entry.lastUsedAt,
|
|
1068
|
+
turnsServed: entry.turnsServed,
|
|
1069
|
+
inflight: entry.inflight !== null
|
|
1070
|
+
}));
|
|
1071
|
+
}
|
|
1072
|
+
function _evictAllForTest(reason = "test-tear-down") {
|
|
1073
|
+
for (const sessionKey of Array.from(clientPool.keys())) {
|
|
1074
|
+
const entry = clientPool.get(sessionKey);
|
|
1075
|
+
if (!entry) continue;
|
|
1076
|
+
clientPool.delete(sessionKey);
|
|
1077
|
+
try {
|
|
1078
|
+
entry.userQueue.close();
|
|
1079
|
+
} catch {
|
|
1080
|
+
}
|
|
1081
|
+
try {
|
|
1082
|
+
entry.query.close();
|
|
1083
|
+
} catch {
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
void reason;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
export {
|
|
1090
|
+
findFirstSubstantiveUserMessage,
|
|
1091
|
+
getSession,
|
|
1092
|
+
registerSession,
|
|
1093
|
+
registerResumedSession,
|
|
1094
|
+
getSessionMessages,
|
|
1095
|
+
clearSessionHistory,
|
|
1096
|
+
bufferPendingTurn,
|
|
1097
|
+
getPendingTurnCount,
|
|
1098
|
+
drainPendingTurns,
|
|
1099
|
+
isFlushError,
|
|
1100
|
+
maybeFlushConversationBuffer,
|
|
1101
|
+
isDmChannelSessionKey,
|
|
1102
|
+
getAgentNameForSession,
|
|
1103
|
+
listAdminSessionsInProgress,
|
|
1104
|
+
validateSession,
|
|
1105
|
+
storeAgentSessionId,
|
|
1106
|
+
getAgentSessionId,
|
|
1107
|
+
setAgentSessionId,
|
|
1108
|
+
storePendingCompactionSummary,
|
|
1109
|
+
consumePendingCompactionSummary,
|
|
1110
|
+
getAccountIdForSession,
|
|
1111
|
+
getUserIdForSession,
|
|
1112
|
+
getUserNameForSession,
|
|
1113
|
+
getRoleForSession,
|
|
1114
|
+
getConversationIdForSession,
|
|
1115
|
+
setConversationIdForSession,
|
|
1116
|
+
unregisterSession,
|
|
1117
|
+
getGroupSlugForSession,
|
|
1118
|
+
getVisitorIdForSession,
|
|
1119
|
+
setGroupContextForSession,
|
|
1120
|
+
registerGrantSession,
|
|
1121
|
+
getGrantForSession,
|
|
1122
|
+
completeGrantSetup,
|
|
1123
|
+
getMessageHistory,
|
|
1124
|
+
appendMessage,
|
|
1125
|
+
storePendingTrimmedMessages,
|
|
1126
|
+
consumePendingTrimmedMessages,
|
|
1127
|
+
storeStalledSubagent,
|
|
1128
|
+
consumeStalledSubagents,
|
|
1129
|
+
setPendingCommitmentOffers,
|
|
1130
|
+
getAgentTypeForSession,
|
|
1131
|
+
clearMessageHistory,
|
|
1132
|
+
clearAgentSessionId,
|
|
1133
|
+
evictPool,
|
|
1134
|
+
storeRecoveryHandoff,
|
|
1135
|
+
consumeRecoveryHandoff,
|
|
1136
|
+
getLastClearReason,
|
|
1137
|
+
clearLastClearReason,
|
|
1138
|
+
storeStallResume,
|
|
1139
|
+
consumeStallResume,
|
|
1140
|
+
isoTs,
|
|
1141
|
+
isBrowserTool,
|
|
1142
|
+
runFailureDiagnostic,
|
|
1143
|
+
agentLogStream,
|
|
1144
|
+
preConversationLogStream,
|
|
1145
|
+
sigtermFlushStreamLogs,
|
|
1146
|
+
acquireClient,
|
|
1147
|
+
pushUserMessage,
|
|
1148
|
+
pushUserToolResult,
|
|
1149
|
+
setInflight,
|
|
1150
|
+
recordCachedTokens,
|
|
1151
|
+
getActiveClient,
|
|
1152
|
+
interruptClient,
|
|
1153
|
+
evictClient,
|
|
1154
|
+
acquireOneShotClient,
|
|
1155
|
+
recordCrash,
|
|
1156
|
+
startIdleEvictTick,
|
|
1157
|
+
stopIdleEvictTick,
|
|
1158
|
+
_poolSnapshotForTest,
|
|
1159
|
+
_evictAllForTest
|
|
1160
|
+
};
|