agent-companion 0.1.2 → 0.1.4
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 +371 -68
- package/bridge/server.mjs +413 -90
- package/package.json +1 -6
- package/relay/server.mjs +11 -2
- package/scripts/agent-runner.mjs +207 -104
- package/scripts/laptop-companion.mjs +184 -5
- 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
|
}
|
|
@@ -290,6 +347,7 @@ function parseCodexFile(file, nowMs) {
|
|
|
290
347
|
agentType: "CODEX",
|
|
291
348
|
title,
|
|
292
349
|
repo,
|
|
350
|
+
workspacePath: cwd || "",
|
|
293
351
|
branch: "main",
|
|
294
352
|
state,
|
|
295
353
|
lastUpdated: effectiveLastUpdated,
|
|
@@ -334,24 +392,19 @@ function parseCodexFile(file, nowMs) {
|
|
|
334
392
|
}
|
|
335
393
|
|
|
336
394
|
function parseClaudeFile(file, nowMs) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
raw = fs.readFileSync(file, "utf8");
|
|
340
|
-
} catch {
|
|
341
|
-
return null;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if (!raw.trim()) return null;
|
|
345
|
-
|
|
346
|
-
const lines = raw.split(/\r?\n/).filter(Boolean);
|
|
395
|
+
const lines = readRecentJsonlLines(file);
|
|
396
|
+
if (!lines.length) return null;
|
|
347
397
|
let sourceSessionId = "";
|
|
348
398
|
let cwd = "";
|
|
349
399
|
let branch = "main";
|
|
350
400
|
let latestTs = 0;
|
|
351
401
|
let firstPrompt = "";
|
|
352
402
|
let latestAssistantText = "";
|
|
403
|
+
let pendingAssistantText = "";
|
|
404
|
+
let pendingAssistantTs = 0;
|
|
353
405
|
let pendingHint = "";
|
|
354
406
|
let pendingHintTs = 0;
|
|
407
|
+
let sawFinalAnswer = false;
|
|
355
408
|
let sawError = false;
|
|
356
409
|
const chatTurns = [];
|
|
357
410
|
let usage = {
|
|
@@ -379,6 +432,9 @@ function parseClaudeFile(file, nowMs) {
|
|
|
379
432
|
branch = record.gitBranch && record.gitBranch !== "HEAD" ? record.gitBranch : branch;
|
|
380
433
|
|
|
381
434
|
if (record.type === "user") {
|
|
435
|
+
if (isClaudeMetaUserRecord(record)) {
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
382
438
|
const userText = extractClaudeUserText(record.message);
|
|
383
439
|
if (userText && !firstPrompt) firstPrompt = userText;
|
|
384
440
|
appendDirectTurn(chatTurns, {
|
|
@@ -396,18 +452,32 @@ function parseClaudeFile(file, nowMs) {
|
|
|
396
452
|
|
|
397
453
|
if (record.type === "assistant") {
|
|
398
454
|
const assistantText = extractClaudeAssistantText(record.message);
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
createdAt: ts || readFileMtimeMs(file, nowMs)
|
|
405
|
-
});
|
|
406
|
-
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)) {
|
|
407
460
|
pendingHint = assistantText;
|
|
408
461
|
pendingHintTs = ts || pendingHintTs;
|
|
409
462
|
}
|
|
410
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
|
+
}
|
|
411
481
|
|
|
412
482
|
const u = record.message?.usage;
|
|
413
483
|
if (u) {
|
|
@@ -423,21 +493,24 @@ function parseClaudeFile(file, nowMs) {
|
|
|
423
493
|
|
|
424
494
|
const fallbackId = path.basename(file).replace(/\.jsonl$/i, "");
|
|
425
495
|
const sessionId = `claude:${sourceSessionId || fallbackId}`;
|
|
426
|
-
const finalizedTurns = finalizeDirectTurns(chatTurns, sessionId);
|
|
427
496
|
const effectiveLastUpdated = latestTs || readFileMtimeMs(file, nowMs);
|
|
428
497
|
const ageSec = Math.max(0, Math.floor((nowMs - effectiveLastUpdated) / 1000));
|
|
429
498
|
const pendingStillActive =
|
|
430
499
|
Boolean(pendingHint) &&
|
|
431
500
|
pendingHintTs > 0 &&
|
|
432
501
|
pendingHintTs >= effectiveLastUpdated - PENDING_FRESH_WINDOW_MS;
|
|
502
|
+
const hasBufferedAssistant = Boolean(sanitizeDirectText(pendingAssistantText));
|
|
433
503
|
|
|
434
504
|
const state = deriveState({
|
|
435
505
|
ageSec,
|
|
436
|
-
sawFinalAnswer
|
|
506
|
+
sawFinalAnswer,
|
|
437
507
|
sawError,
|
|
438
|
-
hasPendingHint: pendingStillActive
|
|
508
|
+
hasPendingHint: pendingStillActive,
|
|
509
|
+
hasBufferedAssistant
|
|
439
510
|
});
|
|
440
511
|
|
|
512
|
+
const finalizedTurns = finalizeDirectTurns(chatTurns, sessionId);
|
|
513
|
+
|
|
441
514
|
if (!firstPrompt && !latestAssistantText && finalizedTurns.length === 0) {
|
|
442
515
|
return null;
|
|
443
516
|
}
|
|
@@ -461,6 +534,7 @@ function parseClaudeFile(file, nowMs) {
|
|
|
461
534
|
agentType: "CLAUDE",
|
|
462
535
|
title,
|
|
463
536
|
repo,
|
|
537
|
+
workspacePath: cwd || "",
|
|
464
538
|
branch,
|
|
465
539
|
state,
|
|
466
540
|
lastUpdated: effectiveLastUpdated,
|
|
@@ -504,11 +578,13 @@ function parseClaudeFile(file, nowMs) {
|
|
|
504
578
|
return { session, pendingInput, event, chatTurns: finalizedTurns };
|
|
505
579
|
}
|
|
506
580
|
|
|
507
|
-
function deriveState({ ageSec, sawFinalAnswer, sawError, hasPendingHint }) {
|
|
581
|
+
function deriveState({ ageSec, sawFinalAnswer, sawError, hasPendingHint, hasBufferedAssistant }) {
|
|
508
582
|
if (hasPendingHint) return "WAITING_INPUT";
|
|
509
|
-
if (sawError && ageSec >
|
|
510
|
-
if (ageSec < 20) return "RUNNING";
|
|
583
|
+
if (sawError && ageSec > DIRECT_RUNNING_FRESH_WINDOW_SEC) return "FAILED";
|
|
511
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";
|
|
512
588
|
return "COMPLETED";
|
|
513
589
|
}
|
|
514
590
|
|
|
@@ -517,43 +593,73 @@ function extractCodexText(content) {
|
|
|
517
593
|
for (const item of content) {
|
|
518
594
|
if (typeof item?.text === "string" && item.text.trim()) {
|
|
519
595
|
const text = sanitizeDirectText(item.text);
|
|
520
|
-
if (
|
|
596
|
+
if (shouldIncludeDirectText(text)) return text;
|
|
521
597
|
}
|
|
522
598
|
}
|
|
523
599
|
return "";
|
|
524
600
|
}
|
|
525
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
|
+
|
|
526
622
|
function extractClaudeUserText(message) {
|
|
527
623
|
if (!message) return "";
|
|
528
624
|
if (typeof message.content === "string") {
|
|
529
625
|
const text = sanitizeDirectText(message.content);
|
|
530
|
-
return
|
|
626
|
+
return shouldIncludeDirectUserText(text) ? text : "";
|
|
531
627
|
}
|
|
532
628
|
if (!Array.isArray(message.content)) return "";
|
|
533
629
|
|
|
630
|
+
const collected = [];
|
|
534
631
|
for (const part of message.content) {
|
|
535
632
|
if (typeof part === "string") {
|
|
536
633
|
const text = sanitizeDirectText(part);
|
|
537
|
-
if (
|
|
634
|
+
if (shouldIncludeDirectUserText(text)) {
|
|
635
|
+
collected.push(text);
|
|
636
|
+
}
|
|
637
|
+
continue;
|
|
538
638
|
}
|
|
639
|
+
const partType = String(part?.type || "").trim().toLowerCase();
|
|
640
|
+
if (partType && partType !== "text" && partType !== "input_text") continue;
|
|
539
641
|
if (typeof part?.text === "string") {
|
|
540
642
|
const text = sanitizeDirectText(part.text);
|
|
541
|
-
if (
|
|
643
|
+
if (shouldIncludeDirectUserText(text)) {
|
|
644
|
+
collected.push(text);
|
|
645
|
+
}
|
|
542
646
|
}
|
|
543
647
|
if (typeof part?.content === "string" && part.content.trim()) {
|
|
544
648
|
const text = sanitizeDirectText(part.content);
|
|
545
|
-
if (
|
|
649
|
+
if (shouldIncludeDirectUserText(text)) {
|
|
650
|
+
collected.push(text);
|
|
651
|
+
}
|
|
546
652
|
}
|
|
547
653
|
}
|
|
548
654
|
|
|
549
|
-
return "";
|
|
655
|
+
return collected.join("\n\n").trim();
|
|
550
656
|
}
|
|
551
657
|
|
|
552
658
|
function extractClaudeAssistantText(message) {
|
|
553
659
|
if (!message) return "";
|
|
554
660
|
if (typeof message.content === "string") {
|
|
555
661
|
const text = sanitizeDirectText(message.content);
|
|
556
|
-
return
|
|
662
|
+
return shouldIncludeDirectAssistantText(text) ? text : "";
|
|
557
663
|
}
|
|
558
664
|
if (!Array.isArray(message.content)) return "";
|
|
559
665
|
|
|
@@ -562,12 +668,12 @@ function extractClaudeAssistantText(message) {
|
|
|
562
668
|
for (const part of message.content) {
|
|
563
669
|
if (part?.type === "text" && typeof part?.text === "string" && part.text.trim()) {
|
|
564
670
|
const text = sanitizeDirectText(part.text);
|
|
565
|
-
if (
|
|
671
|
+
if (shouldIncludeDirectAssistantText(text)) {
|
|
566
672
|
collected.push(text);
|
|
567
673
|
}
|
|
568
674
|
} else if (!part?.type && typeof part?.text === "string" && part.text.trim()) {
|
|
569
675
|
const text = sanitizeDirectText(part.text);
|
|
570
|
-
if (
|
|
676
|
+
if (shouldIncludeDirectAssistantText(text)) {
|
|
571
677
|
collected.push(text);
|
|
572
678
|
}
|
|
573
679
|
}
|
|
@@ -576,6 +682,33 @@ function extractClaudeAssistantText(message) {
|
|
|
576
682
|
return collected.join("\n\n").trim();
|
|
577
683
|
}
|
|
578
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
|
+
|
|
579
712
|
function safeDateMs(value) {
|
|
580
713
|
const parsed = Date.parse(String(value || ""));
|
|
581
714
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
@@ -594,6 +727,33 @@ function readFileMtimeMs(file, fallback = Date.now()) {
|
|
|
594
727
|
}
|
|
595
728
|
}
|
|
596
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
|
+
|
|
597
757
|
function truncate(value, max) {
|
|
598
758
|
const text = String(value || "").trim();
|
|
599
759
|
if (text.length <= max) return text;
|
|
@@ -608,21 +768,59 @@ function isNoisePrompt(text) {
|
|
|
608
768
|
if (raw.includes("Filesystem sandboxing defines")) return true;
|
|
609
769
|
if (raw.includes("AGENT_COMPANION_PERSIST_SERVER_HINT_V1")) return true;
|
|
610
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;
|
|
774
|
+
if (/^\[background-service\]/i.test(raw.trim())) return true;
|
|
775
|
+
if (/"service"\s*:\s*\{/.test(raw) && /"localhostUrl"\s*:/.test(raw)) return true;
|
|
611
776
|
if (raw === "Answer questions?") return true;
|
|
612
777
|
return false;
|
|
613
778
|
}
|
|
614
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
|
+
|
|
615
806
|
function appendDirectTurn(turns, input) {
|
|
616
807
|
const text = sanitizeDirectText(input?.text);
|
|
617
|
-
if (!text ||
|
|
808
|
+
if (!text || !shouldIncludeDirectText(text)) return;
|
|
618
809
|
|
|
619
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";
|
|
620
817
|
const createdAt = safeNumber(input?.createdAt, Date.now());
|
|
621
818
|
const normalized = normalizeComparableText(text);
|
|
622
819
|
const last = turns[turns.length - 1];
|
|
623
820
|
if (
|
|
624
821
|
last &&
|
|
625
822
|
last.role === role &&
|
|
823
|
+
last.kind === kind &&
|
|
626
824
|
normalizeComparableText(last.text) === normalized &&
|
|
627
825
|
Math.abs(safeNumber(last.createdAt, 0) - createdAt) <= 3_000
|
|
628
826
|
) {
|
|
@@ -632,7 +830,7 @@ function appendDirectTurn(turns, input) {
|
|
|
632
830
|
turns.push({
|
|
633
831
|
sessionId: input?.sessionId || "",
|
|
634
832
|
role,
|
|
635
|
-
kind
|
|
833
|
+
kind,
|
|
636
834
|
text,
|
|
637
835
|
createdAt,
|
|
638
836
|
source: "DIRECT"
|
|
@@ -672,6 +870,12 @@ function sanitizeDirectText(value) {
|
|
|
672
870
|
}
|
|
673
871
|
|
|
674
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
|
+
|
|
675
879
|
const records = [];
|
|
676
880
|
|
|
677
881
|
if (!fs.existsSync(rootDir)) return records;
|
|
@@ -708,5 +912,104 @@ function getRecentJsonlFiles(rootDir, limit) {
|
|
|
708
912
|
}
|
|
709
913
|
|
|
710
914
|
records.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
711
|
-
|
|
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;
|
|
712
1015
|
}
|