@rubytech/create-maxy 1.0.885 → 1.0.886
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/neo4j/schema.cypher +7 -0
- package/payload/platform/scripts/check-no-conversation-id-leaks.mjs +165 -0
- package/payload/platform/scripts/conversation-id-allowlist.txt +151 -0
- package/payload/platform/scripts/seed-neo4j.sh +46 -0
- package/payload/server/chunk-IFMZ5I3E.js +1460 -0
- package/payload/server/chunk-MOAY7KG2.js +11667 -0
- package/payload/server/client-pool-M6NS5G2U.js +34 -0
- package/payload/server/maxy-edge.js +2 -2
- package/payload/server/server.js +61 -19
|
@@ -0,0 +1,1460 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createNewAdminConversation,
|
|
3
|
+
ensureConversation,
|
|
4
|
+
isMessageUseful,
|
|
5
|
+
persistMessage,
|
|
6
|
+
setConversationAgentSessionId,
|
|
7
|
+
setSessionStoreRef
|
|
8
|
+
} from "./chunk-DOIAYD3J.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, devNull } 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, sessionKey) {
|
|
160
|
+
if (!sessionKey) {
|
|
161
|
+
throw new Error(`agentLogStream: sessionKey is required (name=${name}) \u2014 chat-route entry binds it for every channel`);
|
|
162
|
+
}
|
|
163
|
+
const filenameBytes = Buffer.byteLength(name) + Buffer.byteLength(sessionKey) + 5;
|
|
164
|
+
if (filenameBytes > 240) {
|
|
165
|
+
logTeeLog(
|
|
166
|
+
`[log-tee] writer-bind-rejected reason=key-too-long sessionKey-bytes=${Buffer.byteLength(sessionKey)} name=${name} filename-bytes=${filenameBytes}`
|
|
167
|
+
);
|
|
168
|
+
return openNoOpStream(sessionKey, name);
|
|
169
|
+
}
|
|
170
|
+
const logDir = resolve(accountDir, "logs");
|
|
171
|
+
mkdirSync(logDir, { recursive: true });
|
|
172
|
+
purgeOldLogs(logDir, `${name}-`);
|
|
173
|
+
const logPath = resolve(logDir, `${name}-${sessionKey}.log`);
|
|
174
|
+
const stream = createWriteStream(logPath, { flags: "a" });
|
|
175
|
+
stream.once("error", (err) => {
|
|
176
|
+
logTeeLog(
|
|
177
|
+
`[log-tee] writer-bind-failed sessionKey=${sessionKey.slice(0, 8)} name=${name} errno=${err.code ?? "unknown"} path=${JSON.stringify(logPath)}`
|
|
178
|
+
);
|
|
179
|
+
openStreamLogs.delete(stream);
|
|
180
|
+
});
|
|
181
|
+
registerStreamLog(stream, { path: logPath, sessionKey, name });
|
|
182
|
+
return stream;
|
|
183
|
+
}
|
|
184
|
+
function openNoOpStream(sessionKey, name) {
|
|
185
|
+
const stream = createWriteStream(devNull, { flags: "a" });
|
|
186
|
+
stream.once("error", (err) => {
|
|
187
|
+
logTeeLog(`[log-tee] devnull-error sessionKey=${sessionKey.slice(0, 8)} name=${name} errno=${err.code ?? "unknown"}`);
|
|
188
|
+
openStreamLogs.delete(stream);
|
|
189
|
+
});
|
|
190
|
+
registerStreamLog(stream, { path: devNull, sessionKey, name });
|
|
191
|
+
return stream;
|
|
192
|
+
}
|
|
193
|
+
var openStreamLogs = /* @__PURE__ */ new Map();
|
|
194
|
+
function registerStreamLog(stream, entry) {
|
|
195
|
+
openStreamLogs.set(stream, entry);
|
|
196
|
+
stream.once("close", () => {
|
|
197
|
+
openStreamLogs.delete(stream);
|
|
198
|
+
logTeeLog(`[log-tee] deregister sessionKey=${entry.sessionKey.slice(0, 8)} path=${JSON.stringify(entry.path)}`);
|
|
199
|
+
});
|
|
200
|
+
logTeeLog(`[log-tee] file-created sessionKey=${entry.sessionKey.slice(0, 8)} path=${JSON.stringify(entry.path)} first-token-at=${isoTs()}`);
|
|
201
|
+
}
|
|
202
|
+
var LOG_TEE_TAG_RE = /^\[[a-zA-Z][a-zA-Z0-9:_\-]*\]/;
|
|
203
|
+
var originalConsoleError = null;
|
|
204
|
+
var originalConsoleLog = null;
|
|
205
|
+
var logTeeInstalled = false;
|
|
206
|
+
var logTeeCycleTimer = null;
|
|
207
|
+
var logTeeAdherenceTimer = null;
|
|
208
|
+
var logTeeLinesEmitted = 0;
|
|
209
|
+
var logTeeLinesRouted = 0;
|
|
210
|
+
var logTeeBytesRouted = 0;
|
|
211
|
+
var logTeeFailCount = 0;
|
|
212
|
+
function logTeeLog(line) {
|
|
213
|
+
(originalConsoleError ?? console.error.bind(console))(line);
|
|
214
|
+
}
|
|
215
|
+
function appendToActiveStreams(line) {
|
|
216
|
+
if (openStreamLogs.size === 0) return;
|
|
217
|
+
const ts = isoTs();
|
|
218
|
+
const teeLine = `[${ts}] ${line.replace(/\n$/, "")}
|
|
219
|
+
`;
|
|
220
|
+
let routed = 0;
|
|
221
|
+
for (const entry of openStreamLogs.values()) {
|
|
222
|
+
try {
|
|
223
|
+
appendFileSync(entry.path, teeLine);
|
|
224
|
+
routed++;
|
|
225
|
+
logTeeBytesRouted += teeLine.length;
|
|
226
|
+
} catch (err) {
|
|
227
|
+
logTeeFailCount++;
|
|
228
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
229
|
+
logTeeLog(`[log-tee] FAIL emit reason=${JSON.stringify(msg.slice(0, 80))} path=${JSON.stringify(entry.path)}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (routed > 0) {
|
|
233
|
+
logTeeLinesRouted += routed;
|
|
234
|
+
logTeeLinesEmitted++;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function installLogTee() {
|
|
238
|
+
if (logTeeInstalled) return;
|
|
239
|
+
logTeeInstalled = true;
|
|
240
|
+
originalConsoleError = console.error.bind(console);
|
|
241
|
+
originalConsoleLog = console.log.bind(console);
|
|
242
|
+
const wrap = (orig) => {
|
|
243
|
+
return (...args) => {
|
|
244
|
+
orig(...args);
|
|
245
|
+
const rendered = args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ");
|
|
246
|
+
if (LOG_TEE_TAG_RE.test(rendered)) {
|
|
247
|
+
appendToActiveStreams(rendered);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
console.error = wrap(originalConsoleError);
|
|
252
|
+
console.log = wrap(originalConsoleLog);
|
|
253
|
+
logTeeCycleTimer = setInterval(() => {
|
|
254
|
+
const active = openStreamLogs.size;
|
|
255
|
+
logTeeLog(
|
|
256
|
+
`[log-tee] cycle activeSessions=${active} linesEmitted=${logTeeLinesEmitted} linesRouted=${logTeeLinesRouted} bytesRouted=${logTeeBytesRouted} failCount=${logTeeFailCount}`
|
|
257
|
+
);
|
|
258
|
+
}, 3e4);
|
|
259
|
+
logTeeCycleTimer.unref?.();
|
|
260
|
+
logTeeAdherenceTimer = setInterval(() => {
|
|
261
|
+
runAdherenceCheck();
|
|
262
|
+
}, 60 * 60 * 1e3);
|
|
263
|
+
logTeeAdherenceTimer.unref?.();
|
|
264
|
+
logTeeLog(`[log-tee] installed pid=${process.pid}`);
|
|
265
|
+
}
|
|
266
|
+
function safeStringify(value) {
|
|
267
|
+
try {
|
|
268
|
+
if (value instanceof Error) return value.stack ?? value.message;
|
|
269
|
+
return JSON.stringify(value);
|
|
270
|
+
} catch {
|
|
271
|
+
return String(value);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function emitMissingOnResolve(sessionKey, surface, reason) {
|
|
275
|
+
logTeeLog(`[log-tee] missing-on-resolve sessionKey=${sessionKey.slice(0, 8)} surface=${surface} reason=${JSON.stringify(reason.slice(0, 120))}`);
|
|
276
|
+
}
|
|
277
|
+
function runAdherenceCheck() {
|
|
278
|
+
const start = Date.now();
|
|
279
|
+
let sessions = 0;
|
|
280
|
+
let misses = 0;
|
|
281
|
+
try {
|
|
282
|
+
const accountsRoot = resolveAccountsRoot();
|
|
283
|
+
if (!accountsRoot) {
|
|
284
|
+
logTeeLog(`[log-tee] adherence-check window=24h sessions=0 misses=0 ts=${isoTs()} note=accounts-root-unresolved`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const sessionKeysOnDisk = collectSessionKeysFromOpenStreams();
|
|
288
|
+
sessions = sessionKeysOnDisk.size;
|
|
289
|
+
for (const key of sessionKeysOnDisk) {
|
|
290
|
+
if (!resolveStreamLogPath(accountsRoot, key)) {
|
|
291
|
+
misses++;
|
|
292
|
+
emitMissingOnResolve(key, "adherence-check", "file-not-on-disk");
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} catch (err) {
|
|
296
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
297
|
+
logTeeLog(`[log-tee] adherence-check-err reason=${JSON.stringify(msg.slice(0, 120))}`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const ms = Date.now() - start;
|
|
301
|
+
logTeeLog(`[log-tee] adherence-check window=24h sessions=${sessions} misses=${misses} ms=${ms} ts=${isoTs()}`);
|
|
302
|
+
}
|
|
303
|
+
function collectSessionKeysFromOpenStreams() {
|
|
304
|
+
const keys = /* @__PURE__ */ new Set();
|
|
305
|
+
for (const entry of openStreamLogs.values()) {
|
|
306
|
+
keys.add(entry.sessionKey);
|
|
307
|
+
}
|
|
308
|
+
return keys;
|
|
309
|
+
}
|
|
310
|
+
function resolveAccountsRoot() {
|
|
311
|
+
const envRoot = process.env.ADHERENCE_INSTALL_DIR;
|
|
312
|
+
if (envRoot) return resolve(envRoot, "data", "accounts");
|
|
313
|
+
const platformRoot2 = process.env.MAXY_PLATFORM_ROOT;
|
|
314
|
+
if (platformRoot2) return resolve(platformRoot2, "..", "data", "accounts");
|
|
315
|
+
return resolve(process.cwd(), "..", "..", "data", "accounts");
|
|
316
|
+
}
|
|
317
|
+
function resolveStreamLogPath(accountsRoot, sessionKey) {
|
|
318
|
+
let entries;
|
|
319
|
+
try {
|
|
320
|
+
entries = readdirSync(accountsRoot);
|
|
321
|
+
} catch {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
for (const acct of entries) {
|
|
325
|
+
const logsDir = resolve(accountsRoot, acct, "logs");
|
|
326
|
+
let logFiles;
|
|
327
|
+
try {
|
|
328
|
+
logFiles = readdirSync(logsDir);
|
|
329
|
+
} catch {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const wanted = `claude-agent-stream-${sessionKey}.log`;
|
|
333
|
+
if (logFiles.includes(wanted)) return resolve(logsDir, wanted);
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
if (process.env.NODE_ENV !== "test") {
|
|
338
|
+
installLogTee();
|
|
339
|
+
}
|
|
340
|
+
function sigtermFlushStreamLogs(reason, source) {
|
|
341
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
342
|
+
for (const entry of openStreamLogs.values()) {
|
|
343
|
+
const line = `[${ts}] [server-sigterm] reason=${reason} sessionKey=${entry.sessionKey} name=${entry.name} source=${source}
|
|
344
|
+
`;
|
|
345
|
+
try {
|
|
346
|
+
appendFileSync(entry.path, line);
|
|
347
|
+
} catch (err) {
|
|
348
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
349
|
+
console.error(`[server-sigterm-flush-err] path=${entry.path} reason=${msg}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function purgeOldLogs(logDir, prefix) {
|
|
354
|
+
const cutoff = Date.now() - LOG_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
|
|
355
|
+
let entries;
|
|
356
|
+
try {
|
|
357
|
+
entries = readdirSync(logDir);
|
|
358
|
+
} catch (err) {
|
|
359
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
360
|
+
console.error(`[log-purge-err] readdir dir=${logDir} prefix=${prefix} reason=${msg}`);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
for (const file of entries) {
|
|
364
|
+
if (!file.startsWith(prefix)) continue;
|
|
365
|
+
const filePath = resolve(logDir, file);
|
|
366
|
+
try {
|
|
367
|
+
if (statSync(filePath).mtimeMs < cutoff) unlinkSync(filePath);
|
|
368
|
+
} catch (err) {
|
|
369
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
370
|
+
console.error(`[log-purge-err] file=${file} reason=${msg}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// app/lib/claude-agent/session-store.ts
|
|
376
|
+
import { createHash, createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
377
|
+
import { readFileSync as readFileSync3, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
378
|
+
import { dirname } from "path";
|
|
379
|
+
|
|
380
|
+
// app/lib/paths.ts
|
|
381
|
+
import { homedir } from "os";
|
|
382
|
+
import { resolve as resolve2, join } from "path";
|
|
383
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
384
|
+
var configDirName = ".maxy";
|
|
385
|
+
var commercialMode = false;
|
|
386
|
+
var vncDisplayNum = 99;
|
|
387
|
+
var rfbPortNum = 5900;
|
|
388
|
+
var websockifyPortNum = 6080;
|
|
389
|
+
var cdpPortNum = 9222;
|
|
390
|
+
var portSource = "dev-default";
|
|
391
|
+
var platformRoot = process.env.MAXY_PLATFORM_ROOT;
|
|
392
|
+
if (platformRoot) {
|
|
393
|
+
const brandPath = join(platformRoot, "config", "brand.json");
|
|
394
|
+
if (existsSync(brandPath)) {
|
|
395
|
+
let brand;
|
|
396
|
+
try {
|
|
397
|
+
brand = JSON.parse(readFileSync2(brandPath, "utf-8"));
|
|
398
|
+
} catch (err) {
|
|
399
|
+
const detail = (err instanceof Error ? err.message : String(err)).slice(0, 160);
|
|
400
|
+
console.error(`[paths] error reason=brand-config-missing path=${brandPath} detail="parse failed: ${detail.replace(/"/g, "'")}"`);
|
|
401
|
+
throw err;
|
|
402
|
+
}
|
|
403
|
+
if (typeof brand.configDir === "string") configDirName = brand.configDir;
|
|
404
|
+
if (brand.commercialMode === true) commercialMode = true;
|
|
405
|
+
if (typeof brand.vncDisplay === "number") vncDisplayNum = brand.vncDisplay;
|
|
406
|
+
const brandLabel = configDirName.replace(/^\./, "");
|
|
407
|
+
const required = [
|
|
408
|
+
["rfbPort", brand.rfbPort],
|
|
409
|
+
["websockifyPort", brand.websockifyPort],
|
|
410
|
+
["cdpPort", brand.cdpPort]
|
|
411
|
+
];
|
|
412
|
+
for (const [field, value] of required) {
|
|
413
|
+
if (typeof value !== "number") {
|
|
414
|
+
const keys = Object.keys(brand).join(",");
|
|
415
|
+
console.error(`[paths] error reason=cdp-port-unresolved brand=${brandLabel} path=${brandPath} field=${field} json_keys=${keys}`);
|
|
416
|
+
throw new Error(`brand.json at ${brandPath} missing required field: ${field}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
rfbPortNum = brand.rfbPort;
|
|
420
|
+
websockifyPortNum = brand.websockifyPort;
|
|
421
|
+
cdpPortNum = brand.cdpPort;
|
|
422
|
+
portSource = "brand.json";
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
var MAXY_DIR = resolve2(homedir(), configDirName);
|
|
426
|
+
var BRAND_NAME = configDirName.replace(/^\./, "");
|
|
427
|
+
var COMMERCIAL_MODE = commercialMode;
|
|
428
|
+
var VNC_DISPLAY = `:${vncDisplayNum}`;
|
|
429
|
+
var RFB_PORT = rfbPortNum;
|
|
430
|
+
var WEBSOCKIFY_PORT = websockifyPortNum;
|
|
431
|
+
var CDP_PORT = cdpPortNum;
|
|
432
|
+
console.log(
|
|
433
|
+
`[paths] brand=${configDirName.replace(/^\./, "")} vncDisplay=${vncDisplayNum} rfbPort=${RFB_PORT} websockifyPort=${WEBSOCKIFY_PORT} cdpPort=${CDP_PORT} source=${portSource}`
|
|
434
|
+
);
|
|
435
|
+
var CHROMIUM_PROFILE_DIR = resolve2(homedir(), configDirName, "chromium-profile");
|
|
436
|
+
var PLATFORM_ROOT = process.env.MAXY_PLATFORM_ROOT ?? resolve2(process.cwd(), "..");
|
|
437
|
+
var USERS_FILE = resolve2(MAXY_DIR, "users.json");
|
|
438
|
+
var LOG_DIR = resolve2(MAXY_DIR, "logs");
|
|
439
|
+
var BIN_DIR = resolve2(MAXY_DIR, "bin");
|
|
440
|
+
var REMOTE_PASSWORD_FILE = resolve2(MAXY_DIR, ".remote-password");
|
|
441
|
+
var REMOTE_SESSION_SECRET_FILE = resolve2(MAXY_DIR, "credentials", "remote-session-secret");
|
|
442
|
+
var ADMIN_SESSION_SECRET_FILE = resolve2(MAXY_DIR, "credentials", "admin-session-secret");
|
|
443
|
+
var TELEGRAM_WEBHOOK_SECRET_FILE = resolve2(MAXY_DIR, ".telegram-webhook-secret");
|
|
444
|
+
var TELEGRAM_ADMIN_WEBHOOK_SECRET_FILE = resolve2(MAXY_DIR, ".telegram-admin-webhook-secret");
|
|
445
|
+
var CLAUDE_CREDENTIALS_FILE = resolve2(MAXY_DIR, ".claude", ".credentials.json");
|
|
446
|
+
|
|
447
|
+
// app/lib/claude-agent/session-store.ts
|
|
448
|
+
function findFirstSubstantiveUserMessage(turns) {
|
|
449
|
+
for (const t of turns) {
|
|
450
|
+
if (t.role !== "user") continue;
|
|
451
|
+
if (isMessageUseful(t.content)) return t.content;
|
|
452
|
+
}
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
var sessionStore = /* @__PURE__ */ new Map();
|
|
456
|
+
function getSession(cacheKey) {
|
|
457
|
+
return sessionStore.get(cacheKey);
|
|
458
|
+
}
|
|
459
|
+
setSessionStoreRef(sessionStore);
|
|
460
|
+
var TOKEN_PREFIX = "v1.";
|
|
461
|
+
var TOKEN_VERSION = "adm";
|
|
462
|
+
var TOKEN_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
463
|
+
var TOKEN_SECRET_BYTES = 32;
|
|
464
|
+
var TOKEN_NONCE_BYTES = 16;
|
|
465
|
+
var cachedAdminSecret = null;
|
|
466
|
+
function getAdminSessionSecret() {
|
|
467
|
+
if (cachedAdminSecret) return cachedAdminSecret;
|
|
468
|
+
try {
|
|
469
|
+
const hex2 = readFileSync3(ADMIN_SESSION_SECRET_FILE, "utf-8").trim();
|
|
470
|
+
if (hex2.length === TOKEN_SECRET_BYTES * 2) {
|
|
471
|
+
cachedAdminSecret = Buffer.from(hex2, "hex");
|
|
472
|
+
return cachedAdminSecret;
|
|
473
|
+
}
|
|
474
|
+
} catch {
|
|
475
|
+
}
|
|
476
|
+
const fresh = randomBytes(TOKEN_SECRET_BYTES).toString("hex");
|
|
477
|
+
try {
|
|
478
|
+
mkdirSync2(dirname(ADMIN_SESSION_SECRET_FILE), { recursive: true, mode: 448 });
|
|
479
|
+
writeFileSync(ADMIN_SESSION_SECRET_FILE, fresh, { mode: 384, flag: "wx" });
|
|
480
|
+
} catch {
|
|
481
|
+
}
|
|
482
|
+
let hex;
|
|
483
|
+
try {
|
|
484
|
+
hex = readFileSync3(ADMIN_SESSION_SECRET_FILE, "utf-8").trim();
|
|
485
|
+
} catch (err) {
|
|
486
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
487
|
+
throw new Error(`admin-session-secret unreadable at ${ADMIN_SESSION_SECRET_FILE}: ${msg}`);
|
|
488
|
+
}
|
|
489
|
+
cachedAdminSecret = Buffer.from(hex, "hex");
|
|
490
|
+
return cachedAdminSecret;
|
|
491
|
+
}
|
|
492
|
+
function base64urlEncode(buf) {
|
|
493
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
494
|
+
}
|
|
495
|
+
function base64urlDecode(s) {
|
|
496
|
+
const padded = s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - s.length % 4) % 4);
|
|
497
|
+
return Buffer.from(padded, "base64");
|
|
498
|
+
}
|
|
499
|
+
function signDigest(payloadJson, secret) {
|
|
500
|
+
return createHmac("sha256", secret).update(payloadJson).digest();
|
|
501
|
+
}
|
|
502
|
+
function fingerprintSessionKey(raw) {
|
|
503
|
+
if (typeof raw !== "string" || !raw) return raw;
|
|
504
|
+
if (/^sk_[a-f0-9]{16}$/.test(raw)) return raw;
|
|
505
|
+
if (!raw.startsWith("v1.")) return raw;
|
|
506
|
+
return `sk_${createHash("sha256").update(raw).digest("hex").slice(0, 16)}`;
|
|
507
|
+
}
|
|
508
|
+
function mintAdminSessionToken(identity) {
|
|
509
|
+
const payload = {
|
|
510
|
+
v: TOKEN_VERSION,
|
|
511
|
+
a: identity.accountId,
|
|
512
|
+
u: identity.userId,
|
|
513
|
+
c: Date.now(),
|
|
514
|
+
n: randomBytes(TOKEN_NONCE_BYTES).toString("hex")
|
|
515
|
+
};
|
|
516
|
+
const payloadJson = JSON.stringify(payload);
|
|
517
|
+
const payloadB64 = base64urlEncode(Buffer.from(payloadJson, "utf-8"));
|
|
518
|
+
const sigB64 = base64urlEncode(signDigest(payloadJson, getAdminSessionSecret()));
|
|
519
|
+
return `${TOKEN_PREFIX}${payloadB64}.${sigB64}`;
|
|
520
|
+
}
|
|
521
|
+
function parseAdminSessionToken(token) {
|
|
522
|
+
if (!token.startsWith(TOKEN_PREFIX)) return null;
|
|
523
|
+
const rest = token.slice(TOKEN_PREFIX.length);
|
|
524
|
+
const dot = rest.indexOf(".");
|
|
525
|
+
if (dot === -1) return null;
|
|
526
|
+
const payloadB64 = rest.slice(0, dot);
|
|
527
|
+
const sigB64 = rest.slice(dot + 1);
|
|
528
|
+
if (!payloadB64 || !sigB64) return null;
|
|
529
|
+
let payloadJson;
|
|
530
|
+
let providedDigest;
|
|
531
|
+
try {
|
|
532
|
+
payloadJson = base64urlDecode(payloadB64).toString("utf-8");
|
|
533
|
+
providedDigest = base64urlDecode(sigB64);
|
|
534
|
+
} catch {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
const expectedDigest = signDigest(payloadJson, getAdminSessionSecret());
|
|
538
|
+
if (providedDigest.length !== expectedDigest.length || !timingSafeEqual(providedDigest, expectedDigest)) {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
let parsed;
|
|
542
|
+
try {
|
|
543
|
+
parsed = JSON.parse(payloadJson);
|
|
544
|
+
} catch {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
548
|
+
const p = parsed;
|
|
549
|
+
if (p.v !== TOKEN_VERSION) return null;
|
|
550
|
+
if (typeof p.a !== "string" || !p.a) return null;
|
|
551
|
+
if (typeof p.u !== "string" || !p.u) return null;
|
|
552
|
+
if (typeof p.c !== "number" || !Number.isFinite(p.c)) return null;
|
|
553
|
+
if (typeof p.n !== "string" || !p.n) return null;
|
|
554
|
+
return p;
|
|
555
|
+
}
|
|
556
|
+
function tryRehydrateAdminSession(cacheKey, signedToken) {
|
|
557
|
+
const payload = parseAdminSessionToken(signedToken);
|
|
558
|
+
if (!payload) return { kind: "invalid-token" };
|
|
559
|
+
const ageMs = Date.now() - payload.c;
|
|
560
|
+
if (ageMs > TOKEN_TTL_MS) return { kind: "expired", ageMs };
|
|
561
|
+
sessionStore.set(cacheKey, {
|
|
562
|
+
createdAt: Date.now(),
|
|
563
|
+
agentType: "admin",
|
|
564
|
+
accountId: payload.a,
|
|
565
|
+
userId: payload.u,
|
|
566
|
+
wantsPriorConversation: true
|
|
567
|
+
});
|
|
568
|
+
console.log(`[session-rehydrate-from-token] cacheKey=${cacheKey.slice(0, 12)}\u2026 accountId=${payload.a.slice(0, 8)} userId=${payload.u.slice(0, 8)} ageMs=${ageMs}`);
|
|
569
|
+
return { kind: "ok", payload, ageMs };
|
|
570
|
+
}
|
|
571
|
+
function setWantsPriorConversation(cacheKey) {
|
|
572
|
+
const session = sessionStore.get(cacheKey);
|
|
573
|
+
if (session) session.wantsPriorConversation = true;
|
|
574
|
+
}
|
|
575
|
+
function consumeWantsPriorConversation(cacheKey) {
|
|
576
|
+
const session = sessionStore.get(cacheKey);
|
|
577
|
+
if (!session || !session.wantsPriorConversation) return false;
|
|
578
|
+
delete session.wantsPriorConversation;
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
function registerSession(cacheKey, agentType, accountId, agentName, userId, userName, role) {
|
|
582
|
+
const existing = sessionStore.get(cacheKey);
|
|
583
|
+
if (existing) {
|
|
584
|
+
existing.agentType = agentType;
|
|
585
|
+
existing.accountId = accountId;
|
|
586
|
+
existing.agentName = agentName ?? existing.agentName;
|
|
587
|
+
existing.userId = userId ?? existing.userId;
|
|
588
|
+
existing.userName = userName ?? existing.userName;
|
|
589
|
+
existing.role = role ?? existing.role;
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
sessionStore.set(cacheKey, { createdAt: Date.now(), agentType, accountId, agentName, userId, userName, role });
|
|
593
|
+
}
|
|
594
|
+
function registerResumedSession(cacheKey, accountId, agentName, conversationId, messages) {
|
|
595
|
+
const messageHistory = messages.map((m) => ({
|
|
596
|
+
role: m.role,
|
|
597
|
+
content: m.content,
|
|
598
|
+
timestamp: m.timestamp ?? Date.now()
|
|
599
|
+
}));
|
|
600
|
+
sessionStore.set(cacheKey, {
|
|
601
|
+
createdAt: Date.now(),
|
|
602
|
+
agentType: "public",
|
|
603
|
+
accountId,
|
|
604
|
+
agentName,
|
|
605
|
+
conversationId,
|
|
606
|
+
messageHistory
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
function getSessionMessages(cacheKey) {
|
|
610
|
+
return sessionStore.get(cacheKey)?.messageHistory;
|
|
611
|
+
}
|
|
612
|
+
function clearSessionHistory(cacheKey) {
|
|
613
|
+
const session = sessionStore.get(cacheKey);
|
|
614
|
+
if (!session) return void 0;
|
|
615
|
+
const previousConversationId = session.conversationId;
|
|
616
|
+
session.agentSessionId = void 0;
|
|
617
|
+
session.pendingCompactionSummary = void 0;
|
|
618
|
+
session.stalledSubagents = void 0;
|
|
619
|
+
session.pendingTrimmedMessages = void 0;
|
|
620
|
+
session.pendingCommitmentOffers = void 0;
|
|
621
|
+
session.pendingTurns = void 0;
|
|
622
|
+
session.stallResume = void 0;
|
|
623
|
+
session.assistantTurnCount = void 0;
|
|
624
|
+
return previousConversationId;
|
|
625
|
+
}
|
|
626
|
+
function bufferPendingTurn(cacheKey, turn) {
|
|
627
|
+
const session = sessionStore.get(cacheKey);
|
|
628
|
+
if (!session) {
|
|
629
|
+
console.error(`[conversation-gate] bufferPendingTurn: session not found cacheKey=${cacheKey.slice(0, 8)}\u2026`);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (!session.pendingTurns) session.pendingTurns = [];
|
|
633
|
+
session.pendingTurns.push(turn);
|
|
634
|
+
console.log(`[conversation-gate] ${(/* @__PURE__ */ new Date()).toISOString()} buffered cacheKey=${cacheKey.slice(0, 8)} role=${turn.role} turnCount=${session.pendingTurns.filter((t) => t.role === "user").length}`);
|
|
635
|
+
}
|
|
636
|
+
function getPendingTurnCount(cacheKey) {
|
|
637
|
+
const buf = sessionStore.get(cacheKey)?.pendingTurns;
|
|
638
|
+
if (!buf) return 0;
|
|
639
|
+
let n = 0;
|
|
640
|
+
for (const t of buf) if (t.role === "user") n++;
|
|
641
|
+
return n;
|
|
642
|
+
}
|
|
643
|
+
function drainPendingTurns(cacheKey) {
|
|
644
|
+
const session = sessionStore.get(cacheKey);
|
|
645
|
+
if (!session?.pendingTurns || session.pendingTurns.length === 0) return void 0;
|
|
646
|
+
const drained = session.pendingTurns;
|
|
647
|
+
session.pendingTurns = void 0;
|
|
648
|
+
return drained;
|
|
649
|
+
}
|
|
650
|
+
function isFlushError(o) {
|
|
651
|
+
return o !== null && "error" in o;
|
|
652
|
+
}
|
|
653
|
+
async function maybeFlushConversationBuffer(cacheKey, agentType, accountId) {
|
|
654
|
+
const sk8 = cacheKey.slice(0, 8);
|
|
655
|
+
const session = sessionStore.get(cacheKey);
|
|
656
|
+
if (!session) {
|
|
657
|
+
console.log(`[admin/conversation-flush] cacheKey=${sk8} agentType=${agentType} result=missing-session`);
|
|
658
|
+
return { error: "missing-session" };
|
|
659
|
+
}
|
|
660
|
+
if (session.conversationId) {
|
|
661
|
+
console.log(`[admin/conversation-flush] cacheKey=${sk8} agentType=${agentType} result=already-flushed conversationId=${session.conversationId.slice(0, 8)}`);
|
|
662
|
+
return { conversationId: session.conversationId, buffered: [] };
|
|
663
|
+
}
|
|
664
|
+
const bufferedCount = session.pendingTurns?.length ?? 0;
|
|
665
|
+
if (bufferedCount === 0) {
|
|
666
|
+
console.log(`[admin/conversation-flush] cacheKey=${sk8} agentType=${agentType} result=empty-buffer`);
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
if (session.flushInFlight) return session.flushInFlight;
|
|
670
|
+
const attempt = (async () => {
|
|
671
|
+
let conversationId = null;
|
|
672
|
+
if (agentType === "admin") {
|
|
673
|
+
const userId = session.userId;
|
|
674
|
+
if (!userId) {
|
|
675
|
+
console.error(`[admin/conversation-flush] cacheKey=${sk8} agentType=admin result=missing-userId bufferedCount=${bufferedCount}`);
|
|
676
|
+
return { error: "missing-userId" };
|
|
677
|
+
}
|
|
678
|
+
conversationId = await createNewAdminConversation(userId, accountId, cacheKey, session.userName);
|
|
679
|
+
} else {
|
|
680
|
+
conversationId = (await ensureConversation(accountId, "public", cacheKey, void 0, session.agentName, void 0)).conversationId;
|
|
681
|
+
}
|
|
682
|
+
if (!conversationId) {
|
|
683
|
+
console.error(`[admin/conversation-flush] cacheKey=${sk8} agentType=${agentType} result=writer-failed bufferedCount=${bufferedCount}`);
|
|
684
|
+
return { error: "writer-failed" };
|
|
685
|
+
}
|
|
686
|
+
session.conversationId = conversationId;
|
|
687
|
+
if (agentType === "admin" && session.agentSessionId) {
|
|
688
|
+
setConversationAgentSessionId(conversationId, session.agentSessionId).catch(() => {
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
const buffered = drainPendingTurns(cacheKey) ?? [];
|
|
692
|
+
for (const turn of buffered) {
|
|
693
|
+
persistMessage(conversationId, turn.role, turn.content, accountId, turn.tokens, turn.timestamp, turn.sender, turn.components, turn.attachments).catch((err) => {
|
|
694
|
+
console.error(`[admin/conversation-flush] replay persistMessage failed role=${turn.role}: ${err instanceof Error ? err.message : String(err)}`);
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
console.log(`[admin/conversation-flush] cacheKey=${sk8} agentType=${agentType} result=ok conversationId=${conversationId.slice(0, 8)} bufferedMessages=${buffered.length}`);
|
|
698
|
+
return { conversationId, buffered };
|
|
699
|
+
})();
|
|
700
|
+
session.flushInFlight = attempt;
|
|
701
|
+
try {
|
|
702
|
+
return await attempt;
|
|
703
|
+
} finally {
|
|
704
|
+
if (session.flushInFlight === attempt) session.flushInFlight = void 0;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function isDmChannelCacheKey(cacheKey) {
|
|
708
|
+
return cacheKey.startsWith("whatsapp:") || cacheKey.startsWith("telegram:");
|
|
709
|
+
}
|
|
710
|
+
function getAgentNameForSession(cacheKey) {
|
|
711
|
+
return sessionStore.get(cacheKey)?.agentName;
|
|
712
|
+
}
|
|
713
|
+
function listAdminSessionsInProgress(accountId, userId) {
|
|
714
|
+
const rows = [];
|
|
715
|
+
for (const [cacheKey, session] of Array.from(sessionStore.entries())) {
|
|
716
|
+
if (session.agentType !== "admin") continue;
|
|
717
|
+
if (session.accountId !== accountId) continue;
|
|
718
|
+
if (session.userId !== userId) continue;
|
|
719
|
+
if (session.conversationId) continue;
|
|
720
|
+
rows.push({ cacheKey, createdAt: session.createdAt });
|
|
721
|
+
}
|
|
722
|
+
rows.sort((a, b) => b.createdAt - a.createdAt);
|
|
723
|
+
return rows;
|
|
724
|
+
}
|
|
725
|
+
function validateSession(cacheKey, agentType, signedSessionToken) {
|
|
726
|
+
const session = sessionStore.get(cacheKey);
|
|
727
|
+
if (!session) {
|
|
728
|
+
if (agentType === "admin" && signedSessionToken) {
|
|
729
|
+
const outcome = tryRehydrateAdminSession(cacheKey, signedSessionToken);
|
|
730
|
+
if (outcome.kind === "ok") return { ok: true };
|
|
731
|
+
if (outcome.kind === "expired") return { ok: false, reason: "session-expired-age" };
|
|
732
|
+
}
|
|
733
|
+
return { ok: false, reason: "session-not-registered" };
|
|
734
|
+
}
|
|
735
|
+
if (session.agentType !== agentType) return { ok: false, reason: "agent-type-mismatch" };
|
|
736
|
+
if (Date.now() - session.createdAt > 24 * 60 * 60 * 1e3) {
|
|
737
|
+
sessionStore.delete(cacheKey);
|
|
738
|
+
return { ok: false, reason: "session-expired-age" };
|
|
739
|
+
}
|
|
740
|
+
if (session.grantExpiresAt && Date.now() > session.grantExpiresAt) {
|
|
741
|
+
sessionStore.delete(cacheKey);
|
|
742
|
+
return { ok: false, reason: "grant-expired" };
|
|
743
|
+
}
|
|
744
|
+
return { ok: true };
|
|
745
|
+
}
|
|
746
|
+
function storeAgentSessionId(cacheKey, agentSessionId) {
|
|
747
|
+
const session = sessionStore.get(cacheKey);
|
|
748
|
+
if (session) {
|
|
749
|
+
session.agentSessionId = agentSessionId;
|
|
750
|
+
console.error(`[session-store] storeAgentSessionId cacheKey=${cacheKey.slice(0, 12)}\u2026 sessionId=${agentSessionId.slice(0, 8)}\u2026`);
|
|
751
|
+
if (session.agentType === "admin" && session.conversationId) {
|
|
752
|
+
setConversationAgentSessionId(session.conversationId, agentSessionId).catch(() => {
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
} else {
|
|
756
|
+
console.error(`[session-store] storeAgentSessionId SKIPPED \u2014 no session entry cacheKey=${cacheKey.slice(0, 12)}\u2026 sessionId=${agentSessionId.slice(0, 8)}\u2026`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
function getAgentSessionId(cacheKey) {
|
|
760
|
+
return sessionStore.get(cacheKey)?.agentSessionId;
|
|
761
|
+
}
|
|
762
|
+
function setAgentSessionId(cacheKey, agentSessionId) {
|
|
763
|
+
const session = sessionStore.get(cacheKey);
|
|
764
|
+
if (!session) return false;
|
|
765
|
+
session.agentSessionId = agentSessionId;
|
|
766
|
+
return true;
|
|
767
|
+
}
|
|
768
|
+
function storePendingCompactionSummary(cacheKey, summary) {
|
|
769
|
+
const session = sessionStore.get(cacheKey);
|
|
770
|
+
if (session) session.pendingCompactionSummary = summary;
|
|
771
|
+
}
|
|
772
|
+
function consumePendingCompactionSummary(cacheKey) {
|
|
773
|
+
const session = sessionStore.get(cacheKey);
|
|
774
|
+
if (!session) return void 0;
|
|
775
|
+
const summary = session.pendingCompactionSummary;
|
|
776
|
+
delete session.pendingCompactionSummary;
|
|
777
|
+
return summary;
|
|
778
|
+
}
|
|
779
|
+
function getAccountIdForSession(cacheKey) {
|
|
780
|
+
return sessionStore.get(cacheKey)?.accountId;
|
|
781
|
+
}
|
|
782
|
+
function getUserIdForSession(cacheKey) {
|
|
783
|
+
return sessionStore.get(cacheKey)?.userId;
|
|
784
|
+
}
|
|
785
|
+
function getUserNameForSession(cacheKey) {
|
|
786
|
+
return sessionStore.get(cacheKey)?.userName;
|
|
787
|
+
}
|
|
788
|
+
function getRoleForSession(cacheKey) {
|
|
789
|
+
return sessionStore.get(cacheKey)?.role;
|
|
790
|
+
}
|
|
791
|
+
function getConversationIdForSession(cacheKey) {
|
|
792
|
+
return sessionStore.get(cacheKey)?.conversationId;
|
|
793
|
+
}
|
|
794
|
+
function getSessionKeyByConversationId(conversationId) {
|
|
795
|
+
for (const [cacheKey, session] of sessionStore.entries()) {
|
|
796
|
+
if (session.conversationId === conversationId) return cacheKey;
|
|
797
|
+
}
|
|
798
|
+
return void 0;
|
|
799
|
+
}
|
|
800
|
+
function setConversationIdForSession(cacheKey, conversationId) {
|
|
801
|
+
const session = sessionStore.get(cacheKey);
|
|
802
|
+
if (!session) return false;
|
|
803
|
+
session.conversationId = conversationId;
|
|
804
|
+
return true;
|
|
805
|
+
}
|
|
806
|
+
function unregisterSession(cacheKey) {
|
|
807
|
+
return sessionStore.delete(cacheKey);
|
|
808
|
+
}
|
|
809
|
+
function getGroupSlugForSession(cacheKey) {
|
|
810
|
+
return sessionStore.get(cacheKey)?.groupSlug;
|
|
811
|
+
}
|
|
812
|
+
function getVisitorIdForSession(cacheKey) {
|
|
813
|
+
return sessionStore.get(cacheKey)?.visitorId;
|
|
814
|
+
}
|
|
815
|
+
function setGroupContextForSession(cacheKey, context) {
|
|
816
|
+
const session = sessionStore.get(cacheKey);
|
|
817
|
+
if (!session) return false;
|
|
818
|
+
session.groupSlug = context.groupSlug;
|
|
819
|
+
session.groupName = context.groupName;
|
|
820
|
+
session.conversationId = context.conversationId;
|
|
821
|
+
session.visitorId = context.visitorId;
|
|
822
|
+
session.senderDisplayName = context.senderDisplayName;
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
function registerGrantSession(cacheKey, accountId, agentName, opts) {
|
|
826
|
+
sessionStore.set(cacheKey, {
|
|
827
|
+
createdAt: Date.now(),
|
|
828
|
+
agentType: "public",
|
|
829
|
+
accountId,
|
|
830
|
+
agentName,
|
|
831
|
+
grantId: opts.grantId,
|
|
832
|
+
grantExpiresAt: opts.grantExpiresAt,
|
|
833
|
+
grantStatus: opts.grantStatus,
|
|
834
|
+
grantDisplayName: opts.grantDisplayName,
|
|
835
|
+
grantContactValue: opts.grantContactValue,
|
|
836
|
+
setupRequired: opts.setupRequired
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
function getGrantForSession(cacheKey) {
|
|
840
|
+
const session = sessionStore.get(cacheKey);
|
|
841
|
+
if (!session?.grantId) return void 0;
|
|
842
|
+
return {
|
|
843
|
+
grantId: session.grantId,
|
|
844
|
+
grantExpiresAt: session.grantExpiresAt,
|
|
845
|
+
grantStatus: session.grantStatus,
|
|
846
|
+
grantDisplayName: session.grantDisplayName,
|
|
847
|
+
grantContactValue: session.grantContactValue,
|
|
848
|
+
setupRequired: session.setupRequired
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
function completeGrantSetup(cacheKey) {
|
|
852
|
+
const session = sessionStore.get(cacheKey);
|
|
853
|
+
if (session) {
|
|
854
|
+
session.setupRequired = false;
|
|
855
|
+
session.grantStatus = "active";
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
function getMessageHistory(cacheKey) {
|
|
859
|
+
const session = sessionStore.get(cacheKey);
|
|
860
|
+
if (!session) return [];
|
|
861
|
+
if (!session.messageHistory) session.messageHistory = [];
|
|
862
|
+
return session.messageHistory;
|
|
863
|
+
}
|
|
864
|
+
function appendMessage(cacheKey, role, content, timestamp) {
|
|
865
|
+
if (!content) return;
|
|
866
|
+
const session = sessionStore.get(cacheKey);
|
|
867
|
+
if (!session) {
|
|
868
|
+
console.error(`[managed] appendMessage: session not found for key ${cacheKey.slice(0, 8)}\u2026 (store size: ${sessionStore.size})`);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (!session.messageHistory) session.messageHistory = [];
|
|
872
|
+
session.messageHistory.push({ role, content, timestamp: timestamp ?? Date.now() });
|
|
873
|
+
}
|
|
874
|
+
function storePendingTrimmedMessages(cacheKey, messages) {
|
|
875
|
+
const session = sessionStore.get(cacheKey);
|
|
876
|
+
if (session) session.pendingTrimmedMessages = messages;
|
|
877
|
+
}
|
|
878
|
+
function consumePendingTrimmedMessages(cacheKey) {
|
|
879
|
+
const session = sessionStore.get(cacheKey);
|
|
880
|
+
if (!session) return void 0;
|
|
881
|
+
const messages = session.pendingTrimmedMessages;
|
|
882
|
+
delete session.pendingTrimmedMessages;
|
|
883
|
+
return messages;
|
|
884
|
+
}
|
|
885
|
+
function storeStalledSubagent(cacheKey, info) {
|
|
886
|
+
const session = sessionStore.get(cacheKey);
|
|
887
|
+
if (!session) {
|
|
888
|
+
console.error(`[stall-recovery] storeStalledSubagent: session not found for key ${cacheKey.slice(0, 8)}\u2026 \u2014 stall context lost`);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
if (!session.stalledSubagents) session.stalledSubagents = [];
|
|
892
|
+
session.stalledSubagents.push(info);
|
|
893
|
+
}
|
|
894
|
+
function consumeStalledSubagents(cacheKey) {
|
|
895
|
+
const session = sessionStore.get(cacheKey);
|
|
896
|
+
if (!session) return void 0;
|
|
897
|
+
const stalls = session.stalledSubagents;
|
|
898
|
+
delete session.stalledSubagents;
|
|
899
|
+
return stalls && stalls.length > 0 ? stalls : void 0;
|
|
900
|
+
}
|
|
901
|
+
function setPendingCommitmentOffers(cacheKey, offers) {
|
|
902
|
+
const session = sessionStore.get(cacheKey);
|
|
903
|
+
if (session) session.pendingCommitmentOffers = offers;
|
|
904
|
+
}
|
|
905
|
+
function getAgentTypeForSession(cacheKey) {
|
|
906
|
+
return sessionStore.get(cacheKey)?.agentType;
|
|
907
|
+
}
|
|
908
|
+
function clearMessageHistory(cacheKey) {
|
|
909
|
+
const session = sessionStore.get(cacheKey);
|
|
910
|
+
if (session) session.messageHistory = [];
|
|
911
|
+
}
|
|
912
|
+
var clearAgentSessionIdHandlers = [];
|
|
913
|
+
function onClearAgentSessionId(handler) {
|
|
914
|
+
clearAgentSessionIdHandlers.push(handler);
|
|
915
|
+
}
|
|
916
|
+
var evictPoolHandlers = [];
|
|
917
|
+
function onEvictPool(handler) {
|
|
918
|
+
evictPoolHandlers.push(handler);
|
|
919
|
+
}
|
|
920
|
+
function clearAgentSessionId(cacheKey, reason) {
|
|
921
|
+
const session = sessionStore.get(cacheKey);
|
|
922
|
+
if (session) {
|
|
923
|
+
session.agentSessionId = void 0;
|
|
924
|
+
session.lastClearReason = reason;
|
|
925
|
+
console.error(`[session-store] clearAgentSessionId cacheKey=${cacheKey.slice(0, 12)}\u2026 reason=${reason}`);
|
|
926
|
+
} else {
|
|
927
|
+
console.error(`[session-store] clearAgentSessionId SKIPPED \u2014 no session entry cacheKey=${cacheKey.slice(0, 12)}\u2026 reason=${reason}`);
|
|
928
|
+
}
|
|
929
|
+
for (const handler of clearAgentSessionIdHandlers) {
|
|
930
|
+
try {
|
|
931
|
+
handler(cacheKey, reason);
|
|
932
|
+
} catch (err) {
|
|
933
|
+
console.error(`[session-store] clearAgentSessionId handler threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
function evictPool(cacheKey, reason) {
|
|
938
|
+
for (const handler of evictPoolHandlers) {
|
|
939
|
+
try {
|
|
940
|
+
handler(cacheKey, reason);
|
|
941
|
+
} catch (err) {
|
|
942
|
+
console.error(`[session-store] evictPool handler threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
function storeRecoveryHandoff(cacheKey, info) {
|
|
947
|
+
const session = sessionStore.get(cacheKey);
|
|
948
|
+
if (!session) {
|
|
949
|
+
console.error(`[recovery-handoff] storeRecoveryHandoff: session not found for key ${cacheKey.slice(0, 8)}\u2026 \u2014 handoff context lost reason=${info.reason}`);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
session.recoveryHandoff = { reason: info.reason, summary: info.summary, createdAt: Date.now() };
|
|
953
|
+
}
|
|
954
|
+
function consumeRecoveryHandoff(cacheKey) {
|
|
955
|
+
const session = sessionStore.get(cacheKey);
|
|
956
|
+
if (!session?.recoveryHandoff) return void 0;
|
|
957
|
+
const handoff = session.recoveryHandoff;
|
|
958
|
+
delete session.recoveryHandoff;
|
|
959
|
+
delete session.lastClearReason;
|
|
960
|
+
return handoff;
|
|
961
|
+
}
|
|
962
|
+
function getLastClearReason(cacheKey) {
|
|
963
|
+
return sessionStore.get(cacheKey)?.lastClearReason;
|
|
964
|
+
}
|
|
965
|
+
function clearLastClearReason(cacheKey) {
|
|
966
|
+
const session = sessionStore.get(cacheKey);
|
|
967
|
+
if (session) delete session.lastClearReason;
|
|
968
|
+
}
|
|
969
|
+
function storeStallResume(cacheKey, info) {
|
|
970
|
+
const session = sessionStore.get(cacheKey);
|
|
971
|
+
if (!session) {
|
|
972
|
+
console.error(`[stall-resume] storeStallResume: session not found for key ${cacheKey.slice(0, 8)}\u2026 \u2014 resume context lost kind=${info.kind}`);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
session.stallResume = { ...info, createdAt: Date.now() };
|
|
976
|
+
}
|
|
977
|
+
function consumeStallResume(cacheKey) {
|
|
978
|
+
const session = sessionStore.get(cacheKey);
|
|
979
|
+
if (!session?.stallResume) return void 0;
|
|
980
|
+
const payload = session.stallResume;
|
|
981
|
+
delete session.stallResume;
|
|
982
|
+
return payload;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// app/lib/claude-agent/client-pool.ts
|
|
986
|
+
var AsyncQueue = class {
|
|
987
|
+
buffer = [];
|
|
988
|
+
resolvers = [];
|
|
989
|
+
closed = false;
|
|
990
|
+
push(item) {
|
|
991
|
+
if (this.closed) return;
|
|
992
|
+
const resolver = this.resolvers.shift();
|
|
993
|
+
if (resolver) resolver({ value: item, done: false });
|
|
994
|
+
else this.buffer.push(item);
|
|
995
|
+
}
|
|
996
|
+
close() {
|
|
997
|
+
if (this.closed) return;
|
|
998
|
+
this.closed = true;
|
|
999
|
+
while (this.resolvers.length > 0) {
|
|
1000
|
+
const r = this.resolvers.shift();
|
|
1001
|
+
r({ value: void 0, done: true });
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
[Symbol.asyncIterator]() {
|
|
1005
|
+
return {
|
|
1006
|
+
next: () => new Promise((resolve3) => {
|
|
1007
|
+
if (this.buffer.length > 0) {
|
|
1008
|
+
resolve3({ value: this.buffer.shift(), done: false });
|
|
1009
|
+
} else if (this.closed) {
|
|
1010
|
+
resolve3({ value: void 0, done: true });
|
|
1011
|
+
} else {
|
|
1012
|
+
this.resolvers.push(resolve3);
|
|
1013
|
+
}
|
|
1014
|
+
}),
|
|
1015
|
+
return: async () => {
|
|
1016
|
+
this.close();
|
|
1017
|
+
return { value: void 0, done: true };
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
var clientPool = /* @__PURE__ */ new Map();
|
|
1023
|
+
var DEFAULT_IDLE_MS = 30 * 60 * 1e3;
|
|
1024
|
+
var idleEvictMs = (() => {
|
|
1025
|
+
const v = Number(process.env.CLAUDE_CLIENT_IDLE_MS);
|
|
1026
|
+
return Number.isFinite(v) && v > 0 ? v : DEFAULT_IDLE_MS;
|
|
1027
|
+
})();
|
|
1028
|
+
var ABORT_SDK_TIMEOUT_MS = 2e3;
|
|
1029
|
+
function acquireClient(cacheKey, opts, streamLog) {
|
|
1030
|
+
const existing = clientPool.get(cacheKey);
|
|
1031
|
+
if (existing) {
|
|
1032
|
+
existing.lastUsedAt = Date.now();
|
|
1033
|
+
existing.turnsServed += 1;
|
|
1034
|
+
safeWrite(
|
|
1035
|
+
streamLog,
|
|
1036
|
+
`[${isoTs()}] [client-warm-reuse] cacheKey=${sk(cacheKey)} ageMs=${Date.now() - existing.createdAt} turnsServed=${existing.turnsServed} cachedTokens=${existing.cachedTokensLastTurn}
|
|
1037
|
+
`
|
|
1038
|
+
);
|
|
1039
|
+
return { entry: existing, isCold: false };
|
|
1040
|
+
}
|
|
1041
|
+
const userQueue = new AsyncQueue();
|
|
1042
|
+
const abortController = new AbortController();
|
|
1043
|
+
let q;
|
|
1044
|
+
try {
|
|
1045
|
+
const sdkOptions = opts.buildSdkOptions();
|
|
1046
|
+
q = query({ prompt: userQueue, options: { ...sdkOptions, abortController } });
|
|
1047
|
+
} catch (err) {
|
|
1048
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1049
|
+
safeWrite(
|
|
1050
|
+
streamLog,
|
|
1051
|
+
`[${isoTs()}] [client-spawn-error] cacheKey=${sk(cacheKey)} reason=${JSON.stringify(reason.slice(0, 200))}
|
|
1052
|
+
`
|
|
1053
|
+
);
|
|
1054
|
+
throw err;
|
|
1055
|
+
}
|
|
1056
|
+
const entry = {
|
|
1057
|
+
query: q,
|
|
1058
|
+
userQueue,
|
|
1059
|
+
abortController,
|
|
1060
|
+
inflight: null,
|
|
1061
|
+
createdAt: Date.now(),
|
|
1062
|
+
lastUsedAt: Date.now(),
|
|
1063
|
+
turnsServed: 1,
|
|
1064
|
+
resumedFromSessionId: opts.resumedFromSessionId,
|
|
1065
|
+
cachedTokensLastTurn: -1,
|
|
1066
|
+
accountId: opts.accountId,
|
|
1067
|
+
accountDir: opts.accountDir,
|
|
1068
|
+
logKey: opts.logKey
|
|
1069
|
+
};
|
|
1070
|
+
clientPool.set(cacheKey, entry);
|
|
1071
|
+
safeWrite(
|
|
1072
|
+
streamLog,
|
|
1073
|
+
`[${isoTs()}] [client-cold-create] cacheKey=${sk(cacheKey)} resumedFrom=${opts.resumedFromSessionId ?? "none"} createdAtMs=${entry.createdAt}
|
|
1074
|
+
`
|
|
1075
|
+
);
|
|
1076
|
+
safeWrite(
|
|
1077
|
+
streamLog,
|
|
1078
|
+
`[${isoTs()}] [client-sdk-argv] cacheKey=${sk(cacheKey)} cwd=${opts.accountDir} resume=${opts.resumedFromSessionId ?? "none"} configDir=${process.env.CLAUDE_CONFIG_DIR ?? "<unset>"}
|
|
1079
|
+
`
|
|
1080
|
+
);
|
|
1081
|
+
return { entry, isCold: true };
|
|
1082
|
+
}
|
|
1083
|
+
function pushUserMessage(entry, content) {
|
|
1084
|
+
const msg = {
|
|
1085
|
+
type: "user",
|
|
1086
|
+
message: { role: "user", content },
|
|
1087
|
+
parent_tool_use_id: null
|
|
1088
|
+
};
|
|
1089
|
+
entry.userQueue.push(msg);
|
|
1090
|
+
}
|
|
1091
|
+
function pushUserToolResult(entry, toolUseId, content, isError = false) {
|
|
1092
|
+
const msg = {
|
|
1093
|
+
type: "user",
|
|
1094
|
+
message: {
|
|
1095
|
+
role: "user",
|
|
1096
|
+
content: [{ type: "tool_result", tool_use_id: toolUseId, content, is_error: isError }]
|
|
1097
|
+
},
|
|
1098
|
+
parent_tool_use_id: null
|
|
1099
|
+
};
|
|
1100
|
+
entry.userQueue.push(msg);
|
|
1101
|
+
}
|
|
1102
|
+
function setInflight(entry, promise) {
|
|
1103
|
+
entry.inflight = promise;
|
|
1104
|
+
promise.finally(() => {
|
|
1105
|
+
if (entry.inflight === promise) entry.inflight = null;
|
|
1106
|
+
}).catch(() => {
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
function recordCachedTokens(entry, cachedTokens) {
|
|
1110
|
+
entry.cachedTokensLastTurn = cachedTokens;
|
|
1111
|
+
}
|
|
1112
|
+
function getActiveClient(cacheKey) {
|
|
1113
|
+
return clientPool.get(cacheKey);
|
|
1114
|
+
}
|
|
1115
|
+
function appendAbortLine(entry, line) {
|
|
1116
|
+
const path = resolvePath(entry.accountDir, "logs", `claude-agent-stream-${entry.logKey}.log`);
|
|
1117
|
+
try {
|
|
1118
|
+
appendFileSync2(path, line);
|
|
1119
|
+
} catch {
|
|
1120
|
+
}
|
|
1121
|
+
console.error(line.trimEnd());
|
|
1122
|
+
}
|
|
1123
|
+
async function interruptClient(cacheKey, _streamLog) {
|
|
1124
|
+
void _streamLog;
|
|
1125
|
+
const entry = clientPool.get(cacheKey);
|
|
1126
|
+
if (!entry) return;
|
|
1127
|
+
const startedAt = Date.now();
|
|
1128
|
+
let resolved = false;
|
|
1129
|
+
const timeout = new Promise(
|
|
1130
|
+
(resolve3) => setTimeout(() => resolve3("timeout"), ABORT_SDK_TIMEOUT_MS)
|
|
1131
|
+
);
|
|
1132
|
+
const sdkAbort = entry.query.interrupt().then(() => {
|
|
1133
|
+
resolved = true;
|
|
1134
|
+
return "ok";
|
|
1135
|
+
}).catch((err) => {
|
|
1136
|
+
resolved = true;
|
|
1137
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
1138
|
+
});
|
|
1139
|
+
const result = await Promise.race([sdkAbort, timeout]);
|
|
1140
|
+
const durationMs = Date.now() - startedAt;
|
|
1141
|
+
if (result === "timeout" && !resolved) {
|
|
1142
|
+
appendAbortLine(
|
|
1143
|
+
entry,
|
|
1144
|
+
`[${isoTs()}] [client-abort] cacheKey=${sk(cacheKey)} method=signal durationMs=${durationMs}
|
|
1145
|
+
`
|
|
1146
|
+
);
|
|
1147
|
+
evictClient(cacheKey, "abort-signal-fallback", null);
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
if (typeof result === "object" && "error" in result) {
|
|
1151
|
+
appendAbortLine(
|
|
1152
|
+
entry,
|
|
1153
|
+
`[${isoTs()}] [client-abort] cacheKey=${sk(cacheKey)} method=sdk durationMs=${durationMs} error=${JSON.stringify(result.error.slice(0, 120))}
|
|
1154
|
+
`
|
|
1155
|
+
);
|
|
1156
|
+
evictClient(cacheKey, "abort-error", null);
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
appendAbortLine(
|
|
1160
|
+
entry,
|
|
1161
|
+
`[${isoTs()}] [client-abort] cacheKey=${sk(cacheKey)} method=sdk durationMs=${durationMs}
|
|
1162
|
+
`
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
function evictClient(cacheKey, reason, streamLog) {
|
|
1166
|
+
const entry = clientPool.get(cacheKey);
|
|
1167
|
+
if (!entry) return false;
|
|
1168
|
+
const ageMs = Date.now() - entry.createdAt;
|
|
1169
|
+
clientPool.delete(cacheKey);
|
|
1170
|
+
try {
|
|
1171
|
+
entry.userQueue.close();
|
|
1172
|
+
} catch {
|
|
1173
|
+
}
|
|
1174
|
+
try {
|
|
1175
|
+
entry.query.close();
|
|
1176
|
+
} catch {
|
|
1177
|
+
}
|
|
1178
|
+
const line = `[${isoTs()}] [client-evict] reason=${reason} cacheKey=${sk(cacheKey)} ageMs=${ageMs} turnsServed=${entry.turnsServed}
|
|
1179
|
+
`;
|
|
1180
|
+
if (streamLog) {
|
|
1181
|
+
safeWrite(streamLog, line);
|
|
1182
|
+
} else {
|
|
1183
|
+
appendAbortLine(entry, line);
|
|
1184
|
+
}
|
|
1185
|
+
return true;
|
|
1186
|
+
}
|
|
1187
|
+
function acquireOneShotClient(cacheKey, opts, streamLog) {
|
|
1188
|
+
const userQueue = new AsyncQueue();
|
|
1189
|
+
const abortController = new AbortController();
|
|
1190
|
+
let q;
|
|
1191
|
+
try {
|
|
1192
|
+
const sdkOptions = opts.buildSdkOptions();
|
|
1193
|
+
q = query({ prompt: userQueue, options: { ...sdkOptions, abortController } });
|
|
1194
|
+
} catch (err) {
|
|
1195
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1196
|
+
safeWrite(
|
|
1197
|
+
streamLog,
|
|
1198
|
+
`[${isoTs()}] [client-spawn-error] cacheKey=${sk(cacheKey)} site=compaction-one-shot reason=${JSON.stringify(reason.slice(0, 200))}
|
|
1199
|
+
`
|
|
1200
|
+
);
|
|
1201
|
+
throw err;
|
|
1202
|
+
}
|
|
1203
|
+
const entry = {
|
|
1204
|
+
query: q,
|
|
1205
|
+
userQueue,
|
|
1206
|
+
abortController,
|
|
1207
|
+
inflight: null,
|
|
1208
|
+
createdAt: Date.now(),
|
|
1209
|
+
lastUsedAt: Date.now(),
|
|
1210
|
+
turnsServed: 1,
|
|
1211
|
+
resumedFromSessionId: opts.resumedFromSessionId,
|
|
1212
|
+
cachedTokensLastTurn: -1,
|
|
1213
|
+
accountId: opts.accountId,
|
|
1214
|
+
accountDir: opts.accountDir,
|
|
1215
|
+
logKey: opts.logKey
|
|
1216
|
+
};
|
|
1217
|
+
safeWrite(
|
|
1218
|
+
streamLog,
|
|
1219
|
+
`[${isoTs()}] [client-cold-create] reason=compaction-one-shot cacheKey=${sk(cacheKey)} createdAtMs=${entry.createdAt}
|
|
1220
|
+
`
|
|
1221
|
+
);
|
|
1222
|
+
let evicted = false;
|
|
1223
|
+
const evict = (reason) => {
|
|
1224
|
+
if (evicted) return;
|
|
1225
|
+
evicted = true;
|
|
1226
|
+
const ageMs = Date.now() - entry.createdAt;
|
|
1227
|
+
try {
|
|
1228
|
+
entry.userQueue.close();
|
|
1229
|
+
} catch {
|
|
1230
|
+
}
|
|
1231
|
+
try {
|
|
1232
|
+
entry.query.close();
|
|
1233
|
+
} catch {
|
|
1234
|
+
}
|
|
1235
|
+
safeWrite(
|
|
1236
|
+
streamLog,
|
|
1237
|
+
`[${isoTs()}] [client-evict] reason=${reason} cacheKey=${sk(cacheKey)} ageMs=${ageMs}
|
|
1238
|
+
`
|
|
1239
|
+
);
|
|
1240
|
+
};
|
|
1241
|
+
return { entry, evict };
|
|
1242
|
+
}
|
|
1243
|
+
function recordCrash(cacheKey, reason, streamLog) {
|
|
1244
|
+
const entry = clientPool.get(cacheKey);
|
|
1245
|
+
if (!entry) return;
|
|
1246
|
+
const tail = JSON.stringify(reason.slice(-512));
|
|
1247
|
+
clientPool.delete(cacheKey);
|
|
1248
|
+
try {
|
|
1249
|
+
entry.userQueue.close();
|
|
1250
|
+
} catch {
|
|
1251
|
+
}
|
|
1252
|
+
try {
|
|
1253
|
+
entry.query.close();
|
|
1254
|
+
} catch {
|
|
1255
|
+
}
|
|
1256
|
+
safeWrite(
|
|
1257
|
+
streamLog,
|
|
1258
|
+
`[${isoTs()}] [client-crash] cacheKey=${sk(cacheKey)} reason=${JSON.stringify(reason.slice(0, 80))} tail=${tail}
|
|
1259
|
+
`
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
var IDLE_TICK_MS = 6e4;
|
|
1263
|
+
var idleTickHandle = null;
|
|
1264
|
+
function evictIdleTick() {
|
|
1265
|
+
const now = Date.now();
|
|
1266
|
+
for (const [cacheKey, entry] of Array.from(clientPool.entries())) {
|
|
1267
|
+
if (entry.inflight !== null) continue;
|
|
1268
|
+
if (now - entry.lastUsedAt < idleEvictMs) continue;
|
|
1269
|
+
const ageMs = now - entry.createdAt;
|
|
1270
|
+
clientPool.delete(cacheKey);
|
|
1271
|
+
try {
|
|
1272
|
+
entry.userQueue.close();
|
|
1273
|
+
} catch {
|
|
1274
|
+
}
|
|
1275
|
+
try {
|
|
1276
|
+
entry.query.close();
|
|
1277
|
+
} catch {
|
|
1278
|
+
}
|
|
1279
|
+
console.error(
|
|
1280
|
+
`[${isoTs()}] [client-evict] reason=idle cacheKey=${sk(cacheKey)} ageMs=${ageMs} idleMs=${now - entry.lastUsedAt} turnsServed=${entry.turnsServed}`
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
function startIdleEvictTick() {
|
|
1285
|
+
if (idleTickHandle) return;
|
|
1286
|
+
idleTickHandle = setInterval(evictIdleTick, IDLE_TICK_MS);
|
|
1287
|
+
if (idleTickHandle.unref) idleTickHandle.unref();
|
|
1288
|
+
}
|
|
1289
|
+
function stopIdleEvictTick() {
|
|
1290
|
+
if (idleTickHandle) {
|
|
1291
|
+
clearInterval(idleTickHandle);
|
|
1292
|
+
idleTickHandle = null;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
startIdleEvictTick();
|
|
1296
|
+
onClearAgentSessionId((cacheKey, reason) => {
|
|
1297
|
+
const entry = clientPool.get(cacheKey);
|
|
1298
|
+
if (!entry) return;
|
|
1299
|
+
const ageMs = Date.now() - entry.createdAt;
|
|
1300
|
+
clientPool.delete(cacheKey);
|
|
1301
|
+
try {
|
|
1302
|
+
entry.userQueue.close();
|
|
1303
|
+
} catch {
|
|
1304
|
+
}
|
|
1305
|
+
try {
|
|
1306
|
+
entry.query.close();
|
|
1307
|
+
} catch {
|
|
1308
|
+
}
|
|
1309
|
+
console.error(
|
|
1310
|
+
`[${isoTs()}] [client-evict] reason=clearAgentSessionId-${reason} cacheKey=${sk(cacheKey)} ageMs=${ageMs} turnsServed=${entry.turnsServed}`
|
|
1311
|
+
);
|
|
1312
|
+
});
|
|
1313
|
+
onEvictPool((cacheKey, reason) => {
|
|
1314
|
+
const entry = clientPool.get(cacheKey);
|
|
1315
|
+
if (!entry) return;
|
|
1316
|
+
const ageMs = Date.now() - entry.createdAt;
|
|
1317
|
+
clientPool.delete(cacheKey);
|
|
1318
|
+
try {
|
|
1319
|
+
entry.userQueue.close();
|
|
1320
|
+
} catch {
|
|
1321
|
+
}
|
|
1322
|
+
try {
|
|
1323
|
+
entry.query.close();
|
|
1324
|
+
} catch {
|
|
1325
|
+
}
|
|
1326
|
+
console.error(
|
|
1327
|
+
`[${isoTs()}] [client-evict] reason=evictPool-${reason} cacheKey=${sk(cacheKey)} ageMs=${ageMs} turnsServed=${entry.turnsServed}`
|
|
1328
|
+
);
|
|
1329
|
+
});
|
|
1330
|
+
function sk(cacheKey) {
|
|
1331
|
+
return cacheKey.length > 12 ? cacheKey.slice(0, 12) + "\u2026" : cacheKey;
|
|
1332
|
+
}
|
|
1333
|
+
function safeWrite(stream, line) {
|
|
1334
|
+
if (!stream || stream.destroyed) return;
|
|
1335
|
+
if (stream.writableEnded) return;
|
|
1336
|
+
try {
|
|
1337
|
+
stream.write(line);
|
|
1338
|
+
} catch {
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
function _poolSnapshotForTest() {
|
|
1342
|
+
const now = Date.now();
|
|
1343
|
+
return Array.from(clientPool.entries()).map(([cacheKey, entry]) => ({
|
|
1344
|
+
cacheKey,
|
|
1345
|
+
ageMs: now - entry.createdAt,
|
|
1346
|
+
idleMs: now - entry.lastUsedAt,
|
|
1347
|
+
turnsServed: entry.turnsServed,
|
|
1348
|
+
inflight: entry.inflight !== null
|
|
1349
|
+
}));
|
|
1350
|
+
}
|
|
1351
|
+
function _evictAllForTest(reason = "test-tear-down") {
|
|
1352
|
+
for (const cacheKey of Array.from(clientPool.keys())) {
|
|
1353
|
+
const entry = clientPool.get(cacheKey);
|
|
1354
|
+
if (!entry) continue;
|
|
1355
|
+
clientPool.delete(cacheKey);
|
|
1356
|
+
try {
|
|
1357
|
+
entry.userQueue.close();
|
|
1358
|
+
} catch {
|
|
1359
|
+
}
|
|
1360
|
+
try {
|
|
1361
|
+
entry.query.close();
|
|
1362
|
+
} catch {
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
void reason;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
export {
|
|
1369
|
+
MAXY_DIR,
|
|
1370
|
+
BRAND_NAME,
|
|
1371
|
+
COMMERCIAL_MODE,
|
|
1372
|
+
VNC_DISPLAY,
|
|
1373
|
+
RFB_PORT,
|
|
1374
|
+
WEBSOCKIFY_PORT,
|
|
1375
|
+
CDP_PORT,
|
|
1376
|
+
CHROMIUM_PROFILE_DIR,
|
|
1377
|
+
USERS_FILE,
|
|
1378
|
+
LOG_DIR,
|
|
1379
|
+
BIN_DIR,
|
|
1380
|
+
REMOTE_PASSWORD_FILE,
|
|
1381
|
+
REMOTE_SESSION_SECRET_FILE,
|
|
1382
|
+
TELEGRAM_WEBHOOK_SECRET_FILE,
|
|
1383
|
+
TELEGRAM_ADMIN_WEBHOOK_SECRET_FILE,
|
|
1384
|
+
CLAUDE_CREDENTIALS_FILE,
|
|
1385
|
+
findFirstSubstantiveUserMessage,
|
|
1386
|
+
getSession,
|
|
1387
|
+
fingerprintSessionKey,
|
|
1388
|
+
mintAdminSessionToken,
|
|
1389
|
+
setWantsPriorConversation,
|
|
1390
|
+
consumeWantsPriorConversation,
|
|
1391
|
+
registerSession,
|
|
1392
|
+
registerResumedSession,
|
|
1393
|
+
getSessionMessages,
|
|
1394
|
+
clearSessionHistory,
|
|
1395
|
+
bufferPendingTurn,
|
|
1396
|
+
getPendingTurnCount,
|
|
1397
|
+
drainPendingTurns,
|
|
1398
|
+
isFlushError,
|
|
1399
|
+
maybeFlushConversationBuffer,
|
|
1400
|
+
isDmChannelCacheKey,
|
|
1401
|
+
getAgentNameForSession,
|
|
1402
|
+
listAdminSessionsInProgress,
|
|
1403
|
+
validateSession,
|
|
1404
|
+
storeAgentSessionId,
|
|
1405
|
+
getAgentSessionId,
|
|
1406
|
+
setAgentSessionId,
|
|
1407
|
+
storePendingCompactionSummary,
|
|
1408
|
+
consumePendingCompactionSummary,
|
|
1409
|
+
getAccountIdForSession,
|
|
1410
|
+
getUserIdForSession,
|
|
1411
|
+
getUserNameForSession,
|
|
1412
|
+
getRoleForSession,
|
|
1413
|
+
getConversationIdForSession,
|
|
1414
|
+
getSessionKeyByConversationId,
|
|
1415
|
+
setConversationIdForSession,
|
|
1416
|
+
unregisterSession,
|
|
1417
|
+
getGroupSlugForSession,
|
|
1418
|
+
getVisitorIdForSession,
|
|
1419
|
+
setGroupContextForSession,
|
|
1420
|
+
registerGrantSession,
|
|
1421
|
+
getGrantForSession,
|
|
1422
|
+
completeGrantSetup,
|
|
1423
|
+
getMessageHistory,
|
|
1424
|
+
appendMessage,
|
|
1425
|
+
storePendingTrimmedMessages,
|
|
1426
|
+
consumePendingTrimmedMessages,
|
|
1427
|
+
storeStalledSubagent,
|
|
1428
|
+
consumeStalledSubagents,
|
|
1429
|
+
setPendingCommitmentOffers,
|
|
1430
|
+
getAgentTypeForSession,
|
|
1431
|
+
clearMessageHistory,
|
|
1432
|
+
clearAgentSessionId,
|
|
1433
|
+
evictPool,
|
|
1434
|
+
storeRecoveryHandoff,
|
|
1435
|
+
consumeRecoveryHandoff,
|
|
1436
|
+
getLastClearReason,
|
|
1437
|
+
clearLastClearReason,
|
|
1438
|
+
storeStallResume,
|
|
1439
|
+
consumeStallResume,
|
|
1440
|
+
isoTs,
|
|
1441
|
+
isBrowserTool,
|
|
1442
|
+
runFailureDiagnostic,
|
|
1443
|
+
agentLogStream,
|
|
1444
|
+
emitMissingOnResolve,
|
|
1445
|
+
sigtermFlushStreamLogs,
|
|
1446
|
+
acquireClient,
|
|
1447
|
+
pushUserMessage,
|
|
1448
|
+
pushUserToolResult,
|
|
1449
|
+
setInflight,
|
|
1450
|
+
recordCachedTokens,
|
|
1451
|
+
getActiveClient,
|
|
1452
|
+
interruptClient,
|
|
1453
|
+
evictClient,
|
|
1454
|
+
acquireOneShotClient,
|
|
1455
|
+
recordCrash,
|
|
1456
|
+
startIdleEvictTick,
|
|
1457
|
+
stopIdleEvictTick,
|
|
1458
|
+
_poolSnapshotForTest,
|
|
1459
|
+
_evictAllForTest
|
|
1460
|
+
};
|