omp-vcc 0.1.2 → 0.1.5

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 CHANGED
@@ -1,105 +1,113 @@
1
- # @zhulinchng/omp-vcc — Algorithmic VCC Compaction for oh-my-pi
1
+ # Algorithmic VCC Compaction for oh-my-pi
2
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).
3
+ > Fast, deterministic, lossless compaction. No LLM calls. Port of [`sting8k/pi-vcc@0.7.0`](https://github.com/sting8k/pi-vcc) into [oh-my-pi](https://github.com/can1357/oh-my-pi). Paper [`arxiv:2603.29678`](https://arxiv.org/pdf/2603.29678) (Zhang & Agrawala).
4
4
 
5
5
  ## Quick start
6
6
 
7
- > **New here?** → [`docs/setup.md`](docs/setup.md) — 5-minute install with prerequisites, 4 install options (link / npm / git), verify, first `/omp-vcc`, recall, config, and troubleshooting. The commands below are the tldr.
8
-
9
7
  ```sh
10
- # from source (local)
11
- omp plugin link /Users/zhu/code/projects/omp-vcc
8
+ omp plugin link /Users/zhu/code/projects/omp-vcc # local
9
+ omp plugin install omp-vcc # npmjs, no auth
10
+ omp plugin install @zhulinchng/omp-vcc # GitHub Packages, needs PAT
11
+ omp plugin install github:zhulinchng/omp-vcc # git, latest main
12
12
  omp plugin list --json | jq '.[] | select(.name|contains("omp-vcc"))'
13
-
14
- # from npm (unscoped)
15
- omp plugin install omp-vcc
16
-
17
- # from GitHub Packages (scoped, requires auth — see below)
18
- omp plugin install @zhulinchng/omp-vcc
19
-
20
- # from git
21
- omp plugin install github:zhulinchng/omp-vcc
22
13
  ```
23
14
 
24
- ## What you get
15
+ New here [`docs/setup.md`](docs/setup.md).
25
16
 
26
- - **Auto** threshold/overflow compaction via `session_before_compact` hook — no LLM summary, 30–470 ms, 35–99% reduction.
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
- - **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.
17
+ ## What you get
29
18
 
19
+ - **Manual** `/omp-vcc [keep:N] [focus]` (`/pi-vcc` alias) — compact only, single-line toast `90k→22k` — e.g. `/omp-vcc keep:2 fix auth` (detailed savings via `/vcc-stats`).
20
+ - **Recall** `vcc_recall({query, scope, page, mode, expand})` + `/vcc-recall` — regex → TF-IDF OR, 5/page, `mode:'touched'`, `#N:path` drill-down.
21
+ - **Savings** toast `90k→22k (76% saved)` single line (inline `Last compaction` detail removed; use `/vcc-stats`), divider, `/vcc-stats` + `vcc_stats` tool (50-capped history, per-pi), `details.savings` persisted, `/tmp/omp-vcc-debug.json`.
30
22
  ## Commands
31
23
 
32
- | Command | Description |
33
- | --- | --- |
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
- | `/pi-vcc` | Alias for migration |
36
- | `/vcc-recall [query] [scope:all] [page:N]` | Search compacted history (V_adapt). Plain keywords best. |
37
- | `/pi-vcc-recall` | Alias |
24
+ | `/omp-vcc [keep:N] [focus]` | Compact only. `keep:0` = all. Smart-keep boosts `keep:1` (5k→25k) unless explicit. Toast single line; detail via `/vcc-stats`. |
25
+ | `/pi-vcc` | Legacy alias for `/omp-vcc` |
26
+ | `/vcc-recall [query] [scope:all] [page:N] [mode:touched]` | Search compacted history |
27
+ | `/pi-vcc-recall` | Legacy alias for `/vcc-recall` |
28
+ | `/vcc-stats [history\|all]` | Last savings + history table (single, no alias) |
38
29
 
39
- Tool `vcc_recall` mirrors the command, plus `expand:[indices]` and `mode:'touched'` for file index.
30
+ Tools: `vcc_recall`, `vcc_stats` (approval `read`). Extension-only no `commands/*.md` file slash commands (avoids duplicate `/omp-vcc`).
40
31
 
41
32
  ## Configuration
42
33
 
43
- 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`):
34
+ File `~/.omp/omp-vcc/config.json` XDG: `$OMP_VCC_CONFIG_PATH` > `$PI_VCC_CONFIG_PATH` > `$OMP_DIR`/`$PI_CODING_AGENT_DIR` > `~/.omp/omp-vcc/config.json` (migrates `~/.pi/agent/pi-vcc-config.json` once).
44
35
 
45
36
  ```json
46
37
  {
38
+ "vccEnabled": true,
47
39
  "overrideDefaultCompaction": true,
48
40
  "smartKeepTail": true,
49
41
  "continueAfterThresholdCompact": true,
50
- "debug": false
42
+ "debug": false,
43
+ "chainShakeHint": false
51
44
  }
52
45
  ```
53
46
 
47
+ | Flag | Default | Effect |
48
+ |---|---|---|
49
+ | `vccEnabled` | `true` | Master switch. `false` = no interception. |
50
+ | `overrideDefaultCompaction` | `true` | `true` = VCC handles threshold/overflow. `false` = host `methodOrder` handles them; `/omp-vcc` still works. |
51
+ | `smartKeepTail` | `true` | Grow `keep:1` when tail <5k (cap 25k). Explicit `keep:N` never boosted. |
52
+ | `continueAfterThresholdCompact` | `true` | Invisible-continue (`omp-vcc-auto-continue`) after threshold compact. |
53
+ | `debug` | `false` | Write `/tmp/omp-vcc-debug.json` per compaction. |
54
+ | `chainShakeHint` | `false` | Eager post-VCC `shake` chain. Host rescue already auto-shakes on dead-end; this forces it. |
55
+
56
+ Toggle live: `omp config set plugins."@zhulinchng/omp-vcc".debug true` or `/settings` → `@zhulinchng/omp-vcc`. File is source of truth; `ctx.settings` overlays at runtime.
57
+
54
58
  ## How it works
55
59
 
56
- VCC compiles the raw JSONL trace via **lex → parse IR → monotonic line assignment → view lowering** into three views sharing one coordinate system:
60
+ VCC compiles JSONL trace via **lex → parse IR → monotonic line assignment → view lowering** into `V_full` (identity), `V_ui` (5 sections + ranked brief), `V_adapt(b,ρ)` (recall). Pointer invariant `V_ui → V_full[s:e]` via `(#N)` refs.
57
61
 
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)
62
+ ```
63
+ Calibrate cpt SmartKeep BuildOwnCut Normalize → FilterNoise → BuildSections → Brief (1100→2000 tok, 120 lines) → Merge
64
+ ```
65
+
66
+ See [`docs/harness.md`](docs/harness.md) for host impact and [`docs/architecture.md`](docs/architecture.md) for pipeline.
67
+
68
+ ## Combining with shake and snapcompact
61
69
 
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. Diagrams and pipeline in [`docs/architecture.md`](docs/architecture.md); paper mapping in [`docs/paper-notes.md`](docs/paper-notes.md); setup in [`docs/setup.md`](docs/setup.md).
70
+ VCC summarizes **history**; `shake` elides `artifact://` blocks in **kept tail** (disjoint); `snapcompact` is an alternative history archiver (vision bitmaps, needs `model.input includes "image"`). One `CompactionEntry` per trigger combos are additive or sequential, never double-summarization.
63
71
 
64
- ## Related work
72
+ | Trigger | `override` | Result |
73
+ |---|---|---|
74
+ | threshold, `override:true` (default) | VCC handles; host auto-shakes if still over band (`#rescueCompactionDeadEnd`) | VCC + shake if dead-end |
75
+ | threshold, `override:false` | Host walks `methodOrder` (`remote→snapcompact→handoff→shake→soft`) | VCC → snapcompact/shake fallback |
76
+ | `/omp-vcc keep:2` | Always VCC (sentinel `__omp_vcc__`) | VCC |
77
+ | `/omp-vcc` then `/compact snapcompact` | Sequential | VCC entry, then snapcompact entry |
65
78
 
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
79
+ Additive VCC+shake is automatic. Eager chain: `chainShakeHint:true`. Explicit mode (`snapcompact`/`shake`/`soft`/…) bypasses VCC even when `override:true` (`hook.ts:733`). Full table: [`docs/setup.md`](docs/setup.md) + [`docs/harness.md §8`](docs/harness.md#8-working-with-existing-compaction-strategies).
69
80
 
70
- ## Develop
81
+ ## Development
71
82
 
72
- ```bash
73
- omp plugin link .
74
- omp plugin doctor
83
+ ```sh
75
84
  bunx tsc --noEmit
76
- bun run smoke # or bun run scripts/smoke.ts
77
- bun test
85
+ bun test # 515 tests, 48 files, 1443 expects, 0 fail
86
+ bun test tests/e2e --timeout 120000 # 111 E2E
87
+ bun run smoke # 9 checks: 3 hooks + 4 cmds + 2 tools + pipeline
88
+ omp plugin link . && omp plugin doctor
78
89
  ```
79
90
 
80
- Capabilities: `extension`, `skill`, `command` entry `extensions/main.ts`.
91
+ `type:module`, `allowImportingTsExtensions`, `// @ts-nocheck` on vendored `extensions/vcc-core/**`, zero `dist/`. `prepublishOnly` runs `tsc && test && smoke`.
81
92
 
82
93
  ## Verification
83
94
 
84
95
  ```sh
85
- bunx tsc --noEmit
86
- bun test # 310 tests across 33 files, 768 expects
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
96
+ bunx tsc --noEmit && bun test && bun run smoke
97
+ omp -e @zhulinchng/omp-vcc
98
+ /omp-vcc keep:1 # expect [Session Goal] + toast omp-vcc: kept 1/2 turns
99
+ /vcc-stats # table + history
100
+ cat /tmp/omp-vcc-debug.json # when debug:true
89
101
  ```
90
102
 
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`. Full proof matrix and mermaid flows in [`docs/verification.md`](docs/verification.md).
92
- ## Publish
103
+ Full matrix: [`docs/verification.md`](docs/verification.md). Harness map: [`docs/harness.md §9`](docs/harness.md#9-verification-map-claim--evidence).
104
+
105
+ ## Docs
106
+
107
+ [`setup`](docs/setup.md) · [`configuration`](docs/configuration.md) · [`architecture`](docs/architecture.md) · [`harness`](docs/harness.md) · [`verification`](docs/verification.md) · [`omp-compaction`](docs/omp-compaction.md) · [`omp-snapcompact`](docs/omp-snapcompact.md) · [`paper-notes`](docs/paper-notes.md) · [`PUBLISHING`](docs/PUBLISHING.md)
93
108
 
94
- - npmjs (unscoped): `npm publish --registry https://registry.npmjs.org --access public` (package `omp-vcc`)
95
- - GitHub Packages (scoped): `npm publish --registry https://npm.pkg.github.com` (package `@zhulinchng/omp-vcc`, needs `GITHUB_TOKEN` with `read:packages, write:packages`)
96
- - Consumer GPR auth: add to `~/.npmrc`:
97
- ```
98
- @zhulinchng:registry=https://npm.pkg.github.com
99
- //npm.pkg.github.com/:_authToken=YOUR_GITHUB_PAT
100
- ```
101
- - Marketplace: add an entry to `.omp-plugin/marketplace.json` (see `plugin-skill/assets/templates/marketplace-entry.json.template`).
109
+ Publishing: `npm publish --access public` (unscoped `omp-vcc`), `gh release create vX.Y.Z` → GPR (`@zhulinchng/omp-vcc`). Consumer GPR auth: `@zhulinchng:registry=https://npm.pkg.github.com`.
102
110
 
103
111
  ## License
104
112
 
105
- MIT — pi-vcc core files retain original MIT.
113
+ MIT
@@ -6,14 +6,20 @@
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 { migrateStalePluginEntries } from "./vcc-core/core/migrate-stale";
10
+ import { loadAllMessages } from "./vcc-core/core/load-messages";
9
11
  import {
10
12
  registerBeforeCompactHook,
11
13
  PI_VCC_COMPACT_INSTRUCTION,
12
14
  OMP_VCC_COMPACT_INSTRUCTION,
13
15
  getLastCompactionStats,
16
+ getCompactionHistory,
17
+ formatLastStatsDetail,
18
+ formatStatsTable,
14
19
  scheduleCompactionStatsNotify,
20
+ registerVccStatsTool as registerVccStatsToolHook,
21
+ registerVccStatsCommand as registerVccStatsCommandHook,
15
22
  } from "./vcc-core/hook";
16
- import { loadAllMessages } from "./vcc-core/core/load-messages";
17
23
  import { searchEntriesDetailed, getTouchedFiles } from "./vcc-core/core/search-entries";
18
24
  import { formatRecallOutput, formatTouchedOutput } from "./vcc-core/core/format-recall";
19
25
  import { getActiveLineageEntryIds } from "./vcc-core/core/lineage";
@@ -40,9 +46,11 @@ const parseRecallCommandArgs = (
40
46
 
41
47
  const DEFAULT_RECENT = 25;
42
48
  const PAGE_SIZE = 5;
43
-
44
49
  export default function (pi: ExtensionAPI): void {
45
50
  scaffoldSettings();
51
+ try {
52
+ migrateStalePluginEntries();
53
+ } catch {}
46
54
  registerBeforeCompactHook(pi);
47
55
 
48
56
  // ── vcc_recall tool — implements VCC V_adapt via rho predicate (paper §2.1 eq.2) ──
@@ -152,27 +160,27 @@ export default function (pi: ExtensionAPI): void {
152
160
  return { content: [{ type: "text", text: output }], details: undefined };
153
161
  },
154
162
  } as unknown as Parameters<ExtensionAPI["registerTool"]>[0]);
163
+ // ── vcc_stats tool — stats surface for savings (paper § verification) ──
164
+ registerVccStatsToolHook(pi);
155
165
 
156
- // ── /omp-vcc command — manual algorithmic compaction (V_ui) ──
157
166
  pi.registerCommand("omp-vcc", {
158
167
  description: "Compact conversation with omp-vcc structured summary (keep:N + optional focus)",
159
168
  handler: async (args: string, ctx: unknown) => {
160
169
  const c = ctx as {
161
170
  compact: (instructions?: string) => Promise<void>;
162
171
  ui: { notify: (msg: string, level?: string) => void };
172
+ sessionManager?: { getSessionFile?: () => string | undefined };
163
173
  };
164
174
  const parsed = parseKeepAndPrompt(args);
165
175
  const keep = parsed.keepUserTurns;
166
176
  const followUpPrompt = parsed.followUpPrompt;
167
177
  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
178
  try {
171
179
  c.ui.notify(`omp-vcc: compacting with keep:${keep ?? 1}${followUpPrompt ? ` + focus` : ""}...`, "info");
172
180
  } catch {}
173
181
  try {
174
182
  await c.compact(customInstructions);
175
- const stats = getLastCompactionStats();
183
+ const stats = getLastCompactionStats(pi);
176
184
  if (stats) {
177
185
  scheduleCompactionStatsNotify(c as unknown as { ui: { notify: (msg: string, level?: string) => void } }, stats);
178
186
  } else {
@@ -209,7 +217,7 @@ export default function (pi: ExtensionAPI): void {
209
217
  const customInstructions = buildPiVccCustomInstructions(keep);
210
218
  try {
211
219
  await c.compact(customInstructions);
212
- const stats = getLastCompactionStats();
220
+ const stats = getLastCompactionStats(pi);
213
221
  if (stats) scheduleCompactionStatsNotify(c as unknown as { ui: { notify: (msg: string, level?: string) => void } }, stats);
214
222
  else try { c.ui.notify("Compacted with pi-vcc (via omp-vcc)", "info"); } catch {}
215
223
  if (followUpPrompt) {
@@ -309,11 +317,15 @@ export default function (pi: ExtensionAPI): void {
309
317
  const header = totalPages > 1 ? `Page ${page}/${totalPages} (${hits.length} total matches${scopeSuffix}${truncationNote})` : `${hits.length} matches${scopeSuffix}${truncationNote}`;
310
318
  const footer = page < totalPages ? `\n--- /pi-vcc-recall ${query}${scopeArg} page:${page + 1} ---` : "";
311
319
  const output = formatRecallOutput(pageResults, query, header) + footer;
320
+
312
321
  try { piAny.sendMessage?.({ customType: "vcc-recall", content: output, display: true }, { triggerTurn: false }); } catch {}
313
322
  },
314
323
  });
324
+ // ── /vcc-stats commands — show savings table (PR3) ──
325
+ registerVccStatsCommandHook(pi);
326
+
315
327
  }
316
328
  // ── Re-exports for pi-vcc test compatibility (not dead: tests import via hook directly,
317
329
  // 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";
330
+ 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
331
  export { buildPiVccCustomInstructions, parseKeepAndPrompt } from "./vcc-core/core/compact-args";
@@ -0,0 +1,162 @@
1
+ // @ts-nocheck
2
+ // One-time migration: remove stale @zhu/omp-vcc lock+symlink left by renames.
3
+ // correctly installed `omp-vcc` or npm `dependencies` entries (real dirs).
4
+ // Mirrors scripts/uninstall-reset.js:cleanupStalePluginEntries.
5
+
6
+ import * as fsSync from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+
10
+ const HISTORIC = ["@zhu/omp-vcc", "@zhulinchng/omp-vcc"];
11
+ const CURRENT = "omp-vcc";
12
+
13
+ function readJson(p: string): unknown | null {
14
+ try {
15
+ return JSON.parse(fsSync.readFileSync(p, "utf8"));
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
21
+ function isSymlink(p: string): boolean {
22
+ try {
23
+ return fsSync.lstatSync(p).isSymbolicLink();
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ export function migrateStalePluginEntries(home?: string): string {
30
+ const baseHome = home || homedir();
31
+ const pluginsDir = join(baseHome, ".omp", "plugins");
32
+ const lockPath = join(pluginsDir, "omp-plugins.lock.json");
33
+ const pkgPath = join(pluginsDir, "package.json");
34
+ const nm = join(pluginsDir, "node_modules");
35
+
36
+ const lockRaw = readJson(lockPath) as { plugins?: Record<string, unknown>; settings?: Record<string, unknown> } | null;
37
+ if (!lockRaw || typeof lockRaw !== "object") return "no-lock";
38
+
39
+ const pkgRaw = readJson(pkgPath) as { dependencies?: Record<string, string> } | null;
40
+ const deps: Record<string, string> = (pkgRaw && typeof pkgRaw.dependencies === "object" && pkgRaw.dependencies) || {};
41
+
42
+ const pluginKeys: string[] = lockRaw.plugins ? Object.keys(lockRaw.plugins) : [];
43
+ const ompKeys = pluginKeys.filter((k) => k.includes("omp-vcc"));
44
+ const candidates = new Set([...ompKeys, ...HISTORIC, CURRENT]);
45
+
46
+ let removedLocks: string[] = [];
47
+ let removedLinks: string[] = [];
48
+
49
+ const getPkgName = (name: string): string | null => {
50
+ const j = readJson(join(nm, name, "package.json")) as { name?: string } | null;
51
+ return j && typeof j.name === "string" ? j.name : null;
52
+ };
53
+
54
+ const getReal = (name: string): string | null => {
55
+ const p = join(nm, name);
56
+ if (!isSymlink(p)) return null;
57
+ try {
58
+ return fsSync.realpathSync(p);
59
+ } catch {
60
+ return null;
61
+ }
62
+ };
63
+
64
+ const realToKeys = new Map<string, string[]>();
65
+ for (const k of ompKeys) {
66
+ const rp = getReal(k);
67
+ if (!rp) continue;
68
+ const list = realToKeys.get(rp) || [];
69
+ list.push(k);
70
+ realToKeys.set(rp, list);
71
+ }
72
+
73
+ for (const k of [...ompKeys]) {
74
+ if (k in deps) continue;
75
+ const pkgName = getPkgName(k);
76
+ if (pkgName && pkgName !== k) {
77
+ const isHistoric = HISTORIC.includes(k) || k !== CURRENT;
78
+ if (isHistoric) {
79
+ const p = join(nm, k);
80
+ if (isSymlink(p)) {
81
+ try {
82
+ fsSync.rmSync(p, { force: true });
83
+ removedLinks.push(k);
84
+ if (k.startsWith("@")) {
85
+ const scopeDir = join(nm, k.split("/")[0]!);
86
+ try {
87
+ if (fsSync.readdirSync(scopeDir).length === 0) fsSync.rmSync(scopeDir, { force: true });
88
+ } catch {}
89
+ }
90
+ } catch {}
91
+ }
92
+ if (lockRaw.plugins && (lockRaw.plugins as Record<string, unknown>)[k]) {
93
+ delete (lockRaw.plugins as Record<string, unknown>)[k];
94
+ removedLocks.push(k);
95
+ }
96
+ if (lockRaw.settings && (lockRaw.settings as Record<string, unknown>)[k]) delete (lockRaw.settings as Record<string, unknown>)[k];
97
+ }
98
+ }
99
+ }
100
+
101
+ for (const [, keys] of realToKeys) {
102
+ if (keys.length <= 1) continue;
103
+ let keeper: string | null = null;
104
+ for (const k of keys) {
105
+ if (getPkgName(k) === k) {
106
+ keeper = k;
107
+ break;
108
+ }
109
+ }
110
+ if (!keeper) keeper = keys.includes(CURRENT) ? CURRENT : keys[0]!;
111
+ for (const k of keys) {
112
+ if (k === keeper) continue;
113
+ if (k in deps) continue;
114
+ const p = join(nm, k);
115
+ if (isSymlink(p)) {
116
+ try {
117
+ fsSync.rmSync(p, { force: true });
118
+ if (!removedLinks.includes(k)) removedLinks.push(k);
119
+ if (k.startsWith("@")) {
120
+ const scopeDir = join(nm, k.split("/")[0]!);
121
+ try {
122
+ if (fsSync.readdirSync(scopeDir).length === 0) fsSync.rmSync(scopeDir, { force: true });
123
+ } catch {}
124
+ }
125
+ } catch {}
126
+ }
127
+ if (lockRaw.plugins && (lockRaw.plugins as Record<string, unknown>)[k]) {
128
+ delete (lockRaw.plugins as Record<string, unknown>)[k];
129
+ if (!removedLocks.includes(k)) removedLocks.push(k);
130
+ }
131
+ if (lockRaw.settings && (lockRaw.settings as Record<string, unknown>)[k]) delete (lockRaw.settings as Record<string, unknown>)[k];
132
+ }
133
+ }
134
+
135
+ for (const cand of candidates) {
136
+ const p = join(nm, cand);
137
+ if (!isSymlink(p)) continue;
138
+ const hasLock = !!(lockRaw.plugins && (lockRaw.plugins as Record<string, unknown>)[cand]);
139
+ if (hasLock) continue;
140
+ const pkgName = getPkgName(cand);
141
+ if (pkgName === CURRENT || pkgName === "@zhu/omp-vcc" || pkgName === "@zhulinchng/omp-vcc") {
142
+ try {
143
+ fsSync.rmSync(p, { force: true });
144
+ if (!removedLinks.includes(cand)) removedLinks.push(cand);
145
+ if (cand.startsWith("@")) {
146
+ const scopeDir = join(nm, cand.split("/")[0]!);
147
+ try {
148
+ if (fsSync.readdirSync(scopeDir).length === 0) fsSync.rmSync(scopeDir, { force: true });
149
+ } catch {}
150
+ }
151
+ } catch {}
152
+ }
153
+ }
154
+
155
+ if (removedLocks.length > 0 || removedLinks.length > 0) {
156
+ try {
157
+ fsSync.writeFileSync(lockPath, JSON.stringify(lockRaw, null, 2));
158
+ } catch {}
159
+ return `migrated locks:${removedLocks.join(",") || "none"} links:${removedLinks.join(",") || "none"}`;
160
+ }
161
+ return "no-stale";
162
+ }
@@ -56,6 +56,16 @@ export interface PiVccSettings {
56
56
  continueAfterThresholdCompact: boolean;
57
57
  /** Write debug snapshot to /tmp/omp-vcc-debug.json on each compaction. */
58
58
  debug: boolean;
59
+ /**
60
+ * When true, after a successful VCC threshold/overflow compaction, eagerly
61
+ * trigger a follow-up shake via ctx.compact when the host rescue would not.
62
+ * Default false: host's #rescueCompactionDeadEnd already runs shake elide
63
+ * automatically when VCC didn't create enough headroom, and leaving shake in
64
+ * methodOrder covers that case without a second entry. Set true only if you
65
+ * want a chained shake even when VCC already made headroom (costs a second
66
+ * CompactionEntry).
67
+ */
68
+ chainShakeHint: boolean;
59
69
  }
60
70
 
61
71
  export const DEFAULT_SETTINGS: PiVccSettings = {
@@ -64,6 +74,7 @@ export const DEFAULT_SETTINGS: PiVccSettings = {
64
74
  smartKeepTail: true,
65
75
  continueAfterThresholdCompact: true,
66
76
  debug: false,
77
+ chainShakeHint: false,
67
78
  };
68
79
 
69
80
  const readJson = (path: string): Record<string, unknown> | null => {
@@ -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
  }