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/CHANGELOG.md +61 -0
- package/README.md +48 -4
- package/assets/logo.svg +51 -0
- package/hooks.json +1 -1
- package/install.ps1 +86 -42
- package/mcp/index.js +323 -101
- package/package.json +18 -2
- package/plugin.json +9 -2
- package/rules/AGENTS.md +9 -4
- package/scripts/execution-guard-hook.mjs +347 -280
- package/scripts/install-register.mjs +178 -64
- package/scripts/provision-rtk.mjs +67 -14
- package/scripts/release-preflight.mjs +147 -0
- package/skills/context-governance/SKILL.md +26 -50
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
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
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 (
|
|
59
|
+
} else if (statusFlag) {
|
|
45
60
|
runStatus();
|
|
46
61
|
process.exit(0);
|
|
47
|
-
} else if (
|
|
48
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
376
|
+
try {
|
|
377
|
+
for await (const line of rl) {
|
|
378
|
+
if (!line.trim()) continue;
|
|
379
|
+
totalSteps++;
|
|
380
|
+
totalBytes += line.length;
|
|
320
381
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
382
|
+
if (totalSteps > MAX_LINES) {
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
324
385
|
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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:
|
|
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
|
|
473
|
-
if (
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
|
510
|
-
const
|
|
511
|
-
const brainCandidate = path.join(
|
|
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(
|
|
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
|
-
|
|
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 (!
|
|
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
|
|
701
|
-
|
|
702
|
-
|
|
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
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
|
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
|
|
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
|
|
899
|
-
const
|
|
900
|
-
const
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
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: ${
|
|
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: ${
|
|
1243
|
+
`- Matched Steps: ${matchedCount} (showing ${finalItems.length})`,
|
|
1022
1244
|
"",
|
|
1023
1245
|
"---",
|
|
1024
1246
|
""
|