omp-vcc 0.1.13 → 0.1.14

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.
@@ -5,7 +5,7 @@
5
5
  // pi-vcc port: sting8k/pi-vcc @0.7.0 — algorithmic, zero-LLM, brief transcript + 5 sections, token-budgeted
6
6
 
7
7
  import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
8
- import { scaffoldSettings } from "./vcc-core/core/settings";
8
+ import { scaffoldSettings, loadSettings, loadSettingsWithPluginOverlay } from "./vcc-core/core/settings";
9
9
  import { migrateStalePluginEntries } from "./vcc-core/core/migrate-stale";
10
10
  import { loadAllMessages } from "./vcc-core/core/load-messages";
11
11
  import {
@@ -26,8 +26,9 @@ import {
26
26
  import { searchEntriesDetailed, getTouchedFiles } from "./vcc-core/core/search-entries";
27
27
  import { formatRecallOutput, formatTouchedOutput } from "./vcc-core/core/format-recall";
28
28
  import { getActiveLineageEntryIds } from "./vcc-core/core/lineage";
29
- import { normalizeRecallScope, normalizeRecallMode, parseRecallScope } from "./vcc-core/core/recall-scope";
29
+ import { normalizeRecallScope, normalizeRecallMode, parseRecallScope, parseRecallMode } from "./vcc-core/core/recall-scope";
30
30
  import { parseDrillDown, expandEntryFile, parseEntryRef, expandEntry } from "./vcc-core/core/drill-down";
31
+ import { capRecallBlocks, type RecallBudgetBlock } from "./vcc-core/core/recall-budget";
31
32
  import { buildPiVccCustomInstructions, parseKeepAndPrompt } from "./vcc-core/core/compact-args";
32
33
 
33
34
  // Build omp sentinel instructions; keep pi sentinel for backward compat in hook
@@ -39,14 +40,25 @@ const buildOmpCustomInstructions = (keepUserTurns: number | null): string => {
39
40
  // Helper to parse recall command text: supports `query ... scope:all page:N`
40
41
  const parseRecallCommandArgs = (
41
42
  raw: string,
42
- ): { query: string; scope: "lineage" | "all"; page: number } => {
43
- const parsed = parseRecallScope(raw);
43
+ ): { query: string; scope: "lineage" | "all"; mode: "hybrid" | "touched" | "file"; page: number } => {
44
+ const scoped = parseRecallScope(raw);
45
+ const parsed = parseRecallMode(scoped.text);
44
46
  const pageMatch = parsed.text.match(/\bpage:(\d+)\b/i);
45
47
  const page = pageMatch ? Math.max(1, Number.parseInt(pageMatch[1] ?? "1", 10)) : 1;
46
48
  const query = parsed.text.replace(/\bpage:\d+\b/i, "").trim();
47
- return { query, scope: parsed.scope, page };
49
+ return { query, scope: scoped.scope, mode: normalizeRecallMode(parsed.mode), page };
48
50
  };
49
51
 
52
+ const capModelRecall = (settings: { recallResponseMaxChars: number }, blocks: Array<RecallBudgetBlock | string>, blockKind = "entry"): string =>
53
+ capRecallBlocks(blocks, settings.recallResponseMaxChars, { blockKind });
54
+ const loadRecallMessages = (ctx: unknown, sessionFile: string, full: boolean, lineageEntryIds?: Set<string>) =>
55
+ loadAllMessages(sessionFile, full, lineageEntryIds, (event) => {
56
+ if (!loadSettings(ctx).debug) return;
57
+ try {
58
+ (ctx as any)?.ui?.notify?.(`omp-vcc: ${event.kind} (${event.parseErrors} malformed lines)`, "warning");
59
+ } catch {}
60
+ });
61
+
50
62
  const DEFAULT_RECENT = 25;
51
63
  const PAGE_SIZE = 5;
52
64
  export default function (pi: ExtensionAPI): void {
@@ -63,14 +75,14 @@ export default function (pi: ExtensionAPI): void {
63
75
  name: "vcc_recall",
64
76
  label: "VCC Recall",
65
77
  description:
66
- "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.",
78
+ "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, mode:'file' to search only file tool arguments, 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.",
67
79
  approval: "read",
68
80
  parameters: pi.zod.object({
69
81
  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."),
70
82
  expand: pi.zod.array(pi.zod.number()).optional().describe("Entry indices to return full untruncated content for"),
71
83
  page: pi.zod.number().optional().describe("Page number (1-based) for paginated search results. Default: 1."),
72
84
  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."),
73
- mode: pi.zod.enum(["hybrid", "touched"]).optional().describe("What to show. hybrid (default) = normal search; touched = aggregated files-by-path with entry indices."),
85
+ mode: pi.zod.enum(["hybrid", "touched", "file"]).optional().describe("What to show. hybrid (default) = normal search; touched = aggregated files-by-path; file = only file tool arguments."),
74
86
  }),
75
87
  async execute(_toolCallId: string, params: unknown, _signal: unknown, _onUpdate: unknown, ctx: unknown) {
76
88
  const p = params as {
@@ -83,6 +95,7 @@ export default function (pi: ExtensionAPI): void {
83
95
  const c = ctx as {
84
96
  sessionManager?: { getSessionFile?: () => string | undefined; getBranch?: () => unknown[]; getEntries?: () => unknown[] };
85
97
  };
98
+ const settings = await loadSettingsWithPluginOverlay(ctx);
86
99
  const sessionFile = c.sessionManager?.getSessionFile?.();
87
100
  if (!sessionFile) {
88
101
  return {
@@ -95,12 +108,14 @@ export default function (pi: ExtensionAPI): void {
95
108
  const lineageEntryIds = scope === "lineage" ? getActiveLineageEntryIds(c.sessionManager as unknown as { getBranch: () => { id?: string }[] }) : undefined;
96
109
 
97
110
  const q = p.query?.trim();
111
+ const mode = normalizeRecallMode(p.mode);
112
+ const bounded = (text: string, id: string | number, kind = "entry"): string => capModelRecall(settings, [{ id, text }], kind);
98
113
 
99
114
  const entryRef = q ? parseEntryRef(q) : null;
100
115
  if (entryRef) {
101
116
  const ref = entryRef;
102
117
  if (lineageEntryIds) {
103
- const { rendered } = loadAllMessages(sessionFile, false, lineageEntryIds);
118
+ const { rendered } = loadRecallMessages(ctx, sessionFile, false, lineageEntryIds);
104
119
  const exists = rendered.some((m) => m.index === ref.index);
105
120
  if (!exists) {
106
121
  return {
@@ -110,14 +125,31 @@ export default function (pi: ExtensionAPI): void {
110
125
  }
111
126
  }
112
127
  const text = expandEntry(sessionFile, ref.index, ref.full, ref.offset, ref.limit);
113
- return { content: [{ type: "text", text }], details: undefined };
128
+ return { content: [{ type: "text", text: bounded(text, `#${ref.index}`) }], details: undefined };
129
+ }
130
+ const textMatch = q?.match(/^#(\d+):text(?::(full|\d+(?::\d+)?))?$/);
131
+ if (textMatch) {
132
+ const index = Number.parseInt(textMatch[1] ?? "0", 10);
133
+ const suffix = textMatch[2];
134
+ const full = suffix === "full";
135
+ const parts = suffix && !full ? suffix.split(":") : [];
136
+ const offset = parts[0] !== undefined ? Number.parseInt(parts[0], 10) : undefined;
137
+ const limit = parts[1] !== undefined ? Number.parseInt(parts[1], 10) : undefined;
138
+ if (lineageEntryIds) {
139
+ const { rendered } = loadRecallMessages(ctx, sessionFile, false, lineageEntryIds);
140
+ if (!rendered.some((m) => m.index === index)) {
141
+ return { content: [{ type: "text", text: `Cannot expand indices outside active lineage: ${index}. Use scope:'all' to reach other branches.` }], details: undefined };
142
+ }
143
+ }
144
+ const text = expandEntry(sessionFile, index, full, offset, limit);
145
+ return { content: [{ type: "text", text: bounded(text, `#${index}:text`) }], details: undefined };
114
146
  }
115
147
 
116
148
  const drill = q ? parseDrillDown(q) : null;
117
149
  if (drill) {
118
150
  const parsed = drill;
119
151
  if (lineageEntryIds) {
120
- const { rendered } = loadAllMessages(sessionFile, false, lineageEntryIds);
152
+ const { rendered } = loadRecallMessages(ctx, sessionFile, false, lineageEntryIds);
121
153
  const exists = rendered.some((m) => m.index === parsed.index);
122
154
  if (!exists) {
123
155
  return {
@@ -127,20 +159,26 @@ export default function (pi: ExtensionAPI): void {
127
159
  }
128
160
  }
129
161
  const text = expandEntryFile(sessionFile, parsed.index, parsed.pathPattern, parsed.full, parsed.offset, parsed.limit);
130
- return { content: [{ type: "text", text }], details: undefined };
162
+ return { content: [{ type: "text", text: bounded(text, `#${parsed.index}:path`) }], details: undefined };
131
163
  }
132
164
 
133
- if (normalizeRecallMode(p.mode) === "touched") {
134
- const { rendered, rawMessages } = loadAllMessages(sessionFile, false, lineageEntryIds);
165
+ if (mode === "touched") {
166
+ const { rendered, rawMessages } = loadRecallMessages(ctx, sessionFile, false, lineageEntryIds);
135
167
  const touched = getTouchedFiles(rawMessages as unknown[], rendered);
136
168
  const text = formatTouchedOutput(touched, p.page);
137
- return { content: [{ type: "text", text }], details: undefined };
169
+ return { content: [{ type: "text", text: bounded(text, `page:${p.page ?? 1}`, "page") }], details: undefined };
170
+ }
171
+ if (mode === "file" && !q) {
172
+ const { rendered, rawMessages } = loadRecallMessages(ctx, sessionFile, false, lineageEntryIds);
173
+ const { hits } = searchEntriesDetailed(rendered, rawMessages as unknown[], undefined, { mode });
174
+ const output = (scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(hits);
175
+ return { content: [{ type: "text", text: capModelRecall(settings, [{ id: "file", text: output }], "file") }], details: undefined };
138
176
  }
139
177
 
140
178
  const expandSet = new Set(p.expand ?? []);
141
179
  const hasExpand = expandSet.size > 0;
142
180
  if (hasExpand) {
143
- const { rendered: fullMsgs } = loadAllMessages(sessionFile, true, lineageEntryIds);
181
+ const { rendered: fullMsgs } = loadRecallMessages(ctx, sessionFile, true, lineageEntryIds);
144
182
  const requested = [...expandSet];
145
183
  const byIndex = new Map(fullMsgs.map((m) => [m.index, m]));
146
184
  const invalid = invalidExpandIndices(requested, new Set(byIndex.keys()));
@@ -151,13 +189,14 @@ export default function (pi: ExtensionAPI): void {
151
189
  };
152
190
  }
153
191
  const expanded = requested.map((i) => byIndex.get(i)).filter((m): m is NonNullable<typeof m> => Boolean(m));
154
- const output = (scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(expanded);
192
+ const blocks = expanded.map((entry) => ({ id: `#${entry.index}`, text: formatRecallOutput([entry]) }));
193
+ const output = (scope === "all" ? "Scope: all\n\n" : "") + capModelRecall(settings, blocks, "entry");
155
194
  return { content: [{ type: "text", text: output }], details: undefined };
156
195
  }
157
196
 
158
- const { rendered: msgs, rawMessages } = loadAllMessages(sessionFile, false, lineageEntryIds);
197
+ const { rendered: msgs, rawMessages } = loadRecallMessages(ctx, sessionFile, false, lineageEntryIds);
159
198
  if (q) {
160
- const { hits, totalBeforeCap, truncated } = searchEntriesDetailed(msgs, rawMessages as unknown[], q);
199
+ const { hits, totalBeforeCap, truncated } = searchEntriesDetailed(msgs, rawMessages as unknown[], q, { mode });
161
200
  const page = Math.max(1, p.page ?? 1);
162
201
  const totalPages = Math.ceil(hits.length / PAGE_SIZE);
163
202
  const scopeSuffix = scope === "all" ? " (scope: all)" : "";
@@ -165,16 +204,18 @@ export default function (pi: ExtensionAPI): void {
165
204
  if (hits.length > 0 && page > totalPages) {
166
205
  const guidance = truncated ? `Use a page between 1 and ${totalPages}.` : `Use a page between 1 and ${totalPages}, or refine your query.`;
167
206
  const text = `Page ${page} is outside the available range 1-${totalPages} (${hits.length} matches${scopeSuffix}${truncationNote}). ${guidance}`;
168
- return { content: [{ type: "text", text }], details: undefined };
207
+ return { content: [{ type: "text", text: bounded(text, `page:${page}`, "page") }], details: undefined };
169
208
  }
170
209
  const start = (page - 1) * PAGE_SIZE;
171
210
  const pageResults = hits.slice(start, start + PAGE_SIZE);
172
211
  const header = totalPages > 1 ? `Page ${page}/${totalPages} (${hits.length} total matches${scopeSuffix}${truncationNote})` : `${hits.length} matches${scopeSuffix}${truncationNote}`;
173
212
  const footer = page < totalPages ? `\n--- Use page:${page + 1}${scope === "all" ? " with scope:'all'" : ""} for more results ---` : "";
174
213
  const output = formatRecallOutput(pageResults, q, header, { truncated, totalBeforeCap }) + footer;
175
- return { content: [{ type: "text", text: output }], details: undefined };
214
+ return { content: [{ type: "text", text: bounded(output, `page:${page}`, "page") }], details: undefined };
176
215
  }
177
- const output = (scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(msgs.slice(-DEFAULT_RECENT), q);
216
+ const recent = msgs.slice(-DEFAULT_RECENT);
217
+ const blocks = recent.map((entry) => ({ id: `#${entry.index}`, text: formatRecallOutput([entry]) }));
218
+ const output = (scope === "all" ? "Scope: all\n\n" : "") + capModelRecall(settings, blocks, "entry");
178
219
  return { content: [{ type: "text", text: output }], details: undefined };
179
220
  },
180
221
  } as unknown as Parameters<ExtensionAPI["registerTool"]>[0]);
@@ -219,7 +260,7 @@ export default function (pi: ExtensionAPI): void {
219
260
  settled = true;
220
261
  const stats = getLastCompactionStats(pi);
221
262
  if (stats) {
222
- scheduleCompactionStatsNotify(c as unknown as { ui: { notify: (msg: string, level?: string) => void } }, stats);
263
+ scheduleCompactionStatsNotify(pi, c as unknown as { ui: { notify: (msg: string, level?: string) => void } }, stats);
223
264
  } else {
224
265
  try { c.ui.notify(fallbackToast, "info"); } catch {}
225
266
  }
@@ -295,19 +336,30 @@ export default function (pi: ExtensionAPI): void {
295
336
  try { c.ui.notify("No session file available.", "error"); } catch {}
296
337
  return;
297
338
  }
298
- const { query, scope, page } = parseRecallCommandArgs(args);
339
+ const { query, scope, mode, page } = parseRecallCommandArgs(args);
299
340
  const lineageEntryIds = scope === "lineage" ? getActiveLineageEntryIds(c.sessionManager as unknown as { getBranch: () => { id?: string }[] }) : undefined;
300
341
  const piAny = pi as unknown as { sendMessage?: (msg: unknown, opts?: unknown) => void };
301
342
  if (!query) {
302
- const { rendered } = loadAllMessages(sessionFile, false, lineageEntryIds);
343
+ const { rendered, rawMessages } = loadRecallMessages(ctx, sessionFile, false, lineageEntryIds);
344
+ if (mode === "file") {
345
+ const { hits } = searchEntriesDetailed(rendered, rawMessages as unknown[], undefined, { mode });
346
+ const output = (scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(hits);
347
+ try { piAny.sendMessage?.({ customType: "vcc-recall", content: output, display: true }, { triggerTurn: false }); } catch {}
348
+ return;
349
+ }
350
+ if (mode === "touched") {
351
+ const output = formatTouchedOutput(getTouchedFiles(rawMessages as unknown[], rendered), page);
352
+ try { piAny.sendMessage?.({ customType: "vcc-recall", content: output, display: true }, { triggerTurn: false }); } catch {}
353
+ return;
354
+ }
303
355
  const recent = rendered.slice(-DEFAULT_RECENT);
304
356
  const output = (scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(recent);
305
357
  try { piAny.sendMessage?.({ customType: "vcc-recall", content: output, display: true }, { triggerTurn: false }); } catch {}
306
358
  try { c.ui.notify(`vcc_recall: ${recent.length} recent`, "info"); } catch {}
307
359
  return;
308
360
  }
309
- const { rendered, rawMessages } = loadAllMessages(sessionFile, false, lineageEntryIds);
310
- const { hits, totalBeforeCap, truncated } = searchEntriesDetailed(rendered, rawMessages as unknown[], query);
361
+ const { rendered, rawMessages } = loadRecallMessages(ctx, sessionFile, false, lineageEntryIds);
362
+ const { hits, totalBeforeCap, truncated } = searchEntriesDetailed(rendered, rawMessages as unknown[], query, { mode });
311
363
  const totalPages = Math.ceil(hits.length / PAGE_SIZE);
312
364
  const scopeSuffix = scope === "all" ? " (scope: all)" : "";
313
365
  const scopeArg = scope === "all" ? " scope:all" : "";
@@ -341,17 +393,28 @@ export default function (pi: ExtensionAPI): void {
341
393
  try { c.ui.notify("No session file available.", "error"); } catch {}
342
394
  return;
343
395
  }
344
- const { query, scope, page } = parseRecallCommandArgs(args);
396
+ const { query, scope, mode, page } = parseRecallCommandArgs(args);
345
397
  const lineageEntryIds = scope === "lineage" ? getActiveLineageEntryIds(c.sessionManager as unknown as { getBranch: () => { id?: string }[] }) : undefined;
346
398
  const piAny = pi as unknown as { sendMessage?: (msg: unknown, opts?: unknown) => void };
347
- const { rendered, rawMessages } = loadAllMessages(sessionFile, false, lineageEntryIds);
399
+ const { rendered, rawMessages } = loadRecallMessages(ctx, sessionFile, false, lineageEntryIds);
348
400
  if (!query) {
401
+ if (mode === "file") {
402
+ const { hits } = searchEntriesDetailed(rendered, rawMessages as unknown[], undefined, { mode });
403
+ const output = (scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(hits);
404
+ try { piAny.sendMessage?.({ customType: "vcc-recall", content: output, display: true }, { triggerTurn: false }); } catch {}
405
+ return;
406
+ }
407
+ if (mode === "touched") {
408
+ const output = formatTouchedOutput(getTouchedFiles(rawMessages as unknown[], rendered), page);
409
+ try { piAny.sendMessage?.({ customType: "vcc-recall", content: output, display: true }, { triggerTurn: false }); } catch {}
410
+ return;
411
+ }
349
412
  const recent = rendered.slice(-DEFAULT_RECENT);
350
413
  const output = (scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(recent);
351
414
  try { piAny.sendMessage?.({ customType: "vcc-recall", content: output, display: true }, { triggerTurn: false }); } catch {}
352
415
  return;
353
416
  }
354
- const { hits, totalBeforeCap, truncated } = searchEntriesDetailed(rendered, rawMessages as unknown[], query);
417
+ const { hits, totalBeforeCap, truncated } = searchEntriesDetailed(rendered, rawMessages as unknown[], query, { mode });
355
418
  const totalPages = Math.ceil(hits.length / PAGE_SIZE);
356
419
  const scopeSuffix = scope === "all" ? " (scope: all)" : "";
357
420
  const scopeArg = scope === "all" ? " scope:all" : "";
@@ -0,0 +1,301 @@
1
+ // @ts-nocheck
2
+ import type { PiVccAppendDetails, PiVccAppendSegment } from "../details";
3
+ import { estimateScriptAwareTokens } from "./token-estimate";
4
+
5
+ export type ChainEntry = Record<string, unknown>;
6
+
7
+ export interface ActiveCompactionChain {
8
+ details: PiVccAppendDetails[];
9
+ segments: PiVccAppendSegment[];
10
+ trailingSummary: string;
11
+ fallbackSummary?: string;
12
+ }
13
+
14
+ export interface CollectActiveSegmentsOptions {
15
+ /** The complete summary returned to the host for the latest compaction. */
16
+ fallbackSummary?: string;
17
+ }
18
+
19
+ export interface CoverageForMessagesInput {
20
+ selectedIds: Array<string | undefined>;
21
+ firstKeptEntryId: string;
22
+ sourceMessageCount?: number;
23
+ }
24
+
25
+ export interface AppendDetailsInput {
26
+ segment: Omit<PiVccAppendSegment, "sequence">;
27
+ chainStart: boolean;
28
+ trailingSummary: string;
29
+ sections: string[];
30
+ sourceMessageCount: number;
31
+ previousSummaryUsed: boolean;
32
+ previous?: ActiveCompactionChain | null;
33
+ retainedToolOutputProjection?: PiVccAppendDetails["retainedToolOutputProjection"];
34
+ }
35
+
36
+ export interface CompactionThresholds {
37
+ contextWindow?: number;
38
+ chainThreshold: number;
39
+ contextThreshold?: number;
40
+ minimumSaving: number;
41
+ capacity?: number;
42
+ }
43
+
44
+ export interface CompactionDecisionInput {
45
+ manual?: boolean;
46
+ overflow?: boolean;
47
+ willRetry?: boolean;
48
+ pressure?: boolean;
49
+ chainTokens: number;
50
+ rebaseChainTokens?: number;
51
+ contextWindow?: number;
52
+ reserveTokens?: number;
53
+ fullContextTokens?: number;
54
+ }
55
+
56
+ export interface CompactionDecision {
57
+ chainStart: boolean;
58
+ mode: "append" | "rebase";
59
+ pressure: boolean;
60
+ capacityPressure: boolean;
61
+ chainThreshold: number;
62
+ contextThreshold?: number;
63
+ minimumSaving: number;
64
+ capacity?: number;
65
+ chainTokens: number;
66
+ rebaseChainTokens?: number;
67
+ /** Present only when the caller supplied a trusted full-context estimate. */
68
+ fullContextTokens?: number;
69
+ }
70
+ const nonEmptyString = (value: unknown): value is string => typeof value === "string" && value.length > 0;
71
+
72
+ const finitePositive = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value) && value > 0;
73
+
74
+ const finiteNonNegative = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value) && value >= 0;
75
+
76
+ const uniqueEntryPositions = (entries: ChainEntry[]): Map<string, number> => {
77
+ const positions = new Map<string, number>();
78
+ for (let i = 0; i < entries.length; i++) {
79
+ const id = entries[i]?.id;
80
+ if (typeof id !== "string") continue;
81
+ if (positions.has(id)) positions.set(id, -1);
82
+ else positions.set(id, i);
83
+ }
84
+ return positions;
85
+ };
86
+
87
+ const isCompactionEntry = (entry: ChainEntry): boolean => entry.type === "compaction";
88
+
89
+ /** Runtime parser for the immutable v3 persisted details record. */
90
+ export const isPiVccAppendDetails = (value: unknown): value is PiVccAppendDetails => {
91
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
92
+ const candidate = value as Record<string, unknown>;
93
+ if (candidate.compactor !== "omp-vcc" || candidate.version !== 3 || candidate.summaryMode !== "append") return false;
94
+ if (typeof candidate.chainStart !== "boolean" || typeof candidate.trailingSummary !== "string") return false;
95
+ if (!nonEmptyString(candidate.trailingSummary)) return false;
96
+ if (!Array.isArray(candidate.sections) || !candidate.sections.every(nonEmptyString)) return false;
97
+ if (typeof candidate.sourceMessageCount !== "number" || !Number.isInteger(candidate.sourceMessageCount) || candidate.sourceMessageCount < 0) return false;
98
+ if (typeof candidate.previousSummaryUsed !== "boolean" || candidate.segment === null || typeof candidate.segment !== "object" || Array.isArray(candidate.segment)) return false;
99
+ const segment = candidate.segment as Record<string, unknown>;
100
+ if (typeof segment.sequence !== "number" || !Number.isInteger(segment.sequence) || segment.sequence < 1) return false;
101
+ if (!nonEmptyString(segment.summary) || !finiteNonNegative(segment.tokensBefore)) return false;
102
+ if (segment.coverage === null || typeof segment.coverage !== "object" || Array.isArray(segment.coverage)) return false;
103
+ const coverage = segment.coverage as Record<string, unknown>;
104
+ if (!nonEmptyString(coverage.firstCoveredEntryId) || !nonEmptyString(coverage.lastCoveredEntryId) || typeof coverage.firstKeptEntryId !== "string") return false;
105
+ if (typeof coverage.sourceMessageCount !== "number" || !Number.isInteger(coverage.sourceMessageCount) || coverage.sourceMessageCount < 1) return false;
106
+ if (coverage.includesLegacySummary !== undefined && typeof coverage.includesLegacySummary !== "boolean") return false;
107
+ if (coverage.rebasedFromCompactionId !== undefined && typeof coverage.rebasedFromCompactionId !== "string") return false;
108
+ if (candidate.retainedToolOutputProjection !== undefined) {
109
+ const projection = candidate.retainedToolOutputProjection;
110
+ if (projection === null || typeof projection !== "object" || Array.isArray(projection)) return false;
111
+ const record = projection as Record<string, unknown>;
112
+ if (record.version !== 1 || !finiteNonNegative(record.retainedTokens) || !finiteNonNegative(record.omittedTokens)) return false;
113
+ if (typeof record.pendingCount !== "number" || !Number.isInteger(record.pendingCount) || record.pendingCount < 0) return false;
114
+ if (!Array.isArray(record.omissions) || !record.omissions.every((value) => value !== null && typeof value === "object" && !Array.isArray(value) &&
115
+ nonEmptyString((value as Record<string, unknown>).entryId) && nonEmptyString((value as Record<string, unknown>).marker))) return false;
116
+ }
117
+ return true;
118
+ };
119
+
120
+ const validCoverageForBranch = (coverage: PiVccAppendSegment["coverage"], positions: Map<string, number>): boolean => {
121
+ const first = positions.get(coverage.firstCoveredEntryId);
122
+ const last = positions.get(coverage.lastCoveredEntryId);
123
+ if (first === undefined || last === undefined || first < 0 || last < 0 || first > last) return false;
124
+ if (coverage.firstKeptEntryId.length === 0) return true;
125
+ const kept = positions.get(coverage.firstKeptEntryId);
126
+ return kept !== undefined && kept > last;
127
+ };
128
+
129
+
130
+ /**
131
+ * Collect the append chain ending at the latest compaction. A non-append
132
+ * compaction before the chain start is a legal legacy base; any malformed v3
133
+ * record, summary mismatch, branch mismatch, or sequence gap invalidates the
134
+ * entire chain.
135
+ */
136
+ export const collectActiveSegments = (
137
+ branchEntries: ChainEntry[],
138
+ options: CollectActiveSegmentsOptions = {},
139
+ ): ActiveCompactionChain | null => {
140
+ if (!Array.isArray(branchEntries)) return null;
141
+ const compactions = branchEntries.filter(isCompactionEntry);
142
+ if (compactions.length === 0) return null;
143
+ const collected: Array<{ details: PiVccAppendDetails; compaction: ChainEntry }> = [];
144
+ const positions = uniqueEntryPositions(branchEntries);
145
+ for (let i = compactions.length - 1; i >= 0; i--) {
146
+ const compaction = compactions[i];
147
+ const candidate = compaction.details;
148
+ if (!isPiVccAppendDetails(candidate)) {
149
+ if (collected.length === 0) return null;
150
+ break;
151
+ }
152
+ if (compaction.summary !== candidate.trailingSummary || compaction.firstKeptEntryId !== candidate.segment.coverage.firstKeptEntryId || !validCoverageForBranch(candidate.segment.coverage, positions)) return null;
153
+ collected.push({ details: candidate, compaction });
154
+ if (candidate.chainStart) break;
155
+ }
156
+ if (collected.length === 0 || !collected[collected.length - 1].details.chainStart) return null;
157
+ collected.reverse();
158
+ const details = collected.map((value) => value.details);
159
+ for (let i = 0; i < details.length; i++) {
160
+ if (details[i].segment.sequence !== i + 1 || (i > 0 && details[i].chainStart)) return null;
161
+ if (i > 0 && details[i - 1].segment.coverage.firstKeptEntryId.length > 0 &&
162
+ details[i].segment.coverage.firstCoveredEntryId !== details[i - 1].segment.coverage.firstKeptEntryId) return null;
163
+ }
164
+ const firstCoverage = details[0].segment.coverage;
165
+ if (firstCoverage.rebasedFromCompactionId !== undefined &&
166
+ !compactions.some((value) => value.id === firstCoverage.rebasedFromCompactionId)) return null;
167
+ const latest = details[details.length - 1];
168
+ if (options.fallbackSummary !== undefined && options.fallbackSummary !== latest.trailingSummary) return null;
169
+ return {
170
+ details,
171
+ segments: details.map((detail) => detail.segment),
172
+ trailingSummary: latest.trailingSummary,
173
+ fallbackSummary: latest.trailingSummary,
174
+ };
175
+ };
176
+
177
+ /** Derive immutable coverage from selected ids; an undefined id fails closed. */
178
+ export const coverageForMessages = (input: CoverageForMessagesInput): PiVccAppendSegment["coverage"] | null => {
179
+ const ids = input.selectedIds;
180
+ if (!Array.isArray(ids) || ids.length === 0 || ids.some((id) => !nonEmptyString(id))) return null;
181
+ const first = ids[0];
182
+ const last = ids[ids.length - 1];
183
+ if (!nonEmptyString(first) || !nonEmptyString(last)) return null;
184
+ const sourceMessageCount = input.sourceMessageCount ?? ids.length;
185
+ if (!Number.isInteger(sourceMessageCount) || sourceMessageCount < 1) return null;
186
+ return {
187
+ firstCoveredEntryId: first,
188
+ lastCoveredEntryId: last,
189
+ firstKeptEntryId: typeof input.firstKeptEntryId === "string" ? input.firstKeptEntryId : "",
190
+ sourceMessageCount,
191
+ };
192
+ };
193
+
194
+ /** Build a v3 details record and reject a missing/invalid prior chain. */
195
+ export const buildAppendOnlyDetails = (input: AppendDetailsInput): PiVccAppendDetails | null => {
196
+ if (!input || !input.segment || !nonEmptyString(input.segment.summary) || !nonEmptyString(input.trailingSummary)) return null;
197
+ if (!input.segment.coverage || !nonEmptyString(input.segment.coverage.firstCoveredEntryId) || !nonEmptyString(input.segment.coverage.lastCoveredEntryId)) return null;
198
+ if (typeof input.segment.coverage.firstKeptEntryId !== "string" || input.segment.coverage.sourceMessageCount < 1) return null;
199
+ if (!input.chainStart && (!input.previous || input.previous.segments.length === 0)) return null;
200
+ const priorDetails = input.previous?.details[input.previous.details.length - 1];
201
+ if (!input.chainStart && (!priorDetails || !isPiVccAppendDetails(priorDetails) || priorDetails.segment.sequence !== input.previous?.segments.length)) return null;
202
+ const prior = input.previous?.segments[input.previous.segments.length - 1];
203
+ const sequence = input.chainStart ? 1 : (prior?.sequence ?? 0) + 1;
204
+ if (!Number.isInteger(sequence) || sequence < 1) return null;
205
+ const details: PiVccAppendDetails = {
206
+ compactor: "omp-vcc",
207
+ version: 3,
208
+ summaryMode: "append",
209
+ chainStart: input.chainStart,
210
+ segment: { ...input.segment, sequence },
211
+ trailingSummary: input.trailingSummary,
212
+ sections: input.sections.slice(),
213
+ sourceMessageCount: input.sourceMessageCount,
214
+ previousSummaryUsed: input.previousSummaryUsed,
215
+ };
216
+ if (input.retainedToolOutputProjection !== undefined) details.retainedToolOutputProjection = input.retainedToolOutputProjection;
217
+ return isPiVccAppendDetails(details) ? details : null;
218
+ };
219
+
220
+ export const estimateChainTokens = (chain: ActiveCompactionChain): number => {
221
+ let total = estimateScriptAwareTokens(chain.trailingSummary);
222
+ for (const segment of chain.segments) total += estimateScriptAwareTokens(segment.summary);
223
+ return total;
224
+ };
225
+
226
+ export const compactionThresholds = (contextWindow?: number, reserveTokens?: number): CompactionThresholds => {
227
+ if (!finitePositive(contextWindow)) {
228
+ return { chainThreshold: 34_000, minimumSaving: 24_000 };
229
+ }
230
+ const capacity = typeof reserveTokens === "number" && Number.isFinite(reserveTokens) && reserveTokens >= 0
231
+ ? Math.max(0, contextWindow - reserveTokens)
232
+ : undefined;
233
+ return {
234
+ contextWindow,
235
+ chainThreshold: Math.floor(contextWindow / 8),
236
+ contextThreshold: Math.floor(contextWindow / 2),
237
+ minimumSaving: Math.max(1, Math.min(24_000, Math.floor(24_000 * contextWindow / 272_000))),
238
+ capacity,
239
+ };
240
+ };
241
+
242
+ export const decideAppendMode = (input: CompactionDecisionInput): CompactionDecision => {
243
+ const thresholds = compactionThresholds(input.contextWindow, input.reserveTokens);
244
+ const capacityPressure = thresholds.capacity !== undefined && finiteNonNegative(input.fullContextTokens) && input.fullContextTokens > thresholds.capacity;
245
+ const pressure = input.pressure === true || input.chainTokens >= thresholds.chainThreshold;
246
+ const explicit = input.overflow === true || input.willRetry === true || capacityPressure;
247
+ const rebaseTokens = finiteNonNegative(input.rebaseChainTokens) ? input.rebaseChainTokens : undefined;
248
+ const saving = finiteNonNegative(input.fullContextTokens) && rebaseTokens !== undefined
249
+ ? Math.max(0, input.fullContextTokens - rebaseTokens)
250
+ : undefined;
251
+ let mode: "append" | "rebase" = "append";
252
+ if (input.manual === true) mode = "rebase";
253
+ else if (explicit && rebaseTokens !== undefined) mode = rebaseTokens < input.chainTokens ? "rebase" : "append";
254
+ else if (pressure && saving !== undefined && saving >= thresholds.minimumSaving && (rebaseTokens === undefined || rebaseTokens < input.chainTokens)) mode = "rebase";
255
+ const decision: CompactionDecision = {
256
+ chainStart: input.manual === true && mode === "rebase",
257
+ mode,
258
+ pressure,
259
+ capacityPressure,
260
+ chainThreshold: thresholds.chainThreshold,
261
+ minimumSaving: thresholds.minimumSaving,
262
+ chainTokens: input.chainTokens,
263
+ rebaseChainTokens: rebaseTokens,
264
+ };
265
+ if (thresholds.contextThreshold !== undefined) decision.contextThreshold = thresholds.contextThreshold;
266
+ if (thresholds.contextWindow !== undefined) decision.contextWindow = thresholds.contextWindow;
267
+ if (thresholds.capacity !== undefined) decision.capacity = thresholds.capacity;
268
+ if (finiteNonNegative(input.fullContextTokens)) decision.fullContextTokens = input.fullContextTokens;
269
+ return decision;
270
+ };
271
+
272
+ export const APPEND_SEGMENT_CUSTOM_TYPE = "omp-vcc-append-segment";
273
+ export const APPEND_TRAILING_CUSTOM_TYPE = "omp-vcc-append-trailing";
274
+
275
+ export interface AppendContextProjectionInput {
276
+ messages: ChainEntry[];
277
+ chain: ActiveCompactionChain;
278
+ fallbackSummary: string;
279
+ }
280
+
281
+ /** Replace exactly one host fallback summary with hidden segment + trailing messages. */
282
+ export const projectAppendOnlyContext = (input: AppendContextProjectionInput): ChainEntry[] => {
283
+ if (!input || !Array.isArray(input.messages) || !input.chain || !nonEmptyString(input.fallbackSummary)) return input?.messages;
284
+ if (input.fallbackSummary !== input.chain.fallbackSummary && input.fallbackSummary !== input.chain.trailingSummary) return input.messages;
285
+ const matches: number[] = [];
286
+ for (let i = 0; i < input.messages.length; i++) {
287
+ const message = input.messages[i];
288
+ if ((message.role === "compactionSummary" || message.role === "branchSummary") && message.summary === input.fallbackSummary) matches.push(i);
289
+ }
290
+ if (matches.length !== 1) return input.messages;
291
+ const replacement: ChainEntry[] = input.chain.segments.map((segment) => ({
292
+ role: "custom",
293
+ customType: APPEND_SEGMENT_CUSTOM_TYPE,
294
+ display: false,
295
+ content: segment.summary,
296
+ }));
297
+ replacement.push({ role: "custom", customType: APPEND_TRAILING_CUSTOM_TYPE, display: false, content: input.chain.trailingSummary });
298
+ const result = input.messages.slice();
299
+ result.splice(matches[0], 1, ...replacement);
300
+ return result;
301
+ };
@@ -99,15 +99,22 @@ function formatToolCallContent(
99
99
  const MAX_FULL_BYTES = 50 * 1024;
100
100
 
101
101
  if (full) {
102
- // Full content: capped at 50KB
103
- if (Buffer.byteLength(body, "utf8") > MAX_FULL_BYTES) {
104
- const truncated = body.slice(0, MAX_FULL_BYTES);
102
+ // Full content is capped at 50 KiB of UTF-8 without splitting a code point.
103
+ const totalBytes = Buffer.byteLength(body, "utf8");
104
+ if (totalBytes > MAX_FULL_BYTES) {
105
+ const bytes = Buffer.from(body, "utf8");
106
+ let end = MAX_FULL_BYTES;
107
+ if (end < bytes.length && (bytes[end] & 0xc0) === 0x80) {
108
+ while (end > 0 && (bytes[end] & 0xc0) === 0x80) end--;
109
+ }
110
+ const truncated = bytes.subarray(0, end).toString("utf8");
111
+ const omittedBytes = bytes.length - end;
105
112
  return `File: ${tc.path}
106
113
  Tool: ${tc.name}
107
114
 
108
115
  ${truncated}
109
116
 
110
- ... (${Buffer.byteLength(body, "utf8") - MAX_FULL_BYTES} more bytes — file exceeds 50KB display limit. Use #${entryIndex}:${tc.path}:${previewLimit} for next page.)`;
117
+ ... (${omittedBytes} more bytes — file exceeds 50KB display limit. Use #${entryIndex}:${tc.path}:${previewLimit} for next page.)`;
111
118
  }
112
119
  return `File: ${tc.path}
113
120
  Tool: ${tc.name}
@@ -94,14 +94,30 @@ export const formatRecallOutput = (
94
94
 
95
95
  const lines = entries.map((e) => {
96
96
  const fileSuffix = e.files?.length ? ` files:[${e.files.join(", ")}]` : "";
97
+ const fileDetails = e.fileMatches?.length
98
+ ? `\n${e.fileMatches
99
+ .map((m) => {
100
+ const header = ` ${m.path} (${m.toolName}, ${m.lineCount} lines)`;
101
+ if (!m.snippet) return header;
102
+ const snippet = m.snippet
103
+ .split("\n")
104
+ .map((line) => ` ${line}`)
105
+ .join("\n");
106
+ return `${header}\n${snippet}`;
107
+ })
108
+ .join("\n")}`
109
+ : "";
97
110
  const body = query && e.snippet ? e.snippet : e.summary;
98
- return `#${e.index} [${e.role}]${fileSuffix} ${body}`;
111
+ const separator = fileDetails ? "\n" : " ";
112
+ return `#${e.index} [${e.role}]${fileSuffix}${fileDetails}${separator}${body}`;
99
113
  });
100
114
 
101
115
  const body = `${header}\n\n${lines.join("\n\n")}`;
102
116
  // Every hit ref resolves: #N expands the full entry (see expandEntry in
103
117
  // drill-down.ts). Surface the hint when results are capped or clipped.
104
- const clipped = entries.some((e) => e.snippet?.includes("...("));
118
+ const clipped = entries.some(
119
+ (e) => e.snippet?.includes("...(") || e.fileMatches?.some((m) => m.snippet.includes("...(")),
120
+ );
105
121
  if (opts?.truncated || clipped) {
106
122
  return `${body}\n\n--- Use #N for full entry text ---`;
107
123
  }