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.
- package/extensions/main.ts +92 -29
- package/extensions/vcc-core/core/compaction-chain.ts +301 -0
- package/extensions/vcc-core/core/drill-down.ts +11 -4
- package/extensions/vcc-core/core/format-recall.ts +18 -2
- package/extensions/vcc-core/core/global-indices.ts +46 -0
- package/extensions/vcc-core/core/load-messages.ts +116 -21
- package/extensions/vcc-core/core/normalize.ts +13 -13
- package/extensions/vcc-core/core/recall-budget.ts +107 -0
- package/extensions/vcc-core/core/recall-scope.ts +16 -9
- package/extensions/vcc-core/core/search-entries.ts +201 -36
- package/extensions/vcc-core/core/session-lines.ts +81 -0
- package/extensions/vcc-core/core/settings.ts +239 -99
- package/extensions/vcc-core/core/summarize.ts +8 -2
- package/extensions/vcc-core/core/token-estimate.ts +55 -0
- package/extensions/vcc-core/core/tool-output-budget.ts +217 -0
- package/extensions/vcc-core/details.ts +35 -0
- package/extensions/vcc-core/hook.ts +677 -134
- package/package.json +77 -1
- package/types.d.ts +8 -0
|
@@ -1,11 +1,30 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import { writeFileSync } from "fs";
|
|
5
|
-
import {
|
|
4
|
+
import { appendFileSync, mkdirSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
|
|
5
|
+
import { dirname, join } from "path";
|
|
6
|
+
import { compileRanked, compileSegment } from "./core/summarize";
|
|
7
|
+
import { buildGlobalIndex, type PersistedSessionEntry } from "./core/global-indices";
|
|
8
|
+
import { scanSessionEntries } from "./core/session-lines";
|
|
9
|
+
import {
|
|
10
|
+
buildAppendOnlyDetails,
|
|
11
|
+
collectActiveSegments,
|
|
12
|
+
compactionThresholds,
|
|
13
|
+
coverageForMessages,
|
|
14
|
+
decideAppendMode,
|
|
15
|
+
estimateChainTokens,
|
|
16
|
+
isPiVccAppendDetails,
|
|
17
|
+
projectAppendOnlyContext,
|
|
18
|
+
} from "./core/compaction-chain";
|
|
19
|
+
import {
|
|
20
|
+
applyRetainedToolOutputProjection,
|
|
21
|
+
buildRetainedToolOutputProjection,
|
|
22
|
+
type RetainedToolOutputProjection,
|
|
23
|
+
} from "./core/tool-output-budget";
|
|
6
24
|
import { buildPiVccCustomInstructions, parseKeepAndPrompt, PI_VCC_COMPACT_INSTRUCTION } from "./core/compact-args";
|
|
7
|
-
import { loadSettings,
|
|
8
|
-
import { calibrateCharsPerToken, estimateMessageContentChars,
|
|
25
|
+
import { loadSettings, loadSettingsWithPluginOverlay, loadSettingsWithSourcesAsync, getSettingsPath, DEFAULT_SETTINGS, type PiVccSettings, type VccConfigView } from "./core/settings";
|
|
26
|
+
import { calibrateCharsPerToken, estimateMessageContentChars, estimateScriptAwareTokens, estimateScriptAwareMessageContentTokens, collectUsageStats } from "./core/token-estimate";
|
|
27
|
+
import { sanitize } from "./core/sanitize";
|
|
9
28
|
import type { PiVccCompactionDetails } from "./details";
|
|
10
29
|
import type { CompactionReason } from "./types";
|
|
11
30
|
|
|
@@ -185,33 +204,60 @@ export const evaluateGrowthGuard = (prefixChars: number, netNewSummaryChars: num
|
|
|
185
204
|
let lastStats: CompactionStats | null = null;
|
|
186
205
|
let lastCompactWasPiVcc = false;
|
|
187
206
|
let pendingFollowUpPrompt: string | null = null;
|
|
188
|
-
let pendingAutoContinueTimer:
|
|
207
|
+
let pendingAutoContinueTimer: unknown = null;
|
|
189
208
|
let globalHistory: CompactionStats[] = [];
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
209
|
+
|
|
210
|
+
interface PerPiState {
|
|
211
|
+
lastStats: CompactionStats | null;
|
|
212
|
+
lastCompactWasPiVcc: boolean;
|
|
213
|
+
pendingFollowUpPrompt: string | null;
|
|
214
|
+
pendingAutoContinueTimer: unknown;
|
|
215
|
+
statsHistory: CompactionStats[];
|
|
216
|
+
generation: number;
|
|
217
|
+
sessionId?: string;
|
|
218
|
+
timers: Set<unknown>;
|
|
219
|
+
pendingDisplay?: { text: string; sourceEntryId?: string; truncated: boolean };
|
|
220
|
+
autoCompaction?: { generation: number; sessionId?: string; reason: string; action: string; willRetry: boolean };
|
|
221
|
+
pendingCompactionFingerprint?: string;
|
|
222
|
+
pendingPreviousStats?: CompactionStats | null;
|
|
223
|
+
pendingStatsHistoryLength?: number;
|
|
224
|
+
lastSettings?: PiVccSettings;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const perPi = new WeakMap<any, PerPiState>();
|
|
196
228
|
const perPiKeys = new Set<any>();
|
|
197
|
-
// Guard eager chainShakeHint to avoid recursion: tracks pis currently chaining.
|
|
198
229
|
const pendingChainShake = new WeakSet<object>();
|
|
199
|
-
const getPerPi = (pi: any) => {
|
|
230
|
+
const getPerPi = (pi: any): PerPiState | null => {
|
|
200
231
|
if (!pi || typeof pi !== "object") return null;
|
|
201
|
-
let
|
|
202
|
-
if (!
|
|
203
|
-
|
|
204
|
-
|
|
232
|
+
let state = perPi.get(pi);
|
|
233
|
+
if (!state) {
|
|
234
|
+
state = {
|
|
235
|
+
lastStats: null,
|
|
236
|
+
lastCompactWasPiVcc: false,
|
|
237
|
+
pendingFollowUpPrompt: null,
|
|
238
|
+
pendingAutoContinueTimer: null,
|
|
239
|
+
statsHistory: [],
|
|
240
|
+
generation: 0,
|
|
241
|
+
timers: new Set<unknown>(),
|
|
242
|
+
pendingDisplay: undefined,
|
|
243
|
+
};
|
|
244
|
+
perPi.set(pi, state);
|
|
245
|
+
perPiKeys.add(pi);
|
|
246
|
+
}
|
|
247
|
+
if (!state.statsHistory) state.statsHistory = [];
|
|
248
|
+
if (!state.timers) state.timers = new Set<unknown>();
|
|
249
|
+
if (!state.pendingDisplay) state.pendingDisplay = undefined;
|
|
250
|
+
return state;
|
|
205
251
|
};
|
|
206
252
|
const setLastStats = (pi: any, v: CompactionStats | null) => {
|
|
207
253
|
if (v && v.timestamp == null) v.timestamp = Date.now();
|
|
208
254
|
lastStats = v;
|
|
209
|
-
const
|
|
210
|
-
if (
|
|
211
|
-
|
|
255
|
+
const state = getPerPi(pi);
|
|
256
|
+
if (state) {
|
|
257
|
+
state.lastStats = v;
|
|
212
258
|
if (v) {
|
|
213
|
-
|
|
214
|
-
if (
|
|
259
|
+
state.statsHistory.push(v);
|
|
260
|
+
if (state.statsHistory.length > 50) state.statsHistory.shift();
|
|
215
261
|
}
|
|
216
262
|
}
|
|
217
263
|
if (v) {
|
|
@@ -219,26 +265,103 @@ const setLastStats = (pi: any, v: CompactionStats | null) => {
|
|
|
219
265
|
if (globalHistory.length > 50) globalHistory.shift();
|
|
220
266
|
}
|
|
221
267
|
};
|
|
222
|
-
const setLastCompactWasPiVcc = (pi: any, v: boolean) => {
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
268
|
+
const setLastCompactWasPiVcc = (pi: any, v: boolean) => {
|
|
269
|
+
lastCompactWasPiVcc = v;
|
|
270
|
+
const state = getPerPi(pi);
|
|
271
|
+
if (state) state.lastCompactWasPiVcc = v;
|
|
272
|
+
};
|
|
273
|
+
const setPendingFollowUpPrompt = (pi: any, v: string | null) => {
|
|
274
|
+
pendingFollowUpPrompt = v;
|
|
275
|
+
const state = getPerPi(pi);
|
|
276
|
+
if (state) state.pendingFollowUpPrompt = v;
|
|
277
|
+
};
|
|
278
|
+
const getPendingFollowUpPrompt = (pi: any) => {
|
|
279
|
+
const state = getPerPi(pi);
|
|
280
|
+
return state ? state.pendingFollowUpPrompt : pendingFollowUpPrompt;
|
|
281
|
+
};
|
|
282
|
+
const sessionIdOf = (ctx: any): string | undefined => {
|
|
283
|
+
try {
|
|
284
|
+
const id = ctx?.sessionManager?.getSessionId?.();
|
|
285
|
+
return typeof id === "string" ? id : undefined;
|
|
286
|
+
} catch {
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
const isCurrentGeneration = (pi: any, ctx: any, generation: number, sessionId: string | undefined): boolean => {
|
|
291
|
+
const state = getPerPi(pi);
|
|
292
|
+
if (!state || state.generation !== generation) return false;
|
|
293
|
+
return (state.sessionId ?? sessionIdOf(ctx)) === sessionId;
|
|
294
|
+
};
|
|
295
|
+
const clearTimerHandle = (ctx: any, timer: unknown): void => {
|
|
296
|
+
if (timer == null) return;
|
|
297
|
+
try {
|
|
298
|
+
if (typeof ctx?.clearTimer === "function") ctx.clearTimer(timer);
|
|
299
|
+
else clearTimeout(timer as Parameters<typeof clearTimeout>[0]);
|
|
300
|
+
} catch {}
|
|
301
|
+
};
|
|
302
|
+
const scheduleManaged = (
|
|
303
|
+
pi: any,
|
|
304
|
+
ctx: any,
|
|
305
|
+
callback: () => void,
|
|
306
|
+
delay: number,
|
|
307
|
+
kind: string,
|
|
308
|
+
): unknown => {
|
|
309
|
+
const state = getPerPi(pi);
|
|
310
|
+
const generation = state?.generation ?? 0;
|
|
311
|
+
const sessionId = state?.sessionId ?? sessionIdOf(ctx);
|
|
312
|
+
let handle: unknown;
|
|
313
|
+
const guarded = () => {
|
|
314
|
+
if (state) state.timers.delete(handle);
|
|
315
|
+
if (state && (state.generation !== generation || state.sessionId !== sessionId)) {
|
|
316
|
+
logMetrics(loadSettings(ctx), { event: "stale-callback", kind, generation, sessionId });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
try { callback(); } catch (error) { throw error; }
|
|
320
|
+
};
|
|
321
|
+
handle = typeof ctx?.setTimeout === "function" ? ctx.setTimeout(guarded, delay) : setTimeout(guarded, delay);
|
|
322
|
+
state?.timers.add(handle);
|
|
323
|
+
return handle;
|
|
324
|
+
};
|
|
325
|
+
const advanceSessionGeneration = (pi: any, ctx: any): void => {
|
|
326
|
+
const state = getPerPi(pi);
|
|
327
|
+
if (!state) return;
|
|
328
|
+
for (const timer of state.timers) clearTimerHandle(ctx, timer);
|
|
329
|
+
state.timers.clear();
|
|
330
|
+
state.generation++;
|
|
331
|
+
state.sessionId = sessionIdOf(ctx);
|
|
332
|
+
state.lastStats = null;
|
|
333
|
+
state.pendingCompactionFingerprint = undefined;
|
|
334
|
+
state.pendingPreviousStats = undefined;
|
|
335
|
+
state.pendingStatsHistoryLength = undefined;
|
|
336
|
+
state.lastSettings = undefined;
|
|
337
|
+
state.pendingAutoContinueTimer = null;
|
|
338
|
+
state.statsHistory = [];
|
|
339
|
+
state.pendingDisplay = undefined;
|
|
340
|
+
state.autoCompaction = undefined;
|
|
341
|
+
pendingFollowUpPrompt = null;
|
|
229
342
|
pendingAutoContinueTimer = null;
|
|
230
|
-
|
|
343
|
+
lastCompactWasPiVcc = false;
|
|
344
|
+
pendingChainShake.delete(pi);
|
|
231
345
|
};
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
346
|
+
const clearPendingAutoContinueForPi = (pi: any, ctx?: any): void => {
|
|
347
|
+
const state = getPerPi(pi);
|
|
348
|
+
const timer = state ? state.pendingAutoContinueTimer : pendingAutoContinueTimer;
|
|
349
|
+
clearTimerHandle(ctx, timer);
|
|
350
|
+
if (state) {
|
|
351
|
+
state.timers.delete(timer);
|
|
352
|
+
state.pendingAutoContinueTimer = null;
|
|
353
|
+
} else {
|
|
236
354
|
pendingAutoContinueTimer = null;
|
|
237
|
-
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
const scheduleAutoContinueForPi = (pi: any, ctx?: any): void => {
|
|
358
|
+
clearPendingAutoContinueForPi(pi, ctx);
|
|
359
|
+
const state = getPerPi(pi);
|
|
360
|
+
const timer = scheduleManaged(pi, ctx, () => {
|
|
361
|
+
if (state) state.pendingAutoContinueTimer = null;
|
|
238
362
|
try { triggerInvisibleContinue(pi); } catch {}
|
|
239
|
-
}, 0);
|
|
240
|
-
pendingAutoContinueTimer = timer;
|
|
241
|
-
if (s) s.pendingAutoContinueTimer = timer;
|
|
363
|
+
}, 0, "auto-continue");
|
|
364
|
+
if (state) state.pendingAutoContinueTimer = timer;
|
|
242
365
|
};
|
|
243
366
|
// the LLM context with a user-visible continue prompt. triggerInvisibleContinue
|
|
244
367
|
// sends a custom message marked with a dedicated customType (content:[],
|
|
@@ -323,22 +446,19 @@ export const clearCompactionHistoryForTests = () => {
|
|
|
323
446
|
lastStats = null;
|
|
324
447
|
lastCompactWasPiVcc = false;
|
|
325
448
|
pendingFollowUpPrompt = null;
|
|
326
|
-
|
|
449
|
+
clearTimerHandle(undefined, pendingAutoContinueTimer);
|
|
327
450
|
pendingAutoContinueTimer = null;
|
|
328
451
|
for (const pi of perPiKeys) {
|
|
329
|
-
const
|
|
330
|
-
if (
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
452
|
+
const state = perPi.get(pi);
|
|
453
|
+
if (state) {
|
|
454
|
+
for (const timer of state.timers) clearTimerHandle(undefined, timer);
|
|
455
|
+
state.timers.clear();
|
|
456
|
+
state.statsHistory = [];
|
|
457
|
+
state.lastStats = null;
|
|
458
|
+
state.lastCompactWasPiVcc = false;
|
|
459
|
+
state.pendingFollowUpPrompt = null;
|
|
460
|
+
state.pendingAutoContinueTimer = null;
|
|
337
461
|
}
|
|
338
|
-
// Remove strong ref so pi can be GC'd and WeakMap entry cleared; fresh
|
|
339
|
-
// getPerPi(pi) will recreate if this pi is reused, but tests create fresh
|
|
340
|
-
// pi objects each time, so clearing prevents unbounded Set growth across
|
|
341
|
-
// the 377-test suite.
|
|
342
462
|
perPi.delete(pi);
|
|
343
463
|
}
|
|
344
464
|
perPiKeys.clear();
|
|
@@ -400,17 +520,189 @@ const readCompactionEventContext = (event: unknown): { reason?: CompactionReason
|
|
|
400
520
|
: undefined;
|
|
401
521
|
return { reason, willRetry: raw.willRetry === true };
|
|
402
522
|
};
|
|
523
|
+
const resolveGlobalIndex = (ctx: any): Map<string, number> | undefined => {
|
|
524
|
+
try {
|
|
525
|
+
const manager = ctx?.sessionManager;
|
|
526
|
+
if (typeof manager?.getEntries === "function") {
|
|
527
|
+
const entries = manager.getEntries();
|
|
528
|
+
if (Array.isArray(entries)) return buildGlobalIndex(entries as PersistedSessionEntry[]).indexById;
|
|
529
|
+
}
|
|
530
|
+
const sessionFile = typeof manager?.getSessionFile === "function" ? manager.getSessionFile() : undefined;
|
|
531
|
+
if (typeof sessionFile !== "string") return undefined;
|
|
532
|
+
const entries: PersistedSessionEntry[] = [];
|
|
533
|
+
const scan = scanSessionEntries(sessionFile, (entry) => entries.push(entry as PersistedSessionEntry));
|
|
534
|
+
if (scan.missing) return undefined;
|
|
535
|
+
return buildGlobalIndex(entries).indexById;
|
|
536
|
+
} catch {
|
|
537
|
+
return undefined;
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const trustedFullContextTokens = (branchEntries: any[], preparation: any, ctx: any): number | undefined => {
|
|
542
|
+
let boundary = -1;
|
|
543
|
+
for (let i = branchEntries.length - 1; i >= 0; i--) {
|
|
544
|
+
if (branchEntries[i]?.type === "compaction" || branchEntries[i]?.type === "reset_boundary") {
|
|
545
|
+
boundary = i;
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
for (let i = branchEntries.length - 1; i > boundary; i--) {
|
|
550
|
+
const entry = branchEntries[i];
|
|
551
|
+
const message = entry?.type === "message" ? entry.message : undefined;
|
|
552
|
+
if (message?.role !== "assistant" || message.stopReason === "error" || message.stopReason === "aborted") continue;
|
|
553
|
+
const usage = message.usage;
|
|
554
|
+
if (!usage || typeof usage !== "object") continue;
|
|
555
|
+
const expectedModel = ctx?.model?.id;
|
|
556
|
+
const actualModel = typeof message.model === "string" ? message.model : undefined;
|
|
557
|
+
if (expectedModel && actualModel && expectedModel !== actualModel) continue;
|
|
558
|
+
const authoritative = typeof preparation?.tokensBefore === "number" && preparation.tokensBefore > 0
|
|
559
|
+
? preparation.tokensBefore
|
|
560
|
+
: typeof usage.contextTokens === "number" && Number.isFinite(usage.contextTokens) && usage.contextTokens > 0
|
|
561
|
+
? usage.contextTokens
|
|
562
|
+
: undefined;
|
|
563
|
+
if (authoritative === undefined) continue;
|
|
564
|
+
let postAnchor = 0;
|
|
565
|
+
for (let j = i + 1; j < branchEntries.length; j++) {
|
|
566
|
+
const post = branchEntries[j];
|
|
567
|
+
if (post?.type !== "message") continue;
|
|
568
|
+
postAnchor += estimateScriptAwareMessageContentTokens(post.message?.content);
|
|
569
|
+
}
|
|
570
|
+
return Math.max(0, authoritative + postAnchor);
|
|
571
|
+
}
|
|
572
|
+
return undefined;
|
|
573
|
+
};
|
|
574
|
+
const sourceIndicesFor = (selectedIds: Array<string | undefined>, indexById?: Map<string, number>): Array<number | undefined> =>
|
|
575
|
+
selectedIds.map((id) => id && indexById ? indexById.get(id) : undefined);
|
|
576
|
+
|
|
577
|
+
const convertSelectedMessages = (
|
|
578
|
+
selectedMessages: any[],
|
|
579
|
+
selectedIds: Array<string | undefined>,
|
|
580
|
+
sourceIndices: Array<number | undefined>,
|
|
581
|
+
): { messages: any[]; sourceIndices: Array<number | undefined> } => {
|
|
582
|
+
const messages: any[] = [];
|
|
583
|
+
const aligned: Array<number | undefined> = [];
|
|
584
|
+
for (let i = 0; i < selectedMessages.length; i++) {
|
|
585
|
+
const converted = convertToLlm([selectedMessages[i]]);
|
|
586
|
+
for (const message of converted) {
|
|
587
|
+
messages.push(message);
|
|
588
|
+
aligned.push(sourceIndices[i]);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return { messages, sourceIndices: aligned };
|
|
592
|
+
};
|
|
593
|
+
export const __convertSelectedMessagesForTests = convertSelectedMessages;
|
|
594
|
+
const nativeMemoryQuery = (branchEntries: any[]): string => {
|
|
595
|
+
for (let i = branchEntries.length - 1; i >= 0; i--) {
|
|
596
|
+
const entry = branchEntries[i];
|
|
597
|
+
const message = entry?.type === "message" ? entry.message : undefined;
|
|
598
|
+
if (message?.role !== "user") continue;
|
|
599
|
+
let text = "";
|
|
600
|
+
if (typeof message.content === "string") text = message.content;
|
|
601
|
+
else if (Array.isArray(message.content)) {
|
|
602
|
+
text = message.content
|
|
603
|
+
.filter((part: any) => part?.type === "text" && typeof part.text === "string")
|
|
604
|
+
.map((part: any) => part.text)
|
|
605
|
+
.join("\n");
|
|
606
|
+
}
|
|
607
|
+
if (text.trim()) return text.trim().slice(0, 2_000);
|
|
608
|
+
}
|
|
609
|
+
return "";
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
const nativeMemoryBlock = (ctx: any, event: any, branchEntries: any[], settings: PiVccSettings): string | Promise<string> => {
|
|
613
|
+
if (!settings.nativeMemory || !ctx?.memory || typeof ctx.memory.search !== "function") return "";
|
|
614
|
+
if (event?.signal?.aborted) return "";
|
|
615
|
+
const query = nativeMemoryQuery(branchEntries);
|
|
616
|
+
if (!query) return "";
|
|
617
|
+
const format = (result: any): string => {
|
|
618
|
+
const root = result as any;
|
|
619
|
+
const items = Array.isArray(result) ? result : Array.isArray(root?.items) ? root.items : Array.isArray(root?.results) ? root.results : [];
|
|
620
|
+
const seen = new Set<string>();
|
|
621
|
+
const lines: string[] = [];
|
|
622
|
+
for (const raw of items) {
|
|
623
|
+
if (lines.length >= 8) break;
|
|
624
|
+
const item = raw as any;
|
|
625
|
+
const content = typeof item?.content === "string" ? item.content : typeof item?.text === "string" ? item.text : "";
|
|
626
|
+
if (!content) continue;
|
|
627
|
+
const id = typeof item?.id === "string" ? item.id : undefined;
|
|
628
|
+
const source = typeof item?.source === "string" ? item.source : undefined;
|
|
629
|
+
const key = id ?? `${source ?? ""}\u0000${content}`;
|
|
630
|
+
if (seen.has(key)) continue;
|
|
631
|
+
seen.add(key);
|
|
632
|
+
const metadata = [id ? `id=${id}` : undefined, source ? `source=${source}` : undefined].filter(Boolean).join(" ");
|
|
633
|
+
lines.push(`- ${metadata ? `${metadata}: ` : ""}${content.slice(0, 500)}`);
|
|
634
|
+
}
|
|
635
|
+
if (lines.length === 0) return "";
|
|
636
|
+
return `[Host Memory]\n${lines.join("\n")}`.slice(0, 4_000);
|
|
637
|
+
};
|
|
638
|
+
const fail = (error: unknown): string => {
|
|
639
|
+
dbg(settings, { nativeMemory: "error", errorClass: error instanceof Error ? error.name : typeof error });
|
|
640
|
+
logMetrics(settings, { event: "native-memory", status: "error", errorClass: error instanceof Error ? error.name : typeof error });
|
|
641
|
+
return "";
|
|
642
|
+
};
|
|
643
|
+
try {
|
|
644
|
+
const result = ctx.memory.search(query, { limit: 8, signal: event.signal });
|
|
645
|
+
return result && typeof result.then === "function" ? Promise.resolve(result).then(format, fail) : format(result);
|
|
646
|
+
} catch (error) {
|
|
647
|
+
return fail(error);
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
const injectBeforeRecallNote = (summary: string, memoryBlock: string): string => {
|
|
652
|
+
if (!memoryBlock) return summary;
|
|
653
|
+
const marker = summary.lastIndexOf("\n\n---\n\n");
|
|
654
|
+
return marker >= 0 ? `${summary.slice(0, marker)}\n\n${memoryBlock}${summary.slice(marker)}` : `${summary}\n\n${memoryBlock}`;
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
const clipUtf8 = (text: string, maxBytes: number): { text: string; truncated: boolean } => {
|
|
658
|
+
let out = "";
|
|
659
|
+
let bytes = 0;
|
|
660
|
+
for (const character of text) {
|
|
661
|
+
const size = Buffer.byteLength(character, "utf8");
|
|
662
|
+
if (bytes + size > maxBytes) return { text: out, truncated: true };
|
|
663
|
+
out += character;
|
|
664
|
+
bytes += size;
|
|
665
|
+
}
|
|
666
|
+
return { text: out, truncated: false };
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
const capturePreCompactionDisplay = (pi: any, selectedMessages: any[], selectedIds: Array<string | undefined>): void => {
|
|
671
|
+
for (let i = selectedMessages.length - 1; i >= 0; i--) {
|
|
672
|
+
const message = selectedMessages[i];
|
|
673
|
+
if (message?.role !== "assistant") continue;
|
|
674
|
+
let text = "";
|
|
675
|
+
if (typeof message.content === "string") text = message.content;
|
|
676
|
+
else if (Array.isArray(message.content)) {
|
|
677
|
+
text = message.content
|
|
678
|
+
.filter((part: any) => part?.type === "text" && typeof part.text === "string")
|
|
679
|
+
.map((part: any) => part.text)
|
|
680
|
+
.join("\n");
|
|
681
|
+
}
|
|
682
|
+
text = sanitize(text).replace(/\u001B\][^\u0007]*(?:\u0007|\u001B\\)/g, "").replace(/[\u0080-\u009f]/g, "");
|
|
683
|
+
if (!text) continue;
|
|
684
|
+
const clipped = clipUtf8(text, 16 * 1024);
|
|
685
|
+
const state = getPerPi(pi);
|
|
686
|
+
if (state) state.pendingDisplay = { text: clipped.text, sourceEntryId: selectedIds[i], truncated: clipped.truncated };
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
};
|
|
403
690
|
|
|
404
|
-
export
|
|
405
|
-
|
|
691
|
+
export function scheduleCompactionStatsNotify(pi: any, ctx: any, stats: CompactionStats): void;
|
|
692
|
+
export function scheduleCompactionStatsNotify(ctx: any, stats: CompactionStats): void;
|
|
693
|
+
export function scheduleCompactionStatsNotify(piOrCtx: any, ctxOrStats: any, maybeStats?: CompactionStats): void {
|
|
694
|
+
const hasManagedContext = maybeStats !== undefined;
|
|
695
|
+
const pi = hasManagedContext ? piOrCtx : undefined;
|
|
696
|
+
const ctx = hasManagedContext ? ctxOrStats : piOrCtx;
|
|
697
|
+
const stats: CompactionStats = maybeStats ?? ctxOrStats;
|
|
698
|
+
const notify = () => {
|
|
406
699
|
try {
|
|
407
|
-
ctx?.ui?.notify?.(
|
|
408
|
-
formatCompactionStats(stats),
|
|
409
|
-
"info",
|
|
410
|
-
);
|
|
700
|
+
ctx?.ui?.notify?.(formatCompactionStats(stats), "info");
|
|
411
701
|
} catch {}
|
|
412
|
-
}
|
|
413
|
-
|
|
702
|
+
};
|
|
703
|
+
if (hasManagedContext) scheduleManaged(pi, ctx, notify, 500, "stats");
|
|
704
|
+
else setTimeout(notify, 500);
|
|
705
|
+
}
|
|
414
706
|
|
|
415
707
|
const parseCompactionInstructions = (customInstructions?: string): {
|
|
416
708
|
isPiVcc: boolean;
|
|
@@ -455,6 +747,22 @@ const dbg = (settings: PiVccSettings, data: Record<string, unknown>) => {
|
|
|
455
747
|
try { writeFileSync("/tmp/omp-vcc-debug.json", JSON.stringify(data, null, 2)); } catch {}
|
|
456
748
|
try { writeFileSync("/tmp/pi-vcc-debug.json", JSON.stringify(data, null, 2)); } catch {}
|
|
457
749
|
};
|
|
750
|
+
const METRICS_MAX_BYTES = 10 * 1024 * 1024;
|
|
751
|
+
const logMetrics = (settings: PiVccSettings, data: Record<string, unknown>): void => {
|
|
752
|
+
if (!settings.debugLog) return;
|
|
753
|
+
try {
|
|
754
|
+
const path = join(dirname(getSettingsPath()), "debug-metrics.jsonl");
|
|
755
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
756
|
+
const line = `${JSON.stringify({ timestamp: Date.now(), ...data })}\n`;
|
|
757
|
+
let size = 0;
|
|
758
|
+
try { size = statSync(path).size; } catch {}
|
|
759
|
+
if (size > 0 && size + Buffer.byteLength(line, "utf8") > METRICS_MAX_BYTES) {
|
|
760
|
+
try { rmSync(`${path}.1`, { force: true }); } catch {}
|
|
761
|
+
renameSync(path, `${path}.1`);
|
|
762
|
+
}
|
|
763
|
+
appendFileSync(path, line, "utf8");
|
|
764
|
+
} catch {}
|
|
765
|
+
};
|
|
458
766
|
|
|
459
767
|
const previewContent = (content: unknown): string => {
|
|
460
768
|
if (typeof content === "string") return content.slice(0, 300);
|
|
@@ -498,6 +806,9 @@ interface EntryWithMessage {
|
|
|
498
806
|
entry: { id: string; type: string };
|
|
499
807
|
message: { role: string; content: unknown };
|
|
500
808
|
}
|
|
809
|
+
const selectedEntryId = (entry: { id?: unknown }): string | undefined =>
|
|
810
|
+
typeof entry.id === "string" && entry.id.length > 0 ? entry.id : undefined;
|
|
811
|
+
|
|
501
812
|
|
|
502
813
|
// Convert a non-message entry that carries LLM-context text (custom_message /
|
|
503
814
|
// branch_summary) into its agent-message form, mirroring pi-core's
|
|
@@ -541,6 +852,7 @@ export type OwnCutResult =
|
|
|
541
852
|
requestedKeepUserTurns: number;
|
|
542
853
|
keepFallbackToCompactAll: boolean;
|
|
543
854
|
budgetCut?: BudgetCutKind;
|
|
855
|
+
selectedIds: Array<string | undefined>;
|
|
544
856
|
}
|
|
545
857
|
| { ok: false; reason: OwnCutCancelReason };
|
|
546
858
|
|
|
@@ -625,6 +937,7 @@ export function buildOwnCut(branchEntries: any[], keepUserTurns = 1, explicitKee
|
|
|
625
937
|
const compactAll = (keepFallbackToCompactAll: boolean) => ({
|
|
626
938
|
ok: true as const,
|
|
627
939
|
messages: liveMessages.map((e) => e.message),
|
|
940
|
+
selectedIds: liveMessages.map((e) => selectedEntryId(e.entry)),
|
|
628
941
|
firstKeptEntryId: "",
|
|
629
942
|
compactAll: true,
|
|
630
943
|
keptUserTurns: 0,
|
|
@@ -651,6 +964,7 @@ export function buildOwnCut(branchEntries: any[], keepUserTurns = 1, explicitKee
|
|
|
651
964
|
return {
|
|
652
965
|
ok: true,
|
|
653
966
|
messages: liveMessages.slice(0, firstUserIdx).map((e) => e.message),
|
|
967
|
+
selectedIds: liveMessages.slice(0, firstUserIdx).map((e) => selectedEntryId(e.entry)),
|
|
654
968
|
firstKeptEntryId: liveMessages[firstUserIdx].entry.id,
|
|
655
969
|
compactAll: false,
|
|
656
970
|
keptUserTurns: userIndices.length,
|
|
@@ -666,6 +980,7 @@ export function buildOwnCut(branchEntries: any[], keepUserTurns = 1, explicitKee
|
|
|
666
980
|
return {
|
|
667
981
|
ok: true,
|
|
668
982
|
messages: liveMessages.slice(0, cutIdx).map((e) => e.message),
|
|
983
|
+
selectedIds: liveMessages.slice(0, cutIdx).map((e) => selectedEntryId(e.entry)),
|
|
669
984
|
firstKeptEntryId: liveMessages[cutIdx].entry.id,
|
|
670
985
|
compactAll: false,
|
|
671
986
|
keptUserTurns: userIndices.length - targetUserIdx,
|
|
@@ -687,7 +1002,7 @@ export const findBudgetCutIndex = (
|
|
|
687
1002
|
let acc = 0;
|
|
688
1003
|
let crossed = -1;
|
|
689
1004
|
for (let i = live.length - 1; i >= 0; i--) {
|
|
690
|
-
acc +=
|
|
1005
|
+
acc += estimateScriptAwareMessageContentTokens(live[i].message.content);
|
|
691
1006
|
if (acc >= maxTokens) {
|
|
692
1007
|
crossed = i;
|
|
693
1008
|
break;
|
|
@@ -714,6 +1029,7 @@ export const applyTailBudget = (
|
|
|
714
1029
|
const budgetResult = (idx: number, budgetCut: BudgetCutKind): OwnCutResult => ({
|
|
715
1030
|
ok: true,
|
|
716
1031
|
messages: live.slice(0, idx).map((m) => m.message),
|
|
1032
|
+
selectedIds: live.slice(0, idx).map((m) => selectedEntryId(m.entry)),
|
|
717
1033
|
firstKeptEntryId: live[idx].entry.id,
|
|
718
1034
|
compactAll: false,
|
|
719
1035
|
keptUserTurns: live.slice(idx).filter((m) => m.message.role === "user").length,
|
|
@@ -737,7 +1053,7 @@ export const applyTailBudget = (
|
|
|
737
1053
|
const tailStart = cut.messages.length; // equals the cut index in the live window
|
|
738
1054
|
let tailTokens = 0;
|
|
739
1055
|
for (let i = tailStart; i < live.length; i++) {
|
|
740
|
-
tailTokens +=
|
|
1056
|
+
tailTokens += estimateScriptAwareMessageContentTokens(live[i].message.content);
|
|
741
1057
|
}
|
|
742
1058
|
if (tailTokens <= maxTokens * factor) return cut;
|
|
743
1059
|
const idx = findBudgetCutIndex(live, maxTokens, opts.charsPerToken);
|
|
@@ -790,11 +1106,10 @@ const tailTokensForKeep = (branchEntries: any[], keepUserTurns: number, charsPer
|
|
|
790
1106
|
const live = collectLiveMessages(branchEntries);
|
|
791
1107
|
const keptIdx = live.findIndex((e) => e.entry.id === cut.firstKeptEntryId);
|
|
792
1108
|
if (keptIdx < 0) return null;
|
|
793
|
-
|
|
794
|
-
(sum: number, e) => sum +
|
|
1109
|
+
return live.slice(keptIdx).reduce(
|
|
1110
|
+
(sum: number, e) => sum + estimateScriptAwareMessageContentTokens(e.message?.content),
|
|
795
1111
|
0,
|
|
796
1112
|
);
|
|
797
|
-
return estimateTokensFromChars(chars, charsPerToken);
|
|
798
1113
|
};
|
|
799
1114
|
|
|
800
1115
|
/**
|
|
@@ -844,23 +1159,103 @@ const REASON_MESSAGES: Record<OwnCutCancelReason, string> = {
|
|
|
844
1159
|
export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
845
1160
|
// Filter our invisible-continue marker out of the LLM context payload so the
|
|
846
1161
|
// model just continues from the compaction summary (matched by customType ONLY).
|
|
847
|
-
pi.on("context", (event) => {
|
|
848
|
-
|
|
1162
|
+
pi.on("context", (event, ctx) => {
|
|
1163
|
+
let messages = event.messages;
|
|
1164
|
+
const filtered = event.messages.filter((message) => {
|
|
849
1165
|
if (message.role !== "custom") return true;
|
|
850
1166
|
return message.customType !== AUTO_CONTINUE_CUSTOM_TYPE && message.customType !== LEGACY_AUTO_CONTINUE_CUSTOM_TYPE;
|
|
851
1167
|
});
|
|
852
|
-
if (
|
|
1168
|
+
if (filtered.length !== event.messages.length) messages = filtered;
|
|
1169
|
+
let entries: any[] = [];
|
|
1170
|
+
try {
|
|
1171
|
+
const branch = ctx?.sessionManager?.getBranch?.();
|
|
1172
|
+
if (Array.isArray(branch)) entries = branch;
|
|
1173
|
+
else {
|
|
1174
|
+
const value = ctx?.sessionManager?.getEntries?.();
|
|
1175
|
+
if (Array.isArray(value)) entries = value;
|
|
1176
|
+
}
|
|
1177
|
+
} catch {}
|
|
1178
|
+
const latest = [...entries].reverse().find((entry) => entry?.type === "compaction");
|
|
1179
|
+
if (latest && typeof latest.summary === "string") {
|
|
1180
|
+
if (isPiVccAppendDetails(latest.details)) {
|
|
1181
|
+
const chain = collectActiveSegments(entries, { fallbackSummary: latest.summary });
|
|
1182
|
+
if (chain) {
|
|
1183
|
+
const projected = projectAppendOnlyContext({ messages, chain, fallbackSummary: latest.summary });
|
|
1184
|
+
if (projected !== messages) messages = projected;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
const projection: RetainedToolOutputProjection | undefined = latest.details?.retainedToolOutputProjection;
|
|
1188
|
+
if (projection) {
|
|
1189
|
+
const serializedByEntryId: Record<string, string> = {};
|
|
1190
|
+
const omissionToolCallIds: Record<string, string> = {};
|
|
1191
|
+
for (const entry of entries) {
|
|
1192
|
+
if (entry?.type !== "message" || typeof entry.id !== "string") continue;
|
|
1193
|
+
try { serializedByEntryId[entry.id] = JSON.stringify(entry.message); } catch {}
|
|
1194
|
+
if (typeof entry.message?.toolCallId === "string") omissionToolCallIds[entry.id] = entry.message.toolCallId;
|
|
1195
|
+
}
|
|
1196
|
+
const projected = applyRetainedToolOutputProjection(messages, projection, { serializedByEntryId, omissionToolCallIds });
|
|
1197
|
+
if (projected !== messages) messages = projected;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
if (messages !== event.messages) return { messages };
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
for (const eventName of ["session_start", "session_switch", "session_branch", "session_shutdown"]) {
|
|
1204
|
+
pi.on(eventName, (_event, ctx) => advanceSessionGeneration(pi, ctx));
|
|
1205
|
+
}
|
|
1206
|
+
pi.on("auto_compaction_start", (event, ctx) => {
|
|
1207
|
+
const state = getPerPi(pi);
|
|
1208
|
+
if (!state) return;
|
|
1209
|
+
state.autoCompaction = {
|
|
1210
|
+
generation: state.generation,
|
|
1211
|
+
sessionId: state.sessionId ?? sessionIdOf(ctx),
|
|
1212
|
+
reason: typeof (event as any)?.reason === "string" ? (event as any).reason : "unknown",
|
|
1213
|
+
action: typeof (event as any)?.action === "string" ? (event as any).action : "unknown",
|
|
1214
|
+
willRetry: false,
|
|
1215
|
+
};
|
|
1216
|
+
});
|
|
1217
|
+
pi.on("auto_compaction_end", (event, ctx) => {
|
|
1218
|
+
const state = getPerPi(pi);
|
|
1219
|
+
if (!state) return;
|
|
1220
|
+
const auto = state.autoCompaction;
|
|
1221
|
+
state.autoCompaction = undefined;
|
|
1222
|
+
logMetrics(loadSettings(ctx), {
|
|
1223
|
+
event: "auto-compaction-end",
|
|
1224
|
+
action: (event as any)?.action,
|
|
1225
|
+
aborted: (event as any)?.aborted === true,
|
|
1226
|
+
willRetry: (event as any)?.willRetry === true,
|
|
1227
|
+
generation: auto?.generation,
|
|
1228
|
+
});
|
|
853
1229
|
});
|
|
854
1230
|
|
|
855
|
-
pi.on("before_agent_start", () => {
|
|
856
|
-
clearPendingAutoContinueForPi(pi);
|
|
1231
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
1232
|
+
clearPendingAutoContinueForPi(pi, ctx);
|
|
857
1233
|
});
|
|
858
1234
|
|
|
859
1235
|
pi.on("session_before_compact", (event, ctx) => {
|
|
860
|
-
const
|
|
861
|
-
const
|
|
862
|
-
const
|
|
863
|
-
|
|
1236
|
+
const attemptState = getPerPi(pi);
|
|
1237
|
+
const attemptGeneration = attemptState?.generation ?? 0;
|
|
1238
|
+
const attemptSessionId = sessionIdOf(ctx);
|
|
1239
|
+
const attemptCurrent = (): boolean => !event?.signal?.aborted && isCurrentGeneration(pi, ctx, attemptGeneration, attemptSessionId);
|
|
1240
|
+
const settingsResult = loadSettingsWithPluginOverlay(ctx);
|
|
1241
|
+
const runBefore = (settings: PiVccSettings) => {
|
|
1242
|
+
if (!attemptCurrent()) return;
|
|
1243
|
+
if (attemptState) {
|
|
1244
|
+
attemptState.pendingCompactionFingerprint = undefined;
|
|
1245
|
+
attemptState.pendingPreviousStats = attemptState.lastStats;
|
|
1246
|
+
attemptState.pendingStatsHistoryLength = attemptState.statsHistory.length;
|
|
1247
|
+
}
|
|
1248
|
+
if (attemptState) {
|
|
1249
|
+
attemptState.pendingDisplay = undefined;
|
|
1250
|
+
attemptState.lastSettings = settings;
|
|
1251
|
+
}
|
|
1252
|
+
const { preparation, branchEntries, customInstructions } = event;
|
|
1253
|
+
const eventContext = readCompactionEventContext(event);
|
|
1254
|
+
const auto = attemptState?.autoCompaction;
|
|
1255
|
+
const autoReason = auto?.reason === "threshold" || auto?.reason === "overflow" || auto?.reason === "manual" ? auto.reason : undefined;
|
|
1256
|
+
const reason = eventContext.reason ?? autoReason;
|
|
1257
|
+
const willRetry = eventContext.willRetry || auto?.willRetry === true;
|
|
1258
|
+
if (!settings.vccEnabled) return;
|
|
864
1259
|
|
|
865
1260
|
// Always handle explicit /pi-vcc or /omp-vcc marker.
|
|
866
1261
|
// Otherwise, only handle when user opted in via settings.
|
|
@@ -886,6 +1281,9 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
886
1281
|
// still handled (isPiVcc path falls through below).
|
|
887
1282
|
if (!isPiVcc && pendingChainShake.has(pi as unknown as object)) return;
|
|
888
1283
|
if (!isPiVcc && !settings.overrideDefaultCompaction) return;
|
|
1284
|
+
const memoryResult = nativeMemoryBlock(ctx, event, branchEntries as any[], settings);
|
|
1285
|
+
function runBody(memoryBlock: string) {
|
|
1286
|
+
if (!attemptCurrent()) return;
|
|
889
1287
|
|
|
890
1288
|
const calibrationCut = buildOwnCut(branchEntries as any[], 0);
|
|
891
1289
|
const calibrationMessageChars = calibrationCut.ok
|
|
@@ -1036,18 +1434,21 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
1036
1434
|
setPendingFollowUpPrompt(pi, followUpPrompt);
|
|
1037
1435
|
const agentMessages = ownCut.messages;
|
|
1038
1436
|
const firstKeptEntryId = ownCut.firstKeptEntryId;
|
|
1039
|
-
const
|
|
1437
|
+
const globalIndexById = resolveGlobalIndex(ctx);
|
|
1438
|
+
const selectedSourceIndices = sourceIndicesFor(ownCut.selectedIds, globalIndexById);
|
|
1439
|
+
const converted = convertSelectedMessages(agentMessages, ownCut.selectedIds, selectedSourceIndices);
|
|
1440
|
+
const messages = converted.messages;
|
|
1441
|
+
const sourceIndices = converted.sourceIndices;
|
|
1040
1442
|
|
|
1041
|
-
// Count kept messages and estimate tokens
|
|
1443
|
+
// Count kept messages and estimate tokens with the script-aware estimator.
|
|
1042
1444
|
const keptIdx = (branchEntries as any[]).findIndex((e: any) => e.id === firstKeptEntryId);
|
|
1043
1445
|
const keptEntries = keptIdx >= 0
|
|
1044
1446
|
? (branchEntries as any[]).slice(keptIdx).filter((e: any) => e.type === "message")
|
|
1045
1447
|
: [];
|
|
1046
|
-
const
|
|
1047
|
-
(sum: number,
|
|
1448
|
+
const keptTokensEst = keptEntries.reduce(
|
|
1449
|
+
(sum: number, entry: any) => sum + estimateScriptAwareMessageContentTokens(entry.message?.content),
|
|
1048
1450
|
0,
|
|
1049
1451
|
);
|
|
1050
|
-
const keptTokensEst = estimateTokensFromChars(keptChars, tokenEstimate.charsPerToken);
|
|
1051
1452
|
const config = settings;
|
|
1052
1453
|
|
|
1053
1454
|
// Ranked compaction: keep the highest-signal blocks under a token budget
|
|
@@ -1069,8 +1470,9 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
1069
1470
|
const RANKED_BRIEF_BUDGET_TOKENS = 1100;
|
|
1070
1471
|
const RANKED_BRIEF_CEILING_TOKENS = 2000;
|
|
1071
1472
|
const RANKED_BRIEF_TOKENS_PER_BLOCK = 15;
|
|
1072
|
-
|
|
1473
|
+
let summary = compileRanked({
|
|
1073
1474
|
messages,
|
|
1475
|
+
sourceIndices,
|
|
1074
1476
|
previousSummary: preparation.previousSummary,
|
|
1075
1477
|
fileOps: {
|
|
1076
1478
|
readFiles: [...preparation.fileOps.read],
|
|
@@ -1082,6 +1484,7 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
1082
1484
|
briefCharsPerBlock: Math.round(RANKED_BRIEF_TOKENS_PER_BLOCK * tokenEstimate.charsPerToken),
|
|
1083
1485
|
},
|
|
1084
1486
|
});
|
|
1487
|
+
summary = injectBeforeRecallNote(summary, memoryBlock);
|
|
1085
1488
|
|
|
1086
1489
|
// Keep-all cut with an empty prefix and no previous summary yields nothing
|
|
1087
1490
|
// new to summarize. Never hand the host an empty summary — cancel and keep
|
|
@@ -1117,8 +1520,11 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
1117
1520
|
const guard = evaluateGrowthGuard(prefixChars, netNewSummaryChars);
|
|
1118
1521
|
const { netGrowthChars, toleranceChars } = guard;
|
|
1119
1522
|
if (guard.trip) {
|
|
1120
|
-
const prefixTok =
|
|
1121
|
-
|
|
1523
|
+
const prefixTok = agentMessages.reduce(
|
|
1524
|
+
(sum: number, message: any) => sum + estimateScriptAwareMessageContentTokens(message.content),
|
|
1525
|
+
0,
|
|
1526
|
+
);
|
|
1527
|
+
const netNewTok = estimateScriptAwareTokens(String(Math.max(0, netNewSummaryChars)));
|
|
1122
1528
|
dbg(settings, {
|
|
1123
1529
|
growthGuard: true,
|
|
1124
1530
|
cancelled: reason !== "overflow" && !willRetry,
|
|
@@ -1141,7 +1547,7 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
1141
1547
|
}
|
|
1142
1548
|
|
|
1143
1549
|
const tokensBefore = typeof preparation.tokensBefore === "number" ? preparation.tokensBefore : 0;
|
|
1144
|
-
const summaryTokensEst =
|
|
1550
|
+
const summaryTokensEst = estimateScriptAwareTokens(summary);
|
|
1145
1551
|
const tokensAfterEst = summaryTokensEst + keptTokensEst;
|
|
1146
1552
|
const tokensSavedEst = tokensBefore > 0 ? Math.max(0, tokensBefore - tokensAfterEst) : 0;
|
|
1147
1553
|
const savedPercentEst = tokensBefore > 0 && tokensSavedEst > 0 ? Math.round((tokensSavedEst / tokensBefore) * 100) : 0;
|
|
@@ -1178,6 +1584,8 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
1178
1584
|
preview: e.type === "message" ? previewContent(e.message?.content) : undefined,
|
|
1179
1585
|
}))
|
|
1180
1586
|
: [];
|
|
1587
|
+
const retainedCandidates = collectLiveMessages(branchEntries as any[]).map(({ entry, message }) => ({ id: entry.id, type: entry.type, message }));
|
|
1588
|
+
const retainedProjection = buildRetainedToolOutputProjection(retainedCandidates, settings.retainedToolOutputMaxTokens, globalIndexById);
|
|
1181
1589
|
|
|
1182
1590
|
const KNOWN_SECTIONS = new Set(["Session Goal", "Files And Changes", "Commits", "Outstanding Context", "User Preferences"]);
|
|
1183
1591
|
const extractKnownSections = (text: string) =>
|
|
@@ -1208,13 +1616,100 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
1208
1616
|
savedPercentEst,
|
|
1209
1617
|
},
|
|
1210
1618
|
});
|
|
1619
|
+
const appendMode = settings.compactionSummaryMode === "append";
|
|
1620
|
+
const latestCompaction = [...branchEntries].reverse().find((entry: any) => entry?.type === "compaction");
|
|
1621
|
+
const hasPriorCompaction = latestCompaction !== undefined;
|
|
1622
|
+
const previousChain = appendMode && hasPriorCompaction && typeof preparation.previousSummary === "string"
|
|
1623
|
+
? collectActiveSegments(branchEntries, { fallbackSummary: preparation.previousSummary })
|
|
1624
|
+
: null;
|
|
1625
|
+
const legacyRewriteBase = isPiVcc && hasPriorCompaction
|
|
1626
|
+
&& (latestCompaction?.details?.compactor === "omp-vcc" || latestCompaction?.details?.compactor === "pi-vcc")
|
|
1627
|
+
&& latestCompaction?.details?.version === 2
|
|
1628
|
+
&& typeof preparation.previousSummary === "string"
|
|
1629
|
+
&& latestCompaction.summary === preparation.previousSummary;
|
|
1630
|
+
const appendEligible = appendMode && (!hasPriorCompaction || previousChain !== null || legacyRewriteBase);
|
|
1631
|
+
const freshSummary = appendEligible
|
|
1632
|
+
? compileSegment({
|
|
1633
|
+
messages,
|
|
1634
|
+
sourceIndices,
|
|
1635
|
+
fileOps: {
|
|
1636
|
+
readFiles: [...preparation.fileOps.read],
|
|
1637
|
+
modifiedFiles: [...preparation.fileOps.written, ...preparation.fileOps.edited],
|
|
1638
|
+
},
|
|
1639
|
+
})
|
|
1640
|
+
: "";
|
|
1641
|
+
const appendCoverage = appendEligible
|
|
1642
|
+
? coverageForMessages({ selectedIds: ownCut.selectedIds, firstKeptEntryId, sourceMessageCount: agentMessages.length })
|
|
1643
|
+
: null;
|
|
1644
|
+
const contextWindow = typeof ctx?.model?.contextWindow === "number" && Number.isFinite(ctx.model.contextWindow) && ctx.model.contextWindow > 0
|
|
1645
|
+
? ctx.model.contextWindow
|
|
1646
|
+
: undefined;
|
|
1647
|
+
const reserveTokens = typeof preparation.settings?.reserveTokens === "number" ? preparation.settings.reserveTokens : undefined;
|
|
1648
|
+
const chainTokens = (previousChain ? estimateChainTokens(previousChain) : 0) + estimateScriptAwareTokens(freshSummary);
|
|
1649
|
+
const rebaseChainTokens = estimateScriptAwareTokens(summary);
|
|
1650
|
+
const thresholds = compactionThresholds(contextWindow, reserveTokens);
|
|
1651
|
+
const fullContextTokens = trustedFullContextTokens(branchEntries, preparation, ctx);
|
|
1652
|
+
const pressure = chainTokens >= thresholds.chainThreshold
|
|
1653
|
+
|| (thresholds.contextThreshold !== undefined && fullContextTokens !== undefined && fullContextTokens >= thresholds.contextThreshold)
|
|
1654
|
+
|| (thresholds.capacity !== undefined && fullContextTokens !== undefined && fullContextTokens > thresholds.capacity);
|
|
1655
|
+
const decision = decideAppendMode({
|
|
1656
|
+
manual: isPiVcc,
|
|
1657
|
+
overflow: reason === "overflow",
|
|
1658
|
+
willRetry,
|
|
1659
|
+
pressure,
|
|
1660
|
+
chainTokens,
|
|
1661
|
+
rebaseChainTokens,
|
|
1662
|
+
contextWindow,
|
|
1663
|
+
reserveTokens,
|
|
1664
|
+
fullContextTokens,
|
|
1665
|
+
});
|
|
1666
|
+
const appendDetails = appendEligible && appendCoverage && freshSummary
|
|
1667
|
+
? buildAppendOnlyDetails({
|
|
1668
|
+
segment: { summary: freshSummary, coverage: appendCoverage, tokensBefore },
|
|
1669
|
+
chainStart: !previousChain || decision.mode === "rebase",
|
|
1670
|
+
trailingSummary: summary,
|
|
1671
|
+
sections: extractKnownSections(summary),
|
|
1672
|
+
sourceMessageCount: agentMessages.length,
|
|
1673
|
+
previousSummaryUsed: Boolean(previousChain) || legacyRewriteBase,
|
|
1674
|
+
previous: decision.mode === "rebase" ? null : previousChain,
|
|
1675
|
+
retainedToolOutputProjection: retainedProjection,
|
|
1676
|
+
})
|
|
1677
|
+
: null;
|
|
1678
|
+
if (appendDetails) {
|
|
1679
|
+
Object.assign(appendDetails, {
|
|
1680
|
+
reason,
|
|
1681
|
+
willRetry,
|
|
1682
|
+
savings: {
|
|
1683
|
+
tokensBefore,
|
|
1684
|
+
summaryChars,
|
|
1685
|
+
summaryTokensEst,
|
|
1686
|
+
keptTokensEst,
|
|
1687
|
+
tokensAfterEst,
|
|
1688
|
+
tokensSavedEst,
|
|
1689
|
+
savedPercentEst,
|
|
1690
|
+
},
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
logMetrics(settings, {
|
|
1694
|
+
event: "append-decision",
|
|
1695
|
+
mode: decision.mode,
|
|
1696
|
+
chainStart: !previousChain || decision.mode === "rebase",
|
|
1697
|
+
pressure,
|
|
1698
|
+
chainTokens,
|
|
1699
|
+
rebaseChainTokens,
|
|
1700
|
+
retainedTokens: retainedProjection?.retainedTokens ?? 0,
|
|
1701
|
+
omittedTokens: retainedProjection?.omittedTokens ?? 0,
|
|
1702
|
+
pendingCount: retainedProjection?.pendingCount ?? 0,
|
|
1703
|
+
});
|
|
1211
1704
|
|
|
1212
|
-
|
|
1705
|
+
|
|
1706
|
+
const details = appendDetails ?? {
|
|
1213
1707
|
compactor: "omp-vcc",
|
|
1214
1708
|
version: 2,
|
|
1215
1709
|
sections: extractKnownSections(summary),
|
|
1216
1710
|
sourceMessageCount: agentMessages.length,
|
|
1217
1711
|
previousSummaryUsed: Boolean(preparation.previousSummary),
|
|
1712
|
+
retainedToolOutputProjection: retainedProjection,
|
|
1218
1713
|
reason,
|
|
1219
1714
|
willRetry,
|
|
1220
1715
|
savings: {
|
|
@@ -1227,34 +1722,79 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
1227
1722
|
savedPercentEst,
|
|
1228
1723
|
},
|
|
1229
1724
|
};
|
|
1725
|
+
capturePreCompactionDisplay(pi, agentMessages, ownCut.selectedIds);
|
|
1230
1726
|
|
|
1231
1727
|
setLastCompactWasPiVcc(pi, isPiVcc);
|
|
1232
1728
|
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
firstKeptEntryId,
|
|
1239
|
-
},
|
|
1729
|
+
const compaction = {
|
|
1730
|
+
summary,
|
|
1731
|
+
details,
|
|
1732
|
+
tokensBefore: preparation.tokensBefore,
|
|
1733
|
+
firstKeptEntryId,
|
|
1240
1734
|
};
|
|
1735
|
+
if (attemptState) {
|
|
1736
|
+
attemptState.pendingCompactionFingerprint = JSON.stringify({
|
|
1737
|
+
summary: compaction.summary,
|
|
1738
|
+
firstKeptEntryId: compaction.firstKeptEntryId,
|
|
1739
|
+
details: compaction.details,
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
return { compaction };
|
|
1743
|
+
};
|
|
1744
|
+
if (typeof memoryResult !== "string") return memoryResult.then((memoryBlock) => {
|
|
1745
|
+
if (!attemptCurrent()) return;
|
|
1746
|
+
return runBody(memoryBlock);
|
|
1747
|
+
});
|
|
1748
|
+
return runBody(memoryResult);
|
|
1749
|
+
};
|
|
1750
|
+
if (settingsResult && typeof (settingsResult as any).then === "function") {
|
|
1751
|
+
return settingsResult.then((settings) => {
|
|
1752
|
+
if (!attemptCurrent()) return;
|
|
1753
|
+
return runBefore(settings);
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
return runBefore(settingsResult as PiVccSettings);
|
|
1241
1757
|
});
|
|
1242
1758
|
pi.on("session_compact", async (event, ctx) => {
|
|
1243
|
-
const
|
|
1244
|
-
|
|
1759
|
+
const per = getPerPi(pi);
|
|
1760
|
+
const generation = per?.generation ?? 0;
|
|
1761
|
+
const sessionId = per?.sessionId ?? sessionIdOf(ctx);
|
|
1762
|
+
const isCurrent = () => isCurrentGeneration(pi, ctx, generation, sessionId);
|
|
1763
|
+
const settings = await loadSettingsWithPluginOverlay(ctx);
|
|
1764
|
+
if (!isCurrent()) return;
|
|
1765
|
+
const entry: any = (event as any).compactionEntry;
|
|
1766
|
+
const committedFingerprint = entry
|
|
1767
|
+
? JSON.stringify({ summary: entry.summary, firstKeptEntryId: entry.firstKeptEntryId, details: entry.details })
|
|
1768
|
+
: undefined;
|
|
1769
|
+
const pendingFingerprint = per?.pendingCompactionFingerprint;
|
|
1770
|
+
const legacyCompletionShape = !entry || (entry.summary === undefined && entry.details === undefined);
|
|
1771
|
+
const ownsCompaction = event.fromExtension === true
|
|
1772
|
+
&& (!pendingFingerprint || committedFingerprint === pendingFingerprint || legacyCompletionShape);
|
|
1773
|
+
const pendingDisplay = per?.pendingDisplay;
|
|
1245
1774
|
const followUpPrompt = getPendingFollowUpPrompt(pi);
|
|
1775
|
+
if (per) {
|
|
1776
|
+
if (!ownsCompaction && pendingFingerprint && per.pendingStatsHistoryLength !== undefined) {
|
|
1777
|
+
per.statsHistory.length = per.pendingStatsHistoryLength;
|
|
1778
|
+
per.lastStats = per.pendingPreviousStats;
|
|
1779
|
+
}
|
|
1780
|
+
per.pendingDisplay = undefined;
|
|
1781
|
+
per.pendingCompactionFingerprint = undefined;
|
|
1782
|
+
per.pendingPreviousStats = undefined;
|
|
1783
|
+
per.pendingStatsHistoryLength = undefined;
|
|
1784
|
+
}
|
|
1246
1785
|
setPendingFollowUpPrompt(pi, null);
|
|
1247
|
-
|
|
1786
|
+
if (!ownsCompaction) return;
|
|
1787
|
+
if (pendingDisplay && settings.showPreCompactionMessage) {
|
|
1788
|
+
try { ctx?.ui?.notify?.(`[Previous output — display only]\n${pendingDisplay.text}`, "info"); } catch {}
|
|
1789
|
+
}
|
|
1248
1790
|
const stats = per ? per.lastStats : lastStats;
|
|
1249
1791
|
if (!stats) return;
|
|
1250
|
-
// Enrich with authoritative tokensAfter from host if available (even for pi-vcc manual, before early return)
|
|
1251
|
-
const entry: any = (event as any).compactionEntry;
|
|
1252
1792
|
if (entry && typeof entry.tokensAfter === "number" && typeof entry.tokensBefore === "number") {
|
|
1253
1793
|
const before = entry.tokensBefore;
|
|
1254
1794
|
const after = entry.tokensAfter;
|
|
1255
1795
|
const saved = Math.max(0, before - after);
|
|
1256
1796
|
const percent = before > 0 && saved > 0 ? Math.round((saved / before) * 100) : 0;
|
|
1257
|
-
if (per
|
|
1797
|
+
if (per?.lastStats) {
|
|
1258
1798
|
per.lastStats.tokensAfter = after;
|
|
1259
1799
|
per.lastStats.tokensSaved = saved;
|
|
1260
1800
|
per.lastStats.savedPercent = percent;
|
|
@@ -1271,9 +1811,8 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
1271
1811
|
(stats as any).savedPercent = percent;
|
|
1272
1812
|
(stats as any).tokensBefore = before;
|
|
1273
1813
|
try {
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
dbg(cfg, {
|
|
1814
|
+
if (settings.debug) {
|
|
1815
|
+
dbg(settings, {
|
|
1277
1816
|
authoritativeSavings: { tokensBefore: before, tokensAfter: after, tokensSaved: saved, savedPercent: percent },
|
|
1278
1817
|
eventEntry: { id: entry.id, tokensBefore: entry.tokensBefore, tokensAfter: entry.tokensAfter },
|
|
1279
1818
|
});
|
|
@@ -1281,52 +1820,54 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
1281
1820
|
} catch {}
|
|
1282
1821
|
}
|
|
1283
1822
|
const isPiVccLast = per ? per.lastCompactWasPiVcc : lastCompactWasPiVcc;
|
|
1284
|
-
if (isPiVccLast)
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1823
|
+
if (isPiVccLast) {
|
|
1824
|
+
if (per) per.lastCompactWasPiVcc = false;
|
|
1825
|
+
else lastCompactWasPiVcc = false;
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
const auto = per?.autoCompaction;
|
|
1829
|
+
const hostOwnsContinuation = auto?.generation === generation && (auto.sessionId ?? sessionIdOf(ctx)) === sessionId;
|
|
1830
|
+
const eventContext = readCompactionEventContext(event);
|
|
1831
|
+
const autoReason = auto?.reason === "threshold" || auto?.reason === "overflow" ? auto.reason : undefined;
|
|
1832
|
+
const reason = eventContext.reason ?? autoReason;
|
|
1833
|
+
const willRetry = eventContext.willRetry || auto?.willRetry === true;
|
|
1290
1834
|
const isLargeCompaction = (stats.summarized > 10) || (stats.kept > 5) || (stats.keptTokensEst > 2000);
|
|
1291
|
-
const shouldContinueAfterAutoCompact =
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
// (the call would trigger a spurious default compaction), so pi never
|
|
1298
|
-
// chains — host rescue remains the only shake path there.
|
|
1835
|
+
const shouldContinueAfterAutoCompact = !hostOwnsContinuation
|
|
1836
|
+
&& (reason === "threshold" || reason === "overflow" || (reason == null && isLargeCompaction))
|
|
1837
|
+
&& settings.continueAfterThresholdCompact;
|
|
1838
|
+
if (willRetry) return;
|
|
1839
|
+
scheduleCompactionStatsNotify(pi, ctx, stats);
|
|
1840
|
+
if (hostOwnsContinuation) return;
|
|
1299
1841
|
try {
|
|
1300
|
-
const cfgChain = loadSettings(ctx);
|
|
1301
1842
|
const ctxMaybe = ctx as unknown as Record<string, unknown>;
|
|
1302
1843
|
const compactFn = ctxMaybe["compact"];
|
|
1303
1844
|
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
1845
|
const chainForm = getCompactForm(() => promptOf?.call(ctx));
|
|
1307
|
-
if (
|
|
1846
|
+
if (settings.chainShakeHint && chainForm === "string" && typeof compactFn === "function" && !pendingChainShake.has(pi as unknown as object) && !willRetry) {
|
|
1308
1847
|
pendingChainShake.add(pi as unknown as object);
|
|
1309
|
-
const
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1848
|
+
const startShake = () => {
|
|
1849
|
+
try {
|
|
1850
|
+
const maybePromise = (compactFn as unknown as (o: unknown) => Promise<void>).call(ctx, { mode: "shake" } as unknown);
|
|
1851
|
+
const asPromise = maybePromise as unknown as Promise<void> | void;
|
|
1852
|
+
if (asPromise && typeof (asPromise as unknown as Promise<void>).catch === "function") {
|
|
1853
|
+
(asPromise as unknown as Promise<void>).catch(() => { pendingChainShake.delete(pi as unknown as object); });
|
|
1854
|
+
}
|
|
1855
|
+
} catch {
|
|
1856
|
+
pendingChainShake.delete(pi as unknown as object);
|
|
1857
|
+
}
|
|
1858
|
+
};
|
|
1859
|
+
if (typeof ctx?.setTimeout === "function") scheduleManaged(pi, ctx, startShake, 5, "chain-shake-start");
|
|
1860
|
+
else startShake();
|
|
1861
|
+
scheduleManaged(pi, ctx, () => { try { pendingChainShake.delete(pi as unknown as object); } catch {} }, 2000, "chain-shake-cleanup");
|
|
1318
1862
|
}
|
|
1319
1863
|
} catch {}
|
|
1320
1864
|
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.
|
|
1324
1865
|
try {
|
|
1325
1866
|
const sent = (pi as any).sendUserMessage?.(followUpPrompt) as Promise<void> | undefined;
|
|
1326
1867
|
if (sent && typeof sent.catch === "function") sent.catch(() => {});
|
|
1327
1868
|
} catch {}
|
|
1328
1869
|
} else if (shouldContinueAfterAutoCompact) {
|
|
1329
|
-
scheduleAutoContinueForPi(pi);
|
|
1870
|
+
scheduleAutoContinueForPi(pi, ctx);
|
|
1330
1871
|
}
|
|
1331
1872
|
});
|
|
1332
1873
|
};
|
|
@@ -1405,16 +1946,18 @@ export const formatVccConfigCard = (view: VccConfigView): string => {
|
|
|
1405
1946
|
: view.readPath === view.path
|
|
1406
1947
|
? `Source: file ${view.readPath}`
|
|
1407
1948
|
: `Source: fallback file ${view.readPath}`;
|
|
1408
|
-
const lines = (Object.keys(DEFAULT_SETTINGS) as (keyof PiVccSettings)[]).map(
|
|
1409
|
-
|
|
1410
|
-
|
|
1949
|
+
const lines = (Object.keys(DEFAULT_SETTINGS) as (keyof PiVccSettings)[]).map((key) => {
|
|
1950
|
+
const value = view.values[key];
|
|
1951
|
+
const display = typeof value === "number" ? String(value) : typeof value === "string" ? value : value ? "on" : "off";
|
|
1952
|
+
return `- ${key}: ${display} (${view.sources[key] === "overlay" ? "host overlay" : view.sources[key]})`;
|
|
1953
|
+
});
|
|
1411
1954
|
return [header, status, ...lines].join("\n");
|
|
1412
1955
|
};
|
|
1413
1956
|
|
|
1414
1957
|
export const registerVccConfigCommand = (pi: any) => {
|
|
1415
1958
|
const handler = async (_args: string, ctx: any) => {
|
|
1416
1959
|
// args deliberately ignored — always show the effective config
|
|
1417
|
-
const view =
|
|
1960
|
+
const view = await loadSettingsWithSourcesAsync(ctx);
|
|
1418
1961
|
const output = formatVccConfigCard(view);
|
|
1419
1962
|
const piAny = pi as unknown as { sendMessage?: (msg: unknown, opts?: unknown) => void };
|
|
1420
1963
|
try { piAny.sendMessage?.({ customType: "vcc-config", content: output, display: true }, { triggerTurn: false }); } catch {}
|