omp-vcc 0.1.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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/commands/omp-vcc.md +19 -0
  4. package/commands/vcc-recall.md +21 -0
  5. package/extensions/main.ts +319 -0
  6. package/extensions/vcc-core/commands/vcc-recall.ts +2 -0
  7. package/extensions/vcc-core/core/brief.ts +404 -0
  8. package/extensions/vcc-core/core/build-sections.ts +77 -0
  9. package/extensions/vcc-core/core/compact-args.ts +46 -0
  10. package/extensions/vcc-core/core/content.ts +157 -0
  11. package/extensions/vcc-core/core/drill-down.ts +299 -0
  12. package/extensions/vcc-core/core/filter-noise.ts +42 -0
  13. package/extensions/vcc-core/core/format-recall.ts +101 -0
  14. package/extensions/vcc-core/core/format.ts +82 -0
  15. package/extensions/vcc-core/core/lineage.ts +27 -0
  16. package/extensions/vcc-core/core/load-messages.ts +44 -0
  17. package/extensions/vcc-core/core/normalize.ts +66 -0
  18. package/extensions/vcc-core/core/rank.ts +284 -0
  19. package/extensions/vcc-core/core/recall-scope.ts +31 -0
  20. package/extensions/vcc-core/core/render-entries.ts +55 -0
  21. package/extensions/vcc-core/core/report.ts +233 -0
  22. package/extensions/vcc-core/core/sanitize.ts +6 -0
  23. package/extensions/vcc-core/core/search-entries.ts +576 -0
  24. package/extensions/vcc-core/core/settings.ts +151 -0
  25. package/extensions/vcc-core/core/skill-collapse.ts +36 -0
  26. package/extensions/vcc-core/core/summarize.ts +208 -0
  27. package/extensions/vcc-core/core/token-estimate.ts +101 -0
  28. package/extensions/vcc-core/core/tool-args.ts +17 -0
  29. package/extensions/vcc-core/details.ts +12 -0
  30. package/extensions/vcc-core/extract/commits.ts +70 -0
  31. package/extensions/vcc-core/extract/files.ts +88 -0
  32. package/extensions/vcc-core/extract/goals.ts +80 -0
  33. package/extensions/vcc-core/extract/preferences.ts +56 -0
  34. package/extensions/vcc-core/hook.ts +1017 -0
  35. package/extensions/vcc-core/sections.ts +9 -0
  36. package/extensions/vcc-core/types.ts +17 -0
  37. package/package.json +104 -0
  38. package/scripts/smoke.ts +116 -0
  39. package/scripts/uninstall-reset.js +73 -0
  40. package/skills/omp-vcc/SKILL.md +35 -0
  41. package/types.d.ts +114 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zhu Lin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # @zhulinchng/omp-vcc — Algorithmic VCC Compaction for oh-my-pi
2
+
3
+ > Fast, deterministic, lossless compaction — 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
+
5
+ ## Quick start
6
+
7
+ ```sh
8
+ # from source (local)
9
+ omp plugin link /Users/zhu/code/projects/omp-vcc
10
+ omp plugin list --json | jq '.[] | select(.name|contains("omp-vcc"))'
11
+
12
+ # from npm (unscoped)
13
+ omp plugin install omp-vcc
14
+
15
+ # from GitHub Packages (scoped, requires auth — see below)
16
+ omp plugin install @zhulinchng/omp-vcc
17
+
18
+ # from git
19
+ omp plugin install github:zhulinchng/omp-vcc
20
+ ```
21
+
22
+ ## What you get
23
+
24
+ - **Auto** threshold/overflow compaction via `session_before_compact` hook — no LLM summary, 30–470 ms, 35–99% reduction.
25
+ - **Manual** `/omp-vcc [keep:N] [focus]` (and `/pi-vcc` alias) — e.g. `/omp-vcc keep:2 fix auth` keeps last 2 user turns.
26
+ - **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.
27
+
28
+ ## Commands
29
+
30
+ | Command | Description |
31
+ | --- | --- |
32
+ | `/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. |
33
+ | `/pi-vcc` | Alias for migration |
34
+ | `/vcc-recall [query] [scope:all] [page:N]` | Search compacted history (V_adapt). Plain keywords best. |
35
+ | `/pi-vcc-recall` | Alias |
36
+
37
+ Tool `vcc_recall` mirrors the command, plus `expand:[indices]` and `mode:'touched'` for file index.
38
+
39
+ ## Configuration
40
+
41
+ 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`):
42
+
43
+ ```json
44
+ {
45
+ "overrideDefaultCompaction": true,
46
+ "smartKeepTail": true,
47
+ "continueAfterThresholdCompact": true,
48
+ "debug": false
49
+ }
50
+ ```
51
+
52
+ Manifest `omp.settings` also exposed (`vccEnabled`, `overrideDefaultCompaction`, `smartKeepTail`, `continueAfterThresholdCompact`, `debug`) — appears under plugin section in `/settings` and via `omp config`. See [`docs/configuration.md`](docs/configuration.md).
53
+
54
+ ## How it works
55
+
56
+ VCC compiles the raw JSONL trace via **lex → parse IR → monotonic line assignment → view lowering** into three views sharing one coordinate system:
57
+
58
+ - `V_full` identity (defines coordinates)
59
+ - `V_ui` one-line tool summaries with pointers (`* Read "src/pets.py" (file.txt:18-20)`)
60
+ - `V_adapt(b, ρ)` projection via predicate `ρ` preserving headers/role tags and `(f:s-e)` pointers, two transposed modalities (document vs index oriented)
61
+
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. See [`docs/architecture.md`](docs/architecture.md) and [`docs/paper-notes.md`](docs/paper-notes.md).
63
+
64
+ ## Related work
65
+
66
+ - **pi-vcc** — TypeScript algorithmic compactor, zero LLM, `RANKED_BRIEF_BUDGET_TOKENS=1100` ceil 2000, `charsPerBlock 15`
67
+ - **VCC** — Python `VCC.py` adaptive/transposed views, `SEP`, `match_lines`, `_tokenize`, `_trunc`, projection model
68
+ - **Paper** — AppWorld evaluation: +1.1–4.2 task_goal points, ½–⅔ token halving, smaller memory
69
+
70
+ ## Develop
71
+
72
+ ```bash
73
+ omp plugin link .
74
+ omp plugin doctor
75
+ bunx tsc --noEmit
76
+ bun run smoke # or bun run scripts/smoke.ts
77
+ bun test
78
+ ```
79
+
80
+ Capabilities: `extension`, `skill`, `command` — entry `extensions/main.ts`.
81
+
82
+ ## Verification
83
+
84
+ ```sh
85
+ bunx tsc --noEmit
86
+ bun test # 295 tests across 32 files
87
+ bun run smoke # ok: session_before_compact hooked, vcc_recall registered
88
+ omp plugin link /Users/zhu/code/projects/omp-vcc && omp plugin doctor
89
+ ```
90
+
91
+ In a live `omp` session: `/omp-vcc keep:1` shows `[Session Goal]` toast `omp-vcc: kept 1/5 turns, ~2.1k tok`; with `debug:true` check `/tmp/omp-vcc-debug.json`. See [`docs/verification.md`](docs/verification.md).
92
+
93
+ ## Publish
94
+
95
+ - npmjs (unscoped): `npm publish --registry https://registry.npmjs.org --access public` (package `omp-vcc`)
96
+ - GitHub Packages (scoped): `npm publish --registry https://npm.pkg.github.com` (package `@zhulinchng/omp-vcc`, needs `GITHUB_TOKEN` with `read:packages, write:packages`)
97
+ - Consumer GPR auth: add to `~/.npmrc`:
98
+ ```
99
+ @zhulinchng:registry=https://npm.pkg.github.com
100
+ //npm.pkg.github.com/:_authToken=YOUR_GITHUB_PAT
101
+ ```
102
+ - Marketplace: add an entry to `.omp-plugin/marketplace.json` (see `plugin-skill/assets/templates/marketplace-entry.json.template`).
103
+
104
+ ## License
105
+
106
+ MIT — pi-vcc core files retain original MIT.
@@ -0,0 +1,19 @@
1
+ ---
2
+ description: Compact conversation with omp-vcc structured summary (keep:N + optional focus)
3
+ ---
4
+
5
+ # /omp-vcc
6
+
7
+ Algorithmic VCC compaction — fast lossless no-LLM. Registered by `@zhulinchng/omp-vcc` extension.
8
+
9
+ Usage:
10
+
11
+ - `/omp-vcc` — compact with default keep:1 (smart-keep may boost to keep:N up to 25k tokens)
12
+ - `/omp-vcc keep:2` — keep last 2 user turns, summarize the rest
13
+ - `/omp-vcc keep:0` — compact all (no tail, sentinel firstKeptEntryId="")
14
+ - `/omp-vcc some prompt text` — compact with additional prompt/focus for the summary
15
+ - `/omp-vcc keep:2 some prompt text` — both keep and prompt
16
+
17
+ This command is handled by the `omp-vcc` extension (`extensions/main.ts`). It delegates to the VCC pipeline (calibrate → smart-keep → budget-cut → normalize → filter-noise → build-sections → brief transcript → format → merge) and writes a structured summary with `[Session Goal]` / `[Files And Changes]` / `[Brief transcript]` sections.
18
+
19
+ Arguments: `$ARGUMENTS`
@@ -0,0 +1,21 @@
1
+ ---
2
+ description: Recall earlier parts of this session via ranked search (V_adapt)
3
+ ---
4
+
5
+ # /vcc-recall
6
+
7
+ Search compacted history — implements VCC `V_adapt` with rho predicate (regex or BM25-like ranked OR).
8
+
9
+ Registered by `@zhulinchng/omp-vcc` extension.
10
+
11
+ Usage:
12
+
13
+ - `/vcc-recall` — show 25 most recent entries
14
+ - `/vcc-recall auth token` — keyword search (OR-ranked, TF-IDF)
15
+ - `/vcc-recall "hook|inject" scope:all` — regex search across all branches
16
+ - `/vcc-recall cache page:2` — paginated results (5 per page)
17
+ - `vcc_recall` tool: `{"query":"redis cache","scope":"all","page":1}` — same engine, also supports `mode:'touched'` for file index and `expand:[12,34]` or `#12:path` drill-down
18
+
19
+ The tool preserves VCC invariants: role tags, line range pointers `(#N)`, and progressive disclosure `V_ui → V_adapt → V_full[s:e]`.
20
+
21
+ Arguments: `$ARGUMENTS`
@@ -0,0 +1,319 @@
1
+ // omp-vcc extension entry — VCC-inspired algorithmic compaction for oh-my-pi
2
+ // Factory contract: (pi: ExtensionAPI) => void | Promise<void>
3
+ // Implements: session_before_compact hook via ./vcc-core/hook, vcc_recall tool, /omp-vcc and /vcc-recall commands
4
+ // Paper: arxiv 2603.29678 §2.2-2.4 — lex→parse IR→line assignment→view lowering (V_full identity, V_ui one-liners, V_adapt rho projection)
5
+ // pi-vcc port: sting8k/pi-vcc @0.7.0 — algorithmic, zero-LLM, brief transcript + 5 sections, token-budgeted
6
+
7
+ import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
8
+ import { scaffoldSettings } from "./vcc-core/core/settings";
9
+ import {
10
+ registerBeforeCompactHook,
11
+ PI_VCC_COMPACT_INSTRUCTION,
12
+ OMP_VCC_COMPACT_INSTRUCTION,
13
+ getLastCompactionStats,
14
+ scheduleCompactionStatsNotify,
15
+ } from "./vcc-core/hook";
16
+ import { loadAllMessages } from "./vcc-core/core/load-messages";
17
+ import { searchEntriesDetailed, getTouchedFiles } from "./vcc-core/core/search-entries";
18
+ import { formatRecallOutput, formatTouchedOutput } from "./vcc-core/core/format-recall";
19
+ import { getActiveLineageEntryIds } from "./vcc-core/core/lineage";
20
+ import { normalizeRecallScope, normalizeRecallMode, parseRecallScope } from "./vcc-core/core/recall-scope";
21
+ import { parseDrillDown, expandEntryFile } from "./vcc-core/core/drill-down";
22
+ import { buildPiVccCustomInstructions, parseKeepAndPrompt } from "./vcc-core/core/compact-args";
23
+
24
+ // Build omp sentinel instructions; keep pi sentinel for backward compat in hook
25
+ const buildOmpCustomInstructions = (keepUserTurns: number | null): string => {
26
+ if (keepUserTurns == null) return OMP_VCC_COMPACT_INSTRUCTION;
27
+ return `${OMP_VCC_COMPACT_INSTRUCTION} keep:${keepUserTurns}`;
28
+ };
29
+
30
+ // Helper to parse recall command text: supports `query ... scope:all page:N`
31
+ const parseRecallCommandArgs = (
32
+ raw: string,
33
+ ): { query: string; scope: "lineage" | "all"; page: number } => {
34
+ const parsed = parseRecallScope(raw);
35
+ const pageMatch = parsed.text.match(/\bpage:(\d+)\b/i);
36
+ const page = pageMatch ? Math.max(1, Number.parseInt(pageMatch[1] ?? "1", 10)) : 1;
37
+ const query = parsed.text.replace(/\bpage:\d+\b/i, "").trim();
38
+ return { query, scope: parsed.scope, page };
39
+ };
40
+
41
+ const DEFAULT_RECENT = 25;
42
+ const PAGE_SIZE = 5;
43
+
44
+ export default function (pi: ExtensionAPI): void {
45
+ scaffoldSettings();
46
+ registerBeforeCompactHook(pi);
47
+
48
+ // ── vcc_recall tool — implements VCC V_adapt via rho predicate (paper §2.1 eq.2) ──
49
+ // rho = regex if query parses and hits>0 else BM25-like TF-IDF OR (rank.ts). Preserves skeleton + role tags + (f:s-e) pointers.
50
+ // Two modalities: document-oriented (default, temporal) vs index-oriented flat list (mode:touched)
51
+ pi.registerTool({
52
+ name: "vcc_recall",
53
+ label: "VCC Recall",
54
+ description:
55
+ "Recall earlier parts of the current session — decisions made, files touched, commands run, including anything dropped by compaction. Reach for this before telling the user you no longer have the context. Plain keywords work best; a regex pattern is also accepted. Results are paged (page); pass expand with entry indices to read full untruncated content. Use mode:'touched' to list files worked on in this session with their entry indices, and #N:path to drill into a file's content from an entry (#N:path:full for all lines). Note: apply_patch paths (inside the diff payload) and bash redirects do not appear in the touched index. Only the current session is searchable — earlier sessions are not.",
56
+ approval: "read",
57
+ parameters: pi.zod.object({
58
+ query: pi.zod.string().optional().describe("What to recall, in plain keywords (e.g. 'redis cache decision'). Multi-word queries are ranked by relevance. A regex pattern also works."),
59
+ expand: pi.zod.array(pi.zod.number()).optional().describe("Entry indices to return full untruncated content for"),
60
+ page: pi.zod.number().optional().describe("Page number (1-based) for paginated search results. Default: 1."),
61
+ scope: pi.zod.enum(["lineage", "all", "active"]).optional().describe("Default 'lineage' covers the active conversation path. Use 'all' to also reach messages from other branches, such as turns that were edited or retried."),
62
+ mode: pi.zod.enum(["hybrid", "touched"]).optional().describe("What to show. hybrid (default) = normal search; touched = aggregated files-by-path with entry indices."),
63
+ }),
64
+ async execute(_toolCallId: string, params: unknown, _signal: unknown, _onUpdate: unknown, ctx: unknown) {
65
+ const p = params as {
66
+ query?: string;
67
+ expand?: number[];
68
+ page?: number;
69
+ scope?: string;
70
+ mode?: string;
71
+ };
72
+ const c = ctx as {
73
+ sessionManager?: { getSessionFile?: () => string | undefined; getBranch?: () => unknown[]; getEntries?: () => unknown[] };
74
+ };
75
+ const sessionFile = c.sessionManager?.getSessionFile?.();
76
+ if (!sessionFile) {
77
+ return {
78
+ content: [{ type: "text", text: "No session file available." }],
79
+ details: undefined,
80
+ };
81
+ }
82
+ const rawScope = p.scope === "active" ? "lineage" : p.scope;
83
+ const scope = normalizeRecallScope(rawScope);
84
+ const lineageEntryIds = scope === "lineage" ? getActiveLineageEntryIds(c.sessionManager as unknown as { getBranch: () => { id?: string }[] }) : undefined;
85
+
86
+ const q = p.query?.trim();
87
+
88
+ if (q && parseDrillDown(q)) {
89
+ const parsed = parseDrillDown(q);
90
+ if (!parsed) {
91
+ return { content: [{ type: "text", text: "Invalid drill-down query." }], details: undefined };
92
+ }
93
+ if (lineageEntryIds) {
94
+ const { rendered } = loadAllMessages(sessionFile, false, lineageEntryIds);
95
+ const exists = rendered.some((m) => m.index === parsed.index);
96
+ if (!exists) {
97
+ return {
98
+ content: [{ type: "text", text: `Cannot expand indices outside active lineage: ${parsed.index}. Use scope:'all' to reach other branches.` }],
99
+ details: undefined,
100
+ };
101
+ }
102
+ }
103
+ const text = expandEntryFile(sessionFile, parsed.index, parsed.pathPattern, parsed.full, parsed.offset, parsed.limit);
104
+ return { content: [{ type: "text", text }], details: undefined };
105
+ }
106
+
107
+ if (normalizeRecallMode(p.mode) === "touched") {
108
+ const { rendered, rawMessages } = loadAllMessages(sessionFile, false, lineageEntryIds);
109
+ const touched = getTouchedFiles(rawMessages as unknown[], rendered);
110
+ const text = formatTouchedOutput(touched, p.page);
111
+ return { content: [{ type: "text", text }], details: undefined };
112
+ }
113
+
114
+ const expandSet = new Set(p.expand ?? []);
115
+ const hasExpand = expandSet.size > 0;
116
+ if (hasExpand) {
117
+ const { rendered: fullMsgs } = loadAllMessages(sessionFile, true, lineageEntryIds);
118
+ const requested = [...expandSet];
119
+ const byIndex = new Map(fullMsgs.map((m) => [m.index, m]));
120
+ const invalid = requested.filter((i) => !Number.isInteger(i) || !byIndex.has(i));
121
+ if (invalid.length > 0) {
122
+ return {
123
+ content: [{ type: "text", text: `Cannot expand indices outside ${scope === "all" ? "session history" : "active lineage"}: ${invalid.join(", ")}` }],
124
+ details: undefined,
125
+ };
126
+ }
127
+ const expanded = requested.map((i) => byIndex.get(i)).filter((m): m is NonNullable<typeof m> => Boolean(m));
128
+ const output = (scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(expanded);
129
+ return { content: [{ type: "text", text: output }], details: undefined };
130
+ }
131
+
132
+ const { rendered: msgs, rawMessages } = loadAllMessages(sessionFile, false, lineageEntryIds);
133
+ if (q) {
134
+ const { hits, totalBeforeCap, truncated } = searchEntriesDetailed(msgs, rawMessages as unknown[], q);
135
+ const page = Math.max(1, p.page ?? 1);
136
+ const totalPages = Math.ceil(hits.length / PAGE_SIZE);
137
+ const scopeSuffix = scope === "all" ? " (scope: all)" : "";
138
+ const truncationNote = truncated ? ` — showing ${hits.length} of ${totalBeforeCap} matches, refine your query for more precise results` : "";
139
+ if (hits.length > 0 && page > totalPages) {
140
+ const guidance = truncated ? `Use a page between 1 and ${totalPages}.` : `Use a page between 1 and ${totalPages}, or refine your query.`;
141
+ const text = `Page ${page} is outside the available range 1-${totalPages} (${hits.length} matches${scopeSuffix}${truncationNote}). ${guidance}`;
142
+ return { content: [{ type: "text", text }], details: undefined };
143
+ }
144
+ const start = (page - 1) * PAGE_SIZE;
145
+ const pageResults = hits.slice(start, start + PAGE_SIZE);
146
+ const header = totalPages > 1 ? `Page ${page}/${totalPages} (${hits.length} total matches${scopeSuffix}${truncationNote})` : `${hits.length} matches${scopeSuffix}${truncationNote}`;
147
+ const footer = page < totalPages ? `\n--- Use page:${page + 1}${scope === "all" ? " with scope:'all'" : ""} for more results ---` : "";
148
+ const output = formatRecallOutput(pageResults, q, header) + footer;
149
+ return { content: [{ type: "text", text: output }], details: undefined };
150
+ }
151
+ const output = (scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(msgs.slice(-DEFAULT_RECENT), q);
152
+ return { content: [{ type: "text", text: output }], details: undefined };
153
+ },
154
+ } as unknown as Parameters<ExtensionAPI["registerTool"]>[0]);
155
+
156
+ // ── /omp-vcc command — manual algorithmic compaction (V_ui) ──
157
+ pi.registerCommand("omp-vcc", {
158
+ description: "Compact conversation with omp-vcc structured summary (keep:N + optional focus)",
159
+ handler: async (args: string, ctx: unknown) => {
160
+ const c = ctx as {
161
+ compact: (instructions?: string) => Promise<void>;
162
+ ui: { notify: (msg: string, level?: string) => void };
163
+ };
164
+ const parsed = parseKeepAndPrompt(args);
165
+ const keep = parsed.keepUserTurns;
166
+ const followUpPrompt = parsed.followUpPrompt;
167
+ const customInstructions = buildOmpCustomInstructions(keep);
168
+ // Also accept pi sentinel for legacy: map keep via buildPiVcc if needed? Use omp.
169
+ // Notify before compact for UX parity with pi-vcc
170
+ try {
171
+ c.ui.notify(`omp-vcc: compacting with keep:${keep ?? 1}${followUpPrompt ? ` + focus` : ""}...`, "info");
172
+ } catch {}
173
+ try {
174
+ await c.compact(customInstructions);
175
+ const stats = getLastCompactionStats();
176
+ if (stats) {
177
+ scheduleCompactionStatsNotify(c as unknown as { ui: { notify: (msg: string, level?: string) => void } }, stats);
178
+ } else {
179
+ try { c.ui.notify("Compacted with omp-vcc", "info"); } catch {}
180
+ }
181
+ if (followUpPrompt) {
182
+ try {
183
+ const piAny = pi as unknown as { sendUserMessage?: (content: string) => Promise<void> | void };
184
+ if (piAny.sendUserMessage) await piAny.sendUserMessage(followUpPrompt);
185
+ } catch {}
186
+ }
187
+ } catch (err: unknown) {
188
+ const msg = err instanceof Error ? err.message : String(err);
189
+ if (msg === "Compaction cancelled" || msg === "Already compacted") {
190
+ try { c.ui.notify("Nothing to compact", "warning"); } catch {}
191
+ } else {
192
+ try { c.ui.notify(`Compaction failed: ${msg}`, "error"); } catch {}
193
+ }
194
+ }
195
+ },
196
+ });
197
+
198
+ // Legacy alias: /pi-vcc — keep for migration, same handler
199
+ pi.registerCommand("pi-vcc", {
200
+ description: "Alias for /omp-vcc (pi-vcc compat)",
201
+ handler: async (args: string, ctx: unknown) => {
202
+ const c = ctx as {
203
+ compact: (instructions?: string) => Promise<void>;
204
+ ui: { notify: (msg: string, level?: string) => void };
205
+ };
206
+ const parsed = parseKeepAndPrompt(args);
207
+ const keep = parsed.keepUserTurns;
208
+ const followUpPrompt = parsed.followUpPrompt;
209
+ const customInstructions = buildPiVccCustomInstructions(keep);
210
+ try {
211
+ await c.compact(customInstructions);
212
+ const stats = getLastCompactionStats();
213
+ if (stats) scheduleCompactionStatsNotify(c as unknown as { ui: { notify: (msg: string, level?: string) => void } }, stats);
214
+ else try { c.ui.notify("Compacted with pi-vcc (via omp-vcc)", "info"); } catch {}
215
+ if (followUpPrompt) {
216
+ try {
217
+ const piAny = pi as unknown as { sendUserMessage?: (content: string) => Promise<void> | void };
218
+ if (piAny.sendUserMessage) await piAny.sendUserMessage(followUpPrompt);
219
+ } catch {}
220
+ }
221
+ } catch (err: unknown) {
222
+ const msg = err instanceof Error ? err.message : String(err);
223
+ if (msg === "Compaction cancelled" || msg === "Already compacted") {
224
+ try { c.ui.notify("Nothing to compact", "warning"); } catch {}
225
+ } else {
226
+ try { c.ui.notify(`Compaction failed: ${msg}`, "error"); } catch {}
227
+ }
228
+ }
229
+ },
230
+ });
231
+
232
+ // ── /vcc-recall command — search compacted history (V_adapt) ──
233
+ pi.registerCommand("vcc-recall", {
234
+ description: "Recall earlier parts of this session. Plain keywords work best; add scope:all to reach edited or retried turns.",
235
+ handler: async (args: string, ctx: unknown) => {
236
+ const c = ctx as {
237
+ sessionManager?: { getSessionFile?: () => string | undefined; getBranch?: () => { id?: string }[] };
238
+ ui: { notify: (msg: string, level?: string) => void };
239
+ };
240
+ const sessionFile = c.sessionManager?.getSessionFile?.();
241
+ if (!sessionFile) {
242
+ try { c.ui.notify("No session file available.", "error"); } catch {}
243
+ return;
244
+ }
245
+ const { query, scope, page } = parseRecallCommandArgs(args);
246
+ const lineageEntryIds = scope === "lineage" ? getActiveLineageEntryIds(c.sessionManager as unknown as { getBranch: () => { id?: string }[] }) : undefined;
247
+ const piAny = pi as unknown as { sendMessage?: (msg: unknown, opts?: unknown) => void };
248
+ if (!query) {
249
+ const { rendered } = loadAllMessages(sessionFile, false, lineageEntryIds);
250
+ const recent = rendered.slice(-DEFAULT_RECENT);
251
+ const output = (scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(recent);
252
+ try { piAny.sendMessage?.({ customType: "vcc-recall", content: output, display: true }, { triggerTurn: false }); } catch {}
253
+ try { c.ui.notify(`vcc_recall: ${recent.length} recent`, "info"); } catch {}
254
+ return;
255
+ }
256
+ const { rendered, rawMessages } = loadAllMessages(sessionFile, false, lineageEntryIds);
257
+ const { hits, totalBeforeCap, truncated } = searchEntriesDetailed(rendered, rawMessages as unknown[], query);
258
+ const totalPages = Math.ceil(hits.length / PAGE_SIZE);
259
+ const scopeSuffix = scope === "all" ? " (scope: all)" : "";
260
+ const scopeArg = scope === "all" ? " scope:all" : "";
261
+ const truncationNote = truncated ? ` — showing ${hits.length} of ${totalBeforeCap} matches, refine your query for more precise results` : "";
262
+ if (hits.length > 0 && page > totalPages) {
263
+ const guidance = truncated ? `Use /vcc-recall ${query}${scopeArg} page:N with N between 1 and ${totalPages}.` : `Use /vcc-recall ${query}${scopeArg} page:N with N between 1 and ${totalPages}, or refine your query.`;
264
+ const text = `Page ${page} is outside the available range 1-${totalPages} (${hits.length} matches${scopeSuffix}${truncationNote}). ${guidance}`;
265
+ try { piAny.sendMessage?.({ customType: "vcc-recall", content: text, display: true }, { triggerTurn: false }); } catch {}
266
+ return;
267
+ }
268
+ const start = (page - 1) * PAGE_SIZE;
269
+ const pageResults = hits.slice(start, start + PAGE_SIZE);
270
+ const header = totalPages > 1 ? `Page ${page}/${totalPages} (${hits.length} total matches${scopeSuffix}${truncationNote})` : `${hits.length} matches${scopeSuffix}${truncationNote}`;
271
+ const footer = page < totalPages ? `\n--- /vcc-recall ${query}${scopeArg} page:${page + 1} ---` : "";
272
+ const output = formatRecallOutput(pageResults, query, header) + footer;
273
+ try { piAny.sendMessage?.({ customType: "vcc-recall", content: output, display: true }, { triggerTurn: false }); } catch {}
274
+ try { c.ui.notify(`vcc_recall: ${hits.length} hits`, "info"); } catch {}
275
+ },
276
+ });
277
+
278
+ // Alias: /pi-vcc-recall
279
+ pi.registerCommand("pi-vcc-recall", {
280
+ description: "Alias for /vcc-recall",
281
+ handler: async (args: string, ctx: unknown) => {
282
+ const c = ctx as {
283
+ sessionManager?: { getSessionFile?: () => string | undefined; getBranch?: () => { id?: string }[] };
284
+ ui: { notify: (msg: string, level?: string) => void };
285
+ };
286
+ const sessionFile = c.sessionManager?.getSessionFile?.();
287
+ if (!sessionFile) {
288
+ try { c.ui.notify("No session file available.", "error"); } catch {}
289
+ return;
290
+ }
291
+ const { query, scope, page } = parseRecallCommandArgs(args);
292
+ const lineageEntryIds = scope === "lineage" ? getActiveLineageEntryIds(c.sessionManager as unknown as { getBranch: () => { id?: string }[] }) : undefined;
293
+ const piAny = pi as unknown as { sendMessage?: (msg: unknown, opts?: unknown) => void };
294
+ const { rendered, rawMessages } = loadAllMessages(sessionFile, false, lineageEntryIds);
295
+ if (!query) {
296
+ const { rendered: r } = loadAllMessages(sessionFile, false, lineageEntryIds);
297
+ const recent = r.slice(-DEFAULT_RECENT);
298
+ const output = (scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(recent);
299
+ try { piAny.sendMessage?.({ customType: "vcc-recall", content: output, display: true }, { triggerTurn: false }); } catch {}
300
+ return;
301
+ }
302
+ const { hits, totalBeforeCap, truncated } = searchEntriesDetailed(rendered, rawMessages as unknown[], query);
303
+ const totalPages = Math.ceil(hits.length / PAGE_SIZE);
304
+ const scopeSuffix = scope === "all" ? " (scope: all)" : "";
305
+ const scopeArg = scope === "all" ? " scope:all" : "";
306
+ const truncationNote = truncated ? ` — showing ${hits.length} of ${totalBeforeCap} matches, refine your query for more precise results` : "";
307
+ const start = (page - 1) * PAGE_SIZE;
308
+ const pageResults = hits.slice(start, start + PAGE_SIZE);
309
+ const header = totalPages > 1 ? `Page ${page}/${totalPages} (${hits.length} total matches${scopeSuffix}${truncationNote})` : `${hits.length} matches${scopeSuffix}${truncationNote}`;
310
+ const footer = page < totalPages ? `\n--- /pi-vcc-recall ${query}${scopeArg} page:${page + 1} ---` : "";
311
+ const output = formatRecallOutput(pageResults, query, header) + footer;
312
+ try { piAny.sendMessage?.({ customType: "vcc-recall", content: output, display: true }, { triggerTurn: false }); } catch {}
313
+ },
314
+ });
315
+ }
316
+ // ── Re-exports for pi-vcc test compatibility (not dead: tests import via hook directly,
317
+ // but external consumers and the `vcc-recall` shim may import via main) ──
318
+ export { registerBeforeCompactHook, PI_VCC_COMPACT_INSTRUCTION, OMP_VCC_COMPACT_INSTRUCTION, getLastCompactionStats, scheduleCompactionStatsNotify, formatCompactionStats, AUTO_CONTINUE_CUSTOM_TYPE, LEGACY_AUTO_CONTINUE_CUSTOM_TYPE, invalidExpandIndices, registerRecallTool, registerVccRecallCommand, registerPiVccCommand } from "./vcc-core/hook";
319
+ export { buildPiVccCustomInstructions, parseKeepAndPrompt } from "./vcc-core/core/compact-args";
@@ -0,0 +1,2 @@
1
+ // @ts-nocheck
2
+ export { registerVccRecallCommand } from "../hook";