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.
@@ -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 = 24;
6
- const MAX_SESSION_AGE_MS = 3 * 24 * 60 * 60 * 1000;
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, 12);
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, 12);
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, 30);
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
- let raw = "";
123
- try {
124
- raw = fs.readFileSync(file, "utf8");
125
- } catch {
126
- return null;
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) latestAgentMessage = message;
184
- appendDirectTurn(chatTurns, {
185
- sessionId: "",
186
- role: "ASSISTANT",
187
- text: message,
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
- appendDirectTurn(chatTurns, {
238
- sessionId: "",
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
- let raw = "";
339
- try {
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
- if (assistantText) latestAssistantText = assistantText;
401
- appendDirectTurn(chatTurns, {
402
- sessionId: "",
403
- role: "ASSISTANT",
404
- text: assistantText,
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: ageSec > 25,
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 > 20) return "FAILED";
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 (!isNoisePrompt(text)) return text;
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 isNoisePrompt(text) ? "" : text;
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 (text && !isNoisePrompt(text)) return text;
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 (text && !isNoisePrompt(text)) return text;
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 (text && !isNoisePrompt(text)) return text;
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 isNoisePrompt(text) ? "" : text;
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 (!isNoisePrompt(text)) {
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 (!isNoisePrompt(text)) {
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 || isNoisePrompt(text)) return;
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: "MESSAGE",
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
- return records.slice(0, limit).map((item) => item.file);
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
  }