agy-context-saver 1.3.2 → 1.5.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 +34 -0
- package/README.md +40 -3
- package/hooks.json +1 -1
- package/mcp/index.js +290 -79
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/scripts/execution-guard-hook.mjs +261 -78
- package/scripts/install-register.mjs +29 -11
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,40 @@ All notable changes to `Agy-Context-Saver` will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.5.0] - 2026-10-08
|
|
9
|
+
|
|
10
|
+
### Performance & Memory Optimizations
|
|
11
|
+
- **File-Descriptor Leak Fixes (PERF-01, PERF-02)**: Ensured guaranteed stream destruction via `finally { rl.close(); fileStream.destroy(); }` across `batchFindFullSteps`, `handleCheckContextHealth`, `handleReadTranscript`, and `handleQueryTranscript`, eliminating open handle leaks on early exits and line caps (verified 0/20 leaked).
|
|
12
|
+
- **Subprocess Call Reduction in PreToolUse Hook (PERF-03)**: Dropped redundant `rtk --version` probe in hook initialization; executes `rtk rewrite` directly and falls back to plugin binary only on `ENOENT`, reducing cache miss latency from 302ms to 249ms (-17.5%).
|
|
13
|
+
- **Surgical Tail-Window Task State Resolution (PERF-04, PERF-07)**: Unified task lifecycle checking into a single `getTaskState()` reading the last 256KB tail of `transcript.jsonl` with literal needle matching (`scanTaskMarkers`), dropping check latency from 123ms to 2ms on 10.5MB transcripts (-98%).
|
|
14
|
+
- **Atomic Concurrency-Safe Rewrite Cache (PERF-05, PERF-09)**: Rewrite cache now filters out timeouts and non-zero/non-clean exit codes, re-reads disk state to merge concurrent updates, and persists atomically via temporary files.
|
|
15
|
+
- **Pre-Rewrite Fast Denial Ordering (PERF-10)**: Reordered command checks so raw-command gates (internal state access and running task log denials) execute before invoking `rtk rewrite`, eliminating subprocess overhead for denied commands.
|
|
16
|
+
- **Bounded Query Memory & Raw-Line Prefilter (PERF-11, PERF-12)**: Implemented ring-buffered step retention bounded by `effectiveLimit` with `matchedCount` in `query_transcript`, and added raw-line ASCII substring pre-filtering to skip `JSON.parse` on non-matching lines.
|
|
17
|
+
- **Active Conversation Resolution Cache (PERF-13)**: Cached `resolveTranscriptPath("current")` lookups for 5s and implemented resilient per-entry directory stats.
|
|
18
|
+
- **Sliding-Window Parsing & String Pre-Slicing (PERF-14, PERF-15, PERF-16)**: `read_transcript` now streams raw lines and only parses retained turns; `cleanMessageContent` pre-slices oversized payloads to `4 * maxChars` before running regexes; `check_context_health` skips parsing for steps without tool calls.
|
|
19
|
+
- **Redundant Syscall Elimination (PERF-17, PERF-18, PERF-19)**: Removed unnecessary `existsSync` checks before reads and replaced dynamic task id regexes with literal string searching.
|
|
20
|
+
|
|
21
|
+
### Governance & Configuration Hardening
|
|
22
|
+
- **Installed Hook Matcher Inclusion (OBS-A)**: Added `replace_file_content|write_to_file` to hook matchers in `hooks.json` and `scripts/install-register.mjs`, ensuring benchmark mutation guards (LH-09) are wired in installed environments.
|
|
23
|
+
- **Transcript Authority for Silent Running Tasks (OBS-B)**: Updated task state resolver to treat background tasks with start notices as actively `RUNNING` regardless of log quiet duration until an explicit finish notice appears, preventing premature inspection of quiet background jobs.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## [1.4.0] - 2026-10-08
|
|
28
|
+
|
|
29
|
+
### Added (Output Inspection Governance & Forensic Dereferencing - LH-12)
|
|
30
|
+
- **Folded Surgical Step Dereferencing in `query_transcript` (NFF-05)**: Added optional `stepIndex` (number) and `field` (string) parameters to `query_transcript`, allowing surgical single-step extraction from `transcript_full.jsonl` without creating redundant tools. Recovers un-truncated subagent messages and arguments with zero surrounding context bloat.
|
|
31
|
+
- **Safe-Harbor `rtk read` for Step Output Spillovers (NFF-01 & NFF-02)**: Provided a safe harbor in `execution-guard-hook.mjs` allowing `rtk read` on runtime step output spillovers (`.system_generated/steps/<step>/output.txt`). Native `view_file` and shell `cat` invocations are automatically routed to `rtk read <path>`, leveraging `rtk read`'s built-in line clamping, head/tail windowing, and token truncation.
|
|
32
|
+
- **Lifecycle-Gated Task Output Inspection (NFF-03)**: Added lifecycle governance for background task logs (`tasks/<taskId>.log`) in `execution-guard-hook.mjs`. Invocations are strictly **denied** under Proportional Backoff while tasks are `RUNNING` to stop polling loops, but allowed via `rtk read` once `COMPLETED` / `TERMINATED` for bounded stack trace and failure inspection.
|
|
33
|
+
- **Pointer-Over-Wire (POW) Subagent Contract (NFF-06)**: Integrated the POW protocol into `subagent_brief`. Research findings $>1\text{ KB}$ are written to workspace artifacts (`scratch/...`), while `send_message` transmits only executive summaries and clickable file links, preventing message bus truncations.
|
|
34
|
+
- **Output Governance Test Suite**: Added `tests/test-output-governance.mjs` verifying spillover safe-harbor access, task output lifecycle gating, forensic step extraction, and hook routing.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- **Streamlined Tool Architecture (0 New Tools)**: Eliminated tool proliferation by consolidating capabilities into existing tools and hooks, maintaining strictly the 6 core canonical MCP tools (zero prompt token bloat, zero tool-selection ambiguity).
|
|
38
|
+
- **Typed Argument Descriptors (NFF-04)**: Replaced blind `JSON.stringify().slice(0, 80)` in `formatTranscriptItem()` with typed argument descriptors (`[14.2 KB String]`, `[Array(5)]`, `{Object(3 keys)}`), preserving JSON payload integrity and transcript legibility.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
8
42
|
## [1.3.2] - 2026-10-07
|
|
9
43
|
|
|
10
44
|
### Governance & Command Semantics (LH-11 Hardening)
|
package/README.md
CHANGED
|
@@ -103,6 +103,33 @@ npm run status
|
|
|
103
103
|
|
|
104
104
|
---
|
|
105
105
|
|
|
106
|
+
## Comprehensive Documentation
|
|
107
|
+
|
|
108
|
+
For complete technical references, architectural specifications, and governance rules, visit our **[Zensical Documentation Site](https://paragon-ux.github.io/agy-context-saver/)**:
|
|
109
|
+
|
|
110
|
+
- 📖 **[Getting Started & Installation](docs/installation.md)**: Zero-delay setup, native plugin linking, hooks registration, and verification matrix.
|
|
111
|
+
- 🏗️ **[Architecture & Internals](docs/architecture.md)**: Closed Execution Topology, RTK separation of concerns, and the Four Non-Fragile Architectural Pillars.
|
|
112
|
+
- 🛡️ **[Lifecycle Governance & Hooks](docs/governance.md)**: Proportional Backoff curve, Reactive Wakeup invariants, and the 5-strike circuit breaker.
|
|
113
|
+
- 🧰 **[MCP Tools & CLI Reference](docs/tools.md)**: Complete specifications for all 6 canonical autonomous MCP tools, slash commands, and diagnostic utilities.
|
|
114
|
+
- 🔬 **[Forensic & Output Inspection](docs/architecture.md#6-output-governance--spillover-safe-harbor-0-new-tools)**: Ephemeral tool spillover resolution, post-mortem task retrieval, and Pointer-Over-Wire (POW) contracts.
|
|
115
|
+
- 🚀 **[Publishing & Verification Playbook](docs/publishing.md)**: Clean-room test runs, release procedures, and multi-suite verification matrix.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Local Documentation Server
|
|
120
|
+
|
|
121
|
+
Build or serve the documentation locally using **Zensical**:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Build static documentation site
|
|
125
|
+
zensical build
|
|
126
|
+
|
|
127
|
+
# Serve live preview server
|
|
128
|
+
zensical serve
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
106
133
|
## Zero-Guesswork Testing & Verification Playbook
|
|
107
134
|
|
|
108
135
|
To cleanly test every layer of the framework without guesswork or interactive stalls:
|
|
@@ -119,7 +146,7 @@ node mcp/index.js status
|
|
|
119
146
|
```bash
|
|
120
147
|
npm test
|
|
121
148
|
```
|
|
122
|
-
*Runs all
|
|
149
|
+
*Runs all 7 test suites (hook governance, MCP protocol, output inspection governance, E2E probes, performance probes, installed verification, lifecycle rollback).*
|
|
123
150
|
|
|
124
151
|
### 3. Run Targeted Tests Individually
|
|
125
152
|
```bash
|
|
@@ -129,9 +156,15 @@ node tests/test-hook.mjs
|
|
|
129
156
|
# MCP server protocol & method tests
|
|
130
157
|
node tests/test-mcp.mjs
|
|
131
158
|
|
|
159
|
+
# Output inspection & lifecycle gating tests
|
|
160
|
+
node tests/test-output-governance.mjs
|
|
161
|
+
|
|
132
162
|
# End-to-end edge case & probe tests
|
|
133
163
|
node tests/test-e2e-probes.mjs
|
|
134
164
|
|
|
165
|
+
# Performance, streaming & memory probes
|
|
166
|
+
node tests/test-performance-probes.mjs
|
|
167
|
+
|
|
135
168
|
# Live installed environment verification
|
|
136
169
|
node tests/test-installed-verification.mjs
|
|
137
170
|
|
|
@@ -160,12 +193,16 @@ node tests/test-lifecycle-rollback.mjs
|
|
|
160
193
|
- `/mcp:agy-context-saver:context_shield`: Injects 3-layer governance rules and RTK standards into any session with one click. *(MCP Prompts appear in your chat autocomplete menu; MCP Tools are called autonomously by the agent).*
|
|
161
194
|
* **Agent MCP Tools**:
|
|
162
195
|
1. `check_context_health`: Diagnoses conversation transcript turn budgets, payload size, RTK status, and polling loops.
|
|
163
|
-
2. `subagent_brief`: Formulates scope-isolated prompts for delegated subagents.
|
|
196
|
+
2. `subagent_brief`: Formulates scope-isolated prompts for delegated subagents with Pointer-Over-Wire (POW) delivery contract.
|
|
164
197
|
3. `read_transcript`: Reads recent conversation history in clean Markdown format (compact/full).
|
|
165
|
-
4. `query_transcript`: Forensic filtering
|
|
198
|
+
4. `query_transcript`: Forensic filtering, regex search engine, and surgical single-step dereferencing (`stepIndex`, `field`) from full transcripts.
|
|
166
199
|
5. `get_installation_status`: Inspects live Antigravity plugin link, hook, MCP server, tool schemas, and RTK binary.
|
|
167
200
|
6. `sync_installation`: Re-verifies and repairs all integration layers in ~25ms.
|
|
168
201
|
|
|
202
|
+
* **Streamlined Output Inspection (0 New Tools)**:
|
|
203
|
+
- **Runtime Step Spillovers** (`.system_generated/steps/<step>/output.txt`): Inspected via canonical `rtk read <path>` (safe-harbored in the PreToolUse hook; native `view_file` calls automatically route to `rtk read`).
|
|
204
|
+
- **Completed Background Task Logs**: Inspected via `rtk read tasks/<taskId>.log` (lifecycle-gated in the PreToolUse hook; strictly blocked under Proportional Backoff while running, allowed once completed).
|
|
205
|
+
|
|
169
206
|
---
|
|
170
207
|
|
|
171
208
|
## License
|
package/hooks.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"agy-context-saver": {
|
|
3
3
|
"PreToolUse": [
|
|
4
4
|
{
|
|
5
|
-
"matcher": "manage_task|run_command|
|
|
5
|
+
"matcher": "manage_task|schedule|run_command|view_file|read_file|read_many_files|grep_search|find_by_name|list_dir|replace_file_content|write_to_file",
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
package/mcp/index.js
CHANGED
|
@@ -98,7 +98,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
98
98
|
const rulesPath = path.resolve(__dirname, "../rules/AGENTS.md");
|
|
99
99
|
|
|
100
100
|
const SERVER_NAME = "agy-context-saver";
|
|
101
|
-
const SERVER_VERSION = "1.
|
|
101
|
+
const SERVER_VERSION = "1.5.0";
|
|
102
102
|
|
|
103
103
|
// --- Tools Specification ---
|
|
104
104
|
const TOOLS = [
|
|
@@ -243,6 +243,14 @@ const TOOLS = [
|
|
|
243
243
|
summaryOnly: {
|
|
244
244
|
type: "boolean",
|
|
245
245
|
description: "If true, returns high-density 1-line step summaries instead of full message blocks to minimize UI card height (default: false)."
|
|
246
|
+
},
|
|
247
|
+
stepIndex: {
|
|
248
|
+
type: "number",
|
|
249
|
+
description: "Optional step_index to surgically retrieve full un-truncated content for a single step from transcript_full.jsonl."
|
|
250
|
+
},
|
|
251
|
+
field: {
|
|
252
|
+
type: "string",
|
|
253
|
+
description: "Optional specific field to extract when stepIndex is provided (e.g. 'content', 'thinking', 'tool_calls', 'tool_args', or 'tool_args:Message'). Defaults to full step."
|
|
246
254
|
}
|
|
247
255
|
},
|
|
248
256
|
required: ["conversationId"]
|
|
@@ -305,36 +313,52 @@ async function handleCheckContextHealth(args = {}) {
|
|
|
305
313
|
let corruptLines = 0;
|
|
306
314
|
const MAX_LINES = 100000;
|
|
307
315
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
316
|
+
try {
|
|
317
|
+
for await (const line of rl) {
|
|
318
|
+
if (!line.trim()) continue;
|
|
319
|
+
totalSteps++;
|
|
320
|
+
totalBytes += line.length;
|
|
312
321
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
322
|
+
if (totalSteps > MAX_LINES) {
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
316
325
|
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
326
|
+
// Fast check for common step types without parsing if no tool calls (PERF-16)
|
|
327
|
+
const hasToolCalls = line.includes('"tool_calls"');
|
|
328
|
+
const head = line.slice(0, 300);
|
|
329
|
+
const isUserInput = head.includes('"type":"USER_INPUT"');
|
|
330
|
+
const isPlannerResp = head.includes('"type":"PLANNER_RESPONSE"');
|
|
331
|
+
|
|
332
|
+
if (!hasToolCalls && (isUserInput || isPlannerResp)) {
|
|
333
|
+
if (isUserInput) userTurns++;
|
|
334
|
+
else modelTurns++;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
const item = JSON.parse(line);
|
|
340
|
+
if (item.type === "USER_INPUT") userTurns++;
|
|
341
|
+
if (item.type === "PLANNER_RESPONSE") modelTurns++;
|
|
342
|
+
if (item.tool_calls && item.tool_calls.length) {
|
|
343
|
+
toolCalls += item.tool_calls.length;
|
|
344
|
+
for (const tc of item.tool_calls) {
|
|
345
|
+
const name = tc.tool_name || tc.name;
|
|
346
|
+
const args = tc.args || tc.arguments || {};
|
|
347
|
+
const action = String(args.Action || args.action || "").replace(/^["']|["']$/g, "").trim().toLowerCase();
|
|
348
|
+
const prompt = String(args.Prompt || args.prompt || "").replace(/^["']|["']$/g, "").trim().toLowerCase();
|
|
349
|
+
const cond = String(args.TimerCondition || args.timerCondition || "").replace(/^["']|["']$/g, "").trim().toLowerCase();
|
|
350
|
+
if (name === "manage_task" && action === "status") pollingEvents++;
|
|
351
|
+
if (name === "schedule" && (cond.startsWith("task") || cond.includes("task") || prompt.includes("test") || prompt.includes("check on") || prompt.includes("status"))) pollingEvents++;
|
|
352
|
+
}
|
|
331
353
|
}
|
|
354
|
+
} catch (err) {
|
|
355
|
+
corruptLines++;
|
|
332
356
|
}
|
|
333
|
-
} catch (err) {
|
|
334
|
-
corruptLines++;
|
|
335
357
|
}
|
|
358
|
+
} finally {
|
|
359
|
+
rl.close();
|
|
360
|
+
fileStream.destroy();
|
|
336
361
|
}
|
|
337
|
-
|
|
338
362
|
const kb = (totalBytes / 1024).toFixed(1);
|
|
339
363
|
const healthStatus = pollingEvents > 3 ? "CRITICAL (Active Polling Loops Detected)" : userTurns > 40 ? "WARNING (High Turn Budget)" : "HEALTHY";
|
|
340
364
|
|
|
@@ -379,12 +403,17 @@ function handleSubagentBrief({ objective, scopeFiles = [], expectedDeliverable =
|
|
|
379
403
|
`ROLE: Scope-Isolated Deep Research Subagent`,
|
|
380
404
|
`OBJECTIVE: ${objective}${scopeStr}`,
|
|
381
405
|
``,
|
|
382
|
-
`CRITICAL CONTEXT GOVERNANCE INSTRUCTIONS:`,
|
|
383
|
-
`1.
|
|
384
|
-
`2.
|
|
385
|
-
`
|
|
386
|
-
`
|
|
387
|
-
`
|
|
406
|
+
`CRITICAL CONTEXT GOVERNANCE INSTRUCTIONS (POINTER-OVER-WIRE):`,
|
|
407
|
+
`1. Isolated Workspace: Do all necessary multi-file exploration, searches, and analysis in this subagent sandbox.`,
|
|
408
|
+
`2. Pointer-Over-Wire (POW) Protocol: In-band messaging channels (send_message) have strict transport payload limits (~16-20 KB). If your findings or deliverable exceed 1 KB:`,
|
|
409
|
+
` a. Write the full comprehensive deliverable or raw output to a file in the workspace (e.g., 'scratch/<task>_findings.md' or an artifact).`,
|
|
410
|
+
` b. In 'send_message', transmit ONLY:`,
|
|
411
|
+
` - Executive Summary (3-5 high-signal sentences)`,
|
|
412
|
+
` - Verdict / Scoreboard Table`,
|
|
413
|
+
` - Clickable File Link: [Full Deliverable](file:///absolute/path/to/findings.md)`,
|
|
414
|
+
`3. Fast Turn Termination: Once 'send_message' is dispatched, stop calling tools and conclude your turn immediately so the parent agent wakes up without latency.`,
|
|
415
|
+
`4. Format: ${expectedDeliverable}`,
|
|
416
|
+
`5. Citation Mandate: Keep your final response concise, structured, and cite exact file:line references.`
|
|
388
417
|
].join("\n");
|
|
389
418
|
|
|
390
419
|
return { content: [{ type: "text", text: prompt }] };
|
|
@@ -393,12 +422,13 @@ function handleSubagentBrief({ objective, scopeFiles = [], expectedDeliverable =
|
|
|
393
422
|
function handleGetInstallationStatus() {
|
|
394
423
|
const existing = detectExistingInstallation();
|
|
395
424
|
const rtkInfo = findExistingRtk();
|
|
425
|
+
const schemaText = existing.details.schemas.exists ? "ALL 6 SCHEMAS PRESENT" : "MISSING";
|
|
396
426
|
const report = [
|
|
397
427
|
`### Agy-Context-Saver Installation Status: ${existing.isComplete ? "HEALTHY & ACTIVE 🛡️" : existing.isInstalled ? "PARTIAL INSTALLATION ⚠️" : "NOT INSTALLED ❌"}`,
|
|
398
428
|
`- Native Plugin Link: ${existing.details.plugin.exists ? `ACTIVE (${existing.details.plugin.target})` : "NOT LINKED"}`,
|
|
399
429
|
`- Governor Lifecycle Hook: ${existing.details.hook.registered ? "REGISTERED in hooks.json" : "NOT REGISTERED"} (Script: ${existing.details.hook.scriptExists ? "Present" : "Missing"})`,
|
|
400
430
|
`- Universal MCP Server: ${existing.details.mcp.registered ? "CONFIGURED in mcp_config.json" : "NOT CONFIGURED"}`,
|
|
401
|
-
`- Antigravity Tool Schemas: ${
|
|
431
|
+
`- Antigravity Tool Schemas: ${schemaText} (${existing.details.schemas.dir})`,
|
|
402
432
|
`- RTK Binary: ${rtkInfo ? `INSTALLED (${rtkInfo.version})` : "MISSING"}`,
|
|
403
433
|
`- RTK Hook Optimization: ${existing.details.hook.registered ? "ACTIVE (via PreToolUse)" : "INACTIVE"}`,
|
|
404
434
|
"",
|
|
@@ -445,6 +475,9 @@ function extractConvId(p) {
|
|
|
445
475
|
return match ? match[1] : path.basename(normalized, path.extname(normalized));
|
|
446
476
|
}
|
|
447
477
|
|
|
478
|
+
let cachedActiveConv = null;
|
|
479
|
+
let cachedActiveConvTime = 0;
|
|
480
|
+
|
|
448
481
|
function resolveTranscriptPath(target, mode = "compact") {
|
|
449
482
|
const isFull = mode === "full";
|
|
450
483
|
const fileName = isFull ? "transcript_full.jsonl" : "transcript.jsonl";
|
|
@@ -455,19 +488,38 @@ function resolveTranscriptPath(target, mode = "compact") {
|
|
|
455
488
|
if (process.env.GEMINI_CONVERSATION_ID) {
|
|
456
489
|
raw = process.env.GEMINI_CONVERSATION_ID;
|
|
457
490
|
} else {
|
|
458
|
-
const
|
|
459
|
-
if (
|
|
491
|
+
const now = Date.now();
|
|
492
|
+
if (cachedActiveConv && (now - cachedActiveConvTime) < 5000) {
|
|
493
|
+
raw = cachedActiveConv;
|
|
494
|
+
} else {
|
|
495
|
+
const brainDir = path.join(os.homedir(), ".gemini", "antigravity", "brain");
|
|
460
496
|
try {
|
|
461
|
-
const entries = fs.readdirSync(brainDir, { withFileTypes: true })
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
497
|
+
const entries = fs.readdirSync(brainDir, { withFileTypes: true });
|
|
498
|
+
let bestMtime = 0;
|
|
499
|
+
let bestName = null;
|
|
500
|
+
for (const d of entries) {
|
|
501
|
+
if (!d.isDirectory()) continue;
|
|
502
|
+
try {
|
|
503
|
+
const transPath = path.join(brainDir, d.name, ".system_generated", "logs", "transcript.jsonl");
|
|
504
|
+
const mtime = fs.statSync(transPath).mtimeMs;
|
|
505
|
+
if (mtime > bestMtime) {
|
|
506
|
+
bestMtime = mtime;
|
|
507
|
+
bestName = d.name;
|
|
508
|
+
}
|
|
509
|
+
} catch {
|
|
510
|
+
try {
|
|
511
|
+
const mtime = fs.statSync(path.join(brainDir, d.name)).mtimeMs;
|
|
512
|
+
if (mtime > bestMtime) {
|
|
513
|
+
bestMtime = mtime;
|
|
514
|
+
bestName = d.name;
|
|
515
|
+
}
|
|
516
|
+
} catch {}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (bestName) {
|
|
520
|
+
raw = bestName;
|
|
521
|
+
cachedActiveConv = bestName;
|
|
522
|
+
cachedActiveConvTime = now;
|
|
471
523
|
}
|
|
472
524
|
} catch {}
|
|
473
525
|
}
|
|
@@ -527,6 +579,11 @@ function cleanMessageContent(content, maxChars = MAX_TURN_CHARS) {
|
|
|
527
579
|
return text;
|
|
528
580
|
}
|
|
529
581
|
|
|
582
|
+
const origLength = text.length;
|
|
583
|
+
// Pre-slice huge text to ~4x maxChars before heavy regex and line clamping (PERF-15)
|
|
584
|
+
if (text.length > maxChars * 4) {
|
|
585
|
+
text = text.slice(0, maxChars * 4);
|
|
586
|
+
}
|
|
530
587
|
// Strip huge base64 media blocks only if relevant keywords or large length
|
|
531
588
|
if (text.includes("base64") || text.length > 200) {
|
|
532
589
|
text = text.replace(/data:image\/[^;]+;base64,[A-Za-z0-9+/=]+/g, "[Embedded Media/Binary Omitted]");
|
|
@@ -549,12 +606,35 @@ function cleanMessageContent(content, maxChars = MAX_TURN_CHARS) {
|
|
|
549
606
|
}
|
|
550
607
|
}
|
|
551
608
|
|
|
552
|
-
if (text.length > maxChars) {
|
|
553
|
-
|
|
609
|
+
if (origLength > maxChars || text.length > maxChars) {
|
|
610
|
+
const omitted = Math.max(origLength - maxChars, 0);
|
|
611
|
+
return text.slice(0, maxChars) + `\n\n... [Content Truncated (${omitted} chars omitted to preserve token budget)] ...`;
|
|
554
612
|
}
|
|
555
613
|
return text;
|
|
556
614
|
}
|
|
557
615
|
|
|
616
|
+
function formatArgumentDescriptor(k, v) {
|
|
617
|
+
if (typeof v === "string") {
|
|
618
|
+
if (v.length <= 80 && !v.includes("\n")) {
|
|
619
|
+
return `${k}=${JSON.stringify(v)}`;
|
|
620
|
+
}
|
|
621
|
+
const preview = v.replace(/[\r\n\t]+/g, " ").slice(0, 50).trim();
|
|
622
|
+
const sizeKb = (Buffer.byteLength(v, "utf8") / 1024).toFixed(1);
|
|
623
|
+
return `${k}="${preview}..." [${sizeKb} KB]`;
|
|
624
|
+
}
|
|
625
|
+
if (typeof v === "number" || typeof v === "boolean" || v === null) {
|
|
626
|
+
return `${k}=${v}`;
|
|
627
|
+
}
|
|
628
|
+
if (Array.isArray(v)) {
|
|
629
|
+
return `${k}=[Array(${v.length})]`;
|
|
630
|
+
}
|
|
631
|
+
if (typeof v === "object") {
|
|
632
|
+
const keys = Object.keys(v);
|
|
633
|
+
return `${k}={Object(${keys.length} keys)}`;
|
|
634
|
+
}
|
|
635
|
+
return `${k}=${String(v).slice(0, 60)}`;
|
|
636
|
+
}
|
|
637
|
+
|
|
558
638
|
function formatTranscriptItem(item, options = {}) {
|
|
559
639
|
const { includeThinking = false, includeToolCalls = false, maxContentChars = MAX_TURN_CHARS } = options;
|
|
560
640
|
const stepIdx = item.step_index !== undefined ? item.step_index : "?";
|
|
@@ -583,7 +663,7 @@ function formatTranscriptItem(item, options = {}) {
|
|
|
583
663
|
const name = tc.name || tc.tool_name || "unknown_tool";
|
|
584
664
|
const args = tc.args || tc.arguments || {};
|
|
585
665
|
const compactArgs = Object.entries(args)
|
|
586
|
-
.map(([k, v]) =>
|
|
666
|
+
.map(([k, v]) => formatArgumentDescriptor(k, v))
|
|
587
667
|
.join(", ");
|
|
588
668
|
return `- \`${name}(${compactArgs})\``;
|
|
589
669
|
});
|
|
@@ -626,12 +706,14 @@ function getCompiledRegex(pattern) {
|
|
|
626
706
|
|
|
627
707
|
async function batchFindFullSteps(fullPath, stepIndicesSet) {
|
|
628
708
|
const result = new Map();
|
|
629
|
-
if (!
|
|
630
|
-
|
|
631
|
-
const fileStream = fs.createReadStream(fullPath, { encoding: "utf-8" });
|
|
632
|
-
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
709
|
+
if (!stepIndicesSet || stepIndicesSet.size === 0) return result;
|
|
633
710
|
|
|
711
|
+
let fileStream;
|
|
712
|
+
let rl;
|
|
634
713
|
try {
|
|
714
|
+
fileStream = fs.createReadStream(fullPath, { encoding: "utf-8" });
|
|
715
|
+
rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
716
|
+
|
|
635
717
|
for await (const line of rl) {
|
|
636
718
|
if (!line.trim()) continue;
|
|
637
719
|
try {
|
|
@@ -639,13 +721,15 @@ async function batchFindFullSteps(fullPath, stepIndicesSet) {
|
|
|
639
721
|
if (stepIndicesSet.has(item.step_index)) {
|
|
640
722
|
result.set(item.step_index, item);
|
|
641
723
|
if (result.size >= stepIndicesSet.size) {
|
|
642
|
-
rl.close();
|
|
643
724
|
break;
|
|
644
725
|
}
|
|
645
726
|
}
|
|
646
727
|
} catch {}
|
|
647
728
|
}
|
|
648
|
-
} catch {}
|
|
729
|
+
} catch {} finally {
|
|
730
|
+
if (rl) rl.close();
|
|
731
|
+
if (fileStream) fileStream.destroy();
|
|
732
|
+
}
|
|
649
733
|
return result;
|
|
650
734
|
}
|
|
651
735
|
|
|
@@ -661,35 +745,49 @@ async function handleReadTranscript({ conversationId, mode = "compact", lastTurn
|
|
|
661
745
|
}
|
|
662
746
|
|
|
663
747
|
const { filePath, convId } = resolved;
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
748
|
+
const rawLines = [];
|
|
749
|
+
let fileStream;
|
|
750
|
+
let rl;
|
|
667
751
|
|
|
668
752
|
let totalStepsCount = 0;
|
|
669
753
|
const WINDOW_SIZE = lastTurns > 0 ? Math.max(100, lastTurns * 15) : Infinity;
|
|
670
754
|
|
|
671
755
|
try {
|
|
756
|
+
fileStream = fs.createReadStream(filePath, { encoding: "utf-8" });
|
|
757
|
+
rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
672
758
|
for await (const line of rl) {
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
759
|
+
const trimmed = line.trim();
|
|
760
|
+
if (!trimmed) continue;
|
|
761
|
+
totalStepsCount++;
|
|
762
|
+
if (WINDOW_SIZE === Infinity) {
|
|
763
|
+
rawLines.push(trimmed);
|
|
764
|
+
} else {
|
|
765
|
+
rawLines.push(trimmed);
|
|
766
|
+
if (rawLines.length > WINDOW_SIZE) {
|
|
767
|
+
rawLines.shift();
|
|
680
768
|
}
|
|
681
|
-
} catch {
|
|
682
|
-
// Tolerates incomplete trailing flushes during concurrent writes
|
|
683
769
|
}
|
|
684
770
|
}
|
|
685
771
|
} catch (err) {
|
|
686
772
|
return { isError: true, content: [{ type: "text", text: `Error reading transcript stream: ${err.message}` }] };
|
|
773
|
+
} finally {
|
|
774
|
+
if (rl) rl.close();
|
|
775
|
+
if (fileStream) fileStream.destroy();
|
|
687
776
|
}
|
|
688
777
|
|
|
689
778
|
if (totalStepsCount === 0) {
|
|
690
779
|
return { content: [{ type: "text", text: `Transcript is empty at: ${filePath}` }] };
|
|
691
780
|
}
|
|
692
781
|
|
|
782
|
+
const items = [];
|
|
783
|
+
for (const line of rawLines) {
|
|
784
|
+
try {
|
|
785
|
+
items.push(JSON.parse(line));
|
|
786
|
+
} catch {
|
|
787
|
+
// Tolerates incomplete trailing flushes during concurrent writes
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
693
791
|
let selected = items;
|
|
694
792
|
if (lastTurns > 0) {
|
|
695
793
|
const turnItems = items.filter((it) => it.type === "USER_INPUT" || it.type === "PLANNER_RESPONSE");
|
|
@@ -697,7 +795,6 @@ async function handleReadTranscript({ conversationId, mode = "compact", lastTurn
|
|
|
697
795
|
const minStep = turnItems[cutoff] ? turnItems[cutoff].step_index : 0;
|
|
698
796
|
selected = items.filter((it) => (it.step_index || 0) >= minStep);
|
|
699
797
|
}
|
|
700
|
-
|
|
701
798
|
const header = [
|
|
702
799
|
`# Conversation Transcript: \`${convId}\``,
|
|
703
800
|
`- Mode: **${mode}** (${path.basename(filePath)})`,
|
|
@@ -745,9 +842,105 @@ async function handleQueryTranscript(args = {}) {
|
|
|
745
842
|
includeThinking = false,
|
|
746
843
|
includeToolCalls = false,
|
|
747
844
|
maxResults = 5,
|
|
748
|
-
summaryOnly = false
|
|
845
|
+
summaryOnly = false,
|
|
846
|
+
stepIndex,
|
|
847
|
+
field
|
|
749
848
|
} = args;
|
|
750
849
|
|
|
850
|
+
// Surgical single-step dereferencing (folded from get_step_detail)
|
|
851
|
+
if (stepIndex !== undefined && stepIndex !== null) {
|
|
852
|
+
const numIndex = Number(stepIndex);
|
|
853
|
+
if (isNaN(numIndex)) {
|
|
854
|
+
return {
|
|
855
|
+
isError: true,
|
|
856
|
+
content: [{ type: "text", text: `Error: 'stepIndex' must be a number (received: ${stepIndex}).` }]
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const resolvedFull = resolveTranscriptPath(conversationId, "full");
|
|
861
|
+
let step = null;
|
|
862
|
+
if (resolvedFull && resolvedFull.filePath) {
|
|
863
|
+
step = await findFullStep(resolvedFull.filePath, numIndex);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (!step) {
|
|
867
|
+
const resolvedCompact = resolveTranscriptPath(conversationId, "compact");
|
|
868
|
+
if (resolvedCompact && resolvedCompact.filePath) {
|
|
869
|
+
step = await findFullStep(resolvedCompact.filePath, numIndex);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
if (!step) {
|
|
873
|
+
return {
|
|
874
|
+
isError: true,
|
|
875
|
+
content: [{ type: "text", text: `Step index ${numIndex} not found in transcript.` }]
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const MAX_FIELD_CHARS = 24000;
|
|
880
|
+
|
|
881
|
+
if (field) {
|
|
882
|
+
const f = String(field).trim();
|
|
883
|
+
if (f === "content") {
|
|
884
|
+
const val = step.content || "(No content in step)";
|
|
885
|
+
return {
|
|
886
|
+
content: [{ type: "text", text: `### Step ${numIndex} Content\n\n${val.slice(0, MAX_FIELD_CHARS)}` }]
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
if (f === "thinking") {
|
|
890
|
+
const val = step.thinking || "(No thinking block in step)";
|
|
891
|
+
return {
|
|
892
|
+
content: [{ type: "text", text: `### Step ${numIndex} Thinking\n\n${val.slice(0, MAX_FIELD_CHARS)}` }]
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
if (f === "tool_calls") {
|
|
896
|
+
const val = JSON.stringify(step.tool_calls || [], null, 2);
|
|
897
|
+
return {
|
|
898
|
+
content: [{ type: "text", text: `### Step ${numIndex} Tool Calls\n\`\`\`json\n${val.slice(0, MAX_FIELD_CHARS)}\n\`\`\`` }]
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
if (f === "tool_args" || f.startsWith("tool_args:")) {
|
|
902
|
+
const argName = f.includes(":") ? f.split(":")[1].trim() : null;
|
|
903
|
+
const tc = Array.isArray(step.tool_calls) && step.tool_calls.length > 0 ? step.tool_calls[0] : null;
|
|
904
|
+
if (!tc) {
|
|
905
|
+
return {
|
|
906
|
+
content: [{ type: "text", text: `Step ${numIndex} contains no tool calls.` }]
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
const tcArgs = tc.args || tc.arguments || {};
|
|
910
|
+
if (argName) {
|
|
911
|
+
const argVal = tcArgs[argName];
|
|
912
|
+
const displayVal = typeof argVal === "string" ? argVal : JSON.stringify(argVal, null, 2);
|
|
913
|
+
return {
|
|
914
|
+
content: [
|
|
915
|
+
{
|
|
916
|
+
type: "text",
|
|
917
|
+
text: `### Step ${numIndex} Argument: \`${argName}\`\n\n${String(displayVal).slice(0, MAX_FIELD_CHARS)}`
|
|
918
|
+
}
|
|
919
|
+
]
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
return {
|
|
923
|
+
content: [
|
|
924
|
+
{
|
|
925
|
+
type: "text",
|
|
926
|
+
text: `### Step ${numIndex} Tool Arguments\n\`\`\`json\n${JSON.stringify(tcArgs, null, 2).slice(0, MAX_FIELD_CHARS)}\n\`\`\``
|
|
927
|
+
}
|
|
928
|
+
]
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const formatted = formatTranscriptItem(step, {
|
|
934
|
+
includeThinking: true,
|
|
935
|
+
includeToolCalls: true,
|
|
936
|
+
maxContentChars: MAX_FIELD_CHARS
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
return {
|
|
940
|
+
content: [{ type: "text", text: formatted }]
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
|
|
751
944
|
const resolved = resolveTranscriptPath(conversationId, mode === "full" ? "full" : "compact");
|
|
752
945
|
if (resolved.error) {
|
|
753
946
|
return { isError: true, content: [{ type: "text", text: resolved.error }] };
|
|
@@ -755,20 +948,31 @@ async function handleQueryTranscript(args = {}) {
|
|
|
755
948
|
|
|
756
949
|
const { filePath, convId } = resolved;
|
|
757
950
|
const fullSiblingPath = filePath.replace(/transcript\.jsonl$/, "transcript_full.jsonl");
|
|
758
|
-
const hasFullSibling = fs.existsSync(fullSiblingPath);
|
|
759
951
|
|
|
760
952
|
const roleSet = new Set((Array.isArray(roles) ? roles : [roles]).map((r) => String(r).toLowerCase()));
|
|
761
953
|
const matchAllRoles = roleSet.has("all");
|
|
762
954
|
|
|
763
955
|
const queryRegex = getCompiledRegex(query);
|
|
956
|
+
const isPlainLiteral = query && /^[A-Za-z0-9_ -]+$/.test(query);
|
|
764
957
|
|
|
765
|
-
const
|
|
766
|
-
const
|
|
767
|
-
const
|
|
958
|
+
const effectiveLimit = Math.min(lastTurns > 0 ? lastTurns : Infinity, maxResults > 0 ? maxResults : 5);
|
|
959
|
+
const ringCap = Number.isFinite(effectiveLimit) ? effectiveLimit : 1000;
|
|
960
|
+
const ring = [];
|
|
961
|
+
let matchedCount = 0;
|
|
962
|
+
|
|
963
|
+
let fileStream;
|
|
964
|
+
let rl;
|
|
768
965
|
|
|
769
966
|
try {
|
|
967
|
+
fileStream = fs.createReadStream(filePath, { encoding: "utf-8" });
|
|
968
|
+
rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
969
|
+
|
|
770
970
|
for await (const line of rl) {
|
|
771
971
|
if (!line.trim()) continue;
|
|
972
|
+
|
|
973
|
+
// PERF-12: Raw-line prefilter for plain literal queries
|
|
974
|
+
if (isPlainLiteral && !queryRegex.test(line)) continue;
|
|
975
|
+
|
|
772
976
|
let item;
|
|
773
977
|
try {
|
|
774
978
|
item = JSON.parse(line);
|
|
@@ -810,13 +1014,20 @@ async function handleQueryTranscript(args = {}) {
|
|
|
810
1014
|
if (!matches) continue;
|
|
811
1015
|
}
|
|
812
1016
|
|
|
813
|
-
|
|
1017
|
+
matchedCount++;
|
|
1018
|
+
ring.push(item);
|
|
1019
|
+
if (ring.length > ringCap) {
|
|
1020
|
+
ring.shift();
|
|
1021
|
+
}
|
|
814
1022
|
}
|
|
815
1023
|
} catch (err) {
|
|
816
1024
|
return { isError: true, content: [{ type: "text", text: `Error streaming transcript: ${err.message}` }] };
|
|
1025
|
+
} finally {
|
|
1026
|
+
if (rl) rl.close();
|
|
1027
|
+
if (fileStream) fileStream.destroy();
|
|
817
1028
|
}
|
|
818
1029
|
|
|
819
|
-
if (
|
|
1030
|
+
if (matchedCount === 0) {
|
|
820
1031
|
return {
|
|
821
1032
|
content: [{
|
|
822
1033
|
type: "text",
|
|
@@ -825,8 +1036,8 @@ async function handleQueryTranscript(args = {}) {
|
|
|
825
1036
|
};
|
|
826
1037
|
}
|
|
827
1038
|
|
|
828
|
-
let finalItems =
|
|
829
|
-
if (lastTurns && lastTurns > 0) {
|
|
1039
|
+
let finalItems = ring;
|
|
1040
|
+
if (lastTurns && lastTurns > 0 && finalItems.length > lastTurns) {
|
|
830
1041
|
finalItems = finalItems.slice(-lastTurns);
|
|
831
1042
|
}
|
|
832
1043
|
if (finalItems.length > maxResults) {
|
|
@@ -834,8 +1045,8 @@ async function handleQueryTranscript(args = {}) {
|
|
|
834
1045
|
}
|
|
835
1046
|
|
|
836
1047
|
// Auto-dereferencing if mode is "auto" and fields were truncated (single pass)
|
|
837
|
-
|
|
838
|
-
const truncatedIndices = new Set();
|
|
1048
|
+
// PERF-17: check fs.existsSync(fullSiblingPath) lazily ONLY if truncatedIndices.size > 0
|
|
1049
|
+
if (mode === "auto") { const truncatedIndices = new Set();
|
|
839
1050
|
for (const it of finalItems) {
|
|
840
1051
|
if (Array.isArray(it.truncated_fields) && it.truncated_fields.length > 0 && it.step_index !== undefined) {
|
|
841
1052
|
truncatedIndices.add(it.step_index);
|
|
@@ -869,7 +1080,7 @@ async function handleQueryTranscript(args = {}) {
|
|
|
869
1080
|
const header = [
|
|
870
1081
|
`## Transcript Query Summary: \`${convId}\``,
|
|
871
1082
|
`- Filter: query=${query ? `"${query}"` : "NONE"}, roles=[${Array.from(roleSet).join(", ")}], mode=${mode}`,
|
|
872
|
-
`- Matched Steps: ${
|
|
1083
|
+
`- Matched Steps: ${matchedCount} (showing ${finalItems.length} in compact summary mode)`,
|
|
873
1084
|
"",
|
|
874
1085
|
"---",
|
|
875
1086
|
""
|
|
@@ -885,7 +1096,7 @@ async function handleQueryTranscript(args = {}) {
|
|
|
885
1096
|
const header = [
|
|
886
1097
|
`## Transcript Query Results: \`${convId}\``,
|
|
887
1098
|
`- Filter: query=${query ? `"${query}"` : "NONE"}, roles=[${Array.from(roleSet).join(", ")}], mode=${mode}`,
|
|
888
|
-
`- Matched Steps: ${
|
|
1099
|
+
`- Matched Steps: ${matchedCount} (showing ${finalItems.length})`,
|
|
889
1100
|
"",
|
|
890
1101
|
"---",
|
|
891
1102
|
""
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agy-context-saver",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Universal Model Context Protocol (MCP) server & lifecycle governor purpose-built for Google Antigravity",
|
|
5
5
|
"main": "mcp/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"setup": "node scripts/install-register.mjs",
|
|
13
13
|
"uninstall": "node scripts/install-register.mjs --uninstall",
|
|
14
14
|
"status": "node scripts/install-register.mjs --status",
|
|
15
|
-
"test": "node tests/test-hook.mjs && node tests/test-mcp.mjs && node tests/test-e2e-probes.mjs && node tests/test-performance-probes.mjs && node tests/test-installed-verification.mjs && node tests/test-lifecycle-rollback.mjs"
|
|
15
|
+
"test": "node tests/test-hook.mjs && node tests/test-mcp.mjs && node tests/test-output-governance.mjs && node tests/test-e2e-probes.mjs && node tests/test-performance-probes.mjs && node tests/test-installed-verification.mjs && node tests/test-lifecycle-rollback.mjs"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
18
|
"antigravity",
|
package/plugin.json
CHANGED
|
@@ -66,13 +66,11 @@ function getStateFilePath(convId = "global") {
|
|
|
66
66
|
function getSessionState(convId = "global") {
|
|
67
67
|
const file = getStateFilePath(convId);
|
|
68
68
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return entry;
|
|
75
|
-
}
|
|
69
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
70
|
+
const entry = (parsed && typeof parsed.pollCount === "number") ? parsed : (parsed && parsed[convId]);
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
if (entry && typeof entry.lastPollTime === "number" && (now - entry.lastPollTime) < STATE_TTL_MS) {
|
|
73
|
+
return entry;
|
|
76
74
|
}
|
|
77
75
|
} catch {}
|
|
78
76
|
return { pollCount: 0, denials: 0, taskIds: [], lastPollTime: 0 };
|
|
@@ -86,14 +84,12 @@ function saveSessionState(convId = "global", entry) {
|
|
|
86
84
|
let toWrite;
|
|
87
85
|
if (!convId || convId === "global") {
|
|
88
86
|
let state = {};
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
} catch {}
|
|
96
|
-
}
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
89
|
+
if (parsed && typeof parsed === "object" && typeof parsed.pollCount !== "number") {
|
|
90
|
+
state = parsed;
|
|
91
|
+
}
|
|
92
|
+
} catch {}
|
|
97
93
|
state[convId] = entry;
|
|
98
94
|
|
|
99
95
|
// Prune stale entries
|
|
@@ -123,12 +119,7 @@ function saveSessionState(convId = "global", entry) {
|
|
|
123
119
|
*/
|
|
124
120
|
function isProtectedInternalState(targetPath) {
|
|
125
121
|
if (!targetPath || typeof targetPath !== "string") return false;
|
|
126
|
-
|
|
127
|
-
return (
|
|
128
|
-
normalized.includes("/.system_generated/") ||
|
|
129
|
-
normalized.endsWith("/.system_generated") ||
|
|
130
|
-
/\.system_generated(?:$|[\/\\])/i.test(normalized)
|
|
131
|
-
);
|
|
122
|
+
return /\.system_generated(?:$|[\/\\])/i.test(targetPath);
|
|
132
123
|
}
|
|
133
124
|
|
|
134
125
|
/**
|
|
@@ -184,31 +175,124 @@ function isProtectedBenchmarkPath(targetPath) {
|
|
|
184
175
|
);
|
|
185
176
|
}
|
|
186
177
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
178
|
+
/**
|
|
179
|
+
* Checks if target path references runtime step output spillover file.
|
|
180
|
+
*/
|
|
181
|
+
function isStepSpilloverPath(targetPath) {
|
|
182
|
+
if (!targetPath || typeof targetPath !== "string") return false;
|
|
183
|
+
if (!isProtectedInternalState(targetPath)) return false; // cheap precheck
|
|
184
|
+
const normalized = targetPath.replace(/\\/g, "/");
|
|
185
|
+
return (
|
|
186
|
+
/\.system_generated[/\\]steps[/\\]\d+[/\\]output\.txt/i.test(normalized) ||
|
|
187
|
+
(normalized.includes("/.system_generated/steps/") && normalized.endsWith("output.txt"))
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
190
|
|
|
191
|
-
|
|
191
|
+
/**
|
|
192
|
+
* Extracts the task-log reference from a target path or command, or null.
|
|
193
|
+
* Returns { id, token }: `id` is the bare task id; `token` is the matched path text
|
|
194
|
+
* (relative "tasks/task-1.log" or absolute ".../.system_generated/tasks/task-1.log").
|
|
195
|
+
*/
|
|
196
|
+
function getTaskLogMatch(text) {
|
|
197
|
+
if (!text || typeof text !== "string" || !text.includes(".log")) return null;
|
|
198
|
+
const normalized = text.replace(/\\/g, "/");
|
|
199
|
+
if (!normalized.includes("tasks/")) return null;
|
|
200
|
+
const m = normalized.match(/(?:^|[\s"'])((?:[^\s"']*\/)?tasks\/(?:task-)?([a-zA-Z0-9_-]+)\.log)/i);
|
|
201
|
+
if (!m) return null;
|
|
202
|
+
let token = m[1].replace(/^file:\/\//i, "");
|
|
203
|
+
if (/^\/[A-Za-z]:/.test(token)) token = token.slice(1);
|
|
204
|
+
return { id: m[2], token };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const TASK_TAIL_BYTES = 262144; // transcript tail scanned first (256 KB)
|
|
208
|
+
const TASK_QUIET_MS = 300000; // log-mtime fallback: quiet for 5 min => treated as finished
|
|
209
|
+
|
|
210
|
+
function getBrainDir() {
|
|
211
|
+
return process.env.AGY_BRAIN_DIR || path.join(os.homedir(), ".gemini", "antigravity", "brain");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Reads the last `bytes` of a file. Returns { text, whole } or null. */
|
|
215
|
+
function readTail(file, bytes) {
|
|
216
|
+
let fd;
|
|
192
217
|
try {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
218
|
+
fd = fs.openSync(file, "r");
|
|
219
|
+
const size = fs.fstatSync(fd).size;
|
|
220
|
+
const len = Math.min(size, bytes);
|
|
221
|
+
const buf = Buffer.alloc(len);
|
|
222
|
+
fs.readSync(fd, buf, 0, len, size - len);
|
|
223
|
+
return { text: buf.toString("utf-8"), whole: len === size };
|
|
224
|
+
} catch {
|
|
225
|
+
return null;
|
|
226
|
+
} finally {
|
|
227
|
+
if (fd !== undefined) { try { fs.closeSync(fd); } catch {} }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Looks for the finish / start notices of `taskName` in transcript text using literal needles
|
|
233
|
+
* (JSON-escaped and raw variants). Finish: `<conv>/task-N" finished with result`.
|
|
234
|
+
* Start: `<conv>/task-N` followed by a newline and "Task Description" or "Task logs".
|
|
235
|
+
* Returns "COMPLETED", "RUNNING" or null.
|
|
236
|
+
*/
|
|
237
|
+
function scanTaskMarkers(text, taskName) {
|
|
238
|
+
if (text.includes(`/${taskName}\\" finished`) || text.includes(`/${taskName}" finished`)) return "COMPLETED";
|
|
239
|
+
if (text.includes(`/${taskName}\\nTask `) || text.includes(`/${taskName}\nTask `)) return "RUNNING";
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Resolves a background task's lifecycle state: "RUNNING", "COMPLETED" or "UNKNOWN".
|
|
245
|
+
* Authority order: transcript notices (tail first, full file only if needed), then log mtime.
|
|
246
|
+
*/
|
|
247
|
+
function getTaskState(match, convId) {
|
|
248
|
+
const taskName = `task-${match.id}`;
|
|
249
|
+
const token = match.token;
|
|
250
|
+
let logPath = null;
|
|
251
|
+
let transcript = null;
|
|
252
|
+
|
|
253
|
+
if (/^(?:[A-Za-z]:)?\//.test(token)) {
|
|
254
|
+
logPath = token;
|
|
255
|
+
const sysDir = path.posix.dirname(path.posix.dirname(token));
|
|
256
|
+
if (path.posix.basename(sysDir) === ".system_generated") transcript = `${sysDir}/logs/transcript.jsonl`;
|
|
257
|
+
}
|
|
258
|
+
if (convId && convId !== "global") {
|
|
259
|
+
const sysDir = path.join(getBrainDir(), convId, ".system_generated");
|
|
260
|
+
logPath = logPath || path.join(sysDir, "tasks", `${taskName}.log`);
|
|
261
|
+
transcript = transcript || path.join(sysDir, "logs", "transcript.jsonl");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (transcript) {
|
|
265
|
+
const tail = readTail(transcript, TASK_TAIL_BYTES);
|
|
266
|
+
if (tail) {
|
|
267
|
+
let state = scanTaskMarkers(tail.text, taskName);
|
|
268
|
+
if (!state && !tail.whole) {
|
|
269
|
+
try { state = scanTaskMarkers(fs.readFileSync(transcript, "utf-8"), taskName); } catch {}
|
|
270
|
+
}
|
|
271
|
+
if (state) return state;
|
|
197
272
|
}
|
|
198
|
-
}
|
|
273
|
+
}
|
|
199
274
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const pluginBin = path.join(homeDir, ".gemini", "config", "plugins", "agy-context-saver", "bin", exeName);
|
|
205
|
-
if (fs.existsSync(pluginBin)) {
|
|
206
|
-
resolvedRtkPath = pluginBin;
|
|
207
|
-
return resolvedRtkPath;
|
|
275
|
+
if (logPath) {
|
|
276
|
+
try {
|
|
277
|
+
return (Date.now() - fs.statSync(logPath).mtimeMs) > TASK_QUIET_MS ? "COMPLETED" : "RUNNING";
|
|
278
|
+
} catch {}
|
|
208
279
|
}
|
|
280
|
+
return "UNKNOWN";
|
|
281
|
+
}
|
|
209
282
|
|
|
210
|
-
|
|
211
|
-
|
|
283
|
+
/**
|
|
284
|
+
* Runs `rtk rewrite <cmd>`. Spawns the PATH binary directly (a separate `--version` probe doubled
|
|
285
|
+
* the cost of every cache miss); falls back to the plugin-local binary only when PATH has none.
|
|
286
|
+
*/
|
|
287
|
+
function runRtkRewrite(cmd) {
|
|
288
|
+
const opts = { encoding: "utf-8", timeout: 2000, windowsHide: true };
|
|
289
|
+
const res = spawnSync("rtk", ["rewrite", cmd], opts);
|
|
290
|
+
if (res.error && res.error.code === "ENOENT") {
|
|
291
|
+
const exeName = os.platform() === "win32" ? "rtk.exe" : "rtk";
|
|
292
|
+
const pluginBin = path.join(os.homedir(), ".gemini", "config", "plugins", "agy-context-saver", "bin", exeName);
|
|
293
|
+
if (fs.existsSync(pluginBin)) return spawnSync(pluginBin, ["rewrite", cmd], opts);
|
|
294
|
+
}
|
|
295
|
+
return res;
|
|
212
296
|
}
|
|
213
297
|
|
|
214
298
|
const NON_REWRITABLE_BINARIES = new Set([
|
|
@@ -235,31 +319,39 @@ const REWRITE_CACHE_FILE = path.join(os.tmpdir(), "agy-rtk-rewrite-cache.json");
|
|
|
235
319
|
const REWRITE_CACHE_MAX = 100;
|
|
236
320
|
let inMemoryRewriteCache = null;
|
|
237
321
|
|
|
238
|
-
function
|
|
239
|
-
if (inMemoryRewriteCache) return inMemoryRewriteCache;
|
|
322
|
+
function readRewriteCacheFile() {
|
|
240
323
|
try {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (data && typeof data === "object") {
|
|
244
|
-
inMemoryRewriteCache = data;
|
|
245
|
-
return inMemoryRewriteCache;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
324
|
+
const data = JSON.parse(fs.readFileSync(REWRITE_CACHE_FILE, "utf-8"));
|
|
325
|
+
if (data && typeof data === "object" && !Array.isArray(data)) return data;
|
|
248
326
|
} catch {}
|
|
249
|
-
|
|
327
|
+
return {};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function loadRewriteCache() {
|
|
331
|
+
if (!inMemoryRewriteCache) inMemoryRewriteCache = readRewriteCacheFile();
|
|
250
332
|
return inMemoryRewriteCache;
|
|
251
333
|
}
|
|
252
334
|
|
|
253
|
-
|
|
335
|
+
/**
|
|
336
|
+
* Persists one rewrite result. Re-reads the file first so concurrent hook processes merge instead
|
|
337
|
+
* of overwriting each other, trims to the newest entries, and writes atomically (temp + rename).
|
|
338
|
+
*/
|
|
339
|
+
function rememberRewrite(cmd, rewritten) {
|
|
340
|
+
loadRewriteCache()[cmd] = rewritten;
|
|
254
341
|
try {
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
342
|
+
const disk = readRewriteCacheFile();
|
|
343
|
+
delete disk[cmd];
|
|
344
|
+
disk[cmd] = rewritten;
|
|
345
|
+
const keys = Object.keys(disk);
|
|
346
|
+
for (const k of keys.slice(0, Math.max(0, keys.length - REWRITE_CACHE_MAX))) delete disk[k];
|
|
347
|
+
const tmp = `${REWRITE_CACHE_FILE}.${process.pid}.tmp`;
|
|
348
|
+
fs.writeFileSync(tmp, JSON.stringify(disk), "utf-8");
|
|
349
|
+
try {
|
|
350
|
+
fs.renameSync(tmp, REWRITE_CACHE_FILE);
|
|
351
|
+
} catch {
|
|
352
|
+
fs.writeFileSync(REWRITE_CACHE_FILE, JSON.stringify(disk), "utf-8");
|
|
353
|
+
try { fs.unlinkSync(tmp); } catch {}
|
|
261
354
|
}
|
|
262
|
-
fs.writeFileSync(REWRITE_CACHE_FILE, JSON.stringify(toSave), "utf-8");
|
|
263
355
|
} catch {}
|
|
264
356
|
}
|
|
265
357
|
|
|
@@ -312,13 +404,12 @@ function rewriteCommandWithRtk(rawCmd) {
|
|
|
312
404
|
}
|
|
313
405
|
|
|
314
406
|
let rewritten = trimmed;
|
|
407
|
+
let cacheable = false;
|
|
315
408
|
try {
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
windowsHide: true
|
|
321
|
-
});
|
|
409
|
+
const res = runRtkRewrite(trimmed);
|
|
410
|
+
// Never cache timeouts, spawn errors or signals: a transient failure must not pin a command
|
|
411
|
+
// to "no rewrite" for the lifetime of the cache.
|
|
412
|
+
cacheable = !res.error && !res.signal && typeof res.status === "number" && res.status >= 0 && res.status <= 3;
|
|
322
413
|
|
|
323
414
|
// RTK exits 0 or 3 when rewritten, with new command on stdout
|
|
324
415
|
if ((res.status === 0 || res.status === 3) && res.stdout && res.stdout.trim()) {
|
|
@@ -326,8 +417,7 @@ function rewriteCommandWithRtk(rawCmd) {
|
|
|
326
417
|
}
|
|
327
418
|
} catch {}
|
|
328
419
|
|
|
329
|
-
|
|
330
|
-
saveRewriteCache(cache);
|
|
420
|
+
if (cacheable) rememberRewrite(trimmed, rewritten);
|
|
331
421
|
return rewritten;
|
|
332
422
|
}
|
|
333
423
|
|
|
@@ -342,6 +432,34 @@ function getRequiredBackoffSeconds(pollCount) {
|
|
|
342
432
|
return Math.min(600, Math.round(30 * Math.pow(2.5, Math.max(0, pollCount - 1))));
|
|
343
433
|
}
|
|
344
434
|
|
|
435
|
+
/**
|
|
436
|
+
* Denies reading the log of an actively RUNNING task: records the denial, trips the circuit
|
|
437
|
+
* breaker at 5 cumulative denials, otherwise answers with the Proportional Backoff message.
|
|
438
|
+
*/
|
|
439
|
+
function denyRunningTask(taskId, convId) {
|
|
440
|
+
const session = getSessionState(convId);
|
|
441
|
+
session.denials++;
|
|
442
|
+
saveSessionState(convId, session);
|
|
443
|
+
|
|
444
|
+
if (session.denials >= 5) {
|
|
445
|
+
respond({
|
|
446
|
+
decision: "force_ask",
|
|
447
|
+
reason: `[CIRCUIT BREAKER ACTIVATED] Autonomous loop suspended: reading task log '${taskId}' attempted prematurely ${session.denials} times during active backoff window. Stop calling tools and yield the turn for native Reactive Wakeup (<SYSTEM_MESSAGE>). User confirmation required to proceed.`
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const requiredBackoffSec = Math.max(30, getRequiredBackoffSeconds(session.pollCount));
|
|
452
|
+
const requiredBackoffMs = requiredBackoffSec * 1000;
|
|
453
|
+
const elapsedMs = session.lastPollTime ? (Date.now() - session.lastPollTime) : Infinity;
|
|
454
|
+
const elapsedSec = Math.floor(elapsedMs / 1000);
|
|
455
|
+
const remainingSec = Math.max(1, Math.ceil((requiredBackoffMs - elapsedMs) / 1000));
|
|
456
|
+
|
|
457
|
+
respond({
|
|
458
|
+
decision: "deny",
|
|
459
|
+
reason: `Antigravity Execution Governance: Task '${taskId}' is actively RUNNING. Reading task logs in .system_generated during execution is strictly prohibited to prevent polling loops. Diagnostic check #${session.pollCount + 1} requires at least ${requiredBackoffSec}s elapsed (${elapsedSec}s elapsed, ${remainingSec}s remaining). Tasks execute asynchronously and notify you automatically via native Reactive Wakeup (<SYSTEM_MESSAGE>) upon completion. Stop calling tools and yield your turn now.`
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
345
463
|
// --- Main Hook Entrypoint ---
|
|
346
464
|
let isGovernedTool = false;
|
|
347
465
|
|
|
@@ -389,8 +507,25 @@ try {
|
|
|
389
507
|
});
|
|
390
508
|
}
|
|
391
509
|
|
|
392
|
-
// LH-
|
|
393
|
-
if (
|
|
510
|
+
// LH-09-B: Shell command mutation of protected benchmark artifacts
|
|
511
|
+
if (
|
|
512
|
+
(rawCmd.includes(">") || /\b(?:rm|del|Remove-Item)\b/i.test(rawCmd)) &&
|
|
513
|
+
isProtectedBenchmarkPath(rawCmd)
|
|
514
|
+
) {
|
|
515
|
+
respond({
|
|
516
|
+
decision: "force_ask",
|
|
517
|
+
reason:
|
|
518
|
+
"[BENCHMARK MUTATION DETECTED] Shell command mutation of protected evaluator/benchmark artifact requires explicit user confirmation to prevent benchmark contamination."
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Raw-command governance gates are pure string/fs checks: run them BEFORE the rtk rewrite
|
|
523
|
+
// spawn so denied commands never pay for a subprocess.
|
|
524
|
+
const isSpillover = isStepSpilloverPath(rawCmd);
|
|
525
|
+
const taskMatch = isSpillover ? null : getTaskLogMatch(rawCmd);
|
|
526
|
+
|
|
527
|
+
// Check 1: Other protected internal state (transcripts, scheduler, progress)
|
|
528
|
+
if (!isSpillover && !taskMatch && isProtectedInternalState(rawCmd)) {
|
|
394
529
|
respond({
|
|
395
530
|
decision: "deny",
|
|
396
531
|
reason:
|
|
@@ -400,19 +535,35 @@ try {
|
|
|
400
535
|
});
|
|
401
536
|
}
|
|
402
537
|
|
|
403
|
-
//
|
|
404
|
-
if (
|
|
405
|
-
(
|
|
406
|
-
|
|
407
|
-
|
|
538
|
+
// Check 2: Task Log Inspection (Lifecycle-Gated, 0 New Tools): RUNNING => Proportional Backoff
|
|
539
|
+
if (taskMatch && getTaskState(taskMatch, convId) === "RUNNING") {
|
|
540
|
+
denyRunningTask(taskMatch.id, convId);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const rewritten = rewriteCommandWithRtk(rawCmd);
|
|
544
|
+
const effectiveCmd = rewritten;
|
|
545
|
+
const isRtkRead = /^rtk(?:\.exe)?\s+read\b/i.test(effectiveCmd);
|
|
546
|
+
|
|
547
|
+
// Check 3: Step Output Spillover Safe Harbor (rtk read only; it clamps + windows output)
|
|
548
|
+
if (isSpillover && !isRtkRead) {
|
|
408
549
|
respond({
|
|
409
|
-
decision: "
|
|
550
|
+
decision: "deny",
|
|
410
551
|
reason:
|
|
411
|
-
"
|
|
552
|
+
"Antigravity Execution Governance: Runtime step output spillover files must be inspected using 'rtk read <path>' via run_command to prevent context blowouts."
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Check 4: Terminated / unknown-state task log: allow only through rtk read
|
|
557
|
+
if (taskMatch && !isRtkRead) {
|
|
558
|
+
respond({
|
|
559
|
+
decision: "deny",
|
|
560
|
+
reason:
|
|
561
|
+
"Antigravity Execution Governance: Direct shell access to internal Antigravity execution state (.system_generated) is strictly prohibited to prevent transcript bloat and polling loops.\n" +
|
|
562
|
+
`- Once completed, inspect output using 'rtk read <path>' via run_command.\n` +
|
|
563
|
+
"- If actively running, yield execution turn and rely on native Reactive Wakeup (<SYSTEM_MESSAGE>)."
|
|
412
564
|
});
|
|
413
565
|
}
|
|
414
566
|
|
|
415
|
-
const rewritten = rewriteCommandWithRtk(rawCmd);
|
|
416
567
|
const isDaemon = args.IsDaemon === true || args.isDaemon === true;
|
|
417
568
|
const waitMs = args.WaitMsBeforeAsync !== undefined ? args.WaitMsBeforeAsync : args.waitMsBeforeAsync;
|
|
418
569
|
const targetMaxWait = Number(process.env.AGY_MAX_WAIT_MS) || 10000;
|
|
@@ -456,7 +607,39 @@ try {
|
|
|
456
607
|
"";
|
|
457
608
|
const target = String(rawTarget).trim();
|
|
458
609
|
|
|
459
|
-
// 2a.
|
|
610
|
+
// 2a. Step Output Spillover Safe Harbor (hard-route to rtk read)
|
|
611
|
+
if (isStepSpilloverPath(target)) {
|
|
612
|
+
respond({
|
|
613
|
+
decision: "deny",
|
|
614
|
+
reason: `Antigravity Governance: Native file inspection via '${toolName}' bypasses token optimization. Run 'rtk read ${target}' via run_command instead.`
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// 2b. Task Log Inspection (Lifecycle-Gated)
|
|
619
|
+
const taskMatch = getTaskLogMatch(target);
|
|
620
|
+
if (taskMatch) {
|
|
621
|
+
const state = getTaskState(taskMatch, convId);
|
|
622
|
+
if (state === "RUNNING") {
|
|
623
|
+
denyRunningTask(taskMatch.id, convId);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (state === "COMPLETED") {
|
|
627
|
+
// If terminated / completed, route to rtk read:
|
|
628
|
+
respond({
|
|
629
|
+
decision: "deny",
|
|
630
|
+
reason: `Antigravity Governance: Native file inspection via '${toolName}' bypasses token optimization. Run 'rtk read ${target}' via run_command instead.`
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// If not verified completed (or does not exist):
|
|
635
|
+
respond({
|
|
636
|
+
decision: "deny",
|
|
637
|
+
reason:
|
|
638
|
+
"Antigravity Execution Governance: Direct access to internal Antigravity execution state (.system_generated/tasks) is strictly prohibited to prevent polling loops. rely on native Reactive Wakeup (<SYSTEM_MESSAGE>) upon completion."
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// 2c. Other Internal State Protection by Root (LH-02)
|
|
460
643
|
if (isProtectedInternalState(target) || isProtectedInternalState(JSON.stringify(args))) {
|
|
461
644
|
respond({
|
|
462
645
|
decision: "deny",
|
|
@@ -467,7 +650,7 @@ try {
|
|
|
467
650
|
});
|
|
468
651
|
}
|
|
469
652
|
|
|
470
|
-
//
|
|
653
|
+
// 2d. Allow Antigravity Special Files (SKILL.md, brain artifacts, configs)
|
|
471
654
|
if (isAntigravitySpecialFile(target)) {
|
|
472
655
|
respond({ decision: "allow" });
|
|
473
656
|
}
|
|
@@ -239,7 +239,7 @@ export async function runInstall(options = {}) {
|
|
|
239
239
|
hooksConfig["execution-guard"] = {
|
|
240
240
|
PreToolUse: [
|
|
241
241
|
{
|
|
242
|
-
matcher: "manage_task|schedule|run_command|view_file|read_file|read_many_files|grep_search|find_by_name|list_dir",
|
|
242
|
+
matcher: "manage_task|schedule|run_command|view_file|read_file|read_many_files|grep_search|find_by_name|list_dir|replace_file_content|write_to_file",
|
|
243
243
|
hooks: [
|
|
244
244
|
{
|
|
245
245
|
type: "command",
|
|
@@ -280,13 +280,21 @@ export async function runInstall(options = {}) {
|
|
|
280
280
|
// 5. Mirror Antigravity Tool Schemas in parallel
|
|
281
281
|
await fsPromises.mkdir(antigravityMcpDir, { recursive: true });
|
|
282
282
|
|
|
283
|
-
// Clean up
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
283
|
+
// Clean up retired schemas if present
|
|
284
|
+
const retiredSchemas = [
|
|
285
|
+
"safe_command.json",
|
|
286
|
+
"get_spillover_content.json",
|
|
287
|
+
"read_task_output.json",
|
|
288
|
+
"get_step_detail.json"
|
|
289
|
+
];
|
|
290
|
+
for (const s of retiredSchemas) {
|
|
291
|
+
const sPath = path.join(antigravityMcpDir, s);
|
|
292
|
+
if (fs.existsSync(sPath)) {
|
|
293
|
+
try {
|
|
294
|
+
await fsPromises.unlink(sPath);
|
|
295
|
+
log(`[OK] Cleaned up retired schema from: ${sPath}`);
|
|
296
|
+
} catch {}
|
|
297
|
+
}
|
|
290
298
|
}
|
|
291
299
|
|
|
292
300
|
const instructionsContent = `# Agy-Context-Saver MCP Server Instructions
|
|
@@ -302,15 +310,17 @@ Agy-Context-Saver is the universal Model Context Protocol server and lifecycle g
|
|
|
302
310
|
2. **Never Read Internal State Directly**:
|
|
303
311
|
- Access to \`.system_generated/\` via \`view_file\` is strictly denied to prevent transcript explosion.
|
|
304
312
|
- Use \`read_transcript\` or \`query_transcript\` to inspect conversation history cleanly.
|
|
305
|
-
3. **
|
|
313
|
+
3. **Inspect Output Files Directly with RTK**:
|
|
314
|
+
- Runtime step output spillovers (\`.system_generated/steps/<step>/output.txt\`) and completed background task logs (\`tasks/<taskId>.log\`) are read directly via \`rtk read <path>\` under hook governance.
|
|
315
|
+
4. **Platform-Aware Codebase Inspection**:
|
|
306
316
|
- **Windows**: Use \`rtk read <file>\`, \`rtk find <path>\`, and \`rtk rg "<pattern>" .\` (the trailing \`.\` is required to avoid stdin stalls).
|
|
307
317
|
- **POSIX**: Use \`rtk read <file>\`, \`rtk ls <path>\`, and \`rtk grep "<pattern>"\` or \`rtk rg "<pattern>"\`.
|
|
308
318
|
|
|
309
319
|
## Available MCP Tools
|
|
310
320
|
- \`check_context_health\`: Diagnoses transcript turn counts, byte payload, and polling loops. Accepts \`transcriptPath\` or \`conversationId\`.
|
|
311
|
-
- \`subagent_brief\`: Generates scope-isolated instructions for delegated subagents
|
|
321
|
+
- \`subagent_brief\`: Generates scope-isolated instructions for delegated subagents with Pointer-Over-Wire delivery contract.
|
|
312
322
|
- \`read_transcript\`: Streams recent turns from \`transcript.jsonl\` or \`transcript_full.jsonl\` formatted as clean Markdown.
|
|
313
|
-
- \`query_transcript\`: Forensic regex search and filtering engine across transcript steps with optional \`summaryOnly: true\`.
|
|
323
|
+
- \`query_transcript\`: Forensic regex search and filtering engine across transcript steps with optional \`summaryOnly: true\`, and surgical step extraction via \`stepIndex\` and \`field\`.
|
|
314
324
|
- \`get_installation_status\`: Audits live health of the plugin link, hook, MCP server, tool schemas, and RTK.
|
|
315
325
|
- \`sync_installation\`: Synchronizes and repairs all 4 integration layers in ~25ms.
|
|
316
326
|
|
|
@@ -457,6 +467,14 @@ Agy-Context-Saver is the universal Model Context Protocol server and lifecycle g
|
|
|
457
467
|
summaryOnly: {
|
|
458
468
|
type: "boolean",
|
|
459
469
|
description: "If true, returns high-density 1-line step summaries instead of full message blocks to minimize UI card height (default: false)."
|
|
470
|
+
},
|
|
471
|
+
stepIndex: {
|
|
472
|
+
type: "number",
|
|
473
|
+
description: "Optional step_index to surgically retrieve full un-truncated content for a single step from transcript_full.jsonl."
|
|
474
|
+
},
|
|
475
|
+
field: {
|
|
476
|
+
type: "string",
|
|
477
|
+
description: "Optional specific field to extract when stepIndex is provided (e.g. 'content', 'thinking', 'tool_calls', 'tool_args', or 'tool_args:Message'). Defaults to full step."
|
|
460
478
|
}
|
|
461
479
|
},
|
|
462
480
|
required: ["conversationId"]
|