omp-vcc 0.1.12 → 0.1.13

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.
@@ -21,6 +21,7 @@ import {
21
21
  registerVccStatsCommand as registerVccStatsCommandHook,
22
22
  registerVccConfigCommand as registerVccConfigCommandHook,
23
23
  invalidExpandIndices,
24
+ getCompactForm,
24
25
  } from "./vcc-core/hook";
25
26
  import { searchEntriesDetailed, getTouchedFiles } from "./vcc-core/core/search-entries";
26
27
  import { formatRecallOutput, formatTouchedOutput } from "./vcc-core/core/format-recall";
@@ -180,43 +181,91 @@ export default function (pi: ExtensionAPI): void {
180
181
  // ── vcc_stats tool — stats surface for savings (paper § verification) ──
181
182
  registerVccStatsToolHook(pi);
182
183
 
183
- pi.registerCommand("omp-vcc", {
184
- description: "Compact conversation with omp-vcc structured summary (keep:N + optional focus)",
185
- handler: async (args: string, ctx: unknown) => {
186
- const c = ctx as {
187
- compact: (instructions?: string) => Promise<void>;
188
- ui: { notify: (msg: string, level?: string) => void };
189
- sessionManager?: { getSessionFile?: () => string | undefined };
190
- };
191
- const parsed = parseKeepAndPrompt(args);
192
- const keep = parsed.keepUserTurns;
193
- const followUpPrompt = parsed.followUpPrompt;
194
- const customInstructions = buildOmpCustomInstructions(keep);
184
+ // Shared /omp-vcc + /pi-vcc runner. The two hosts expose incompatible
185
+ // ctx.compact shapes, so the call form branches per live ctx:
186
+ // - omp: compact(string | CompactOptions) => Promise<void>; instructions
187
+ // ride the string (the host splits string|object and drops instructions
188
+ // from the object form), completion = awaited resolution, errors throw
189
+ // ("Compaction cancelled" / "Already compacted" / "Nothing to compact…").
190
+ // - pi: compact(CompactOptions) => void; instructions ONLY via
191
+ // options.customInstructions (a bare string reads as undefined), outcome
192
+ // arrives via onComplete/onError. `settled` keeps one outcome.
193
+ // Form detection is layered (explicit test override → getSystemPrompt
194
+ // shape module scope → legacy omp default) so bundled runtimes without
195
+ // module scope still decide correctly off the live ctx.
196
+ const runCompactCommand = async (
197
+ args: string,
198
+ c: {
199
+ compact: (arg?: unknown) => Promise<void> | void;
200
+ ui: { notify: (msg: string, level?: string) => void };
201
+ },
202
+ buildInstructions: (keep: number | null) => string,
203
+ fallbackToast: string,
204
+ preNotify: boolean,
205
+ compactForm: "object" | "string",
206
+ ): Promise<void> => {
207
+ const parsed = parseKeepAndPrompt(args);
208
+ const keep = parsed.keepUserTurns;
209
+ const followUpPrompt = parsed.followUpPrompt;
210
+ const customInstructions = buildInstructions(keep);
211
+ if (preNotify) {
195
212
  try {
196
213
  c.ui.notify(`omp-vcc: compacting with keep:${keep ?? 1}${followUpPrompt ? ` + focus` : ""}...`, "info");
197
214
  } catch {}
215
+ }
216
+ let settled = false;
217
+ const finishOk = (): void => {
218
+ if (settled) return;
219
+ settled = true;
220
+ const stats = getLastCompactionStats(pi);
221
+ if (stats) {
222
+ scheduleCompactionStatsNotify(c as unknown as { ui: { notify: (msg: string, level?: string) => void } }, stats);
223
+ } else {
224
+ try { c.ui.notify(fallbackToast, "info"); } catch {}
225
+ }
226
+ if (followUpPrompt) {
227
+ try {
228
+ const piAny = pi as unknown as { sendUserMessage?: (content: string) => unknown };
229
+ const sent = piAny.sendUserMessage?.(followUpPrompt) as Promise<void> | undefined;
230
+ if (sent && typeof sent.catch === "function") sent.catch(() => {});
231
+ } catch {}
232
+ }
233
+ };
234
+ const finishErr = (err: unknown): void => {
235
+ if (settled) return;
236
+ settled = true;
237
+ const msg = err instanceof Error ? err.message : String(err);
238
+ if (msg === "Compaction cancelled" || msg === "Already compacted" || msg.startsWith("Nothing to compact")) {
239
+ try { c.ui.notify("Nothing to compact", "warning"); } catch {}
240
+ } else {
241
+ try { c.ui.notify(`Compaction failed: ${msg}`, "error"); } catch {}
242
+ }
243
+ };
244
+ if (compactForm === "object") {
198
245
  try {
199
- await c.compact(customInstructions);
200
- const stats = getLastCompactionStats(pi);
201
- if (stats) {
202
- scheduleCompactionStatsNotify(c as unknown as { ui: { notify: (msg: string, level?: string) => void } }, stats);
203
- } else {
204
- try { c.ui.notify("Compacted with omp-vcc", "info"); } catch {}
205
- }
206
- if (followUpPrompt) {
207
- try {
208
- const piAny = pi as unknown as { sendUserMessage?: (content: string) => Promise<void> | void };
209
- if (piAny.sendUserMessage) await piAny.sendUserMessage(followUpPrompt);
210
- } catch {}
211
- }
246
+ c.compact({ customInstructions, onComplete: finishOk, onError: finishErr });
212
247
  } catch (err: unknown) {
213
- const msg = err instanceof Error ? err.message : String(err);
214
- if (msg === "Compaction cancelled" || msg === "Already compacted") {
215
- try { c.ui.notify("Nothing to compact", "warning"); } catch {}
216
- } else {
217
- try { c.ui.notify(`Compaction failed: ${msg}`, "error"); } catch {}
218
- }
248
+ finishErr(err);
219
249
  }
250
+ return;
251
+ }
252
+ try {
253
+ await c.compact(customInstructions);
254
+ finishOk();
255
+ } catch (err: unknown) {
256
+ finishErr(err);
257
+ }
258
+ };
259
+
260
+ pi.registerCommand("omp-vcc", {
261
+ description: "Compact conversation with omp-vcc structured summary (keep:N + optional focus)",
262
+ handler: async (args: string, ctx: unknown) => {
263
+ const c = ctx as {
264
+ compact: (options?: unknown) => Promise<void> | void;
265
+ ui: { notify: (msg: string, level?: string) => void };
266
+ getSystemPrompt?: () => unknown;
267
+ };
268
+ await runCompactCommand(args, c, buildOmpCustomInstructions, "Compacted with omp-vcc", true, getCompactForm(() => c.getSystemPrompt?.()));
220
269
  },
221
270
  });
222
271
 
@@ -225,30 +274,11 @@ export default function (pi: ExtensionAPI): void {
225
274
  description: "Alias for /omp-vcc (pi-vcc compat)",
226
275
  handler: async (args: string, ctx: unknown) => {
227
276
  const c = ctx as {
228
- compact: (instructions?: string) => Promise<void>;
277
+ compact: (options?: unknown) => Promise<void> | void;
229
278
  ui: { notify: (msg: string, level?: string) => void };
279
+ getSystemPrompt?: () => unknown;
230
280
  };
231
- const parsed = parseKeepAndPrompt(args);
232
- const keep = parsed.keepUserTurns;
233
- const followUpPrompt = parsed.followUpPrompt;
234
- const customInstructions = buildPiVccCustomInstructions(keep);
235
- try {
236
- await c.compact(customInstructions);
237
- const stats = getLastCompactionStats(pi);
238
- if (stats) scheduleCompactionStatsNotify(c as unknown as { ui: { notify: (msg: string, level?: string) => void } }, stats);
239
- else try { c.ui.notify("Compacted with pi-vcc (via omp-vcc)", "info"); } catch {}
240
- if (followUpPrompt) {
241
- try {
242
- const piAny = pi as unknown as { sendUserMessage?: (content: string) => Promise<void> | void };
243
- if (piAny.sendUserMessage) await piAny.sendUserMessage(followUpPrompt);
244
- } catch {}
245
- }
246
- } catch (err: unknown) {
247
- const msg = err instanceof Error ? err.message : String(err);
248
- const cancelled = msg === "Compaction cancelled" || msg === "Already compacted";
249
- const note = cancelled ? "Nothing to compact" : `Compaction failed: ${msg}`;
250
- try { c.ui.notify(note, cancelled ? "warning" : "error"); } catch {}
251
- }
281
+ await runCompactCommand(args, c, buildPiVccCustomInstructions, "Compacted with pi-vcc (via omp-vcc)", false, getCompactForm(() => c.getSystemPrompt?.()));
252
282
  },
253
283
  });
254
284
 
@@ -9,20 +9,99 @@ import { calibrateCharsPerToken, estimateMessageContentChars, estimateMessageCon
9
9
  import type { PiVccCompactionDetails } from "./details";
10
10
  import type { CompactionReason } from "./types";
11
11
 
12
- // convertToLlm shim: try host export, fallback to identity (preserves AgentMessage for omp compileRanked)
12
+ // convertToLlm shim: resolve the host export, fallback to identity (identity
13
+ // is fine for bashExecution/custom, which the pipeline renders natively, but
14
+ // it leaks !!-excluded spans and drops branchSummary entries — so the
15
+ // @earendil-works root (pi's canonical export path) is tried first.
16
+ const CONVERT_TO_LLM_CANDIDATES = [
17
+ "@earendil-works/pi-coding-agent",
18
+ "@oh-my-pi/pi-coding-agent",
19
+ "@oh-my-pi/pi-coding-agent/session/messages",
20
+ ] as const;
21
+ // Pure loader-driven resolver: first candidate whose module exports a
22
+ // convertToLlm function wins, else null (caller keeps identity).
23
+ export const resolveConvertToLlm = (
24
+ load: (id: string) => any,
25
+ ): ((messages: any[]) => any[]) | null => {
26
+ for (const id of CONVERT_TO_LLM_CANDIDATES) {
27
+ try {
28
+ const mod = load(id);
29
+ if (mod && typeof mod.convertToLlm === "function") return mod.convertToLlm;
30
+ } catch {}
31
+ }
32
+ return null;
33
+ };
13
34
  let convertToLlm: (messages: any[]) => any[] = (m) => m;
14
35
  try {
15
36
  const req = createRequire(import.meta.url);
16
- const mod = req("@oh-my-pi/pi-coding-agent/session/messages") as any;
17
- if (mod?.convertToLlm) convertToLlm = mod.convertToLlm;
37
+ convertToLlm = resolveConvertToLlm((id) => req(id)) ?? convertToLlm;
18
38
  } catch {}
19
- try {
20
- if (convertToLlm.length === 0 || (convertToLlm as any).toString().includes("=> m")) {
21
- const req2 = createRequire(import.meta.url);
22
- const mod2 = req2("@oh-my-pi/pi-coding-agent") as any;
23
- if (mod2?.convertToLlm) convertToLlm = mod2.convertToLlm;
39
+ // Test-only override for the module-level binding (mirrors
40
+ // clearCompactionHistoryForTests): lets suites pin convertToLlm wiring without
41
+ // stubbing node module resolution. Null resets to the identity fallback.
42
+ export const __setConvertToLlmForTests = (fn: ((messages: Array<unknown>) => Array<unknown>) | null): void => {
43
+ convertToLlm = fn ?? ((m) => m);
44
+ };
45
+ // Host-kind detection: omp and pi expose incompatible ctx.compact shapes
46
+ // (omp: (string|CompactOptions)=>Promise<void> with instructions on the
47
+ // string; pi: (CompactOptions)=>void with instructions only via
48
+ // options.customInstructions). Three layers, first hit wins:
49
+ // 1. Explicit test override (__setHostKindForTests).
50
+ // 2. Observable ctx shape — works in bundled runtimes where module
51
+ // resolution misses: pi's getSystemPrompt() returns a string, omp's
52
+ // returns string[]. Pure getters, safe to call.
53
+ // 3. Module scope — works in dev/source runtimes: @earendil-works is
54
+ // pi-exclusive (same mechanism as the convertToLlm shim above).
55
+ // Default "omp" preserves the legacy string-form call when host-free.
56
+ const HOST_KIND_CANDIDATES = ["@earendil-works/pi-coding-agent", "@oh-my-pi/pi-coding-agent"] as const;
57
+ export type VccHostKind = "pi" | "omp";
58
+ export type VccCompactForm = "object" | "string";
59
+ // Pure loader-driven resolver: first resolvable scope wins (@earendil-works
60
+ // first, mirroring CONVERT_TO_LLM_CANDIDATES), else "omp".
61
+ export const resolveHostKind = (load: (id: string) => unknown): VccHostKind => {
62
+ for (const id of HOST_KIND_CANDIDATES) {
63
+ try {
64
+ if (load(id)) return id.startsWith("@earendil-works") ? "pi" : "omp";
65
+ } catch {}
24
66
  }
67
+ return "omp";
68
+ };
69
+ let defaultHostKind: VccHostKind = "omp";
70
+ try {
71
+ defaultHostKind = resolveHostKind((id) => createRequire(import.meta.url)(id));
25
72
  } catch {}
73
+ let hostKindOverride: VccHostKind | null = null;
74
+ export const getHostKind = (): VccHostKind => hostKindOverride ?? defaultHostKind;
75
+ // Test-only override (mirrors __setConvertToLlmForTests). Null restores the
76
+ // detected default.
77
+ export const __setHostKindForTests = (kind: VccHostKind | null): void => {
78
+ hostKindOverride = kind;
79
+ };
80
+ // Layered compact-form decision for a live ctx. getSystemPrompt is read off
81
+ // the calling ctx (command or event); absent (host-free mocks) falls through
82
+ // to module scope, then the legacy default.
83
+ export const resolveCompactForm = (
84
+ load: (id: string) => unknown,
85
+ getSystemPrompt?: () => unknown,
86
+ ): VccCompactForm => {
87
+ if (hostKindOverride) return hostKindOverride === "pi" ? "object" : "string";
88
+ try {
89
+ const sp = getSystemPrompt?.();
90
+ if (typeof sp === "string") return "object";
91
+ if (Array.isArray(sp)) return "string";
92
+ } catch {}
93
+ return resolveHostKind(load) === "pi" ? "object" : "string";
94
+ };
95
+ export const getCompactForm = (getSystemPrompt?: () => unknown): VccCompactForm => {
96
+ let load: (id: string) => unknown = () => {
97
+ throw new Error("no loader");
98
+ };
99
+ try {
100
+ const req = createRequire(import.meta.url);
101
+ load = (id) => req(id);
102
+ } catch {}
103
+ return resolveCompactForm(load, getSystemPrompt);
104
+ };
26
105
 
27
106
  export { PI_VCC_COMPACT_INSTRUCTION } from "./core/compact-args";
28
107
  export const OMP_VCC_COMPACT_INSTRUCTION = "__omp_vcc__";
@@ -786,18 +865,26 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
786
865
  // Always handle explicit /pi-vcc or /omp-vcc marker.
787
866
  // Otherwise, only handle when user opted in via settings.
788
867
  const { isPiVcc, keepUserTurns, keepUserTurnsExplicit, followUpPrompt } = parseCompactionInstructions(customInstructions);
789
- setPendingFollowUpPrompt(pi, null);
790
868
  // Explicit host mode bypass: when the host signals an explicit compact mode
791
- // (e.g. /compact snapcompact or --mode shake), let the host walker handle it
792
- // even though overrideDefaultCompaction is true. This enables sequential
793
- // VCC → snapcompact/shake combinations. The event field is only present when
794
- // the optional native vcc patch is applied or a future host exposes it; when
795
- // absent this branch is no-op and the existing override semantics remain.
869
+ // via an event field, let the host walker handle it even though
870
+ // overrideDefaultCompaction is true. This enables sequential VCC →
871
+ // snapcompact/shake combinations. No shipped host exposes such a field
872
+ // today omp carries the mode in the compact() options (never the event)
873
+ // and pi has no modes (its /compact text is raw focus instructions, so
874
+ // lone mode words must NEVER bypass: on pi `/compact shake` means
875
+ // "focus on shake"). The branch stays as the contract for the optional
876
+ // native patch / future hosts; unpatched, override:true serves explicit
877
+ // omp modes via VCC (use override:false for native modes).
796
878
  const explicitMode = (event as any).compactMode ?? (event as any).explicitMode ?? (event as any).mode;
797
879
  if (!isPiVcc && typeof explicitMode === "string" && explicitMode) {
798
880
  const m = explicitMode.toLowerCase();
799
881
  if (m === "snapcompact" || m === "shake" || m === "soft" || m === "remote" || m === "handoff") return;
800
882
  }
883
+ // Chain-shake yield: while a {mode:"shake"} chain is in flight (see
884
+ // session_compact below), let the host run it — otherwise VCC would
885
+ // swallow the modeless call into a second VCC pass. Sentinel compactions
886
+ // still handled (isPiVcc path falls through below).
887
+ if (!isPiVcc && pendingChainShake.has(pi as unknown as object)) return;
801
888
  if (!isPiVcc && !settings.overrideDefaultCompaction) return;
802
889
 
803
890
  const calibrationCut = buildOwnCut(branchEntries as any[], 0);
@@ -1203,13 +1290,21 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
1203
1290
  const isLargeCompaction = (stats.summarized > 10) || (stats.kept > 5) || (stats.keptTokensEst > 2000);
1204
1291
  const shouldContinueAfterAutoCompact = (reason === "threshold" || reason === "overflow" || (reason == null && isLargeCompaction)) && loadSettings(ctx).continueAfterThresholdCompact;
1205
1292
  scheduleCompactionStatsNotify(ctx, stats);
1206
- // Eager post-VCC shake chain (chainShakeHint). Host rescue already handles
1207
- // dead-end; this forces a second shake entry even when headroom was made.
1293
+ // Eager post-VCC shake chain (chainShakeHint, omp only). {mode:"shake"}
1294
+ // is the omp native spelling; the before handler above yields while
1295
+ // pendingChainShake is set so the host actually runs shake instead of
1296
+ // VCC swallowing the modeless call. pi's CompactOptions has no mode key
1297
+ // (the call would trigger a spurious default compaction), so pi never
1298
+ // chains — host rescue remains the only shake path there.
1208
1299
  try {
1209
1300
  const cfgChain = loadSettings(ctx);
1210
1301
  const ctxMaybe = ctx as unknown as Record<string, unknown>;
1211
1302
  const compactFn = ctxMaybe["compact"];
1212
- if (cfgChain.chainShakeHint && typeof compactFn === "function" && !pendingChainShake.has(pi as unknown as object) && !willRetry && !isPiVccLast) {
1303
+ const promptOf = ctxMaybe["getSystemPrompt"] as ((this: unknown) => unknown) | undefined;
1304
+ // Form is detected off the live ctx so bundled runtimes (no module
1305
+ // scope) still decide correctly.
1306
+ const chainForm = getCompactForm(() => promptOf?.call(ctx));
1307
+ if (cfgChain.chainShakeHint && chainForm === "string" && typeof compactFn === "function" && !pendingChainShake.has(pi as unknown as object) && !willRetry && !isPiVccLast) {
1213
1308
  pendingChainShake.add(pi as unknown as object);
1214
1309
  const maybePromise = (compactFn as unknown as (o: unknown) => Promise<void>).call(ctx, { mode: "shake" } as unknown);
1215
1310
  const asPromise = maybePromise as unknown as Promise<void> | void;
@@ -1223,8 +1318,12 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
1223
1318
  }
1224
1319
  } catch {}
1225
1320
  if (followUpPrompt) {
1321
+ // Fire-and-forget: pi's sendUserMessage returns void, omp's returns a
1322
+ // promise — never await either, but swallow async rejections so a
1323
+ // failed redelivery cannot surface as an unhandled rejection.
1226
1324
  try {
1227
- await pi.sendUserMessage(followUpPrompt);
1325
+ const sent = (pi as any).sendUserMessage?.(followUpPrompt) as Promise<void> | undefined;
1326
+ if (sent && typeof sent.catch === "function") sent.catch(() => {});
1228
1327
  } catch {}
1229
1328
  } else if (shouldContinueAfterAutoCompact) {
1230
1329
  scheduleAutoContinueForPi(pi);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omp-vcc",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "description": "Algorithmic VCC compaction for omp - fast lossless no-LLM",
6
6
  "author": "Zhu Lin <zhulin@czl.my>",
package/scripts/smoke.ts CHANGED
@@ -59,6 +59,14 @@ try {
59
59
  "vcc_stats registered",
60
60
  tools.some((t) => t.name === "vcc_stats"),
61
61
  );
62
+ check(
63
+ "vcc_recall schema has query/expand/page/scope/mode",
64
+ (() => {
65
+ const t = tools.find((t) => t.name === "vcc_recall");
66
+ const keys = t?.parameters ? Object.keys(t.parameters) : [];
67
+ return ["query", "expand", "page", "scope", "mode"].every((k) => keys.includes(k));
68
+ })(),
69
+ );
62
70
  check(
63
71
  "omp-vcc command registered",
64
72
  commands.some((c) => c.name === "omp-vcc"),
package/types.d.ts CHANGED
@@ -14,13 +14,13 @@ declare module "@oh-my-pi/pi-coding-agent" {
14
14
  getBranch(fromId?: string): any[];
15
15
  getEntries(): any[];
16
16
  };
17
- compact(instructionsOrOptions?: string | any): Promise<void>;
17
+ compact(instructionsOrOptions?: string | any): Promise<void> | void;
18
18
  sendMessage?: any;
19
19
  sendUserMessage?: any;
20
20
  [key: string]: unknown;
21
21
  }
22
22
  export interface ExtensionCommandContext extends ExtensionContext {
23
- compact(instructionsOrOptions?: string | any): Promise<void>;
23
+ compact(instructionsOrOptions?: string | any): Promise<void> | void;
24
24
  }
25
25
  export interface ExtensionAPI {
26
26
  registerTool(tool: unknown): void;