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.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/commands/omp-vcc.md +19 -0
- package/commands/vcc-recall.md +21 -0
- package/extensions/main.ts +319 -0
- package/extensions/vcc-core/commands/vcc-recall.ts +2 -0
- package/extensions/vcc-core/core/brief.ts +404 -0
- package/extensions/vcc-core/core/build-sections.ts +77 -0
- package/extensions/vcc-core/core/compact-args.ts +46 -0
- package/extensions/vcc-core/core/content.ts +157 -0
- package/extensions/vcc-core/core/drill-down.ts +299 -0
- package/extensions/vcc-core/core/filter-noise.ts +42 -0
- package/extensions/vcc-core/core/format-recall.ts +101 -0
- package/extensions/vcc-core/core/format.ts +82 -0
- package/extensions/vcc-core/core/lineage.ts +27 -0
- package/extensions/vcc-core/core/load-messages.ts +44 -0
- package/extensions/vcc-core/core/normalize.ts +66 -0
- package/extensions/vcc-core/core/rank.ts +284 -0
- package/extensions/vcc-core/core/recall-scope.ts +31 -0
- package/extensions/vcc-core/core/render-entries.ts +55 -0
- package/extensions/vcc-core/core/report.ts +233 -0
- package/extensions/vcc-core/core/sanitize.ts +6 -0
- package/extensions/vcc-core/core/search-entries.ts +576 -0
- package/extensions/vcc-core/core/settings.ts +151 -0
- package/extensions/vcc-core/core/skill-collapse.ts +36 -0
- package/extensions/vcc-core/core/summarize.ts +208 -0
- package/extensions/vcc-core/core/token-estimate.ts +101 -0
- package/extensions/vcc-core/core/tool-args.ts +17 -0
- package/extensions/vcc-core/details.ts +12 -0
- package/extensions/vcc-core/extract/commits.ts +70 -0
- package/extensions/vcc-core/extract/files.ts +88 -0
- package/extensions/vcc-core/extract/goals.ts +80 -0
- package/extensions/vcc-core/extract/preferences.ts +56 -0
- package/extensions/vcc-core/hook.ts +1017 -0
- package/extensions/vcc-core/sections.ts +9 -0
- package/extensions/vcc-core/types.ts +17 -0
- package/package.json +104 -0
- package/scripts/smoke.ts +116 -0
- package/scripts/uninstall-reset.js +73 -0
- package/skills/omp-vcc/SKILL.md +35 -0
- 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";
|