agy-context-saver 1.4.0 → 1.6.0

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/mcp/index.js CHANGED
@@ -28,30 +28,51 @@ import os from "node:os";
28
28
  import path from "node:path";
29
29
  import readline from "node:readline";
30
30
  import { fileURLToPath } from "node:url";
31
- import { runInstall, runUninstall, runStatus, detectExistingInstallation } from "../scripts/install-register.mjs";
31
+ import { runInstall, runUninstall, runStatus, runRestoreBackups, detectExistingInstallation } from "../scripts/install-register.mjs";
32
32
  import { findExistingRtk } from "../scripts/provision-rtk.mjs";
33
33
 
34
34
  // Handle CLI subcommands (e.g. npx agy-context-saver install)
35
- const cliArg = process.argv[2];
36
- const checkFlag = process.argv.includes("--check") || process.argv.includes("-c");
37
- if (cliArg === "install" || cliArg === "--install") {
38
- await runInstall({ checkOnly: checkFlag });
35
+ const cliArgs = process.argv.slice(2);
36
+ const cliArg = cliArgs[0];
37
+ const checkFlag = cliArgs.includes("--check") || cliArgs.includes("-c");
38
+ const statusFlag = cliArgs.includes("--status") || cliArgs.includes("status");
39
+ const uninstallFlag = cliArgs.includes("--uninstall") || cliArgs.includes("uninstall");
40
+ const restoreFlag = cliArgs.includes("--restore-backups") || cliArgs.includes("restore-backups");
41
+ const helpFlag = cliArgs.includes("--help") || cliArgs.includes("-h") || cliArgs.includes("help");
42
+ const allowDegraded = cliArgs.includes("--allow-degraded");
43
+
44
+ if (helpFlag) {
45
+ printHelp();
39
46
  process.exit(0);
40
- } else if (cliArg === "uninstall" || cliArg === "--uninstall") {
41
- const restoreBackups = process.argv.includes("--restore-backups");
42
- await runUninstall({ restoreBackups });
47
+ }
48
+ if ((checkFlag || statusFlag) && (uninstallFlag || restoreFlag)) {
49
+ console.error("Error: Read-only flags (--check, --status) cannot be combined with mutating flags (--uninstall, --restore-backups).");
50
+ process.exit(1);
51
+ }
52
+ if (checkFlag && statusFlag) {
53
+ console.error("Error: Conflicting flags: specify either --check or --status, not both.");
54
+ process.exit(1);
55
+ }
56
+ if (checkFlag) {
57
+ await runInstall({ checkOnly: true, allowDegraded });
43
58
  process.exit(0);
44
- } else if (cliArg === "status" || cliArg === "--status") {
59
+ } else if (statusFlag) {
45
60
  runStatus();
46
61
  process.exit(0);
47
- } else if (cliArg === "--help" || cliArg === "-h" || cliArg === "help") {
48
- printHelp();
62
+ } else if (uninstallFlag) {
63
+ await runUninstall({ restoreBackups: restoreFlag });
64
+ process.exit(0);
65
+ } else if (restoreFlag) {
66
+ await runRestoreBackups();
67
+ process.exit(0);
68
+ } else if (cliArg === "install" || cliArg === "--install") {
69
+ await runInstall({ allowDegraded });
49
70
  process.exit(0);
50
71
  }
51
72
 
52
73
  function printHelp() {
53
74
  console.log(`
54
- Agy-Context-Saver 🛡️ (v1.2.1)
75
+ Agy-Context-Saver 🛡️ (v1.6.0)
55
76
  Universal MCP Server & Lifecycle Governor for Google Antigravity (Powered by RTK)
56
77
 
57
78
  Usage:
@@ -98,7 +119,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
98
119
  const rulesPath = path.resolve(__dirname, "../rules/AGENTS.md");
99
120
 
100
121
  const SERVER_NAME = "agy-context-saver";
101
- const SERVER_VERSION = "1.4.0";
122
+ const SERVER_VERSION = "1.6.0";
102
123
 
103
124
  // --- Tools Specification ---
104
125
  const TOOLS = [
@@ -121,13 +142,21 @@ const TOOLS = [
121
142
  },
122
143
  {
123
144
  name: "subagent_brief",
124
- description: "Generates a scope-isolated, anti-bloat prompt for invoking a subagent. Instructs the subagent to absorb all intermediate file reads/searches and return only high-signal findings.",
145
+ description: "Generates scope-isolated, anti-bloat prompts and ready-to-use invoke_subagent JSON payloads for single or multiple delegated subagents. Instructs subagents to absorb intermediate file reads/searches and return only high-signal findings.",
125
146
  inputSchema: {
126
147
  type: "object",
127
148
  properties: {
128
149
  objective: {
129
150
  type: "string",
130
- description: "The core research or investigative objective."
151
+ description: "The core research or investigative objective (for single subagent)."
152
+ },
153
+ role: {
154
+ type: "string",
155
+ description: "2-5 word role description (e.g. 'Comprehensive Test Engineer', 'Codebase Security Auditor'). Defaults to 'Scope-Isolated Deep Research Subagent'."
156
+ },
157
+ instructions: {
158
+ type: "string",
159
+ description: "Optional step-by-step instructions or specialized directives."
131
160
  },
132
161
  scopeFiles: {
133
162
  type: "array",
@@ -137,9 +166,40 @@ const TOOLS = [
137
166
  expectedDeliverable: {
138
167
  type: "string",
139
168
  description: "The concise format expected back by the parent agent."
169
+ },
170
+ typeName: {
171
+ type: "string",
172
+ description: "Subagent type name: 'self' (default, inherits parent tools/prompt/model) or 'research' (read-only)."
173
+ },
174
+ model: {
175
+ type: "string",
176
+ enum: ["inherit", "flash_lite", "flash", "pro"],
177
+ description: "Model tier: 'inherit' (default), 'flash_lite', 'flash', or 'pro'."
178
+ },
179
+ workspace: {
180
+ type: "string",
181
+ enum: ["inherit", "branch", "share"],
182
+ description: "Workspace isolation: 'inherit' (default), 'branch', or 'share'."
183
+ },
184
+ subagents: {
185
+ type: "array",
186
+ description: "Optional array of subagent configurations to formulate multiple subagents at once.",
187
+ items: {
188
+ type: "object",
189
+ properties: {
190
+ role: { type: "string", description: "2-5 word role description." },
191
+ objective: { type: "string", description: "The core objective for this subagent." },
192
+ instructions: { type: "string", description: "Optional step-by-step instructions." },
193
+ scopeFiles: { type: "array", items: { type: "string" }, description: "Files/directories to investigate." },
194
+ expectedDeliverable: { type: "string", description: "Expected deliverable format." },
195
+ typeName: { type: "string", description: "Subagent type name: 'self' or 'research'." },
196
+ model: { type: "string", enum: ["inherit", "flash_lite", "flash", "pro"], description: "Model tier." },
197
+ workspace: { type: "string", enum: ["inherit", "branch", "share"], description: "Workspace isolation." }
198
+ },
199
+ required: ["objective"]
200
+ }
140
201
  }
141
- },
142
- required: ["objective"]
202
+ }
143
203
  }
144
204
  },
145
205
  {
@@ -313,55 +373,77 @@ async function handleCheckContextHealth(args = {}) {
313
373
  let corruptLines = 0;
314
374
  const MAX_LINES = 100000;
315
375
 
316
- for await (const line of rl) {
317
- if (!line.trim()) continue;
318
- totalSteps++;
319
- totalBytes += line.length;
376
+ try {
377
+ for await (const line of rl) {
378
+ if (!line.trim()) continue;
379
+ totalSteps++;
380
+ totalBytes += line.length;
320
381
 
321
- if (totalSteps > MAX_LINES) {
322
- break;
323
- }
382
+ if (totalSteps > MAX_LINES) {
383
+ break;
384
+ }
324
385
 
325
- try {
326
- const item = JSON.parse(line);
327
- if (item.type === "USER_INPUT") userTurns++;
328
- if (item.type === "PLANNER_RESPONSE") modelTurns++;
329
- if (item.tool_calls && item.tool_calls.length) {
330
- toolCalls += item.tool_calls.length;
331
- for (const tc of item.tool_calls) {
332
- const name = tc.tool_name || tc.name;
333
- const args = tc.args || tc.arguments || {};
334
- const action = String(args.Action || args.action || "").replace(/^["']|["']$/g, "").trim().toLowerCase();
335
- const prompt = String(args.Prompt || args.prompt || "").replace(/^["']|["']$/g, "").trim().toLowerCase();
336
- const cond = String(args.TimerCondition || args.timerCondition || "").replace(/^["']|["']$/g, "").trim().toLowerCase();
337
- if (name === "manage_task" && action === "status") pollingEvents++;
338
- if (name === "schedule" && (cond.startsWith("task") || cond.includes("task") || prompt.includes("test") || prompt.includes("check on") || prompt.includes("status"))) pollingEvents++;
386
+ // Fast check for common step types without parsing if no tool calls (PERF-16)
387
+ const hasToolCalls = line.includes('"tool_calls"');
388
+ const head = line.slice(0, 300);
389
+ const isUserInput = head.includes('"type":"USER_INPUT"');
390
+ const isPlannerResp = head.includes('"type":"PLANNER_RESPONSE"');
391
+
392
+ if (!hasToolCalls && (isUserInput || isPlannerResp)) {
393
+ if (isUserInput) userTurns++;
394
+ else modelTurns++;
395
+ continue;
396
+ }
397
+
398
+ try {
399
+ const item = JSON.parse(line);
400
+ if (item.type === "USER_INPUT") userTurns++;
401
+ if (item.type === "PLANNER_RESPONSE") modelTurns++;
402
+ if (item.tool_calls && item.tool_calls.length) {
403
+ toolCalls += item.tool_calls.length;
404
+ for (const tc of item.tool_calls) {
405
+ const name = tc.tool_name || tc.name;
406
+ const args = tc.args || tc.arguments || {};
407
+ const action = String(args.Action || args.action || "").replace(/^["']|["']$/g, "").trim().toLowerCase();
408
+ const prompt = String(args.Prompt || args.prompt || "").replace(/^["']|["']$/g, "").trim().toLowerCase();
409
+ const cond = String(args.TimerCondition || args.timerCondition || "").replace(/^["']|["']$/g, "").trim().toLowerCase();
410
+ if (name === "manage_task" && action === "status") pollingEvents++;
411
+ if (name === "schedule" && (cond.startsWith("task") || cond.includes("task") || prompt.includes("test") || prompt.includes("check on") || prompt.includes("status"))) pollingEvents++;
412
+ }
339
413
  }
414
+ } catch (err) {
415
+ corruptLines++;
340
416
  }
341
- } catch (err) {
342
- corruptLines++;
343
417
  }
418
+ } finally {
419
+ rl.close();
420
+ fileStream.destroy();
344
421
  }
345
-
346
422
  const kb = (totalBytes / 1024).toFixed(1);
347
423
  const healthStatus = pollingEvents > 3 ? "CRITICAL (Active Polling Loops Detected)" : userTurns > 40 ? "WARNING (High Turn Budget)" : "HEALTHY";
348
424
 
349
425
  const rtkInfo = findExistingRtk();
350
426
  const reportLines = [
351
427
  `### Context Health Report: ${healthStatus}`,
428
+ "",
429
+ "#### [Active Session Metrics]",
352
430
  `- Total Steps: ${totalSteps}`,
353
431
  `- User Turns: ${userTurns}`,
354
432
  `- Assistant Responses: ${modelTurns}`,
355
433
  `- Tool Calls: ${toolCalls}`,
356
434
  `- Detected Busy-Polling Events: ${pollingEvents}`,
357
- `- Approximate Raw Transcript Size: ${kb} KB`,
358
- `- RTK Command Optimizer: ${rtkInfo ? `Active (${rtkInfo.version})` : "Missing (run sync_installation)"}`
435
+ `- Approximate Raw Transcript Size: ${kb} KB`
359
436
  ];
360
437
 
361
438
  if (corruptLines > 0) {
362
439
  reportLines.push(`- Corrupted/Unparsed Lines: ${corruptLines}`);
363
440
  }
364
441
 
442
+ reportLines.push("");
443
+ reportLines.push("#### [Local RTK Environment]");
444
+ reportLines.push(`- RTK Command Optimizer: ${rtkInfo ? `Active (${rtkInfo.version})` : "Missing (run sync_installation)"}`);
445
+ reportLines.push(`- Note: Lifetime token gains from 'rtk gain' represent cumulative machine-wide host savings, not single-session metrics.`);
446
+
365
447
  reportLines.push("");
366
448
  if (pollingEvents > 0) {
367
449
  reportLines.push("⚠️ Recommendation: Background polling detected! Cease manage_task(status) calls and yield execution to native Reactive Wakeup.");
@@ -381,11 +463,18 @@ async function handleCheckContextHealth(args = {}) {
381
463
  }
382
464
  }
383
465
 
384
- function handleSubagentBrief({ objective, scopeFiles = [], expectedDeliverable = "Synthesized factual findings" }) {
385
- const scopeStr = scopeFiles.length ? `\n\nTarget files:\n${scopeFiles.map(f => `- ${f}`).join("\n")}` : "";
386
- const prompt = [
387
- `ROLE: Scope-Isolated Deep Research Subagent`,
388
- `OBJECTIVE: ${objective}${scopeStr}`,
466
+ function formatSingleBrief({
467
+ role = "Scope-Isolated Deep Research Subagent",
468
+ objective = "",
469
+ instructions = "",
470
+ scopeFiles = [],
471
+ expectedDeliverable = "Synthesized factual findings"
472
+ } = {}) {
473
+ const scopeStr = scopeFiles && scopeFiles.length ? `\n\nTarget files:\n${scopeFiles.map(f => `- ${f}`).join("\n")}` : "";
474
+ const instrStr = instructions && instructions.trim() ? `\n\nSPECIFIC INSTRUCTIONS:\n${instructions.trim()}` : "";
475
+ return [
476
+ `ROLE: ${role}`,
477
+ `OBJECTIVE: ${objective}${scopeStr}${instrStr}`,
389
478
  ``,
390
479
  `CRITICAL CONTEXT GOVERNANCE INSTRUCTIONS (POINTER-OVER-WIRE):`,
391
480
  `1. Isolated Workspace: Do all necessary multi-file exploration, searches, and analysis in this subagent sandbox.`,
@@ -399,8 +488,71 @@ function handleSubagentBrief({ objective, scopeFiles = [], expectedDeliverable =
399
488
  `4. Format: ${expectedDeliverable}`,
400
489
  `5. Citation Mandate: Keep your final response concise, structured, and cite exact file:line references.`
401
490
  ].join("\n");
491
+ }
492
+
493
+ function handleSubagentBrief(args = {}) {
494
+ let specs = [];
495
+ if (Array.isArray(args.subagents) && args.subagents.length > 0) {
496
+ specs = args.subagents;
497
+ } else if (args.objective || args.role || args.instructions) {
498
+ specs = [args];
499
+ } else {
500
+ return {
501
+ isError: true,
502
+ content: [{ type: "text", text: "subagent_brief error: provide either 'objective' (for a single subagent) or 'subagents' array (for multiple subagents)." }]
503
+ };
504
+ }
505
+
506
+ const subagentsPayload = [];
507
+ const renderedBriefs = [];
508
+
509
+ for (let i = 0; i < specs.length; i++) {
510
+ const spec = specs[i] || {};
511
+ const role = spec.role || spec.Role || "Scope-Isolated Deep Research Subagent";
512
+ const objective = spec.objective || spec.Objective || "";
513
+ const instructions = spec.instructions || spec.Instructions || "";
514
+ const scopeFiles = spec.scopeFiles || spec.ScopeFiles || [];
515
+ const expectedDeliverable = spec.expectedDeliverable || spec.ExpectedDeliverable || "Synthesized factual findings";
516
+ const typeName = spec.typeName || spec.TypeName || "self";
517
+ const model = spec.model || spec.Model || "inherit";
518
+ const workspace = spec.workspace || spec.Workspace || "inherit";
519
+
520
+ const promptText = formatSingleBrief({
521
+ role,
522
+ objective,
523
+ instructions,
524
+ scopeFiles,
525
+ expectedDeliverable
526
+ });
527
+
528
+ subagentsPayload.push({
529
+ TypeName: typeName,
530
+ Role: role,
531
+ Model: model,
532
+ Workspace: workspace,
533
+ Prompt: promptText
534
+ });
535
+
536
+ if (specs.length === 1) {
537
+ renderedBriefs.push(promptText);
538
+ } else {
539
+ renderedBriefs.push(`### Subagent ${i + 1}: ${role}\n${promptText}`);
540
+ }
541
+ }
542
+
543
+ const payloadJson = JSON.stringify({ Subagents: subagentsPayload }, null, 2);
544
+
545
+ const fullText = [
546
+ renderedBriefs.join("\n\n---\n\n"),
547
+ "",
548
+ "==================================================",
549
+ "READY-TO-USE invoke_subagent PAYLOAD:",
550
+ "```json",
551
+ payloadJson,
552
+ "```"
553
+ ].join("\n");
402
554
 
403
- return { content: [{ type: "text", text: prompt }] };
555
+ return { content: [{ type: "text", text: fullText }] };
404
556
  }
405
557
 
406
558
  function handleGetInstallationStatus() {
@@ -459,6 +611,17 @@ function extractConvId(p) {
459
611
  return match ? match[1] : path.basename(normalized, path.extname(normalized));
460
612
  }
461
613
 
614
+ let cachedActiveConv = null;
615
+ let cachedActiveConvTime = 0;
616
+
617
+ function getProfileHomeDir() {
618
+ return process.env.AGY_TEST_PROFILE_DIR || os.homedir();
619
+ }
620
+
621
+ function getBrainDir() {
622
+ return process.env.AGY_BRAIN_DIR || path.join(getProfileHomeDir(), ".gemini", "antigravity", "brain");
623
+ }
624
+
462
625
  function resolveTranscriptPath(target, mode = "compact") {
463
626
  const isFull = mode === "full";
464
627
  const fileName = isFull ? "transcript_full.jsonl" : "transcript.jsonl";
@@ -469,19 +632,38 @@ function resolveTranscriptPath(target, mode = "compact") {
469
632
  if (process.env.GEMINI_CONVERSATION_ID) {
470
633
  raw = process.env.GEMINI_CONVERSATION_ID;
471
634
  } else {
472
- const brainDir = path.join(os.homedir(), ".gemini", "antigravity", "brain");
473
- if (fs.existsSync(brainDir)) {
635
+ const now = Date.now();
636
+ if (cachedActiveConv && (now - cachedActiveConvTime) < 5000) {
637
+ raw = cachedActiveConv;
638
+ } else {
639
+ const brainDir = getBrainDir();
474
640
  try {
475
- const entries = fs.readdirSync(brainDir, { withFileTypes: true })
476
- .filter((d) => d.isDirectory())
477
- .map((d) => {
478
- const p = path.join(brainDir, d.name);
479
- const stat = fs.statSync(p);
480
- return { name: d.name, mtime: stat.mtimeMs };
481
- })
482
- .sort((a, b) => b.mtime - a.mtime);
483
- if (entries.length > 0) {
484
- raw = entries[0].name;
641
+ const entries = fs.readdirSync(brainDir, { withFileTypes: true });
642
+ let bestMtime = 0;
643
+ let bestName = null;
644
+ for (const d of entries) {
645
+ if (!d.isDirectory()) continue;
646
+ try {
647
+ const transPath = path.join(brainDir, d.name, ".system_generated", "logs", "transcript.jsonl");
648
+ const mtime = fs.statSync(transPath).mtimeMs;
649
+ if (mtime > bestMtime) {
650
+ bestMtime = mtime;
651
+ bestName = d.name;
652
+ }
653
+ } catch {
654
+ try {
655
+ const mtime = fs.statSync(path.join(brainDir, d.name)).mtimeMs;
656
+ if (mtime > bestMtime) {
657
+ bestMtime = mtime;
658
+ bestName = d.name;
659
+ }
660
+ } catch {}
661
+ }
662
+ }
663
+ if (bestName) {
664
+ raw = bestName;
665
+ cachedActiveConv = bestName;
666
+ cachedActiveConvTime = now;
485
667
  }
486
668
  } catch {}
487
669
  }
@@ -506,14 +688,14 @@ function resolveTranscriptPath(target, mode = "compact") {
506
688
  }
507
689
  }
508
690
 
509
- // 4. UUID / Directory resolution under ~/.gemini/antigravity/brain/<id>
510
- const homeDir = os.homedir();
511
- const brainCandidate = path.join(homeDir, ".gemini", "antigravity", "brain", normalized, ".system_generated", "logs", fileName);
691
+ // 4. UUID / Directory resolution under <brainDir>/<id>
692
+ const primaryBrainDir = getBrainDir();
693
+ const brainCandidate = path.join(primaryBrainDir, normalized, ".system_generated", "logs", fileName);
512
694
  if (fs.existsSync(brainCandidate)) {
513
695
  return { filePath: brainCandidate, convId: normalized };
514
696
  }
515
697
  if (isFull) {
516
- const compactFallback = path.join(homeDir, ".gemini", "antigravity", "brain", normalized, ".system_generated", "logs", "transcript.jsonl");
698
+ const compactFallback = path.join(primaryBrainDir, normalized, ".system_generated", "logs", "transcript.jsonl");
517
699
  if (fs.existsSync(compactFallback)) {
518
700
  return { filePath: compactFallback, convId: normalized };
519
701
  }
@@ -541,6 +723,11 @@ function cleanMessageContent(content, maxChars = MAX_TURN_CHARS) {
541
723
  return text;
542
724
  }
543
725
 
726
+ const origLength = text.length;
727
+ // Pre-slice huge text to ~4x maxChars before heavy regex and line clamping (PERF-15)
728
+ if (text.length > maxChars * 4) {
729
+ text = text.slice(0, maxChars * 4);
730
+ }
544
731
  // Strip huge base64 media blocks only if relevant keywords or large length
545
732
  if (text.includes("base64") || text.length > 200) {
546
733
  text = text.replace(/data:image\/[^;]+;base64,[A-Za-z0-9+/=]+/g, "[Embedded Media/Binary Omitted]");
@@ -563,8 +750,9 @@ function cleanMessageContent(content, maxChars = MAX_TURN_CHARS) {
563
750
  }
564
751
  }
565
752
 
566
- if (text.length > maxChars) {
567
- return text.slice(0, maxChars) + `\n\n... [Content Truncated (${text.length - maxChars} chars omitted to preserve token budget)] ...`;
753
+ if (origLength > maxChars || text.length > maxChars) {
754
+ const omitted = Math.max(origLength - maxChars, 0);
755
+ return text.slice(0, maxChars) + `\n\n... [Content Truncated (${omitted} chars omitted to preserve token budget)] ...`;
568
756
  }
569
757
  return text;
570
758
  }
@@ -662,12 +850,14 @@ function getCompiledRegex(pattern) {
662
850
 
663
851
  async function batchFindFullSteps(fullPath, stepIndicesSet) {
664
852
  const result = new Map();
665
- if (!fs.existsSync(fullPath) || !stepIndicesSet || stepIndicesSet.size === 0) return result;
666
-
667
- const fileStream = fs.createReadStream(fullPath, { encoding: "utf-8" });
668
- const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
853
+ if (!stepIndicesSet || stepIndicesSet.size === 0) return result;
669
854
 
855
+ let fileStream;
856
+ let rl;
670
857
  try {
858
+ fileStream = fs.createReadStream(fullPath, { encoding: "utf-8" });
859
+ rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
860
+
671
861
  for await (const line of rl) {
672
862
  if (!line.trim()) continue;
673
863
  try {
@@ -675,13 +865,15 @@ async function batchFindFullSteps(fullPath, stepIndicesSet) {
675
865
  if (stepIndicesSet.has(item.step_index)) {
676
866
  result.set(item.step_index, item);
677
867
  if (result.size >= stepIndicesSet.size) {
678
- rl.close();
679
868
  break;
680
869
  }
681
870
  }
682
871
  } catch {}
683
872
  }
684
- } catch {}
873
+ } catch {} finally {
874
+ if (rl) rl.close();
875
+ if (fileStream) fileStream.destroy();
876
+ }
685
877
  return result;
686
878
  }
687
879
 
@@ -697,35 +889,49 @@ async function handleReadTranscript({ conversationId, mode = "compact", lastTurn
697
889
  }
698
890
 
699
891
  const { filePath, convId } = resolved;
700
- const items = [];
701
- const fileStream = fs.createReadStream(filePath, { encoding: "utf-8" });
702
- const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
892
+ const rawLines = [];
893
+ let fileStream;
894
+ let rl;
703
895
 
704
896
  let totalStepsCount = 0;
705
897
  const WINDOW_SIZE = lastTurns > 0 ? Math.max(100, lastTurns * 15) : Infinity;
706
898
 
707
899
  try {
900
+ fileStream = fs.createReadStream(filePath, { encoding: "utf-8" });
901
+ rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
708
902
  for await (const line of rl) {
709
- if (!line.trim()) continue;
710
- try {
711
- const item = JSON.parse(line);
712
- totalStepsCount++;
713
- items.push(item);
714
- if (items.length > WINDOW_SIZE) {
715
- items.shift();
903
+ const trimmed = line.trim();
904
+ if (!trimmed) continue;
905
+ totalStepsCount++;
906
+ if (WINDOW_SIZE === Infinity) {
907
+ rawLines.push(trimmed);
908
+ } else {
909
+ rawLines.push(trimmed);
910
+ if (rawLines.length > WINDOW_SIZE) {
911
+ rawLines.shift();
716
912
  }
717
- } catch {
718
- // Tolerates incomplete trailing flushes during concurrent writes
719
913
  }
720
914
  }
721
915
  } catch (err) {
722
916
  return { isError: true, content: [{ type: "text", text: `Error reading transcript stream: ${err.message}` }] };
917
+ } finally {
918
+ if (rl) rl.close();
919
+ if (fileStream) fileStream.destroy();
723
920
  }
724
921
 
725
922
  if (totalStepsCount === 0) {
726
923
  return { content: [{ type: "text", text: `Transcript is empty at: ${filePath}` }] };
727
924
  }
728
925
 
926
+ const items = [];
927
+ for (const line of rawLines) {
928
+ try {
929
+ items.push(JSON.parse(line));
930
+ } catch {
931
+ // Tolerates incomplete trailing flushes during concurrent writes
932
+ }
933
+ }
934
+
729
935
  let selected = items;
730
936
  if (lastTurns > 0) {
731
937
  const turnItems = items.filter((it) => it.type === "USER_INPUT" || it.type === "PLANNER_RESPONSE");
@@ -733,7 +939,6 @@ async function handleReadTranscript({ conversationId, mode = "compact", lastTurn
733
939
  const minStep = turnItems[cutoff] ? turnItems[cutoff].step_index : 0;
734
940
  selected = items.filter((it) => (it.step_index || 0) >= minStep);
735
941
  }
736
-
737
942
  const header = [
738
943
  `# Conversation Transcript: \`${convId}\``,
739
944
  `- Mode: **${mode}** (${path.basename(filePath)})`,
@@ -798,17 +1003,16 @@ async function handleQueryTranscript(args = {}) {
798
1003
 
799
1004
  const resolvedFull = resolveTranscriptPath(conversationId, "full");
800
1005
  let step = null;
801
- if (resolvedFull && resolvedFull.filePath && fs.existsSync(resolvedFull.filePath)) {
1006
+ if (resolvedFull && resolvedFull.filePath) {
802
1007
  step = await findFullStep(resolvedFull.filePath, numIndex);
803
1008
  }
804
1009
 
805
1010
  if (!step) {
806
1011
  const resolvedCompact = resolveTranscriptPath(conversationId, "compact");
807
- if (resolvedCompact && resolvedCompact.filePath && fs.existsSync(resolvedCompact.filePath)) {
1012
+ if (resolvedCompact && resolvedCompact.filePath) {
808
1013
  step = await findFullStep(resolvedCompact.filePath, numIndex);
809
1014
  }
810
1015
  }
811
-
812
1016
  if (!step) {
813
1017
  return {
814
1018
  isError: true,
@@ -888,20 +1092,31 @@ async function handleQueryTranscript(args = {}) {
888
1092
 
889
1093
  const { filePath, convId } = resolved;
890
1094
  const fullSiblingPath = filePath.replace(/transcript\.jsonl$/, "transcript_full.jsonl");
891
- const hasFullSibling = fs.existsSync(fullSiblingPath);
892
1095
 
893
1096
  const roleSet = new Set((Array.isArray(roles) ? roles : [roles]).map((r) => String(r).toLowerCase()));
894
1097
  const matchAllRoles = roleSet.has("all");
895
1098
 
896
1099
  const queryRegex = getCompiledRegex(query);
1100
+ const isPlainLiteral = query && /^[A-Za-z0-9_ -]+$/.test(query);
897
1101
 
898
- const matched = [];
899
- const fileStream = fs.createReadStream(filePath, { encoding: "utf-8" });
900
- const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
1102
+ const effectiveLimit = Math.min(lastTurns > 0 ? lastTurns : Infinity, maxResults > 0 ? maxResults : 5);
1103
+ const ringCap = Number.isFinite(effectiveLimit) ? effectiveLimit : 1000;
1104
+ const ring = [];
1105
+ let matchedCount = 0;
1106
+
1107
+ let fileStream;
1108
+ let rl;
901
1109
 
902
1110
  try {
1111
+ fileStream = fs.createReadStream(filePath, { encoding: "utf-8" });
1112
+ rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
1113
+
903
1114
  for await (const line of rl) {
904
1115
  if (!line.trim()) continue;
1116
+
1117
+ // PERF-12: Raw-line prefilter for plain literal queries
1118
+ if (isPlainLiteral && !queryRegex.test(line)) continue;
1119
+
905
1120
  let item;
906
1121
  try {
907
1122
  item = JSON.parse(line);
@@ -943,13 +1158,20 @@ async function handleQueryTranscript(args = {}) {
943
1158
  if (!matches) continue;
944
1159
  }
945
1160
 
946
- matched.push(item);
1161
+ matchedCount++;
1162
+ ring.push(item);
1163
+ if (ring.length > ringCap) {
1164
+ ring.shift();
1165
+ }
947
1166
  }
948
1167
  } catch (err) {
949
1168
  return { isError: true, content: [{ type: "text", text: `Error streaming transcript: ${err.message}` }] };
1169
+ } finally {
1170
+ if (rl) rl.close();
1171
+ if (fileStream) fileStream.destroy();
950
1172
  }
951
1173
 
952
- if (matched.length === 0) {
1174
+ if (matchedCount === 0) {
953
1175
  return {
954
1176
  content: [{
955
1177
  type: "text",
@@ -958,8 +1180,8 @@ async function handleQueryTranscript(args = {}) {
958
1180
  };
959
1181
  }
960
1182
 
961
- let finalItems = matched;
962
- if (lastTurns && lastTurns > 0) {
1183
+ let finalItems = ring;
1184
+ if (lastTurns && lastTurns > 0 && finalItems.length > lastTurns) {
963
1185
  finalItems = finalItems.slice(-lastTurns);
964
1186
  }
965
1187
  if (finalItems.length > maxResults) {
@@ -967,8 +1189,8 @@ async function handleQueryTranscript(args = {}) {
967
1189
  }
968
1190
 
969
1191
  // Auto-dereferencing if mode is "auto" and fields were truncated (single pass)
970
- if (mode === "auto" && hasFullSibling) {
971
- const truncatedIndices = new Set();
1192
+ // PERF-17: check fs.existsSync(fullSiblingPath) lazily ONLY if truncatedIndices.size > 0
1193
+ if (mode === "auto") { const truncatedIndices = new Set();
972
1194
  for (const it of finalItems) {
973
1195
  if (Array.isArray(it.truncated_fields) && it.truncated_fields.length > 0 && it.step_index !== undefined) {
974
1196
  truncatedIndices.add(it.step_index);
@@ -1002,7 +1224,7 @@ async function handleQueryTranscript(args = {}) {
1002
1224
  const header = [
1003
1225
  `## Transcript Query Summary: \`${convId}\``,
1004
1226
  `- Filter: query=${query ? `"${query}"` : "NONE"}, roles=[${Array.from(roleSet).join(", ")}], mode=${mode}`,
1005
- `- Matched Steps: ${matched.length} (showing ${finalItems.length} in compact summary mode)`,
1227
+ `- Matched Steps: ${matchedCount} (showing ${finalItems.length} in compact summary mode)`,
1006
1228
  "",
1007
1229
  "---",
1008
1230
  ""
@@ -1018,7 +1240,7 @@ async function handleQueryTranscript(args = {}) {
1018
1240
  const header = [
1019
1241
  `## Transcript Query Results: \`${convId}\``,
1020
1242
  `- Filter: query=${query ? `"${query}"` : "NONE"}, roles=[${Array.from(roleSet).join(", ")}], mode=${mode}`,
1021
- `- Matched Steps: ${matched.length} (showing ${finalItems.length})`,
1243
+ `- Matched Steps: ${matchedCount} (showing ${finalItems.length})`,
1022
1244
  "",
1023
1245
  "---",
1024
1246
  ""