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.
@@ -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
  }
@@ -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
- let raw = "";
338
- try {
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
- if (assistantText) latestAssistantText = assistantText;
400
- appendDirectTurn(chatTurns, {
401
- sessionId: "",
402
- role: "ASSISTANT",
403
- text: assistantText,
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: ageSec > 25,
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 > 20) return "FAILED";
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 (!isNoisePrompt(text)) return text;
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 isNoisePrompt(text) ? "" : text;
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 (text && !isNoisePrompt(text)) return text;
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 (text && !isNoisePrompt(text)) return text;
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 (text && !isNoisePrompt(text)) return text;
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 isNoisePrompt(text) ? "" : text;
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 (!isNoisePrompt(text)) {
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 (!isNoisePrompt(text)) {
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 || isNoisePrompt(text)) return;
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: "MESSAGE",
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
- 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;
712
1015
  }