omp-vcc 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -18
- package/extensions/main.ts +37 -6
- package/extensions/vcc-core/details.ts +10 -1
- package/extensions/vcc-core/hook.ts +305 -30
- package/package.json +17 -8
- package/scripts/smoke.ts +13 -1
- package/skills/omp-vcc/SKILL.md +102 -20
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @zhulinchng/omp-vcc — Algorithmic VCC Compaction for oh-my-pi
|
|
2
2
|
|
|
3
|
-
> Fast, deterministic, lossless compaction
|
|
3
|
+
> Fast, deterministic, lossless compaction with no LLM calls. Port of [`sting8k/pi-vcc`](https://github.com/sting8k/pi-vcc) (`@0.7.0`) into [oh-my-pi](https://github.com/can1357/oh-my-pi), inspired by [`lllyasviel/VCC`](https://github.com/lllyasviel/VCC) and paper [`arxiv:2603.29678`](https://arxiv.org/pdf/2603.29678) *View-oriented Conversation Compiler for Agent Trace Analysis* (Zhang & Agrawala, 2026-03-31).
|
|
4
4
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
7
|
-
> **New here?** → [`docs/setup.md`](docs/setup.md)
|
|
7
|
+
> **New here?** → [`docs/setup.md`](docs/setup.md)
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
# from source (local)
|
|
@@ -23,27 +23,30 @@ omp plugin install github:zhulinchng/omp-vcc
|
|
|
23
23
|
|
|
24
24
|
## What you get
|
|
25
25
|
|
|
26
|
-
- **Auto** threshold/overflow compaction via `session_before_compact` hook
|
|
26
|
+
- **Auto** threshold/overflow compaction via `session_before_compact` hook (no LLM summary), 30–470 ms, 35–99% reduction.
|
|
27
27
|
- **Manual** `/omp-vcc [keep:N] [focus]` (and `/pi-vcc` alias) — e.g. `/omp-vcc keep:2 fix auth` keeps last 2 user turns.
|
|
28
28
|
- **Recall** `vcc_recall({query:"redis cache", scope:"all", page:1})` or `/vcc-recall hook|inject scope:all page:2` — ranked regex → TF-IDF OR, 5/page, `mode:'touched'` and `#N:path` drill-down.
|
|
29
|
+
- **Savings observability** — toast `90.0k→22.0k (76% saved, ~68.0k)`, divider `── compacted · 90K→22K ·`, `vcc_stats` tool + `/vcc-stats` table + `details.savings` + `/tmp/omp-vcc-debug.json` (authoritative `tokensAfter` from host).
|
|
29
30
|
|
|
30
31
|
## Commands
|
|
31
32
|
|
|
32
33
|
| Command | Description |
|
|
33
34
|
| --- | --- |
|
|
34
|
-
| `/omp-vcc [keep:N] [focus]` | Algorithmic compaction, smart-keep may boost `keep:1` to keep more when tail small (5 k → 25 k). `keep:0` compacts all. |
|
|
35
|
+
| `/omp-vcc [keep:N] [focus]` | Algorithmic compaction, smart-keep may boost `keep:1` to keep more when tail small (5 k → 25 k). `keep:0` compacts all. Add `--stats` / `stats` to show last savings without compacting. |
|
|
35
36
|
| `/pi-vcc` | Alias for migration |
|
|
36
37
|
| `/vcc-recall [query] [scope:all] [page:N]` | Search compacted history (V_adapt). Plain keywords best. |
|
|
37
38
|
| `/pi-vcc-recall` | Alias |
|
|
39
|
+
| `/vcc-stats [history\|all]` | Show last compaction `Before→After/Saved/Kept` + history table (from `CompactionStats` 50-capped). |
|
|
40
|
+
| `/omp-vcc-stats` | Alias for `/vcc-stats` |
|
|
38
41
|
|
|
39
|
-
Tool `vcc_recall` mirrors the command, plus `expand:[indices]` and `mode:'touched'` for file index.
|
|
40
|
-
|
|
42
|
+
Tool `vcc_recall` mirrors the command, plus `expand:[indices]` and `mode:'touched'` for file index. Tool `vcc_stats({history:true})` mirrors `/vcc-stats` (approval `read`), same 50-capped table.
|
|
41
43
|
## Configuration
|
|
42
44
|
|
|
43
45
|
File `~/.omp/omp-vcc/config.json` (XDG-aware: `$OMP_VCC_CONFIG_PATH` > `$PI_VCC_CONFIG_PATH` (legacy) > `$OMP_DIR`/`$PI_CODING_AGENT_DIR` > `~/.omp/omp-vcc/config.json`, migrates legacy `~/.pi/agent/pi-vcc-config.json`):
|
|
44
46
|
|
|
45
47
|
```json
|
|
46
48
|
{
|
|
49
|
+
"vccEnabled": true,
|
|
47
50
|
"overrideDefaultCompaction": true,
|
|
48
51
|
"smartKeepTail": true,
|
|
49
52
|
"continueAfterThresholdCompact": true,
|
|
@@ -55,19 +58,126 @@ File `~/.omp/omp-vcc/config.json` (XDG-aware: `$OMP_VCC_CONFIG_PATH` > `$PI_VCC_
|
|
|
55
58
|
|
|
56
59
|
VCC compiles the raw JSONL trace via **lex → parse IR → monotonic line assignment → view lowering** into three views sharing one coordinate system:
|
|
57
60
|
|
|
58
|
-
- `V_full` identity
|
|
59
|
-
- `V_ui` one-line tool summaries with pointers (`* Read "src/pets.py" (file.txt:18-20)`)
|
|
61
|
+
- `V_full` identity — every message verbatim, defines coordinates `L`
|
|
62
|
+
- `V_ui` one-line tool summaries with stable pointers (`* Read "src/pets.py" (file.txt:18-20)`)
|
|
60
63
|
- `V_adapt(b, ρ)` projection via predicate `ρ` preserving headers/role tags and `(f:s-e)` pointers, two transposed modalities (document vs index oriented)
|
|
61
64
|
|
|
62
|
-
`omp-vcc` implements `V_ui` as the structured summary (5 sections + ranked brief transcript) and `V_adapt` as `vcc_recall`. Pointer invariant `V_ui → V_full[s:e]` holds structurally.
|
|
65
|
+
`omp-vcc` implements `V_ui` as the structured summary (5 sections + ranked brief transcript) and `V_adapt` as `vcc_recall`. Pointer invariant `V_ui → V_full[s:e]` holds structurally.
|
|
66
|
+
|
|
67
|
+
### VCC algorithm (30–470 ms)
|
|
68
|
+
|
|
69
|
+
Traditional compaction ships the whole history to a remote LLM and waits seconds. `omp-vcc` never calls a model: it reuses `branchEntries` already in memory, calibrates token size, cuts, normalizes, and ranks locally.
|
|
70
|
+
|
|
71
|
+
```mermaid
|
|
72
|
+
flowchart TB
|
|
73
|
+
subgraph VCC["omp-vcc — local, deterministic"]
|
|
74
|
+
A["branchEntries\nin memory"] --> B["calibrate\ncpt = chars / tokensBefore"]
|
|
75
|
+
B --> C["buildOwnCut + smartKeep\nkeep tailored to tail size"]
|
|
76
|
+
C --> D["normalize + filter\nstrip ANSI, 123 arrow, harness XML"]
|
|
77
|
+
D --> E["rank TF-IDF\n5 sections + brief transcript"]
|
|
78
|
+
E --> F["summary 1.1k tok\n+ kept tail\n30-470 ms, zero cost"]
|
|
79
|
+
end
|
|
80
|
+
subgraph LLM["native remote LLM compaction"]
|
|
81
|
+
L1["branchEntries"] --> L2["serialize history\nHTTP to LLM"]
|
|
82
|
+
L2 --> L3["wait seconds\n+ token cost\n+ nondeterministic"]
|
|
83
|
+
end
|
|
84
|
+
F -. "next turn sees only F" .-> A
|
|
85
|
+
classDef vcc fill:#e8f5e9,stroke:#2e7d32
|
|
86
|
+
class F vcc
|
|
87
|
+
classDef llm fill:#fce4ec,stroke:#c2185b
|
|
88
|
+
class L3 llm
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
What `omp-vcc` adds vs intercepts in the harness (hooks, tools, commands, settings) and why — see [`docs/harness.md`](docs/harness.md) §§2–3.
|
|
92
|
+
|
|
93
|
+
**Example** — a 80-turn session at 90k tokens, `keep:1` tail is only 3k (wastes budget):
|
|
63
94
|
|
|
64
|
-
|
|
95
|
+
- raw tail `3k` → `smartKeepTail` grows to `keep:4` with tail `21k` (still ≤ 25k cap)
|
|
96
|
+
- older 76 turns are compiled into `V_ui`:
|
|
97
|
+
|
|
98
|
+
```txt
|
|
99
|
+
[Session Goal] Fix auth token refresh
|
|
100
|
+
[Files And Changes] src/auth.ts, src/app.ts
|
|
101
|
+
[Brief transcript] (ranked, TF-IDF, 78 lines)
|
|
102
|
+
(#12) Read src/auth.ts — missing refresh on expiry
|
|
103
|
+
(#18) Edit src/auth.ts:12-34 — add refreshToken()
|
|
104
|
+
(#33) Test auth flow — 2 failed, 1 passed
|
|
105
|
+
---
|
|
106
|
+
Recall is the same idea: `vcc_recall` runs local regex → TF-IDF `rank.ts` and preserves skeleton. `query: "hook|inject"` returns 5 hits with `(#N)` pointers like `(#33) hook registration`; `query: "#18:src/auth.ts"` drills to `V_full[18:e]` verbatim.
|
|
107
|
+
|
|
108
|
+
Docs: [`architecture`](docs/architecture.md) · [`configuration`](docs/configuration.md) · [`verification`](docs/verification.md) · [`harness`](docs/harness.md) · [`paper-notes`](docs/paper-notes.md) · [`setup`](docs/setup.md) · [`PUBLISHING`](docs/PUBLISHING.md) · pinned [`omp-compaction`](docs/omp-compaction.md) / [`omp-snapcompact`](docs/omp-snapcompact.md).
|
|
65
109
|
|
|
66
110
|
- **pi-vcc** — TypeScript algorithmic compactor, zero LLM, `RANKED_BRIEF_BUDGET_TOKENS=1100` ceil 2000, `charsPerBlock 15`
|
|
67
111
|
- **VCC** — Python `VCC.py` adaptive/transposed views, `SEP`, `match_lines`, `_tokenize`, `_trunc`, projection model
|
|
68
112
|
- **Paper** — AppWorld evaluation: +1.1–4.2 task_goal points, ½–⅔ token halving, smaller memory
|
|
69
113
|
|
|
70
|
-
##
|
|
114
|
+
## Best practices
|
|
115
|
+
|
|
116
|
+
> Goal: keep context small enough to stay fast and cheap, but large enough that the agent doesn't lose what you're working on.
|
|
117
|
+
|
|
118
|
+
### 1) Let auto do its job
|
|
119
|
+
|
|
120
|
+
- Keep `overrideDefaultCompaction:true` (default) — threshold/overflow compaction becomes deterministic and instant. Only set `false` if you explicitly want the remote LLM summarizer for `handoff` or you installed the optional native `vcc` dropdown patch and want to toggle per-session in `/settings`.
|
|
121
|
+
- Keep `smartKeepTail:true` and `continueAfterThresholdCompact:true` — the plugin grows `keep:1` to `keep:2…4` when the tail is tiny (5 k → 25 k) and auto-continues after a threshold compact so the agent doesn't stall mid-task.
|
|
122
|
+
- Don't spam `/omp-vcc` every few turns. Auto threshold (derived from your model's context window) already fires at the right moment. Manual compacts are for deliberate boundaries: finishing a sub-task, before a risky refactor, or when you feel the context getting noisy.
|
|
123
|
+
|
|
124
|
+
### 2) Pick the right `keep:N`
|
|
125
|
+
|
|
126
|
+
| Situation | Command | Why |
|
|
127
|
+
| --- | --- | --- |
|
|
128
|
+
| Default, happy path | `/omp-vcc` or `keep:1` | Smallest tail, max savings. `smartKeepTail` will still grow to `keep:3` if the last turn is only 3 k tok so you don't waste budget. |
|
|
129
|
+
| Actively iterating on last edits/tests | `/omp-vcc keep:2` or `keep:3` | Preserves the last 2–3 user turns verbatim (e.g. failing test output + fix). Costs more tokens but avoids recall. |
|
|
130
|
+
| Need maximal reduction (e.g. before context overflow) | `/omp-vcc keep:0` | Summarizes everything, no tail. Next turn starts from pure `V_ui`. Useful before pasting a huge spec. |
|
|
131
|
+
| With a focus prompt | `/omp-vcc keep:2 focus on auth refresh only` | Preserved tail + an injected follow-up prompt so the agent continues with the narrowed scope. |
|
|
132
|
+
|
|
133
|
+
Explicit `keep:N` always wins — smart-keep never overrides it.
|
|
134
|
+
|
|
135
|
+
### 3) Use recall instead of keeping more
|
|
136
|
+
|
|
137
|
+
Keeping a huge tail is the expensive alternative to recall. Prefer a small keep and search when you need history:
|
|
138
|
+
|
|
139
|
+
- Plain keywords first: `/vcc-recall redis cache` or `vcc_recall({query:"redis cache"})` — multi-word is OR + TF-IDF ranked (rare terms rank higher).
|
|
140
|
+
- Regex when you know the pattern: `/vcc-recall hook|inject`, `/vcc-recall fail.*build`.
|
|
141
|
+
- Pagination: `page:2` (5 hits/page): `/vcc-recall auth scope:all page:2`.
|
|
142
|
+
- Scope: default is active lineage (what the current branch actually saw). Add `scope:all` to search abandoned branches/edits/retries.
|
|
143
|
+
- Drill-down: `/vcc-recall #18:src/auth.ts` expands that turn's file slice verbatim from `V_full` — fastest way to rehydrate an edit.
|
|
144
|
+
- Index mode: `vcc_recall({query:"", mode:"touched"})` lists touched files across the session.
|
|
145
|
+
|
|
146
|
+
```mermaid
|
|
147
|
+
flowchart LR
|
|
148
|
+
KEEP["keep small\nkeep:1 + smartKeep"] --> RECALL["need history?\n/vcc-recall keywords"]
|
|
149
|
+
RECALL --> DRILL["#N:path drill\nrehydrate file"]
|
|
150
|
+
KEEP --> FORGET["keep huge tail\nwastes 10-20k tok"]
|
|
151
|
+
style FORGET stroke-dasharray: 3 3
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 4) Help the extractor help you
|
|
155
|
+
|
|
156
|
+
The 5 sections (`[Session Goal]`, `[Files And Changes]`, `[Commits]`, `[Outstanding Context]`, `[User Preferences]`) are regex/heuristics, not an LLM. Make them work better:
|
|
157
|
+
|
|
158
|
+
- State the goal once, plainly, in the first user message: `Goal: fix auth token refresh in src/auth.ts`. That seeds `[Session Goal]` reliably.
|
|
159
|
+
- Declare preferences explicitly with cue words: `always run tests before committing`, `prefer concise diffs`, `never edit src/generated/`. Those go to `[User Preferences]`.
|
|
160
|
+
- Commit frequently — commits populate `[Commits]` and survive compaction better than bare edits.
|
|
161
|
+
- Mark outstanding items as questions/errors (`TODO:`, `failing:`, `why does …?`) so they land in `[Outstanding Context]` until resolved.
|
|
162
|
+
|
|
163
|
+
### 5) Know the difference: compact vs clear
|
|
164
|
+
|
|
165
|
+
- `/omp-vcc` (or `/compact`) **summarizes** — history becomes `V_ui` + searchable `V_full`. The divider `── compacted · 90k→22k · ctrl+o ──` stays in the transcript (expand with `ctrl+o`).
|
|
166
|
+
- `/clear` **erases** — inserts a `reset_boundary` after which even `V_full` is no longer compacted. Previous history stays on disk for `/vcc-recall scope:all` but the live context starts empty. Use `/clear` when you truly want a fresh session; use `/omp-vcc` when you want to keep the story.
|
|
167
|
+
|
|
168
|
+
### 6) Debug/verify habits
|
|
169
|
+
|
|
170
|
+
- One-shot verification after install: `/omp-vcc keep:1 test` → expect toast `omp-vcc: kept …` and an inline `[Session Goal]` block. No toast = `overrideDefaultCompaction:false` or a competing compactor.
|
|
171
|
+
- To tune: set `debug:true` in `~/.omp/omp-vcc/config.json`, run `/omp-vcc`, then `cat /tmp/omp-vcc-debug.json` — check `usedOwnCut`, `tokensBefore`, `tokenEstimate {charsPerToken, mode}`, `summaryLength`, `sections`. Remember to flip `debug:false` after — the file is overwritten every compact.
|
|
172
|
+
- For overflow: if you hit `tokensBefore > 50k` and nothing happens, check `omp plugin doctor` and that `vccEnabled:true`.
|
|
173
|
+
|
|
174
|
+
### 7) Team / long-session hygiene
|
|
175
|
+
|
|
176
|
+
- One compaction covers 30–100 turns; repeated compactions merge bounded (transcript caps at ~120 lines, sections dedup). Long-running sessions (200+ turns) stay healthy — don't fear auto.
|
|
177
|
+
- `snapcompact`/`shake`/`handoff` in `compaction.methodOrder` are orthogonal — `omp-vcc` only intercepts `context-full`. Leave them in the order if you use them; they run when `omp-vcc` explicitly defers (e.g. `vccEnabled:false`).
|
|
178
|
+
- Pin the plugin version in CI or shared dots: `omp plugin install github:zhulinchng/omp-vcc#v0.1.x` so the team shares the same `RANKED_BRIEF_BUDGET_TOKENS=1100` behavior.
|
|
179
|
+
|
|
180
|
+
## Development
|
|
71
181
|
|
|
72
182
|
```bash
|
|
73
183
|
omp plugin link .
|
|
@@ -83,23 +193,28 @@ Capabilities: `extension`, `skill`, `command` — entry `extensions/main.ts`.
|
|
|
83
193
|
|
|
84
194
|
```sh
|
|
85
195
|
bunx tsc --noEmit
|
|
86
|
-
bun test #
|
|
87
|
-
bun run smoke #
|
|
196
|
+
bun test # 378 tests across 36 files, 1007 expects
|
|
197
|
+
bun run smoke # 9 checks: 3 hooks + 4 commands + 2 tools (vcc_recall, vcc_stats)
|
|
88
198
|
omp plugin link /Users/zhu/code/projects/omp-vcc && omp plugin doctor
|
|
89
199
|
```
|
|
90
200
|
|
|
91
|
-
In a live `omp` session: `/omp-vcc keep:1` shows `[Session Goal]` toast `omp-vcc: kept 1/5 turns, ~2.1k tok
|
|
92
|
-
|
|
201
|
+
In a live `omp` session: `/omp-vcc keep:1` shows `[Session Goal]` with toast `omp-vcc: 90.0k→22.0k (76% saved, ~68.0k) · kept 1/5 turns, ~2.1k tok` (fallback `omp-vcc: kept 1/5 turns…` when `tokensBefore` unavailable) + divider `── compacted · 90K→22K · ctrl+o ──`; with `debug:true` check `/tmp/omp-vcc-debug.json` (`savings` + `authoritativeSavings`). `/vcc-stats` / `/omp-vcc --stats` / `vcc_stats({history:true})` show the 50-capped `Before→After/Saved/Kept/Summarized/When` table. Full proof matrix and mermaid flows in [`docs/verification.md`](docs/verification.md); harness impact (adds vs intercepts) in [`docs/harness.md`](docs/harness.md). Smoke checks map to the re-runnable truth table in [`docs/harness.md` §9](docs/harness.md#9-verification-map-claim--evidence).
|
|
202
|
+
|
|
203
|
+
Docs: [`architecture`](docs/architecture.md) · [`configuration`](docs/configuration.md) · [`verification`](docs/verification.md) · [`harness`](docs/harness.md) · [`paper-notes`](docs/paper-notes.md) · [`setup`](docs/setup.md) · [`PUBLISHING`](docs/PUBLISHING.md) · pinned [`omp-compaction`](docs/omp-compaction.md) / [`omp-snapcompact`](docs/omp-snapcompact.md).
|
|
93
204
|
|
|
94
|
-
|
|
95
|
-
|
|
205
|
+
See [`docs/PUBLISHING.md`](docs/PUBLISHING.md) for the full checklist (package shape, gates, dual `omp-vcc` / `@zhulinchng/omp-vcc` flow, verification, deployment matrix, and troubleshooting). TL;DR:
|
|
206
|
+
|
|
207
|
+
- npmjs (unscoped): `npm publish --access public` (package `omp-vcc`, 2FA `auth-and-writes` → browser or `--otp`)
|
|
208
|
+
- GitHub Packages (scoped): `gh release create vX.Y.Z` triggers `.github/workflows/publish-gpr.yml` → `@zhulinchng/omp-vcc` via `GITHUB_TOKEN` (`read:packages, write:packages`); manual fallback `npm pkg set name=@zhulinchng/omp-vcc && GITHUB_TOKEN=$(gh auth token) npm publish --userconfig /tmp/gpr-npmrc --access public` (see guide for the scoped-registry `/tmp/gpr-npmrc` pitfall)
|
|
96
209
|
- Consumer GPR auth: add to `~/.npmrc`:
|
|
210
|
+
|
|
97
211
|
```
|
|
98
212
|
@zhulinchng:registry=https://npm.pkg.github.com
|
|
99
213
|
//npm.pkg.github.com/:_authToken=YOUR_GITHUB_PAT
|
|
100
214
|
```
|
|
215
|
+
|
|
101
216
|
- Marketplace: add an entry to `.omp-plugin/marketplace.json` (see `plugin-skill/assets/templates/marketplace-entry.json.template`).
|
|
102
217
|
|
|
103
218
|
## License
|
|
104
219
|
|
|
105
|
-
MIT
|
|
220
|
+
MIT
|
package/extensions/main.ts
CHANGED
|
@@ -6,14 +6,19 @@
|
|
|
6
6
|
|
|
7
7
|
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
8
8
|
import { scaffoldSettings } from "./vcc-core/core/settings";
|
|
9
|
+
import { loadAllMessages } from "./vcc-core/core/load-messages";
|
|
9
10
|
import {
|
|
10
11
|
registerBeforeCompactHook,
|
|
11
12
|
PI_VCC_COMPACT_INSTRUCTION,
|
|
12
13
|
OMP_VCC_COMPACT_INSTRUCTION,
|
|
13
14
|
getLastCompactionStats,
|
|
15
|
+
getCompactionHistory,
|
|
16
|
+
formatLastStatsDetail,
|
|
17
|
+
formatStatsTable,
|
|
14
18
|
scheduleCompactionStatsNotify,
|
|
19
|
+
registerVccStatsTool as registerVccStatsToolHook,
|
|
20
|
+
registerVccStatsCommand as registerVccStatsCommandHook,
|
|
15
21
|
} from "./vcc-core/hook";
|
|
16
|
-
import { loadAllMessages } from "./vcc-core/core/load-messages";
|
|
17
22
|
import { searchEntriesDetailed, getTouchedFiles } from "./vcc-core/core/search-entries";
|
|
18
23
|
import { formatRecallOutput, formatTouchedOutput } from "./vcc-core/core/format-recall";
|
|
19
24
|
import { getActiveLineageEntryIds } from "./vcc-core/core/lineage";
|
|
@@ -152,15 +157,37 @@ export default function (pi: ExtensionAPI): void {
|
|
|
152
157
|
return { content: [{ type: "text", text: output }], details: undefined };
|
|
153
158
|
},
|
|
154
159
|
} as unknown as Parameters<ExtensionAPI["registerTool"]>[0]);
|
|
160
|
+
// ── vcc_stats tool — stats surface for savings (paper § verification) ──
|
|
161
|
+
registerVccStatsToolHook(pi);
|
|
162
|
+
|
|
155
163
|
|
|
156
|
-
// ── /omp-vcc command — manual algorithmic compaction (V_ui) ──
|
|
157
164
|
pi.registerCommand("omp-vcc", {
|
|
158
|
-
description: "Compact conversation with omp-vcc structured summary (keep:N + optional focus)",
|
|
165
|
+
description: "Compact conversation with omp-vcc structured summary (keep:N + optional focus) — add --stats to show savings",
|
|
159
166
|
handler: async (args: string, ctx: unknown) => {
|
|
160
167
|
const c = ctx as {
|
|
161
168
|
compact: (instructions?: string) => Promise<void>;
|
|
162
169
|
ui: { notify: (msg: string, level?: string) => void };
|
|
170
|
+
sessionManager?: { getSessionFile?: () => string | undefined };
|
|
163
171
|
};
|
|
172
|
+
const trimmed = (args || "").trim();
|
|
173
|
+
const lower = trimmed.toLowerCase();
|
|
174
|
+
if (lower === "--stats" || lower === "stats" || lower.startsWith("--stats ") || lower.startsWith("stats ")) {
|
|
175
|
+
const wantHistory = lower.includes("history") || lower.includes("all");
|
|
176
|
+
const history = getCompactionHistory(pi);
|
|
177
|
+
const last = getLastCompactionStats(pi);
|
|
178
|
+
const piAny = pi as unknown as { sendMessage?: (msg: unknown, opts?: unknown) => void };
|
|
179
|
+
if (!last && history.length === 0) {
|
|
180
|
+
try { piAny.sendMessage?.({ customType: "vcc-stats", content: "No compactions yet. Run /omp-vcc to compact first.", display: true }, { triggerTurn: false }); } catch {}
|
|
181
|
+
try { c.ui.notify("No compactions yet.", "info"); } catch {}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const output = wantHistory
|
|
185
|
+
? `${formatStatsTable(history)}\n\n${last ? formatLastStatsDetail(last) : ""}`
|
|
186
|
+
: `${last ? formatLastStatsDetail(last) : "No last stats"}${history.length > 1 ? `\n\nHistory:\n${formatStatsTable(history)}` : ""}`;
|
|
187
|
+
try { piAny.sendMessage?.({ customType: "vcc-stats", content: output, display: true }, { triggerTurn: false }); } catch {}
|
|
188
|
+
try { c.ui.notify(`vcc_stats: ${history.length} compaction(s)`, "info"); } catch {}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
164
191
|
const parsed = parseKeepAndPrompt(args);
|
|
165
192
|
const keep = parsed.keepUserTurns;
|
|
166
193
|
const followUpPrompt = parsed.followUpPrompt;
|
|
@@ -172,7 +199,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
172
199
|
} catch {}
|
|
173
200
|
try {
|
|
174
201
|
await c.compact(customInstructions);
|
|
175
|
-
const stats = getLastCompactionStats();
|
|
202
|
+
const stats = getLastCompactionStats(pi);
|
|
176
203
|
if (stats) {
|
|
177
204
|
scheduleCompactionStatsNotify(c as unknown as { ui: { notify: (msg: string, level?: string) => void } }, stats);
|
|
178
205
|
} else {
|
|
@@ -209,7 +236,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
209
236
|
const customInstructions = buildPiVccCustomInstructions(keep);
|
|
210
237
|
try {
|
|
211
238
|
await c.compact(customInstructions);
|
|
212
|
-
const stats = getLastCompactionStats();
|
|
239
|
+
const stats = getLastCompactionStats(pi);
|
|
213
240
|
if (stats) scheduleCompactionStatsNotify(c as unknown as { ui: { notify: (msg: string, level?: string) => void } }, stats);
|
|
214
241
|
else try { c.ui.notify("Compacted with pi-vcc (via omp-vcc)", "info"); } catch {}
|
|
215
242
|
if (followUpPrompt) {
|
|
@@ -309,11 +336,15 @@ export default function (pi: ExtensionAPI): void {
|
|
|
309
336
|
const header = totalPages > 1 ? `Page ${page}/${totalPages} (${hits.length} total matches${scopeSuffix}${truncationNote})` : `${hits.length} matches${scopeSuffix}${truncationNote}`;
|
|
310
337
|
const footer = page < totalPages ? `\n--- /pi-vcc-recall ${query}${scopeArg} page:${page + 1} ---` : "";
|
|
311
338
|
const output = formatRecallOutput(pageResults, query, header) + footer;
|
|
339
|
+
|
|
312
340
|
try { piAny.sendMessage?.({ customType: "vcc-recall", content: output, display: true }, { triggerTurn: false }); } catch {}
|
|
313
341
|
},
|
|
314
342
|
});
|
|
343
|
+
// ── /vcc-stats commands — show savings table (PR3) ──
|
|
344
|
+
registerVccStatsCommandHook(pi);
|
|
345
|
+
|
|
315
346
|
}
|
|
316
347
|
// ── Re-exports for pi-vcc test compatibility (not dead: tests import via hook directly,
|
|
317
348
|
// but external consumers and the `vcc-recall` shim may import via main) ──
|
|
318
|
-
export { registerBeforeCompactHook, PI_VCC_COMPACT_INSTRUCTION, OMP_VCC_COMPACT_INSTRUCTION, getLastCompactionStats,
|
|
349
|
+
export { registerBeforeCompactHook, PI_VCC_COMPACT_INSTRUCTION, OMP_VCC_COMPACT_INSTRUCTION, getLastCompactionStats, getCompactionHistory, formatCompactionStats, formatStatsTable, formatLastStatsDetail, scheduleCompactionStatsNotify, AUTO_CONTINUE_CUSTOM_TYPE, LEGACY_AUTO_CONTINUE_CUSTOM_TYPE, invalidExpandIndices, registerRecallTool, registerVccRecallCommand, registerPiVccCommand, registerVccStatsTool, registerVccStatsCommand, clearCompactionHistoryForTests } from "./vcc-core/hook";
|
|
319
350
|
export { buildPiVccCustomInstructions, parseKeepAndPrompt } from "./vcc-core/core/compact-args";
|
|
@@ -2,11 +2,20 @@
|
|
|
2
2
|
import type { CompactionReason } from "./types";
|
|
3
3
|
|
|
4
4
|
export interface PiVccCompactionDetails {
|
|
5
|
-
compactor: "pi-vcc";
|
|
5
|
+
compactor: "pi-vcc" | "omp-vcc";
|
|
6
6
|
version: number;
|
|
7
7
|
sections: string[];
|
|
8
8
|
sourceMessageCount: number;
|
|
9
9
|
previousSummaryUsed: boolean;
|
|
10
10
|
reason?: CompactionReason;
|
|
11
11
|
willRetry?: boolean;
|
|
12
|
+
savings?: {
|
|
13
|
+
tokensBefore: number;
|
|
14
|
+
summaryChars: number;
|
|
15
|
+
summaryTokensEst: number;
|
|
16
|
+
keptTokensEst: number;
|
|
17
|
+
tokensAfterEst: number;
|
|
18
|
+
tokensSavedEst: number;
|
|
19
|
+
savedPercentEst: number;
|
|
20
|
+
};
|
|
12
21
|
}
|
|
@@ -52,6 +52,26 @@ export interface CompactionStats {
|
|
|
52
52
|
smartFromKeep?: number;
|
|
53
53
|
reason?: CompactionReason;
|
|
54
54
|
willRetry?: boolean;
|
|
55
|
+
/** Tokens before compaction (from preparation). */
|
|
56
|
+
tokensBefore?: number;
|
|
57
|
+
/** Summary char length */
|
|
58
|
+
summaryChars?: number;
|
|
59
|
+
/** Summary tokens estimate via calibrated cpt */
|
|
60
|
+
summaryTokensEst?: number;
|
|
61
|
+
/** Estimated tokens after = summaryTokensEst + keptTokensEst */
|
|
62
|
+
tokensAfterEst?: number;
|
|
63
|
+
/** Authoritative tokensAfter from host (compactionEntry) */
|
|
64
|
+
tokensAfter?: number;
|
|
65
|
+
/** Estimated saved = tokensBefore - tokensAfterEst */
|
|
66
|
+
tokensSavedEst?: number;
|
|
67
|
+
/** Authoritative saved */
|
|
68
|
+
tokensSaved?: number;
|
|
69
|
+
/** Estimated percent 0-100 */
|
|
70
|
+
savedPercentEst?: number;
|
|
71
|
+
/** Authoritative percent */
|
|
72
|
+
savedPercent?: number;
|
|
73
|
+
/** When compaction occurred */
|
|
74
|
+
timestamp?: number;
|
|
55
75
|
}
|
|
56
76
|
|
|
57
77
|
export type BudgetCutKind = "no_anchor" | "oversized_tail";
|
|
@@ -61,17 +81,37 @@ let lastStats: CompactionStats | null = null;
|
|
|
61
81
|
let lastCompactWasPiVcc = false;
|
|
62
82
|
let pendingFollowUpPrompt: string | null = null;
|
|
63
83
|
let pendingAutoContinueTimer: any = null;
|
|
84
|
+
let globalHistory: CompactionStats[] = [];
|
|
64
85
|
// Per-pi state to avoid cross-session pollution when multiple sessions share the
|
|
65
86
|
// same ESM module singleton (e.g. main + subagents). Module globals remain as
|
|
66
87
|
// fallback for host-free tests that call getLastCompactionStats() without a pi.
|
|
67
|
-
const perPi = new WeakMap<any, { lastStats: CompactionStats | null; lastCompactWasPiVcc: boolean; pendingFollowUpPrompt: string | null; pendingAutoContinueTimer: any }>();
|
|
88
|
+
const perPi = new WeakMap<any, { lastStats: CompactionStats | null; lastCompactWasPiVcc: boolean; pendingFollowUpPrompt: string | null; pendingAutoContinueTimer: any; statsHistory: CompactionStats[] }>();
|
|
89
|
+
// Track strong refs for test helper clearCompactionHistoryForTests: WeakMap keys
|
|
90
|
+
// cannot be enumerated, so keep a Set for test-only cleanup.
|
|
91
|
+
const perPiKeys = new Set<any>();
|
|
68
92
|
const getPerPi = (pi: any) => {
|
|
69
93
|
if (!pi || typeof pi !== "object") return null;
|
|
70
94
|
let s = perPi.get(pi);
|
|
71
|
-
if (!s) { s = { lastStats: null, lastCompactWasPiVcc: false, pendingFollowUpPrompt: null, pendingAutoContinueTimer: null }; perPi.set(pi, s); }
|
|
95
|
+
if (!s) { s = { lastStats: null, lastCompactWasPiVcc: false, pendingFollowUpPrompt: null, pendingAutoContinueTimer: null, statsHistory: [] }; perPi.set(pi, s); perPiKeys.add(pi); }
|
|
96
|
+
if (!s.statsHistory) s.statsHistory = [];
|
|
72
97
|
return s;
|
|
73
98
|
};
|
|
74
|
-
const setLastStats = (pi: any, v: CompactionStats | null) => {
|
|
99
|
+
const setLastStats = (pi: any, v: CompactionStats | null) => {
|
|
100
|
+
if (v && v.timestamp == null) v.timestamp = Date.now();
|
|
101
|
+
lastStats = v;
|
|
102
|
+
const s = getPerPi(pi);
|
|
103
|
+
if (s) {
|
|
104
|
+
s.lastStats = v;
|
|
105
|
+
if (v) {
|
|
106
|
+
s.statsHistory.push(v);
|
|
107
|
+
if (s.statsHistory.length > 50) s.statsHistory.shift();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (v) {
|
|
111
|
+
globalHistory.push(v);
|
|
112
|
+
if (globalHistory.length > 50) globalHistory.shift();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
75
115
|
const setLastCompactWasPiVcc = (pi: any, v: boolean) => { lastCompactWasPiVcc = v; const s = getPerPi(pi); if (s) s.lastCompactWasPiVcc = v; };
|
|
76
116
|
const setPendingFollowUpPrompt = (pi: any, v: string | null) => { pendingFollowUpPrompt = v; const s = getPerPi(pi); if (s) s.pendingFollowUpPrompt = v; };
|
|
77
117
|
const getPendingFollowUpPrompt = (pi: any) => { const s = getPerPi(pi); return s ? s.pendingFollowUpPrompt : pendingFollowUpPrompt; };
|
|
@@ -135,25 +175,131 @@ const scheduleAutoContinue = (pi: any) => {
|
|
|
135
175
|
}, 0);
|
|
136
176
|
};
|
|
137
177
|
|
|
138
|
-
export const getLastCompactionStats = () =>
|
|
139
|
-
|
|
178
|
+
export const getLastCompactionStats = (pi?: any) => {
|
|
179
|
+
if (pi) {
|
|
180
|
+
const s = getPerPi(pi);
|
|
181
|
+
return s?.lastStats ?? null;
|
|
182
|
+
}
|
|
183
|
+
return lastStats;
|
|
184
|
+
};
|
|
140
185
|
const formatTokens = (n: number): string => {
|
|
141
186
|
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
142
187
|
return String(n);
|
|
143
188
|
};
|
|
144
189
|
|
|
145
190
|
export const formatCompactionStats = (stats: CompactionStats): string => {
|
|
191
|
+
const before = stats.tokensBefore ?? 0;
|
|
192
|
+
const after = stats.tokensAfter ?? stats.tokensAfterEst ?? 0;
|
|
193
|
+
const savedRaw = stats.tokensSaved ?? stats.tokensSavedEst;
|
|
194
|
+
const saved = typeof savedRaw === "number" ? savedRaw : (before > 0 && after > 0 ? Math.max(0, before - after) : 0);
|
|
195
|
+
const percentRaw = stats.savedPercent ?? stats.savedPercentEst;
|
|
196
|
+
const percent = typeof percentRaw === "number" ? percentRaw : (before > 0 && saved > 0 ? Math.round((saved / before) * 100) : 0);
|
|
197
|
+
const hasSavings = before > 0 && after > 0 && before > after && saved > 0 && percent > 0;
|
|
198
|
+
const savingsPrefix = hasSavings ? `${formatTokens(before)}→${formatTokens(after)} (${percent}% saved, ~${formatTokens(saved)}) · ` : "";
|
|
199
|
+
const keptTokens = stats.keptTokensEst ?? 0;
|
|
200
|
+
const summarized = stats.summarized ?? 0;
|
|
201
|
+
const keptTurns = stats.keptUserTurns ?? 0;
|
|
202
|
+
const totalTurns = stats.totalUserTurns ?? 0;
|
|
146
203
|
if (stats.budgetCut) {
|
|
147
204
|
const reason = stats.budgetCut === "no_anchor" ? "no user anchor" : "oversized tail";
|
|
148
|
-
|
|
205
|
+
if (savingsPrefix) {
|
|
206
|
+
return `omp-vcc: ${savingsPrefix}kept ~${formatTokens(keptTokens)} tok tail (mid-turn cut, ${reason}), summarized ${summarized}.`;
|
|
207
|
+
}
|
|
208
|
+
return `omp-vcc: kept ~${formatTokens(keptTokens)} tok tail (mid-turn cut, ${reason}), summarized ${summarized}.`;
|
|
149
209
|
}
|
|
150
|
-
const notes: string[] = [`summarized ${
|
|
210
|
+
const notes: string[] = [`summarized ${summarized}`];
|
|
151
211
|
if (stats.smartKeepAdjusted) {
|
|
152
212
|
notes.push("smart-keep");
|
|
153
213
|
}
|
|
154
|
-
|
|
214
|
+
if (savingsPrefix) {
|
|
215
|
+
return `omp-vcc: ${savingsPrefix}kept ${keptTurns}/${totalTurns} turns, ~${formatTokens(keptTokens)} tok (${notes.join(", ")}).`;
|
|
216
|
+
}
|
|
217
|
+
return `omp-vcc: kept ${keptTurns}/${totalTurns} turns, ~${formatTokens(keptTokens)} tok (${notes.join(", ")}).`;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export const getCompactionHistory = (pi?: any): CompactionStats[] => {
|
|
221
|
+
if (pi) {
|
|
222
|
+
const s = getPerPi(pi);
|
|
223
|
+
if (s?.statsHistory) return [...s.statsHistory];
|
|
224
|
+
}
|
|
225
|
+
return [...globalHistory];
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export const clearCompactionHistoryForTests = () => {
|
|
229
|
+
globalHistory = [];
|
|
230
|
+
lastStats = null;
|
|
231
|
+
lastCompactWasPiVcc = false;
|
|
232
|
+
pendingFollowUpPrompt = null;
|
|
233
|
+
clearTimeout(pendingAutoContinueTimer as any);
|
|
234
|
+
pendingAutoContinueTimer = null;
|
|
235
|
+
for (const pi of perPiKeys) {
|
|
236
|
+
const s = perPi.get(pi);
|
|
237
|
+
if (s) {
|
|
238
|
+
s.statsHistory = [];
|
|
239
|
+
s.lastStats = null;
|
|
240
|
+
s.lastCompactWasPiVcc = false;
|
|
241
|
+
s.pendingFollowUpPrompt = null;
|
|
242
|
+
clearTimeout(s.pendingAutoContinueTimer as any);
|
|
243
|
+
s.pendingAutoContinueTimer = null;
|
|
244
|
+
}
|
|
245
|
+
// Remove strong ref so pi can be GC'd and WeakMap entry cleared; fresh
|
|
246
|
+
// getPerPi(pi) will recreate if this pi is reused, but tests create fresh
|
|
247
|
+
// pi objects each time, so clearing prevents unbounded Set growth across
|
|
248
|
+
// the 377-test suite.
|
|
249
|
+
perPi.delete(pi);
|
|
250
|
+
}
|
|
251
|
+
perPiKeys.clear();
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
export const formatStatsTable = (history: CompactionStats[]): string => {
|
|
255
|
+
if (!history || history.length === 0) return "No compactions yet.";
|
|
256
|
+
const header = "| # | Before → After | Saved | Kept | Summarized | When |";
|
|
257
|
+
const sep = "|---|---|---|---|---|---|---|";
|
|
258
|
+
const rows = history.map((s, idx) => {
|
|
259
|
+
const before = s.tokensBefore ?? 0;
|
|
260
|
+
const after = s.tokensAfter ?? s.tokensAfterEst ?? 0;
|
|
261
|
+
const saved = s.tokensSaved ?? s.tokensSavedEst ?? (before > 0 && after > 0 ? Math.max(0, before - after) : 0);
|
|
262
|
+
const percent = s.savedPercent ?? s.savedPercentEst ?? (before > 0 && saved > 0 ? Math.round((saved / before) * 100) : 0);
|
|
263
|
+
const beforeAfter = before > 0 && after > 0 ? `${formatTokens(before)}→${formatTokens(after)}` : `${formatTokens(before)}→${formatTokens(after)}`;
|
|
264
|
+
const savedStr = saved > 0 ? `${formatTokens(saved)} (${percent}%)` : "—";
|
|
265
|
+
const keptTurns = s.keptUserTurns ?? 0;
|
|
266
|
+
const totalTurns = s.totalUserTurns ?? 0;
|
|
267
|
+
const keptTok = s.keptTokensEst ?? 0;
|
|
268
|
+
const summarized = s.summarized ?? 0;
|
|
269
|
+
const keptStr = `${keptTurns}/${totalTurns} turns, ~${formatTokens(keptTok)} tok${s.budgetCut ? ` (${s.budgetCut})` : ""}`;
|
|
270
|
+
const when = s.timestamp ? new Date(s.timestamp).toISOString().slice(0, 19).replace("T", " ") : "—";
|
|
271
|
+
return `| ${idx + 1} | ${beforeAfter} | ${savedStr} | ${keptStr} | ${summarized} | ${when} |`;
|
|
272
|
+
});
|
|
273
|
+
return [header, sep, ...rows].join("\n");
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
export const formatLastStatsDetail = (stats: CompactionStats | null): string => {
|
|
277
|
+
if (!stats) return "No compaction has run yet.";
|
|
278
|
+
const before = stats.tokensBefore ?? 0;
|
|
279
|
+
const after = stats.tokensAfter ?? stats.tokensAfterEst ?? 0;
|
|
280
|
+
const saved = stats.tokensSaved ?? stats.tokensSavedEst ?? (before > 0 && after > 0 ? Math.max(0, before - after) : 0);
|
|
281
|
+
const percent = stats.savedPercent ?? stats.savedPercentEst ?? (before > 0 && saved > 0 ? Math.round((saved / before) * 100) : 0);
|
|
282
|
+
const kept = stats.kept ?? 0;
|
|
283
|
+
const keptTurns = stats.keptUserTurns ?? 0;
|
|
284
|
+
const totalTurns = stats.totalUserTurns ?? 0;
|
|
285
|
+
const keptTok = stats.keptTokensEst ?? 0;
|
|
286
|
+
const summaryTok = stats.summaryTokensEst ?? 0;
|
|
287
|
+
const summaryChars = stats.summaryChars ?? 0;
|
|
288
|
+
const summarized = stats.summarized ?? 0;
|
|
289
|
+
const lines = [
|
|
290
|
+
`**Last compaction** ${stats.timestamp ? new Date(stats.timestamp).toISOString() : ""}`,
|
|
291
|
+
`- Before → After: **${formatTokens(before)} → ${formatTokens(after)}** (${percent}% saved, ~${formatTokens(saved)})`,
|
|
292
|
+
`- Summary: ~${formatTokens(summaryTok)} tok (${summaryChars} chars), kept tail ~${formatTokens(keptTok)} tok (${kept} msgs, ${keptTurns}/${totalTurns} turns)`,
|
|
293
|
+
`- Summarized: ${summarized} messages${stats.smartKeepAdjusted ? ` (smart-keep ${stats.smartFromKeep}→${keptTurns})` : ""}${stats.budgetCut ? ` · budgetCut:${stats.budgetCut}` : ""}`,
|
|
294
|
+
`- Details: ${stats.reason ? `reason=${stats.reason}` : "reason=auto"}${stats.willRetry ? " willRetry=true" : ""}`,
|
|
295
|
+
];
|
|
296
|
+
if (stats.tokensAfter != null && stats.tokensAfterEst != null && stats.tokensAfter !== stats.tokensAfterEst) {
|
|
297
|
+
lines.push(`- Note: est after ${formatTokens(stats.tokensAfterEst)} vs authoritative ${formatTokens(stats.tokensAfter)}`);
|
|
298
|
+
}
|
|
299
|
+
return lines.join("\n");
|
|
155
300
|
};
|
|
156
301
|
|
|
302
|
+
|
|
157
303
|
const readCompactionEventContext = (event: unknown): { reason?: CompactionReason; willRetry: boolean } => {
|
|
158
304
|
const raw = event as { reason?: unknown; willRetry?: unknown };
|
|
159
305
|
const reason = raw.reason === "manual" || raw.reason === "threshold" || raw.reason === "overflow"
|
|
@@ -726,21 +872,7 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
726
872
|
(sum: number, e: any) => sum + estimateMessageContentChars(e.message?.content),
|
|
727
873
|
0,
|
|
728
874
|
);
|
|
729
|
-
|
|
730
|
-
summarized: agentMessages.length,
|
|
731
|
-
kept: keptEntries.length,
|
|
732
|
-
keptUserTurns: ownCut.keptUserTurns,
|
|
733
|
-
totalUserTurns: ownCut.totalUserTurns,
|
|
734
|
-
requestedKeepUserTurns: ownCut.requestedKeepUserTurns,
|
|
735
|
-
keepUserTurnsExplicit,
|
|
736
|
-
keepFallbackToCompactAll: ownCut.keepFallbackToCompactAll,
|
|
737
|
-
keptTokensEst: estimateTokensFromChars(keptChars, tokenEstimate.charsPerToken),
|
|
738
|
-
smartKeepAdjusted: smartKeep.smartAdjusted,
|
|
739
|
-
smartFromKeep: smartKeep.fromKeep,
|
|
740
|
-
budgetCut: ownCut.ok ? ownCut.budgetCut : undefined,
|
|
741
|
-
reason,
|
|
742
|
-
willRetry,
|
|
743
|
-
});
|
|
875
|
+
const keptTokensEst = estimateTokensFromChars(keptChars, tokenEstimate.charsPerToken);
|
|
744
876
|
const config = settings;
|
|
745
877
|
|
|
746
878
|
// Ranked compaction: keep the highest-signal blocks under a token budget
|
|
@@ -776,6 +908,35 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
776
908
|
},
|
|
777
909
|
});
|
|
778
910
|
|
|
911
|
+
const tokensBefore = typeof preparation.tokensBefore === "number" ? preparation.tokensBefore : 0;
|
|
912
|
+
const summaryChars = summary.length;
|
|
913
|
+
const summaryTokensEst = estimateTokensFromChars(summaryChars, tokenEstimate.charsPerToken);
|
|
914
|
+
const tokensAfterEst = summaryTokensEst + keptTokensEst;
|
|
915
|
+
const tokensSavedEst = tokensBefore > 0 ? Math.max(0, tokensBefore - tokensAfterEst) : 0;
|
|
916
|
+
const savedPercentEst = tokensBefore > 0 && tokensSavedEst > 0 ? Math.round((tokensSavedEst / tokensBefore) * 100) : 0;
|
|
917
|
+
|
|
918
|
+
setLastStats(pi, {
|
|
919
|
+
summarized: agentMessages.length,
|
|
920
|
+
kept: keptEntries.length,
|
|
921
|
+
keptUserTurns: ownCut.keptUserTurns,
|
|
922
|
+
totalUserTurns: ownCut.totalUserTurns,
|
|
923
|
+
requestedKeepUserTurns: ownCut.requestedKeepUserTurns,
|
|
924
|
+
keepUserTurnsExplicit,
|
|
925
|
+
keepFallbackToCompactAll: ownCut.keepFallbackToCompactAll,
|
|
926
|
+
keptTokensEst,
|
|
927
|
+
smartKeepAdjusted: smartKeep.smartAdjusted,
|
|
928
|
+
smartFromKeep: smartKeep.fromKeep,
|
|
929
|
+
budgetCut: ownCut.ok ? ownCut.budgetCut : undefined,
|
|
930
|
+
reason,
|
|
931
|
+
willRetry,
|
|
932
|
+
tokensBefore,
|
|
933
|
+
summaryChars,
|
|
934
|
+
summaryTokensEst,
|
|
935
|
+
tokensAfterEst,
|
|
936
|
+
tokensSavedEst,
|
|
937
|
+
savedPercentEst,
|
|
938
|
+
});
|
|
939
|
+
|
|
779
940
|
const branchIds = branchEntries.map((e: any) => e.id);
|
|
780
941
|
const cutIdx = branchIds.indexOf(firstKeptEntryId);
|
|
781
942
|
const cutWindow = cutIdx >= 0
|
|
@@ -787,6 +948,9 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
787
948
|
}))
|
|
788
949
|
: [];
|
|
789
950
|
|
|
951
|
+
const KNOWN_SECTIONS = new Set(["Session Goal", "Files And Changes", "Commits", "Outstanding Context", "User Preferences"]);
|
|
952
|
+
const extractKnownSections = (text: string) =>
|
|
953
|
+
[...text.matchAll(/^\[(.+?)\]/gm)].map((m) => m[1]).filter((h) => KNOWN_SECTIONS.has(h));
|
|
790
954
|
dbg(config, {
|
|
791
955
|
usedOwnCut: true,
|
|
792
956
|
budgetCut: ownCut.budgetCut,
|
|
@@ -797,21 +961,39 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
797
961
|
convertedMessages: messages.length,
|
|
798
962
|
firstKeptEntryId,
|
|
799
963
|
cutWindow,
|
|
800
|
-
tokensBefore
|
|
964
|
+
tokensBefore,
|
|
801
965
|
tokenEstimate,
|
|
802
966
|
summaryLength: summary.length,
|
|
803
967
|
summaryPreview: summary.slice(0, 500),
|
|
804
|
-
sections:
|
|
968
|
+
sections: extractKnownSections(summary),
|
|
969
|
+
savings: {
|
|
970
|
+
tokensBefore,
|
|
971
|
+
summaryChars,
|
|
972
|
+
summaryTokensEst,
|
|
973
|
+
keptTokensEst,
|
|
974
|
+
tokensAfterEst,
|
|
975
|
+
tokensSavedEst,
|
|
976
|
+
savedPercentEst,
|
|
977
|
+
},
|
|
805
978
|
});
|
|
806
979
|
|
|
807
980
|
const details: PiVccCompactionDetails = {
|
|
808
981
|
compactor: "omp-vcc",
|
|
809
|
-
version:
|
|
810
|
-
sections:
|
|
982
|
+
version: 2,
|
|
983
|
+
sections: extractKnownSections(summary),
|
|
811
984
|
sourceMessageCount: agentMessages.length,
|
|
812
985
|
previousSummaryUsed: Boolean(preparation.previousSummary),
|
|
813
986
|
reason,
|
|
814
987
|
willRetry,
|
|
988
|
+
savings: {
|
|
989
|
+
tokensBefore,
|
|
990
|
+
summaryChars,
|
|
991
|
+
summaryTokensEst,
|
|
992
|
+
keptTokensEst,
|
|
993
|
+
tokensAfterEst,
|
|
994
|
+
tokensSavedEst,
|
|
995
|
+
savedPercentEst,
|
|
996
|
+
},
|
|
815
997
|
};
|
|
816
998
|
|
|
817
999
|
setLastCompactWasPiVcc(pi, isPiVcc);
|
|
@@ -831,11 +1013,44 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
831
1013
|
const followUpPrompt = getPendingFollowUpPrompt(pi);
|
|
832
1014
|
setPendingFollowUpPrompt(pi, null);
|
|
833
1015
|
const per = getPerPi(pi);
|
|
1016
|
+
const stats = per ? per.lastStats : lastStats;
|
|
1017
|
+
if (!stats) return;
|
|
1018
|
+
// Enrich with authoritative tokensAfter from host if available (even for pi-vcc manual, before early return)
|
|
1019
|
+
const entry: any = (event as any).compactionEntry;
|
|
1020
|
+
if (entry && typeof entry.tokensAfter === "number" && typeof entry.tokensBefore === "number") {
|
|
1021
|
+
const before = entry.tokensBefore;
|
|
1022
|
+
const after = entry.tokensAfter;
|
|
1023
|
+
const saved = Math.max(0, before - after);
|
|
1024
|
+
const percent = before > 0 && saved > 0 ? Math.round((saved / before) * 100) : 0;
|
|
1025
|
+
if (per && per.lastStats) {
|
|
1026
|
+
per.lastStats.tokensAfter = after;
|
|
1027
|
+
per.lastStats.tokensSaved = saved;
|
|
1028
|
+
per.lastStats.savedPercent = percent;
|
|
1029
|
+
per.lastStats.tokensBefore = before;
|
|
1030
|
+
}
|
|
1031
|
+
if (lastStats) {
|
|
1032
|
+
lastStats.tokensAfter = after;
|
|
1033
|
+
lastStats.tokensSaved = saved;
|
|
1034
|
+
lastStats.savedPercent = percent;
|
|
1035
|
+
lastStats.tokensBefore = before;
|
|
1036
|
+
}
|
|
1037
|
+
(stats as any).tokensAfter = after;
|
|
1038
|
+
(stats as any).tokensSaved = saved;
|
|
1039
|
+
(stats as any).savedPercent = percent;
|
|
1040
|
+
(stats as any).tokensBefore = before;
|
|
1041
|
+
try {
|
|
1042
|
+
const cfg = loadSettings(ctx);
|
|
1043
|
+
if (cfg.debug) {
|
|
1044
|
+
dbg(cfg, {
|
|
1045
|
+
authoritativeSavings: { tokensBefore: before, tokensAfter: after, tokensSaved: saved, savedPercent: percent },
|
|
1046
|
+
eventEntry: { id: entry.id, tokensBefore: entry.tokensBefore, tokensAfter: entry.tokensAfter },
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
} catch {}
|
|
1050
|
+
}
|
|
834
1051
|
const isPiVccLast = per ? per.lastCompactWasPiVcc : lastCompactWasPiVcc;
|
|
835
1052
|
if (isPiVccLast) return; // /pi-vcc handles its own toast via onComplete
|
|
836
1053
|
if (willRetry) return;
|
|
837
|
-
const stats = per ? per.lastStats : lastStats;
|
|
838
|
-
if (!stats) return;
|
|
839
1054
|
// omp's SessionCompactEvent is {compactionEntry, fromExtension} only
|
|
840
1055
|
// (shared-events.ts:84-89); reason/willRetry are always undefined/false
|
|
841
1056
|
// under real omp runs. Treat undefined as auto (threshold/overflow) when
|
|
@@ -992,7 +1207,7 @@ export const registerPiVccCommand = (pi: any) => {
|
|
|
992
1207
|
ctx.compact({
|
|
993
1208
|
customInstructions: buildPiVccCustomInstructions(keepUserTurns),
|
|
994
1209
|
onComplete: () => {
|
|
995
|
-
const stats = getLastCompactionStats();
|
|
1210
|
+
const stats = getLastCompactionStats(pi);
|
|
996
1211
|
if (stats) {
|
|
997
1212
|
scheduleCompactionStatsNotify(ctx, stats);
|
|
998
1213
|
} else {
|
|
@@ -1014,4 +1229,64 @@ export const registerPiVccCommand = (pi: any) => {
|
|
|
1014
1229
|
});
|
|
1015
1230
|
},
|
|
1016
1231
|
});
|
|
1232
|
+
};
|
|
1233
|
+
export const registerVccStatsTool = (pi: any) => {
|
|
1234
|
+
const hasBoolean = typeof pi?.zod?.boolean === "function";
|
|
1235
|
+
const schema = pi?.zod?.object && hasBoolean
|
|
1236
|
+
? pi.zod.object({
|
|
1237
|
+
history: pi.zod.boolean().optional().describe("Include full history table of all compactions in this session"),
|
|
1238
|
+
})
|
|
1239
|
+
: {};
|
|
1240
|
+
pi.registerTool({
|
|
1241
|
+
name: "vcc_stats",
|
|
1242
|
+
label: "VCC Stats",
|
|
1243
|
+
description: "Show omp-vcc compaction savings — last compaction before→after, tokens saved, percent, and optional history of all compactions in this session. Divider in transcript already shows 256K→20K; this tool surfaces the same numbers with kept/summarized details.",
|
|
1244
|
+
approval: "read",
|
|
1245
|
+
parameters: schema,
|
|
1246
|
+
async execute(_toolCallId: string, params: any, _signal: unknown, _onUpdate: unknown, _ctx: any) {
|
|
1247
|
+
const history = getCompactionHistory(pi);
|
|
1248
|
+
const last = getLastCompactionStats(pi);
|
|
1249
|
+
const wantHistory = params?.history === true;
|
|
1250
|
+
if (!last && history.length === 0) {
|
|
1251
|
+
return { content: [{ type: "text", text: "No compactions yet in this session." }], details: undefined };
|
|
1252
|
+
}
|
|
1253
|
+
if (wantHistory) {
|
|
1254
|
+
const table = formatStatsTable(history);
|
|
1255
|
+
const detail = last ? `\n\n${formatLastStatsDetail(last)}` : "";
|
|
1256
|
+
return { content: [{ type: "text", text: `${table}${detail}` }], details: undefined };
|
|
1257
|
+
}
|
|
1258
|
+
const detail = formatLastStatsDetail(last);
|
|
1259
|
+
const table = history.length > 1 ? `\n\nHistory:\n${formatStatsTable(history)}` : "";
|
|
1260
|
+
return { content: [{ type: "text", text: `${detail}${table}` }], details: undefined };
|
|
1261
|
+
},
|
|
1262
|
+
} as unknown as Parameters<(typeof pi)["registerTool"]>[0]);
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
export const registerVccStatsCommand = (pi: any) => {
|
|
1266
|
+
const handler = async (args: string, ctx: any) => {
|
|
1267
|
+
const raw = (args || "").trim().toLowerCase();
|
|
1268
|
+
const wantHistory = raw.includes("history") || raw.includes("--history") || raw.includes("all");
|
|
1269
|
+
const history = getCompactionHistory(pi);
|
|
1270
|
+
const last = getLastCompactionStats(pi);
|
|
1271
|
+
const piAny = pi as unknown as { sendMessage?: (msg: unknown, opts?: unknown) => void };
|
|
1272
|
+
if (!last && history.length === 0) {
|
|
1273
|
+
try { piAny.sendMessage?.({ customType: "vcc-stats", content: "No compactions yet in this session.", display: true }, { triggerTurn: false }); } catch {}
|
|
1274
|
+
try { ctx?.ui?.notify?.("No compactions yet.", "info"); } catch {}
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
let output: string;
|
|
1278
|
+
if (wantHistory) {
|
|
1279
|
+
const table = formatStatsTable(history);
|
|
1280
|
+
const detail = last ? `\n\n${formatLastStatsDetail(last)}` : "";
|
|
1281
|
+
output = `${table}${detail}`;
|
|
1282
|
+
} else {
|
|
1283
|
+
const detail = formatLastStatsDetail(last);
|
|
1284
|
+
const table = history.length > 1 ? `\n\nHistory (${history.length} compactions):\n${formatStatsTable(history)}` : "";
|
|
1285
|
+
output = `${detail}${table}`;
|
|
1286
|
+
}
|
|
1287
|
+
try { piAny.sendMessage?.({ customType: "vcc-stats", content: output, display: true }, { triggerTurn: false }); } catch {}
|
|
1288
|
+
try { ctx?.ui?.notify?.(`vcc_stats: ${history.length} compaction(s)`, "info"); } catch {}
|
|
1289
|
+
};
|
|
1290
|
+
pi.registerCommand("vcc-stats", { description: "Show omp-vcc compaction savings (last + history)", handler });
|
|
1291
|
+
pi.registerCommand("omp-vcc-stats", { description: "Alias for /vcc-stats", handler });
|
|
1017
1292
|
};
|
package/package.json
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omp-vcc",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Algorithmic VCC compaction for omp - fast lossless no-LLM",
|
|
5
|
-
"
|
|
5
|
+
"author": "Zhu Lin <zhulin@czl.my>",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/zhulinchng/omp-vcc.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/zhulinchng/omp-vcc#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/zhulinchng/omp-vcc/issues"
|
|
13
|
+
},
|
|
6
14
|
"keywords": [
|
|
7
15
|
"oh-my-pi",
|
|
8
16
|
"omp",
|
|
9
|
-
"
|
|
17
|
+
"pi",
|
|
18
|
+
"oh-my-pi",
|
|
19
|
+
"plugin",
|
|
20
|
+
"vcc",
|
|
21
|
+
"compaction",
|
|
22
|
+
"pi-package"
|
|
10
23
|
],
|
|
11
24
|
"license": "MIT",
|
|
12
|
-
"repository": {
|
|
13
|
-
"type": "git",
|
|
14
|
-
"url": "https://github.com/zhulinchng/omp-vcc.git"
|
|
15
|
-
},
|
|
16
25
|
"files": [
|
|
17
26
|
"extensions",
|
|
18
27
|
"skills",
|
|
@@ -99,6 +108,6 @@
|
|
|
99
108
|
"test": "bun test",
|
|
100
109
|
"smoke": "bun run scripts/smoke.ts",
|
|
101
110
|
"postuninstall": "node scripts/uninstall-reset.js || true",
|
|
102
|
-
"prepublishOnly": "npm run typecheck"
|
|
111
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run smoke"
|
|
103
112
|
}
|
|
104
113
|
}
|
package/scripts/smoke.ts
CHANGED
|
@@ -55,6 +55,10 @@ try {
|
|
|
55
55
|
"vcc_recall registered",
|
|
56
56
|
tools.some((t) => t.name === "vcc_recall"),
|
|
57
57
|
);
|
|
58
|
+
check(
|
|
59
|
+
"vcc_stats registered",
|
|
60
|
+
tools.some((t) => t.name === "vcc_stats"),
|
|
61
|
+
);
|
|
58
62
|
check(
|
|
59
63
|
"omp-vcc command registered",
|
|
60
64
|
commands.some((c) => c.name === "omp-vcc"),
|
|
@@ -67,7 +71,15 @@ try {
|
|
|
67
71
|
"pi-vcc alias registered",
|
|
68
72
|
commands.some((c) => c.name === "pi-vcc"),
|
|
69
73
|
);
|
|
70
|
-
|
|
74
|
+
check(
|
|
75
|
+
"vcc-stats command registered",
|
|
76
|
+
commands.some((c) => c.name === "vcc-stats"),
|
|
77
|
+
);
|
|
78
|
+
check(
|
|
79
|
+
"omp-vcc-stats alias registered",
|
|
80
|
+
commands.some((c) => c.name === "omp-vcc-stats"),
|
|
81
|
+
);
|
|
82
|
+
} catch (e) {
|
|
71
83
|
check("extension loads", false, String(e));
|
|
72
84
|
}
|
|
73
85
|
|
package/skills/omp-vcc/SKILL.md
CHANGED
|
@@ -1,35 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: omp-vcc
|
|
3
|
+
description: VCC-inspired algorithmic compaction for oh-my-pi — lossless V_ui summary + ranked brief + V_adapt recall via vcc_recall. Use after auto-compaction (toast 90k→22k), when context grows 50+ turns, or before /omp-vcc keep:N boundaries.
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# omp-vcc Skill — VCC-Inspired Algorithmic Compaction
|
|
2
7
|
|
|
3
|
-
> Lossless,
|
|
8
|
+
> Lossless, deterministic summarization — no LLM. `V_full` = full transcript, `V_ui` = structured summary + ranked brief, `V_adapt(b, ρ)` = structure-preserving recall. Use `V_ui → V_adapt(query) → V_full[s:e]`: scan summary, query, drill to verbatim lines.
|
|
9
|
+
|
|
10
|
+
## When to Use
|
|
11
|
+
|
|
12
|
+
- **After auto compaction** — read `V_ui` first, then `vcc_recall` for anything missing before asking the user to repeat. Toast `omp-vcc: 90.0k→22.0k (76% saved) · kept 1/5 turns` + divider `── 📷 compacted ──` means you just got a `V_ui`.
|
|
13
|
+
- **Context is growing** (50+ turns, heavy tool output) — prefer small keep + recall over a huge tail. Recall is cheap and preserves turn/header/block.
|
|
14
|
+
- **Before risky work** — create a clean boundary: `/omp-vcc keep:2 <focus>` (e.g. `/omp-vcc keep:2 fix auth`). The focus text is sent as the next user message after compaction.
|
|
15
|
+
|
|
16
|
+
## What You Get (V_ui)
|
|
17
|
+
|
|
18
|
+
Compacted summary replaces the old transcript; `V_ui` + kept tail is what you see next:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
[Session Goal]
|
|
22
|
+
- …
|
|
23
|
+
|
|
24
|
+
[Files And Changes]
|
|
25
|
+
- …
|
|
26
|
+
|
|
27
|
+
[Commits]
|
|
28
|
+
- …
|
|
29
|
+
|
|
30
|
+
[Outstanding Context]
|
|
31
|
+
- …
|
|
32
|
+
|
|
33
|
+
[User Preferences]
|
|
34
|
+
- …
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
* Read "src/pets.py" (file.txt:18-20) ← ranked brief, one line per block
|
|
38
|
+
* Edit src/auth.ts { old: "…" (#12:auth.ts:10-40) }
|
|
39
|
+
|
|
40
|
+
Use `vcc_recall` to search for prior work … Do not redo work already completed.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- **5 sections** are extraction-only (no hallucination): Session Goal, Files And Changes, Commits, Outstanding Context, User Preferences.
|
|
44
|
+
- **Ranked brief**: TF-IDF-ranked tool summaries, capped at **120 lines** (`BRIEF_MAX_LINES=120`), token-budgeted **1100 → 2000 tokens** (`RANKED_BRIEF_BUDGET_TOKENS` floor, `CEILING` 2000, ~15 tok/block). `---` separates sections from brief; earlier lines beyond 120 are dropped tail-first.
|
|
45
|
+
- **Every line keeps a pointer** `(#N)` or `(path:s-e)` so `V_ui → V_full[s:e]` is structural. Trust the summary's pointers; drill for verbatim.
|
|
46
|
+
|
|
47
|
+
## Commands & Tools
|
|
48
|
+
|
|
49
|
+
| Task | How | Notes |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| **Force compaction** | `/omp-vcc` or `/omp-vcc keep:2 fix auth` · alias `/pi-vcc` | `keep:1` is default. Explicit `keep:N` always wins. Default `keep:1` may keep 2–4 turns if tail is tiny (smart-keep ≤5k→≤25k). `keep:0` = compact all, next turn from pure `V_ui`. Focus text after `keep:N` becomes the next user message. |
|
|
52
|
+
| **Check savings** | `/omp-vcc --stats` · `/vcc-stats` · `/omp-vcc-stats` · `vcc_stats({history:true})` | Last + history table (50-capped). Use to confirm headroom before long edits. |
|
|
53
|
+
| **Recall search** | `/vcc-recall <query> [scope:all] [page:2]` · alias `/pi-vcc-recall` · tool `vcc_recall({query, scope, mode, page, expand})` | 5 hits/page, up to 50 total. See cookbook below. |
|
|
54
|
+
| **Stats tool** | `vcc_stats({history?: boolean})` | Same as `/vcc-stats`. `history:true` = full 50-row table. |
|
|
55
|
+
|
|
56
|
+
`vcc_recall` params (all optional): `query?: string`, `page?: number` (1-indexed), `scope?: "lineage" | "all"` (default `lineage` = active branch), `mode?: "hybrid" | "touched"` (default `hybrid`), `expand?: number[]` (valid indices only).
|
|
57
|
+
|
|
58
|
+
No config needed for normal use. Auto `threshold`/`overflow` compaction is already `V_ui` (deterministic, no model call, ~30–470 ms benchmark) when `overrideDefaultCompaction:true` (default). Don't fight it — just use the summary.
|
|
59
|
+
|
|
60
|
+
## Recall Cookbook — V_adapt
|
|
61
|
+
|
|
62
|
+
**How search works**: regex first; if invalid or no hits → TF-IDF keyword OR (rare terms weighted, stopwords removed). Each hit preserves turn/header/block, role tags, and `(#N)`. ±2 lines around the match are shown.
|
|
4
63
|
|
|
5
|
-
|
|
64
|
+
```sh
|
|
65
|
+
# basic keyword (TF-IDF OR)
|
|
66
|
+
vcc_recall({query:"redis cache"})
|
|
67
|
+
/vcc-recall redis cache
|
|
6
68
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
69
|
+
# regex (contains | * + ? {} () [] \ ^ $ .)
|
|
70
|
+
vcc_recall({query:"hook|inject"})
|
|
71
|
+
/vcc-recall hook|inject
|
|
10
72
|
|
|
11
|
-
|
|
73
|
+
# scope: include abandoned branches (default is active branch only)
|
|
74
|
+
vcc_recall({query:"auth", scope:"all"})
|
|
75
|
+
/vcc-recall auth scope:all
|
|
12
76
|
|
|
13
|
-
|
|
77
|
+
# pagination (5/page, up to 50)
|
|
78
|
+
vcc_recall({query:"auth", page:2})
|
|
79
|
+
/vcc-recall auth page:2 scope:all
|
|
14
80
|
|
|
15
|
-
|
|
81
|
+
# file index — what was touched, not text search
|
|
82
|
+
vcc_recall({query:"", mode:"touched"})
|
|
83
|
+
/vcc-recall touched mode:touched
|
|
16
84
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
85
|
+
# drill to verbatim lines — resolves (#N) or (path:s-e)
|
|
86
|
+
vcc_recall({query:"#12:src/auth.ts"})
|
|
87
|
+
/vcc-recall #12:src/auth.ts
|
|
88
|
+
vcc_recall({query:"#18"}) # whole turn 18
|
|
89
|
+
vcc_recall({query:"#18:src/auth.ts:40-80"}) # slice (offset/limit via drill-down)
|
|
20
90
|
|
|
21
|
-
|
|
91
|
+
# expand multiple turns by index (from a prior recall's #N)
|
|
92
|
+
vcc_recall({query:"", expand:[12,18,25]})
|
|
93
|
+
```
|
|
22
94
|
|
|
23
|
-
|
|
95
|
+
**Pick the right query:**
|
|
24
96
|
|
|
25
|
-
|
|
97
|
+
- Find an edit/file: `mode:touched` then drill, or `query:"src/auth.ts"` then `#N:path`.
|
|
98
|
+
- Find a decision: `query:"why did we choose|decision|ADR"` (regex).
|
|
99
|
+
- Find an error/tool output: keyword of the error message — full tool output is searchable.
|
|
100
|
+
- Nothing found in `lineage` but you know it existed: retry `scope:all` (abandoned `/clear` branches are excluded by default).
|
|
26
101
|
|
|
27
|
-
|
|
102
|
+
**If you get:**
|
|
28
103
|
|
|
29
|
-
|
|
104
|
+
- `0 matches` — try keywords (no regex chars) or `scope:all`; check spelling of path.
|
|
105
|
+
- `truncated — showing 50 of 120 matches, refine…` — narrow regex/keywords.
|
|
106
|
+
- `Page 3 is outside 1-2 (7 matches)…` — use `page` in range or refine.
|
|
107
|
+
- `Cannot expand indices outside active lineage: 42. Use scope:'all'` — add `scope:"all"` or pick index from the same lineage.
|
|
108
|
+
- `#N` outside active lineage → same: retry with `scope:"all"` or use a `lineage` hit.
|
|
30
109
|
|
|
31
|
-
##
|
|
110
|
+
## Agent Playbook
|
|
32
111
|
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
112
|
+
1. **After any compaction, re-orient from V_ui.** Read Session Goal → Files → Outstanding → brief. Don't re-ask the user for what's already in the summary.
|
|
113
|
+
2. **Small keep + recall beats large keep.** `/omp-vcc keep:1` + `vcc_recall({query:"auth"})` keeps the focused tail small and lets you pull exact history on demand. Only `keep:3+` when you need verbatim recent context immediately after compaction.
|
|
114
|
+
3. **Recall before synthesis.** For any question about prior work (file changed, test added, decision made), call `vcc_recall` proportionally to context size: small session → 1 recall with broad keywords; long/complex session → 2–3 targeted recalls (keywords then drill).
|
|
115
|
+
4. **Create boundaries intentionally.** Before a multi-file refactor or hand-off doc: `/omp-vcc keep:2 continue auth refactor` — next turn starts from a fresh, citable `V_ui`. Verify with `/vcc-stats` (`kept 2/18 turns, 76% saved`) before continuing.
|
|
116
|
+
5. **Don't stall after threshold.** Auto threshold/overflow compaction auto-continues via invisible follow-up (you'll just see the summary and your next turn proceeds). If you issued `/omp-vcc keep:2 <focus>`, that focus text arrives as the next user message — treat it as the goal.
|
|
117
|
+
6. **Use pointers, don't re-derive.** When you quote prior work, cite `(#N)` or `(file:s-e)` from the brief; drill `#N:path` for verbatim to paste, not guessed content.
|