@jmylchreest/aide-plugin 0.0.42 → 0.0.44
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/package.json +1 -1
- package/skills/context-usage/SKILL.md +157 -0
- package/skills/survey/SKILL.md +174 -0
- package/src/core/context-pruning/dedup.ts +173 -0
- package/src/core/context-pruning/index.ts +19 -0
- package/src/core/context-pruning/purge.ts +80 -0
- package/src/core/context-pruning/supersede.ts +67 -0
- package/src/core/context-pruning/tracker.ts +179 -0
- package/src/core/context-pruning/types.ts +63 -0
- package/src/core/session-init.ts +12 -6
- package/src/core/session-summary-logic.ts +16 -13
- package/src/core/skill-matcher.ts +28 -8
- package/src/core/types.ts +4 -0
- package/src/opencode/hooks.ts +116 -12
package/package.json
CHANGED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: context-usage
|
|
3
|
+
description: Analyze current session context and token usage from OpenCode SQLite database
|
|
4
|
+
platforms:
|
|
5
|
+
- opencode
|
|
6
|
+
triggers:
|
|
7
|
+
- context usage
|
|
8
|
+
- token usage
|
|
9
|
+
- session stats
|
|
10
|
+
- how much context
|
|
11
|
+
- context budget
|
|
12
|
+
- how big is this session
|
|
13
|
+
- session size
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Context Usage Analysis
|
|
17
|
+
|
|
18
|
+
**Recommended model tier:** balanced (sonnet) - straightforward SQL queries
|
|
19
|
+
|
|
20
|
+
Analyze the current session's context window consumption, tool usage breakdown,
|
|
21
|
+
and token costs by querying the OpenCode SQLite database directly.
|
|
22
|
+
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
- This skill **only works on OpenCode**. Verify by checking the environment:
|
|
26
|
+
- `$OPENCODE=1` — set by the OpenCode runtime
|
|
27
|
+
- `$AIDE_PLATFORM=opencode` — set by aide when running under OpenCode
|
|
28
|
+
- `$AIDE_SESSION_ID` — the current session ID (injected by aide)
|
|
29
|
+
- The OpenCode database is at `~/.local/share/opencode/opencode.db`.
|
|
30
|
+
- `sqlite3` must be available on the system.
|
|
31
|
+
|
|
32
|
+
If `$OPENCODE` is not `1` or `$AIDE_PLATFORM` is not `opencode`, abort immediately
|
|
33
|
+
and inform the user that this skill is only supported on OpenCode.
|
|
34
|
+
Do **not** attempt to query other databases (e.g. Claude Code's storage) — the
|
|
35
|
+
schema is OpenCode-specific.
|
|
36
|
+
|
|
37
|
+
If `$AIDE_SESSION_ID` is not set, abort with a message explaining that the
|
|
38
|
+
session ID could not be determined.
|
|
39
|
+
|
|
40
|
+
## Workflow
|
|
41
|
+
|
|
42
|
+
Run the following queries **sequentially** in a single Bash call (chain with `&&`).
|
|
43
|
+
Present results to the user in a formatted summary after all queries complete.
|
|
44
|
+
|
|
45
|
+
### Step 1: Validate environment
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
test "$OPENCODE" = "1" && echo "Platform: OpenCode" || echo "ERROR: Not running on OpenCode (OPENCODE=$OPENCODE)"
|
|
49
|
+
test "$AIDE_PLATFORM" = "opencode" && echo "AIDE Platform: opencode" || echo "WARNING: AIDE_PLATFORM=$AIDE_PLATFORM"
|
|
50
|
+
test -n "$AIDE_SESSION_ID" && echo "Session: $AIDE_SESSION_ID" || echo "ERROR: AIDE_SESSION_ID not set"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
If `OPENCODE` is not `1`, stop immediately — this skill cannot work outside OpenCode.
|
|
54
|
+
If `AIDE_SESSION_ID` is not set, stop and inform the user.
|
|
55
|
+
|
|
56
|
+
### Step 2: Session overview
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
sqlite3 ~/.local/share/opencode/opencode.db "
|
|
60
|
+
SELECT
|
|
61
|
+
s.title,
|
|
62
|
+
s.slug,
|
|
63
|
+
ROUND((julianday('now') - julianday(datetime(s.time_created/1000, 'unixepoch'))) * 24, 1) as hours_old,
|
|
64
|
+
(SELECT COUNT(*) FROM message m WHERE m.session_id = s.id) as messages,
|
|
65
|
+
CASE WHEN s.time_compacting IS NOT NULL THEN 'yes' ELSE 'no' END as compacted
|
|
66
|
+
FROM session s
|
|
67
|
+
WHERE s.id = '$AIDE_SESSION_ID';
|
|
68
|
+
"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Step 3: Token totals
|
|
72
|
+
|
|
73
|
+
Sum tokens from `step-finish` parts (each represents one LLM turn):
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
sqlite3 ~/.local/share/opencode/opencode.db "
|
|
77
|
+
SELECT
|
|
78
|
+
SUM(json_extract(data, '$.tokens.input')) as input_tokens,
|
|
79
|
+
SUM(json_extract(data, '$.tokens.output')) as output_tokens,
|
|
80
|
+
SUM(json_extract(data, '$.tokens.cache.read')) as cache_read_tokens,
|
|
81
|
+
SUM(json_extract(data, '$.tokens.cache.write')) as cache_write_tokens,
|
|
82
|
+
SUM(json_extract(data, '$.tokens.total')) as total_tokens,
|
|
83
|
+
COUNT(*) as llm_turns
|
|
84
|
+
FROM part
|
|
85
|
+
WHERE session_id = '$AIDE_SESSION_ID'
|
|
86
|
+
AND json_extract(data, '$.type') = 'step-finish';
|
|
87
|
+
"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Step 4: Tool output breakdown
|
|
91
|
+
|
|
92
|
+
Show tool usage ranked by total output size:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
sqlite3 ~/.local/share/opencode/opencode.db "
|
|
96
|
+
SELECT
|
|
97
|
+
json_extract(data, '$.tool') as tool,
|
|
98
|
+
COUNT(*) as calls,
|
|
99
|
+
SUM(length(json_extract(data, '$.state.output'))) as total_output_bytes,
|
|
100
|
+
ROUND(AVG(length(json_extract(data, '$.state.output')))) as avg_bytes,
|
|
101
|
+
MAX(length(json_extract(data, '$.state.output'))) as max_bytes
|
|
102
|
+
FROM part
|
|
103
|
+
WHERE session_id = '$AIDE_SESSION_ID'
|
|
104
|
+
AND json_extract(data, '$.type') = 'tool'
|
|
105
|
+
GROUP BY tool
|
|
106
|
+
ORDER BY total_output_bytes DESC;
|
|
107
|
+
"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Step 5: Total session size
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
sqlite3 ~/.local/share/opencode/opencode.db "
|
|
114
|
+
SELECT
|
|
115
|
+
SUM(length(json_extract(data, '$.state.output'))) as tool_output_bytes,
|
|
116
|
+
SUM(length(json_extract(data, '$.state.input'))) as tool_input_bytes,
|
|
117
|
+
SUM(length(data)) as total_part_bytes
|
|
118
|
+
FROM part
|
|
119
|
+
WHERE session_id = '$AIDE_SESSION_ID'
|
|
120
|
+
AND json_extract(data, '$.type') = 'tool';
|
|
121
|
+
"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Output Format
|
|
125
|
+
|
|
126
|
+
Present the results as a structured summary:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
## Session Context Usage
|
|
130
|
+
|
|
131
|
+
**Session:** <title> (<slug>)
|
|
132
|
+
**Age:** <hours> hours | **Messages:** <count> | **Compacted:** yes/no
|
|
133
|
+
|
|
134
|
+
### Token Usage
|
|
135
|
+
| Metric | Count |
|
|
136
|
+
|--------|-------|
|
|
137
|
+
| Input tokens | <n> |
|
|
138
|
+
| Output tokens | <n> |
|
|
139
|
+
| Cache read | <n> |
|
|
140
|
+
| Cache write | <n> |
|
|
141
|
+
| **Total tokens** | **<n>** |
|
|
142
|
+
| LLM turns | <n> |
|
|
143
|
+
|
|
144
|
+
### Tool Output Breakdown (by total bytes)
|
|
145
|
+
| Tool | Calls | Total Output | Avg/call | Max |
|
|
146
|
+
|------|-------|-------------|----------|-----|
|
|
147
|
+
| ... | ... | ... | ... | ... |
|
|
148
|
+
|
|
149
|
+
### Session Size
|
|
150
|
+
- Tool outputs: <n> KB
|
|
151
|
+
- Tool inputs: <n> KB
|
|
152
|
+
- Total part storage: <n> KB (includes JSON metadata overhead)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Format byte values as KB (divide by 1024, round to 1 decimal).
|
|
156
|
+
Highlight the top 3 tools by total output as the biggest context consumers.
|
|
157
|
+
If any single tool call exceeds 20KB, flag it as a potential optimization target.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: survey
|
|
3
|
+
description: Explore codebase structure, entry points, tech stack, hotspots, and call graphs
|
|
4
|
+
triggers:
|
|
5
|
+
- survey
|
|
6
|
+
- codebase structure
|
|
7
|
+
- what is this codebase
|
|
8
|
+
- tech stack
|
|
9
|
+
- entry points
|
|
10
|
+
- entrypoints
|
|
11
|
+
- what modules
|
|
12
|
+
- what packages
|
|
13
|
+
- code churn
|
|
14
|
+
- hotspots
|
|
15
|
+
- call graph
|
|
16
|
+
- who calls
|
|
17
|
+
- what calls
|
|
18
|
+
- orient me
|
|
19
|
+
- onboard
|
|
20
|
+
- codebase overview
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# Codebase Survey
|
|
24
|
+
|
|
25
|
+
**Recommended model tier:** balanced (sonnet) - this skill performs structured queries
|
|
26
|
+
|
|
27
|
+
Understand the structure, technology, entry points, and change hotspots of a codebase.
|
|
28
|
+
Survey describes WHAT the codebase IS — not code problems (use `findings` for that).
|
|
29
|
+
|
|
30
|
+
## Available Tools
|
|
31
|
+
|
|
32
|
+
### 1. Survey Stats (`mcp__plugin_aide_aide__survey_stats`)
|
|
33
|
+
|
|
34
|
+
**Start here.** Get an overview of what has been surveyed: total entries, breakdown by analyzer and kind.
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
Is the codebase surveyed?
|
|
38
|
+
→ Uses survey_stats
|
|
39
|
+
→ Returns: counts by analyzer (topology, entrypoints, churn) and kind
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. Survey Run (`mcp__plugin_aide_aide__survey_run`)
|
|
43
|
+
|
|
44
|
+
Run analyzers to populate survey data. Three analyzers available:
|
|
45
|
+
|
|
46
|
+
- **topology** — Modules, packages, workspaces, build systems, tech stack detection
|
|
47
|
+
- **entrypoints** — main() functions, HTTP handlers, gRPC services, CLI roots (cobra/urfave). Uses code index when available; falls back to file scanning
|
|
48
|
+
- **churn** — Git history hotspots (files/dirs that change most often)
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
Survey this codebase
|
|
52
|
+
→ Uses survey_run (no analyzer param = run all)
|
|
53
|
+
→ Returns: entry counts per analyzer
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 3. Survey List (`mcp__plugin_aide_aide__survey_list`)
|
|
57
|
+
|
|
58
|
+
Browse entries filtered by analyzer, kind, or file path. No search query needed.
|
|
59
|
+
|
|
60
|
+
**Kinds:** module, entrypoint, dependency, tech_stack, churn, submodule, workspace, arch_pattern
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
What modules are in this codebase?
|
|
64
|
+
→ Uses survey_list with kind=module
|
|
65
|
+
→ Returns: all module entries
|
|
66
|
+
|
|
67
|
+
What technologies does this use?
|
|
68
|
+
→ Uses survey_list with kind=tech_stack
|
|
69
|
+
→ Returns: detected frameworks, languages, build systems
|
|
70
|
+
|
|
71
|
+
What files change most?
|
|
72
|
+
→ Uses survey_list with kind=churn
|
|
73
|
+
→ Returns: high-churn files ranked by commit count
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 4. Survey Search (`mcp__plugin_aide_aide__survey_search`)
|
|
77
|
+
|
|
78
|
+
Full-text search across entry names, titles, and details. Use when looking for specific modules or technologies.
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
Find anything related to "auth"
|
|
82
|
+
→ Uses survey_search with query="auth"
|
|
83
|
+
→ Returns: modules, entrypoints, churn entries matching "auth"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 5. Call Graph (`mcp__plugin_aide_aide__survey_graph`)
|
|
87
|
+
|
|
88
|
+
Build a call graph for a symbol showing callers, callees, or both. BFS traversal over the code index.
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
Who calls BuildCallGraph?
|
|
92
|
+
→ Uses survey_graph with symbol="BuildCallGraph" direction="callers"
|
|
93
|
+
→ Returns: graph of calling symbols with file:line locations
|
|
94
|
+
|
|
95
|
+
What does handleSurveyRun call?
|
|
96
|
+
→ Uses survey_graph with symbol="handleSurveyRun" direction="callees"
|
|
97
|
+
→ Returns: graph of called symbols
|
|
98
|
+
|
|
99
|
+
Show call neighborhood of RunTopology
|
|
100
|
+
→ Uses survey_graph with symbol="RunTopology" direction="both"
|
|
101
|
+
→ Returns: both callers and callees
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Parameters:**
|
|
105
|
+
|
|
106
|
+
- `symbol` (required): Function/method name
|
|
107
|
+
- `direction`: "both" (default), "callers", "callees"
|
|
108
|
+
- `max_depth`: BFS hops (default 2)
|
|
109
|
+
- `max_nodes`: Max nodes (default 50)
|
|
110
|
+
|
|
111
|
+
**Requires:** Code index must be populated (`aide code index`).
|
|
112
|
+
|
|
113
|
+
## Workflow
|
|
114
|
+
|
|
115
|
+
### Orienting in an unfamiliar codebase
|
|
116
|
+
|
|
117
|
+
1. **Check survey status:**
|
|
118
|
+
- Use `survey_stats` to see if data exists
|
|
119
|
+
- If empty, run `survey_run` to populate
|
|
120
|
+
|
|
121
|
+
2. **Understand the structure:**
|
|
122
|
+
- `survey_list kind=module` — What are the major modules?
|
|
123
|
+
- `survey_list kind=tech_stack` — What technologies are used?
|
|
124
|
+
- `survey_list kind=workspace` — Is this a monorepo?
|
|
125
|
+
|
|
126
|
+
3. **Find entry points:**
|
|
127
|
+
- `survey_list kind=entrypoint` — Where does execution start?
|
|
128
|
+
- Identifies main() functions, HTTP handlers, CLI roots
|
|
129
|
+
|
|
130
|
+
4. **Identify hotspots:**
|
|
131
|
+
- `survey_list kind=churn` — What files change most? (complexity/bug magnets)
|
|
132
|
+
|
|
133
|
+
5. **Trace call relationships:**
|
|
134
|
+
- `survey_graph symbol="handleRequest"` — Map the call neighborhood
|
|
135
|
+
- Use `direction=callers` to find who invokes a function
|
|
136
|
+
- Use `direction=callees` to understand what a function depends on
|
|
137
|
+
|
|
138
|
+
### Answering specific questions
|
|
139
|
+
|
|
140
|
+
| Question | Tool | Parameters |
|
|
141
|
+
| ----------------------------- | --------------- | --------------------------- |
|
|
142
|
+
| "What is this codebase?" | `survey_list` | kind=module |
|
|
143
|
+
| "What tech stack?" | `survey_list` | kind=tech_stack |
|
|
144
|
+
| "Where are the entry points?" | `survey_list` | kind=entrypoint |
|
|
145
|
+
| "What changes most?" | `survey_list` | kind=churn |
|
|
146
|
+
| "Is there an auth module?" | `survey_search` | query="auth" |
|
|
147
|
+
| "Who calls this function?" | `survey_graph` | symbol=X, direction=callers |
|
|
148
|
+
| "What does this call?" | `survey_graph` | symbol=X, direction=callees |
|
|
149
|
+
|
|
150
|
+
## Survey vs Findings vs Code Search
|
|
151
|
+
|
|
152
|
+
| Tool | Purpose | Example |
|
|
153
|
+
| --------------- | -------------------- | ---------------------------------------- |
|
|
154
|
+
| **Survey** | WHAT the codebase IS | Modules, tech stack, entry points, churn |
|
|
155
|
+
| **Findings** | Code PROBLEMS | Complexity, security issues, duplication |
|
|
156
|
+
| **Code Search** | Symbol DEFINITIONS | Find function signatures, call sites |
|
|
157
|
+
|
|
158
|
+
Survey gives you the big picture. Code search gives you specific symbols. Findings gives you problems to fix.
|
|
159
|
+
|
|
160
|
+
## Prerequisites
|
|
161
|
+
|
|
162
|
+
- **Survey data:** Run `aide survey run` or use `survey_run` tool
|
|
163
|
+
- **Code index (for entrypoints + graph):** Run `aide code index`
|
|
164
|
+
- **Git history (for churn):** Must be a git repository (uses go-git, no git binary needed)
|
|
165
|
+
|
|
166
|
+
**Binary location:** The aide binary is at `.aide/bin/aide`. If it's on your `$PATH`, you can use `aide` directly.
|
|
167
|
+
|
|
168
|
+
## Notes
|
|
169
|
+
|
|
170
|
+
- Survey results are cached in BoltDB — re-run analyzers to refresh after significant changes
|
|
171
|
+
- Topology analyzer inspects the filesystem (build files, directory structure)
|
|
172
|
+
- Entrypoints analyzer uses the code index when available; falls back to file scanning
|
|
173
|
+
- Churn analyzer uses go-git to read git history directly (no git binary required)
|
|
174
|
+
- Call graph is computed on demand from the code index (not stored)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dedup strategy: replace repeated identical tool outputs with a short pointer.
|
|
3
|
+
*
|
|
4
|
+
* Safe-to-dedup tools: Read (with mtime check), Glob, Grep, and aide MCP tools
|
|
5
|
+
* like code_search, code_symbols, code_outline, code_references,
|
|
6
|
+
* findings_list, findings_search, memory_list, memory_search.
|
|
7
|
+
*
|
|
8
|
+
* NEVER dedup: Bash, Write, Edit, or any tool with side effects.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { PruneResult, PruneStrategy, ToolRecord } from "./types.js";
|
|
12
|
+
import { statSync } from "fs";
|
|
13
|
+
import { resolve, isAbsolute } from "path";
|
|
14
|
+
|
|
15
|
+
/** Tools that are safe to deduplicate. */
|
|
16
|
+
const SAFE_DEDUP_TOOLS = new Set([
|
|
17
|
+
// Host built-in read-only tools
|
|
18
|
+
"read",
|
|
19
|
+
"glob",
|
|
20
|
+
"grep",
|
|
21
|
+
// aide MCP tools (read-only)
|
|
22
|
+
"mcp__aide__code_search",
|
|
23
|
+
"mcp__aide__code_symbols",
|
|
24
|
+
"mcp__aide__code_outline",
|
|
25
|
+
"mcp__aide__code_references",
|
|
26
|
+
"mcp__aide__code_stats",
|
|
27
|
+
"mcp__aide__findings_list",
|
|
28
|
+
"mcp__aide__findings_search",
|
|
29
|
+
"mcp__aide__findings_stats",
|
|
30
|
+
"mcp__aide__memory_list",
|
|
31
|
+
"mcp__aide__memory_search",
|
|
32
|
+
"mcp__aide__decision_list",
|
|
33
|
+
"mcp__aide__decision_get",
|
|
34
|
+
"mcp__aide__decision_history",
|
|
35
|
+
"mcp__aide__state_get",
|
|
36
|
+
"mcp__aide__state_list",
|
|
37
|
+
"mcp__aide__task_list",
|
|
38
|
+
"mcp__aide__task_get",
|
|
39
|
+
"mcp__aide__message_list",
|
|
40
|
+
// Claude Code naming convention (no mcp__ prefix)
|
|
41
|
+
"code_search",
|
|
42
|
+
"code_symbols",
|
|
43
|
+
"code_outline",
|
|
44
|
+
"code_references",
|
|
45
|
+
"code_stats",
|
|
46
|
+
"findings_list",
|
|
47
|
+
"findings_search",
|
|
48
|
+
"findings_stats",
|
|
49
|
+
"memory_list",
|
|
50
|
+
"memory_search",
|
|
51
|
+
"decision_list",
|
|
52
|
+
"decision_get",
|
|
53
|
+
"decision_history",
|
|
54
|
+
"state_get",
|
|
55
|
+
"state_list",
|
|
56
|
+
"task_list",
|
|
57
|
+
"task_get",
|
|
58
|
+
"message_list",
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
/** Extract the dedup key from tool args (the args that define "same call"). */
|
|
62
|
+
function dedupKey(toolName: string, args: Record<string, unknown>): string {
|
|
63
|
+
const normalized = toolName.toLowerCase();
|
|
64
|
+
// For Read, the key is filePath + offset + limit
|
|
65
|
+
if (normalized === "read") {
|
|
66
|
+
return JSON.stringify({
|
|
67
|
+
tool: "read",
|
|
68
|
+
filePath: args.filePath ?? args.file_path ?? args.path,
|
|
69
|
+
offset: args.offset ?? 0,
|
|
70
|
+
limit: args.limit ?? 2000,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// For Glob, the key is pattern + path
|
|
74
|
+
if (normalized === "glob") {
|
|
75
|
+
return JSON.stringify({
|
|
76
|
+
tool: "glob",
|
|
77
|
+
pattern: args.pattern,
|
|
78
|
+
path: args.path,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// For Grep, the key is pattern + path + include
|
|
82
|
+
if (normalized === "grep") {
|
|
83
|
+
return JSON.stringify({
|
|
84
|
+
tool: "grep",
|
|
85
|
+
pattern: args.pattern,
|
|
86
|
+
path: args.path,
|
|
87
|
+
include: args.include,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// For MCP tools, use all args as the key
|
|
91
|
+
return JSON.stringify({ tool: normalized, ...args });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Check file mtime for Read dedup safety. */
|
|
95
|
+
function getFileMtime(
|
|
96
|
+
args: Record<string, unknown>,
|
|
97
|
+
cwd?: string,
|
|
98
|
+
): number | undefined {
|
|
99
|
+
const filePath =
|
|
100
|
+
(args.filePath as string) ??
|
|
101
|
+
(args.file_path as string) ??
|
|
102
|
+
(args.path as string);
|
|
103
|
+
if (!filePath) return undefined;
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const resolved = isAbsolute(filePath)
|
|
107
|
+
? filePath
|
|
108
|
+
: resolve(cwd || process.cwd(), filePath);
|
|
109
|
+
return statSync(resolved).mtimeMs;
|
|
110
|
+
} catch {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export class DedupStrategy implements PruneStrategy {
|
|
116
|
+
name = "dedup" as const;
|
|
117
|
+
private cwd?: string;
|
|
118
|
+
|
|
119
|
+
constructor(cwd?: string) {
|
|
120
|
+
this.cwd = cwd;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
apply(
|
|
124
|
+
toolName: string,
|
|
125
|
+
args: Record<string, unknown>,
|
|
126
|
+
output: string,
|
|
127
|
+
history: ToolRecord[],
|
|
128
|
+
): PruneResult {
|
|
129
|
+
const normalized = toolName.toLowerCase();
|
|
130
|
+
|
|
131
|
+
// Only apply to safe tools
|
|
132
|
+
if (!SAFE_DEDUP_TOOLS.has(normalized)) {
|
|
133
|
+
return { output, modified: false, bytesSaved: 0 };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const key = dedupKey(toolName, args);
|
|
137
|
+
|
|
138
|
+
// Find the most recent matching call in history
|
|
139
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
140
|
+
const prev = history[i];
|
|
141
|
+
const prevKey = dedupKey(prev.toolName, prev.args);
|
|
142
|
+
|
|
143
|
+
if (prevKey !== key) continue;
|
|
144
|
+
|
|
145
|
+
// For Read: check mtime hasn't changed (file might have been edited)
|
|
146
|
+
if (normalized === "read") {
|
|
147
|
+
const currentMtime = getFileMtime(args, this.cwd);
|
|
148
|
+
if (
|
|
149
|
+
currentMtime !== undefined &&
|
|
150
|
+
prev.fileMtime !== undefined &&
|
|
151
|
+
currentMtime !== prev.fileMtime
|
|
152
|
+
) {
|
|
153
|
+
// File changed — don't dedup
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check if output is identical
|
|
159
|
+
const prevOutput = prev.prunedOutput ?? prev.originalOutput;
|
|
160
|
+
if (output === prevOutput) {
|
|
161
|
+
const replacement = `[aide:dedup] Identical to previous ${toolName} call (callId: ${prev.callId}). Output unchanged.`;
|
|
162
|
+
return {
|
|
163
|
+
output: replacement,
|
|
164
|
+
modified: true,
|
|
165
|
+
strategy: "dedup",
|
|
166
|
+
bytesSaved: output.length - replacement.length,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { output, modified: false, bytesSaved: 0 };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Pruning — reduces context/token usage by deduplicating,
|
|
3
|
+
* superseding, and purging tool outputs.
|
|
4
|
+
*
|
|
5
|
+
* Platform adapters integrate via the ContextPruningTracker:
|
|
6
|
+
* - OpenCode: tool.execute.after hook modifies output.output
|
|
7
|
+
* - Claude Code: PostToolUse hook returns updatedMCPToolOutput
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { ContextPruningTracker } from "./tracker.js";
|
|
11
|
+
export { DedupStrategy } from "./dedup.js";
|
|
12
|
+
export { SupersedeStrategy } from "./supersede.js";
|
|
13
|
+
export { PurgeErrorsStrategy } from "./purge.js";
|
|
14
|
+
export type {
|
|
15
|
+
ToolRecord,
|
|
16
|
+
PruneResult,
|
|
17
|
+
PruneStrategy,
|
|
18
|
+
PruningStats,
|
|
19
|
+
} from "./types.js";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purge-errors strategy: replace large error outputs (stack traces, build
|
|
3
|
+
* failures) with a compact summary.
|
|
4
|
+
*
|
|
5
|
+
* When a Bash command fails and produces a large error output, most of the
|
|
6
|
+
* context is stack frames that aren't useful for the model. This strategy
|
|
7
|
+
* trims the output to the first meaningful error lines.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { PruneResult, PruneStrategy, ToolRecord } from "./types.js";
|
|
11
|
+
|
|
12
|
+
/** Minimum output size to trigger purging (2KB). */
|
|
13
|
+
const MIN_SIZE_FOR_PURGE = 2048;
|
|
14
|
+
|
|
15
|
+
/** Max lines to keep from an error output. */
|
|
16
|
+
const MAX_ERROR_LINES = 30;
|
|
17
|
+
|
|
18
|
+
/** Patterns that indicate an error output. */
|
|
19
|
+
const ERROR_PATTERNS = [
|
|
20
|
+
/^error/im,
|
|
21
|
+
/^ERR!/im,
|
|
22
|
+
/exit code [1-9]/i,
|
|
23
|
+
/FAILED/i,
|
|
24
|
+
/panic:/i,
|
|
25
|
+
/Traceback/i,
|
|
26
|
+
/^Exception/im,
|
|
27
|
+
/compilation failed/i,
|
|
28
|
+
/build failed/i,
|
|
29
|
+
/TypeError:/,
|
|
30
|
+
/SyntaxError:/,
|
|
31
|
+
/ReferenceError:/,
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export class PurgeErrorsStrategy implements PruneStrategy {
|
|
35
|
+
name = "purge" as const;
|
|
36
|
+
|
|
37
|
+
apply(
|
|
38
|
+
toolName: string,
|
|
39
|
+
_args: Record<string, unknown>,
|
|
40
|
+
output: string,
|
|
41
|
+
_history: ToolRecord[],
|
|
42
|
+
): PruneResult {
|
|
43
|
+
const normalized = toolName.toLowerCase();
|
|
44
|
+
|
|
45
|
+
// Only apply to Bash output
|
|
46
|
+
if (normalized !== "bash") {
|
|
47
|
+
return { output, modified: false, bytesSaved: 0 };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Only purge if output is large enough to matter
|
|
51
|
+
if (output.length < MIN_SIZE_FOR_PURGE) {
|
|
52
|
+
return { output, modified: false, bytesSaved: 0 };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check if output looks like an error
|
|
56
|
+
const isError = ERROR_PATTERNS.some((p) => p.test(output));
|
|
57
|
+
if (!isError) {
|
|
58
|
+
return { output, modified: false, bytesSaved: 0 };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Trim to first MAX_ERROR_LINES lines + a note
|
|
62
|
+
const lines = output.split("\n");
|
|
63
|
+
if (lines.length <= MAX_ERROR_LINES) {
|
|
64
|
+
return { output, modified: false, bytesSaved: 0 };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const kept = lines.slice(0, MAX_ERROR_LINES).join("\n");
|
|
68
|
+
const trimmedCount = lines.length - MAX_ERROR_LINES;
|
|
69
|
+
const replacement =
|
|
70
|
+
kept +
|
|
71
|
+
`\n\n[aide:purge] ... ${trimmedCount} additional error lines trimmed. Re-run the command to see full output.`;
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
output: replacement,
|
|
75
|
+
modified: true,
|
|
76
|
+
strategy: "purge",
|
|
77
|
+
bytesSaved: output.length - replacement.length,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supersede strategy: when a Write or Edit completes, mark prior Read outputs
|
|
3
|
+
* of the same file as stale so the model doesn't rely on outdated content.
|
|
4
|
+
*
|
|
5
|
+
* This doesn't replace the current tool output — it annotates previous Read
|
|
6
|
+
* records so that if they're re-read (dedup check), the stale content is flagged.
|
|
7
|
+
*
|
|
8
|
+
* For now, this strategy only adds a note to the current Write/Edit output
|
|
9
|
+
* reminding the model that prior reads of this file are now stale.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { PruneResult, PruneStrategy, ToolRecord } from "./types.js";
|
|
13
|
+
|
|
14
|
+
/** Tools that supersede prior reads. */
|
|
15
|
+
const WRITE_TOOLS = new Set(["write", "edit"]);
|
|
16
|
+
|
|
17
|
+
/** Extract the file path from tool args. */
|
|
18
|
+
function getFilePath(args: Record<string, unknown>): string | undefined {
|
|
19
|
+
return (
|
|
20
|
+
(args.filePath as string) ??
|
|
21
|
+
(args.file_path as string) ??
|
|
22
|
+
(args.path as string) ??
|
|
23
|
+
undefined
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class SupersedeStrategy implements PruneStrategy {
|
|
28
|
+
name = "supersede" as const;
|
|
29
|
+
|
|
30
|
+
apply(
|
|
31
|
+
toolName: string,
|
|
32
|
+
args: Record<string, unknown>,
|
|
33
|
+
output: string,
|
|
34
|
+
history: ToolRecord[],
|
|
35
|
+
): PruneResult {
|
|
36
|
+
const normalized = toolName.toLowerCase();
|
|
37
|
+
|
|
38
|
+
if (!WRITE_TOOLS.has(normalized)) {
|
|
39
|
+
return { output, modified: false, bytesSaved: 0 };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const filePath = getFilePath(args);
|
|
43
|
+
if (!filePath) {
|
|
44
|
+
return { output, modified: false, bytesSaved: 0 };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check if there are prior Read calls for this same file
|
|
48
|
+
const priorReads = history.filter((rec) => {
|
|
49
|
+
if (rec.toolName.toLowerCase() !== "read") return false;
|
|
50
|
+
const recPath = getFilePath(rec.args);
|
|
51
|
+
return recPath === filePath;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (priorReads.length === 0) {
|
|
55
|
+
return { output, modified: false, bytesSaved: 0 };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Annotate: prior reads of this file are now stale
|
|
59
|
+
const note = `\n[aide:supersede] Note: ${priorReads.length} prior Read(s) of "${filePath}" are now stale after this ${toolName}. Re-read if you need current content.`;
|
|
60
|
+
return {
|
|
61
|
+
output: output + note,
|
|
62
|
+
modified: true,
|
|
63
|
+
strategy: "supersede",
|
|
64
|
+
bytesSaved: 0, // We're adding, not saving bytes
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|