agent-companion 0.1.3 → 0.1.5
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/bridge/directIngest.mjs +367 -68
- package/bridge/server.mjs +381 -85
- package/package.json +2 -1
- package/relay/server.mjs +302 -0
- package/scripts/agent-runner.mjs +207 -104
- package/scripts/laptop-companion.mjs +116 -4
- package/scripts/laptop-service.mjs +159 -103
package/bridge/directIngest.mjs
CHANGED
|
@@ -2,14 +2,39 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
const MAX_FILES_PER_PROVIDER =
|
|
6
|
-
|
|
5
|
+
const MAX_FILES_PER_PROVIDER = parseBoundedPositiveInt(
|
|
6
|
+
process.env.AGENT_DIRECT_MAX_FILES_PER_PROVIDER,
|
|
7
|
+
120,
|
|
8
|
+
12,
|
|
9
|
+
400
|
|
10
|
+
);
|
|
11
|
+
const MAX_DIRECT_SESSIONS = parseBoundedPositiveInt(process.env.AGENT_DIRECT_MAX_SESSIONS, 240, 24, 800);
|
|
12
|
+
const MAX_DIRECT_PENDING_INPUTS = parseBoundedPositiveInt(
|
|
13
|
+
process.env.AGENT_DIRECT_MAX_PENDING_INPUTS,
|
|
14
|
+
40,
|
|
15
|
+
8,
|
|
16
|
+
120
|
|
17
|
+
);
|
|
18
|
+
const MAX_DIRECT_EVENTS = parseBoundedPositiveInt(process.env.AGENT_DIRECT_MAX_EVENTS, 120, 20, 300);
|
|
19
|
+
const MAX_SESSION_AGE_DAYS = parseBoundedPositiveInt(process.env.AGENT_DIRECT_MAX_SESSION_AGE_DAYS, 365, 3, 3660);
|
|
20
|
+
const MAX_SESSION_AGE_MS = MAX_SESSION_AGE_DAYS * 24 * 60 * 60 * 1000;
|
|
7
21
|
const PENDING_FRESH_WINDOW_MS = 7_500;
|
|
8
22
|
const MAX_CHAT_TURNS_PER_SESSION = 24;
|
|
9
23
|
const MAX_CHAT_TURNS_TOTAL = 160;
|
|
24
|
+
const FILE_SCAN_CACHE_TTL_MS = 10_000;
|
|
25
|
+
const MAX_JSONL_HEAD_BYTES = 32_000;
|
|
26
|
+
const MAX_JSONL_HEAD_LINES = 80;
|
|
27
|
+
const MAX_JSONL_TAIL_BYTES = 1_500_000;
|
|
28
|
+
const MAX_JSONL_TAIL_LINES = 1200;
|
|
29
|
+
const DIRECT_RUNNING_FRESH_WINDOW_SEC = 20;
|
|
30
|
+
const DIRECT_RUNNING_STALE_TIMEOUT_SEC = 180;
|
|
10
31
|
const PENDING_PATTERN =
|
|
11
32
|
/input required|needs input|waiting for input|approval[_\s-]*required|awaiting approval|please approve|approve (?:this|the) plan|should i (?:proceed|implement|execute)|would you like me to (?:proceed|implement|execute)|ready to (?:implement|execute)|want me to (?:implement|execute)|proceed with implementation/i;
|
|
12
33
|
|
|
34
|
+
const recentFilesCache = new Map();
|
|
35
|
+
const recentJsonlHeadCache = new Map();
|
|
36
|
+
const recentJsonlTailCache = new Map();
|
|
37
|
+
|
|
13
38
|
export function collectDirectSnapshot(nowMs = Date.now()) {
|
|
14
39
|
const codexSessions = collectCodexSessions(nowMs);
|
|
15
40
|
const claudeSessions = collectClaudeSessions(nowMs);
|
|
@@ -25,7 +50,7 @@ export function collectDirectSnapshot(nowMs = Date.now()) {
|
|
|
25
50
|
}
|
|
26
51
|
const sessions = [...sessionsById.values()]
|
|
27
52
|
.sort((a, b) => b.lastUpdated - a.lastUpdated)
|
|
28
|
-
.slice(0,
|
|
53
|
+
.slice(0, MAX_DIRECT_SESSIONS);
|
|
29
54
|
|
|
30
55
|
const pendingById = new Map();
|
|
31
56
|
for (const pending of [...codexSessions.pendingInputs, ...claudeSessions.pendingInputs]) {
|
|
@@ -35,7 +60,7 @@ export function collectDirectSnapshot(nowMs = Date.now()) {
|
|
|
35
60
|
}
|
|
36
61
|
const pendingInputs = [...pendingById.values()]
|
|
37
62
|
.sort((a, b) => b.requestedAt - a.requestedAt)
|
|
38
|
-
.slice(0,
|
|
63
|
+
.slice(0, MAX_DIRECT_PENDING_INPUTS);
|
|
39
64
|
|
|
40
65
|
const eventById = new Map();
|
|
41
66
|
for (const event of [...codexSessions.events, ...claudeSessions.events]) {
|
|
@@ -45,7 +70,7 @@ export function collectDirectSnapshot(nowMs = Date.now()) {
|
|
|
45
70
|
}
|
|
46
71
|
const events = [...eventById.values()]
|
|
47
72
|
.sort((a, b) => b.timestamp - a.timestamp)
|
|
48
|
-
.slice(0,
|
|
73
|
+
.slice(0, MAX_DIRECT_EVENTS);
|
|
49
74
|
|
|
50
75
|
const chatTurnById = new Map();
|
|
51
76
|
for (const turn of [...codexSessions.chatTurns, ...claudeSessions.chatTurns]) {
|
|
@@ -119,21 +144,16 @@ function collectClaudeSessions(nowMs) {
|
|
|
119
144
|
}
|
|
120
145
|
|
|
121
146
|
function parseCodexFile(file, nowMs) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (!raw.trim()) return null;
|
|
130
|
-
|
|
131
|
-
const lines = raw.split(/\r?\n/).filter(Boolean);
|
|
132
|
-
let sourceSessionId = "";
|
|
133
|
-
let cwd = "";
|
|
147
|
+
const codexMeta = readCodexSessionMeta(file);
|
|
148
|
+
const lines = readRecentJsonlLines(file);
|
|
149
|
+
if (!lines.length) return null;
|
|
150
|
+
let sourceSessionId = codexMeta.sessionId;
|
|
151
|
+
let cwd = codexMeta.cwd;
|
|
134
152
|
let latestTs = 0;
|
|
135
153
|
let firstPrompt = "";
|
|
136
154
|
let latestAgentMessage = "";
|
|
155
|
+
let pendingAssistantText = "";
|
|
156
|
+
let pendingAssistantTs = 0;
|
|
137
157
|
let pendingHint = "";
|
|
138
158
|
let pendingHintTs = 0;
|
|
139
159
|
let sawFinalAnswer = false;
|
|
@@ -145,6 +165,26 @@ function parseCodexFile(file, nowMs) {
|
|
|
145
165
|
totalTokens: 0
|
|
146
166
|
};
|
|
147
167
|
|
|
168
|
+
const flushPendingAssistant = () => {
|
|
169
|
+
const text = sanitizeDirectText(pendingAssistantText);
|
|
170
|
+
if (!text || !shouldIncludeDirectText(text)) {
|
|
171
|
+
pendingAssistantText = "";
|
|
172
|
+
pendingAssistantTs = 0;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
appendDirectTurn(chatTurns, {
|
|
177
|
+
sessionId: "",
|
|
178
|
+
role: "ASSISTANT",
|
|
179
|
+
kind: "FINAL_OUTPUT",
|
|
180
|
+
text,
|
|
181
|
+
createdAt: pendingAssistantTs || readFileMtimeMs(file, nowMs)
|
|
182
|
+
});
|
|
183
|
+
latestAgentMessage = text;
|
|
184
|
+
pendingAssistantText = "";
|
|
185
|
+
pendingAssistantTs = 0;
|
|
186
|
+
};
|
|
187
|
+
|
|
148
188
|
for (const line of lines) {
|
|
149
189
|
let record;
|
|
150
190
|
try {
|
|
@@ -162,8 +202,23 @@ function parseCodexFile(file, nowMs) {
|
|
|
162
202
|
continue;
|
|
163
203
|
}
|
|
164
204
|
|
|
205
|
+
if (record.type === "item.started" && record.item && typeof record.item === "object") {
|
|
206
|
+
const itemType = String(record.item.type || "").trim().toLowerCase();
|
|
207
|
+
if (itemType === "command_execution") {
|
|
208
|
+
appendDirectTurn(chatTurns, {
|
|
209
|
+
sessionId: "",
|
|
210
|
+
role: "ASSISTANT",
|
|
211
|
+
kind: "MESSAGE",
|
|
212
|
+
text: formatCodexToolCall(record.item),
|
|
213
|
+
createdAt: ts || readFileMtimeMs(file, nowMs)
|
|
214
|
+
});
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
165
219
|
if (record.type === "event_msg" && record.payload?.type === "user_message") {
|
|
166
220
|
const message = String(record.payload?.message || "").trim();
|
|
221
|
+
flushPendingAssistant();
|
|
167
222
|
if (message && !isNoisePrompt(message) && !firstPrompt) firstPrompt = message;
|
|
168
223
|
appendDirectTurn(chatTurns, {
|
|
169
224
|
sessionId: "",
|
|
@@ -180,13 +235,11 @@ function parseCodexFile(file, nowMs) {
|
|
|
180
235
|
|
|
181
236
|
if (record.type === "event_msg" && record.payload?.type === "agent_message") {
|
|
182
237
|
const message = String(record.payload?.message || "").trim();
|
|
183
|
-
if (message)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
createdAt: ts || readFileMtimeMs(file, nowMs)
|
|
189
|
-
});
|
|
238
|
+
if (message) {
|
|
239
|
+
latestAgentMessage = message;
|
|
240
|
+
pendingAssistantText = message;
|
|
241
|
+
pendingAssistantTs = ts || pendingAssistantTs || readFileMtimeMs(file, nowMs);
|
|
242
|
+
}
|
|
190
243
|
if (PENDING_PATTERN.test(message)) {
|
|
191
244
|
pendingHint = message;
|
|
192
245
|
pendingHintTs = ts || pendingHintTs;
|
|
@@ -219,6 +272,7 @@ function parseCodexFile(file, nowMs) {
|
|
|
219
272
|
const role = record.payload?.role;
|
|
220
273
|
if (role === "user") {
|
|
221
274
|
const text = extractCodexText(record.payload?.content);
|
|
275
|
+
flushPendingAssistant();
|
|
222
276
|
if (text && !isNoisePrompt(text) && !firstPrompt) firstPrompt = text;
|
|
223
277
|
appendDirectTurn(chatTurns, {
|
|
224
278
|
sessionId: "",
|
|
@@ -234,12 +288,8 @@ function parseCodexFile(file, nowMs) {
|
|
|
234
288
|
const text = extractCodexText(record.payload?.content);
|
|
235
289
|
if (text) {
|
|
236
290
|
latestAgentMessage = text;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
role: "ASSISTANT",
|
|
240
|
-
text,
|
|
241
|
-
createdAt: ts || readFileMtimeMs(file, nowMs)
|
|
242
|
-
});
|
|
291
|
+
pendingAssistantText = text;
|
|
292
|
+
pendingAssistantTs = ts || pendingAssistantTs || readFileMtimeMs(file, nowMs);
|
|
243
293
|
if (PENDING_PATTERN.test(text)) {
|
|
244
294
|
pendingHint = text;
|
|
245
295
|
pendingHintTs = ts || pendingHintTs;
|
|
@@ -252,24 +302,31 @@ function parseCodexFile(file, nowMs) {
|
|
|
252
302
|
|
|
253
303
|
const fallbackId = path.basename(file).replace(/\.jsonl$/i, "");
|
|
254
304
|
const sessionId = `codex:${sourceSessionId || fallbackId}`;
|
|
255
|
-
const finalizedTurns = finalizeDirectTurns(chatTurns, sessionId);
|
|
256
305
|
const effectiveLastUpdated = latestTs || readFileMtimeMs(file, nowMs);
|
|
257
306
|
const ageSec = Math.max(0, Math.floor((nowMs - effectiveLastUpdated) / 1000));
|
|
258
307
|
const pendingStillActive =
|
|
259
308
|
Boolean(pendingHint) &&
|
|
260
309
|
pendingHintTs > 0 &&
|
|
261
310
|
pendingHintTs >= effectiveLastUpdated - PENDING_FRESH_WINDOW_MS;
|
|
311
|
+
const hasBufferedAssistant = Boolean(sanitizeDirectText(pendingAssistantText));
|
|
262
312
|
|
|
263
313
|
const state = deriveState({
|
|
264
314
|
ageSec,
|
|
265
315
|
sawFinalAnswer,
|
|
266
316
|
sawError,
|
|
267
|
-
hasPendingHint: pendingStillActive
|
|
317
|
+
hasPendingHint: pendingStillActive,
|
|
318
|
+
hasBufferedAssistant
|
|
268
319
|
});
|
|
269
320
|
|
|
321
|
+
if (sawFinalAnswer) {
|
|
322
|
+
flushPendingAssistant();
|
|
323
|
+
}
|
|
324
|
+
|
|
270
325
|
const title = truncate(firstPrompt || latestAgentMessage || "Codex session", 88);
|
|
271
326
|
const repo = cwd ? path.basename(cwd) : "unknown-repo";
|
|
272
327
|
|
|
328
|
+
const finalizedTurns = finalizeDirectTurns(chatTurns, sessionId);
|
|
329
|
+
|
|
273
330
|
if (!firstPrompt && !latestAgentMessage && finalizedTurns.length === 0) {
|
|
274
331
|
return null;
|
|
275
332
|
}
|
|
@@ -335,24 +392,19 @@ function parseCodexFile(file, nowMs) {
|
|
|
335
392
|
}
|
|
336
393
|
|
|
337
394
|
function parseClaudeFile(file, nowMs) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
raw = fs.readFileSync(file, "utf8");
|
|
341
|
-
} catch {
|
|
342
|
-
return null;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (!raw.trim()) return null;
|
|
346
|
-
|
|
347
|
-
const lines = raw.split(/\r?\n/).filter(Boolean);
|
|
395
|
+
const lines = readRecentJsonlLines(file);
|
|
396
|
+
if (!lines.length) return null;
|
|
348
397
|
let sourceSessionId = "";
|
|
349
398
|
let cwd = "";
|
|
350
399
|
let branch = "main";
|
|
351
400
|
let latestTs = 0;
|
|
352
401
|
let firstPrompt = "";
|
|
353
402
|
let latestAssistantText = "";
|
|
403
|
+
let pendingAssistantText = "";
|
|
404
|
+
let pendingAssistantTs = 0;
|
|
354
405
|
let pendingHint = "";
|
|
355
406
|
let pendingHintTs = 0;
|
|
407
|
+
let sawFinalAnswer = false;
|
|
356
408
|
let sawError = false;
|
|
357
409
|
const chatTurns = [];
|
|
358
410
|
let usage = {
|
|
@@ -380,6 +432,9 @@ function parseClaudeFile(file, nowMs) {
|
|
|
380
432
|
branch = record.gitBranch && record.gitBranch !== "HEAD" ? record.gitBranch : branch;
|
|
381
433
|
|
|
382
434
|
if (record.type === "user") {
|
|
435
|
+
if (isClaudeMetaUserRecord(record)) {
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
383
438
|
const userText = extractClaudeUserText(record.message);
|
|
384
439
|
if (userText && !firstPrompt) firstPrompt = userText;
|
|
385
440
|
appendDirectTurn(chatTurns, {
|
|
@@ -397,18 +452,32 @@ function parseClaudeFile(file, nowMs) {
|
|
|
397
452
|
|
|
398
453
|
if (record.type === "assistant") {
|
|
399
454
|
const assistantText = extractClaudeAssistantText(record.message);
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
createdAt: ts || readFileMtimeMs(file, nowMs)
|
|
406
|
-
});
|
|
407
|
-
if (PENDING_PATTERN.test(assistantText)) {
|
|
455
|
+
const stopReason = normalizeClaudeStopReason(record.message?.stop_reason);
|
|
456
|
+
if (assistantText) {
|
|
457
|
+
latestAssistantText = assistantText;
|
|
458
|
+
}
|
|
459
|
+
if (assistantText && PENDING_PATTERN.test(assistantText)) {
|
|
408
460
|
pendingHint = assistantText;
|
|
409
461
|
pendingHintTs = ts || pendingHintTs;
|
|
410
462
|
}
|
|
411
463
|
if (/error|failed|exception/i.test(assistantText)) sawError = true;
|
|
464
|
+
if (stopReason === "end_turn") {
|
|
465
|
+
sawFinalAnswer = Boolean(assistantText) || sawFinalAnswer;
|
|
466
|
+
if (assistantText) {
|
|
467
|
+
appendDirectTurn(chatTurns, {
|
|
468
|
+
sessionId: "",
|
|
469
|
+
role: "ASSISTANT",
|
|
470
|
+
kind: "FINAL_OUTPUT",
|
|
471
|
+
text: assistantText,
|
|
472
|
+
createdAt: ts || readFileMtimeMs(file, nowMs)
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
pendingAssistantText = "";
|
|
476
|
+
pendingAssistantTs = 0;
|
|
477
|
+
} else if (assistantText) {
|
|
478
|
+
pendingAssistantText = assistantText;
|
|
479
|
+
pendingAssistantTs = ts || pendingAssistantTs || readFileMtimeMs(file, nowMs);
|
|
480
|
+
}
|
|
412
481
|
|
|
413
482
|
const u = record.message?.usage;
|
|
414
483
|
if (u) {
|
|
@@ -424,21 +493,24 @@ function parseClaudeFile(file, nowMs) {
|
|
|
424
493
|
|
|
425
494
|
const fallbackId = path.basename(file).replace(/\.jsonl$/i, "");
|
|
426
495
|
const sessionId = `claude:${sourceSessionId || fallbackId}`;
|
|
427
|
-
const finalizedTurns = finalizeDirectTurns(chatTurns, sessionId);
|
|
428
496
|
const effectiveLastUpdated = latestTs || readFileMtimeMs(file, nowMs);
|
|
429
497
|
const ageSec = Math.max(0, Math.floor((nowMs - effectiveLastUpdated) / 1000));
|
|
430
498
|
const pendingStillActive =
|
|
431
499
|
Boolean(pendingHint) &&
|
|
432
500
|
pendingHintTs > 0 &&
|
|
433
501
|
pendingHintTs >= effectiveLastUpdated - PENDING_FRESH_WINDOW_MS;
|
|
502
|
+
const hasBufferedAssistant = Boolean(sanitizeDirectText(pendingAssistantText));
|
|
434
503
|
|
|
435
504
|
const state = deriveState({
|
|
436
505
|
ageSec,
|
|
437
|
-
sawFinalAnswer
|
|
506
|
+
sawFinalAnswer,
|
|
438
507
|
sawError,
|
|
439
|
-
hasPendingHint: pendingStillActive
|
|
508
|
+
hasPendingHint: pendingStillActive,
|
|
509
|
+
hasBufferedAssistant
|
|
440
510
|
});
|
|
441
511
|
|
|
512
|
+
const finalizedTurns = finalizeDirectTurns(chatTurns, sessionId);
|
|
513
|
+
|
|
442
514
|
if (!firstPrompt && !latestAssistantText && finalizedTurns.length === 0) {
|
|
443
515
|
return null;
|
|
444
516
|
}
|
|
@@ -506,11 +578,13 @@ function parseClaudeFile(file, nowMs) {
|
|
|
506
578
|
return { session, pendingInput, event, chatTurns: finalizedTurns };
|
|
507
579
|
}
|
|
508
580
|
|
|
509
|
-
function deriveState({ ageSec, sawFinalAnswer, sawError, hasPendingHint }) {
|
|
581
|
+
function deriveState({ ageSec, sawFinalAnswer, sawError, hasPendingHint, hasBufferedAssistant }) {
|
|
510
582
|
if (hasPendingHint) return "WAITING_INPUT";
|
|
511
|
-
if (sawError && ageSec >
|
|
512
|
-
if (ageSec < 20) return "RUNNING";
|
|
583
|
+
if (sawError && ageSec > DIRECT_RUNNING_FRESH_WINDOW_SEC) return "FAILED";
|
|
513
584
|
if (sawFinalAnswer) return "COMPLETED";
|
|
585
|
+
if (hasBufferedAssistant && ageSec < DIRECT_RUNNING_STALE_TIMEOUT_SEC) return "RUNNING";
|
|
586
|
+
if (ageSec < DIRECT_RUNNING_FRESH_WINDOW_SEC) return "RUNNING";
|
|
587
|
+
if (hasBufferedAssistant) return "FAILED";
|
|
514
588
|
return "COMPLETED";
|
|
515
589
|
}
|
|
516
590
|
|
|
@@ -519,43 +593,73 @@ function extractCodexText(content) {
|
|
|
519
593
|
for (const item of content) {
|
|
520
594
|
if (typeof item?.text === "string" && item.text.trim()) {
|
|
521
595
|
const text = sanitizeDirectText(item.text);
|
|
522
|
-
if (
|
|
596
|
+
if (shouldIncludeDirectText(text)) return text;
|
|
523
597
|
}
|
|
524
598
|
}
|
|
525
599
|
return "";
|
|
526
600
|
}
|
|
527
601
|
|
|
602
|
+
function formatCodexToolCall(item) {
|
|
603
|
+
const rawCommand =
|
|
604
|
+
typeof item?.command === "string"
|
|
605
|
+
? item.command
|
|
606
|
+
: Array.isArray(item?.command)
|
|
607
|
+
? item.command.map((entry) => String(entry || "")).join(" ")
|
|
608
|
+
: "";
|
|
609
|
+
|
|
610
|
+
let command = String(rawCommand || "").replace(/\s+/g, " ").trim();
|
|
611
|
+
if (command) {
|
|
612
|
+
command = command.replace(/^\/bin\/(?:zsh|bash|sh)\s+-lc\s+/i, "");
|
|
613
|
+
command = command.replace(/^['"`]\s*/, "").replace(/\s*['"`]$/, "");
|
|
614
|
+
}
|
|
615
|
+
if (command.length > 140) {
|
|
616
|
+
command = `${command.slice(0, 137)}...`;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return command ? `[tool] shell ${command}` : "[tool] shell";
|
|
620
|
+
}
|
|
621
|
+
|
|
528
622
|
function extractClaudeUserText(message) {
|
|
529
623
|
if (!message) return "";
|
|
530
624
|
if (typeof message.content === "string") {
|
|
531
625
|
const text = sanitizeDirectText(message.content);
|
|
532
|
-
return
|
|
626
|
+
return shouldIncludeDirectUserText(text) ? text : "";
|
|
533
627
|
}
|
|
534
628
|
if (!Array.isArray(message.content)) return "";
|
|
535
629
|
|
|
630
|
+
const collected = [];
|
|
536
631
|
for (const part of message.content) {
|
|
537
632
|
if (typeof part === "string") {
|
|
538
633
|
const text = sanitizeDirectText(part);
|
|
539
|
-
if (
|
|
634
|
+
if (shouldIncludeDirectUserText(text)) {
|
|
635
|
+
collected.push(text);
|
|
636
|
+
}
|
|
637
|
+
continue;
|
|
540
638
|
}
|
|
639
|
+
const partType = String(part?.type || "").trim().toLowerCase();
|
|
640
|
+
if (partType && partType !== "text" && partType !== "input_text") continue;
|
|
541
641
|
if (typeof part?.text === "string") {
|
|
542
642
|
const text = sanitizeDirectText(part.text);
|
|
543
|
-
if (
|
|
643
|
+
if (shouldIncludeDirectUserText(text)) {
|
|
644
|
+
collected.push(text);
|
|
645
|
+
}
|
|
544
646
|
}
|
|
545
647
|
if (typeof part?.content === "string" && part.content.trim()) {
|
|
546
648
|
const text = sanitizeDirectText(part.content);
|
|
547
|
-
if (
|
|
649
|
+
if (shouldIncludeDirectUserText(text)) {
|
|
650
|
+
collected.push(text);
|
|
651
|
+
}
|
|
548
652
|
}
|
|
549
653
|
}
|
|
550
654
|
|
|
551
|
-
return "";
|
|
655
|
+
return collected.join("\n\n").trim();
|
|
552
656
|
}
|
|
553
657
|
|
|
554
658
|
function extractClaudeAssistantText(message) {
|
|
555
659
|
if (!message) return "";
|
|
556
660
|
if (typeof message.content === "string") {
|
|
557
661
|
const text = sanitizeDirectText(message.content);
|
|
558
|
-
return
|
|
662
|
+
return shouldIncludeDirectAssistantText(text) ? text : "";
|
|
559
663
|
}
|
|
560
664
|
if (!Array.isArray(message.content)) return "";
|
|
561
665
|
|
|
@@ -564,12 +668,12 @@ function extractClaudeAssistantText(message) {
|
|
|
564
668
|
for (const part of message.content) {
|
|
565
669
|
if (part?.type === "text" && typeof part?.text === "string" && part.text.trim()) {
|
|
566
670
|
const text = sanitizeDirectText(part.text);
|
|
567
|
-
if (
|
|
671
|
+
if (shouldIncludeDirectAssistantText(text)) {
|
|
568
672
|
collected.push(text);
|
|
569
673
|
}
|
|
570
674
|
} else if (!part?.type && typeof part?.text === "string" && part.text.trim()) {
|
|
571
675
|
const text = sanitizeDirectText(part.text);
|
|
572
|
-
if (
|
|
676
|
+
if (shouldIncludeDirectAssistantText(text)) {
|
|
573
677
|
collected.push(text);
|
|
574
678
|
}
|
|
575
679
|
}
|
|
@@ -578,6 +682,33 @@ function extractClaudeAssistantText(message) {
|
|
|
578
682
|
return collected.join("\n\n").trim();
|
|
579
683
|
}
|
|
580
684
|
|
|
685
|
+
function normalizeClaudeStopReason(value) {
|
|
686
|
+
return String(value || "")
|
|
687
|
+
.trim()
|
|
688
|
+
.toLowerCase();
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function isClaudeMetaUserRecord(record) {
|
|
692
|
+
if (!record || typeof record !== "object") return false;
|
|
693
|
+
if (record.isMeta === true) return true;
|
|
694
|
+
if (record.sourceToolUseID) return true;
|
|
695
|
+
if (record.toolUseResult && typeof record.toolUseResult === "object") return true;
|
|
696
|
+
|
|
697
|
+
const content = record.message?.content;
|
|
698
|
+
if (!Array.isArray(content)) return false;
|
|
699
|
+
|
|
700
|
+
for (const part of content) {
|
|
701
|
+
const partType = String(part?.type || "").trim().toLowerCase();
|
|
702
|
+
const text = String(part?.text || part?.content || "").trim();
|
|
703
|
+
if (partType === "tool_result") return true;
|
|
704
|
+
if (/^launching skill:/i.test(text)) return true;
|
|
705
|
+
if (/^base directory for this skill:/i.test(text)) return true;
|
|
706
|
+
if (/<command-name>|<command-message>|<command-args>|<local-command-stdout>/i.test(text)) return true;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
|
|
581
712
|
function safeDateMs(value) {
|
|
582
713
|
const parsed = Date.parse(String(value || ""));
|
|
583
714
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
@@ -596,6 +727,33 @@ function readFileMtimeMs(file, fallback = Date.now()) {
|
|
|
596
727
|
}
|
|
597
728
|
}
|
|
598
729
|
|
|
730
|
+
function readCodexSessionMeta(file) {
|
|
731
|
+
const lines = readInitialJsonlLines(file);
|
|
732
|
+
for (const line of lines) {
|
|
733
|
+
try {
|
|
734
|
+
const record = JSON.parse(line);
|
|
735
|
+
if (record?.type !== "session_meta") continue;
|
|
736
|
+
return {
|
|
737
|
+
sessionId: String(record?.payload?.id || "").trim(),
|
|
738
|
+
cwd: String(record?.payload?.cwd || "").trim()
|
|
739
|
+
};
|
|
740
|
+
} catch {
|
|
741
|
+
// keep scanning
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return {
|
|
746
|
+
sessionId: "",
|
|
747
|
+
cwd: ""
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function parseBoundedPositiveInt(value, fallback, min, max) {
|
|
752
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
753
|
+
if (!Number.isFinite(parsed) || parsed < min) return fallback;
|
|
754
|
+
return Math.min(parsed, max);
|
|
755
|
+
}
|
|
756
|
+
|
|
599
757
|
function truncate(value, max) {
|
|
600
758
|
const text = String(value || "").trim();
|
|
601
759
|
if (text.length <= max) return text;
|
|
@@ -610,23 +768,59 @@ function isNoisePrompt(text) {
|
|
|
610
768
|
if (raw.includes("Filesystem sandboxing defines")) return true;
|
|
611
769
|
if (raw.includes("AGENT_COMPANION_PERSIST_SERVER_HINT_V1")) return true;
|
|
612
770
|
if (raw.includes("[Request interrupted by user for tool use]")) return true;
|
|
771
|
+
if (/^launching skill:/i.test(raw.trim())) return true;
|
|
772
|
+
if (/^base directory for this skill:/i.test(raw.trim())) return true;
|
|
773
|
+
if (/<command-name>|<command-message>|<command-args>|<local-command-stdout>/i.test(raw)) return true;
|
|
613
774
|
if (/^\[background-service\]/i.test(raw.trim())) return true;
|
|
614
775
|
if (/"service"\s*:\s*\{/.test(raw) && /"localhostUrl"\s*:/.test(raw)) return true;
|
|
615
776
|
if (raw === "Answer questions?") return true;
|
|
616
777
|
return false;
|
|
617
778
|
}
|
|
618
779
|
|
|
780
|
+
function isDirectArtifactText(text) {
|
|
781
|
+
const raw = String(text || "").trim();
|
|
782
|
+
if (!raw) return true;
|
|
783
|
+
if (/^<task-notification>/i.test(raw)) return true;
|
|
784
|
+
if (/<\/task-notification>\s*$/i.test(raw)) return true;
|
|
785
|
+
if (/<task-id>|<tool-use-id>|<output-file>|<status>|<summary>/i.test(raw)) return true;
|
|
786
|
+
if (/^read the output file to retrieve the result:/i.test(raw)) return true;
|
|
787
|
+
if (/^command running in background with id:/i.test(raw)) return true;
|
|
788
|
+
if (/^file created successfully at:/i.test(raw)) return true;
|
|
789
|
+
if (/^traceback \(most recent call last\):/i.test(raw)) return true;
|
|
790
|
+
if (/^\s*\d+→/.test(raw)) return true;
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function shouldIncludeDirectText(text) {
|
|
795
|
+
return !isNoisePrompt(text) && !isDirectArtifactText(text);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function shouldIncludeDirectUserText(text) {
|
|
799
|
+
return shouldIncludeDirectText(text);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function shouldIncludeDirectAssistantText(text) {
|
|
803
|
+
return shouldIncludeDirectText(text);
|
|
804
|
+
}
|
|
805
|
+
|
|
619
806
|
function appendDirectTurn(turns, input) {
|
|
620
807
|
const text = sanitizeDirectText(input?.text);
|
|
621
|
-
if (!text ||
|
|
808
|
+
if (!text || !shouldIncludeDirectText(text)) return;
|
|
622
809
|
|
|
623
810
|
const role = input?.role === "ASSISTANT" ? "ASSISTANT" : "USER";
|
|
811
|
+
const kind =
|
|
812
|
+
input?.kind === "FINAL_OUTPUT"
|
|
813
|
+
? "FINAL_OUTPUT"
|
|
814
|
+
: input?.kind === "APPROVAL_ACTION"
|
|
815
|
+
? "APPROVAL_ACTION"
|
|
816
|
+
: "MESSAGE";
|
|
624
817
|
const createdAt = safeNumber(input?.createdAt, Date.now());
|
|
625
818
|
const normalized = normalizeComparableText(text);
|
|
626
819
|
const last = turns[turns.length - 1];
|
|
627
820
|
if (
|
|
628
821
|
last &&
|
|
629
822
|
last.role === role &&
|
|
823
|
+
last.kind === kind &&
|
|
630
824
|
normalizeComparableText(last.text) === normalized &&
|
|
631
825
|
Math.abs(safeNumber(last.createdAt, 0) - createdAt) <= 3_000
|
|
632
826
|
) {
|
|
@@ -636,7 +830,7 @@ function appendDirectTurn(turns, input) {
|
|
|
636
830
|
turns.push({
|
|
637
831
|
sessionId: input?.sessionId || "",
|
|
638
832
|
role,
|
|
639
|
-
kind
|
|
833
|
+
kind,
|
|
640
834
|
text,
|
|
641
835
|
createdAt,
|
|
642
836
|
source: "DIRECT"
|
|
@@ -676,6 +870,12 @@ function sanitizeDirectText(value) {
|
|
|
676
870
|
}
|
|
677
871
|
|
|
678
872
|
function getRecentJsonlFiles(rootDir, limit) {
|
|
873
|
+
const now = Date.now();
|
|
874
|
+
const cached = recentFilesCache.get(rootDir);
|
|
875
|
+
if (cached && cached.expiresAt > now) {
|
|
876
|
+
return cached.files.slice(0, limit);
|
|
877
|
+
}
|
|
878
|
+
|
|
679
879
|
const records = [];
|
|
680
880
|
|
|
681
881
|
if (!fs.existsSync(rootDir)) return records;
|
|
@@ -712,5 +912,104 @@ function getRecentJsonlFiles(rootDir, limit) {
|
|
|
712
912
|
}
|
|
713
913
|
|
|
714
914
|
records.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
715
|
-
|
|
915
|
+
const files = records.map((item) => item.file);
|
|
916
|
+
recentFilesCache.set(rootDir, {
|
|
917
|
+
expiresAt: now + FILE_SCAN_CACHE_TTL_MS,
|
|
918
|
+
files
|
|
919
|
+
});
|
|
920
|
+
return files.slice(0, limit);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function readRecentJsonlLines(file) {
|
|
924
|
+
let stat;
|
|
925
|
+
try {
|
|
926
|
+
stat = fs.statSync(file);
|
|
927
|
+
} catch {
|
|
928
|
+
return [];
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const cached = recentJsonlTailCache.get(file);
|
|
932
|
+
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
|
|
933
|
+
return cached.lines;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const size = safeNumber(stat.size, 0);
|
|
937
|
+
if (size <= 0) {
|
|
938
|
+
recentJsonlTailCache.set(file, { mtimeMs: stat.mtimeMs, size, lines: [] });
|
|
939
|
+
return [];
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const start = Math.max(0, size - MAX_JSONL_TAIL_BYTES);
|
|
943
|
+
const length = size - start;
|
|
944
|
+
let text = "";
|
|
945
|
+
|
|
946
|
+
try {
|
|
947
|
+
const fd = fs.openSync(file, "r");
|
|
948
|
+
try {
|
|
949
|
+
const buffer = Buffer.alloc(length);
|
|
950
|
+
fs.readSync(fd, buffer, 0, length, start);
|
|
951
|
+
text = buffer.toString("utf8");
|
|
952
|
+
} finally {
|
|
953
|
+
fs.closeSync(fd);
|
|
954
|
+
}
|
|
955
|
+
} catch {
|
|
956
|
+
return [];
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (start > 0) {
|
|
960
|
+
const newlineIndex = text.indexOf("\n");
|
|
961
|
+
text = newlineIndex >= 0 ? text.slice(newlineIndex + 1) : "";
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const lines = text.split(/\r?\n/).filter(Boolean).slice(-MAX_JSONL_TAIL_LINES);
|
|
965
|
+
recentJsonlTailCache.set(file, {
|
|
966
|
+
mtimeMs: stat.mtimeMs,
|
|
967
|
+
size,
|
|
968
|
+
lines
|
|
969
|
+
});
|
|
970
|
+
return lines;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function readInitialJsonlLines(file) {
|
|
974
|
+
let stat;
|
|
975
|
+
try {
|
|
976
|
+
stat = fs.statSync(file);
|
|
977
|
+
} catch {
|
|
978
|
+
return [];
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const cached = recentJsonlHeadCache.get(file);
|
|
982
|
+
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
|
|
983
|
+
return cached.lines;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const size = safeNumber(stat.size, 0);
|
|
987
|
+
if (size <= 0) {
|
|
988
|
+
recentJsonlHeadCache.set(file, { mtimeMs: stat.mtimeMs, size, lines: [] });
|
|
989
|
+
return [];
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const length = Math.min(size, MAX_JSONL_HEAD_BYTES);
|
|
993
|
+
let text = "";
|
|
994
|
+
|
|
995
|
+
try {
|
|
996
|
+
const fd = fs.openSync(file, "r");
|
|
997
|
+
try {
|
|
998
|
+
const buffer = Buffer.alloc(length);
|
|
999
|
+
fs.readSync(fd, buffer, 0, length, 0);
|
|
1000
|
+
text = buffer.toString("utf8");
|
|
1001
|
+
} finally {
|
|
1002
|
+
fs.closeSync(fd);
|
|
1003
|
+
}
|
|
1004
|
+
} catch {
|
|
1005
|
+
return [];
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const lines = text.split(/\r?\n/).filter(Boolean).slice(0, MAX_JSONL_HEAD_LINES);
|
|
1009
|
+
recentJsonlHeadCache.set(file, {
|
|
1010
|
+
mtimeMs: stat.mtimeMs,
|
|
1011
|
+
size,
|
|
1012
|
+
lines
|
|
1013
|
+
});
|
|
1014
|
+
return lines;
|
|
716
1015
|
}
|