omp-vcc 0.1.12 → 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 +174 -81
- 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 +782 -140
- package/package.json +77 -1
- package/scripts/smoke.ts +8 -0
- package/types.d.ts +10 -2
|
@@ -1,28 +1,126 @@
|
|
|
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
|
|
|
12
|
-
// convertToLlm shim:
|
|
31
|
+
// convertToLlm shim: resolve the host export, fallback to identity (identity
|
|
32
|
+
// is fine for bashExecution/custom, which the pipeline renders natively, but
|
|
33
|
+
// it leaks !!-excluded spans and drops branchSummary entries — so the
|
|
34
|
+
// @earendil-works root (pi's canonical export path) is tried first.
|
|
35
|
+
const CONVERT_TO_LLM_CANDIDATES = [
|
|
36
|
+
"@earendil-works/pi-coding-agent",
|
|
37
|
+
"@oh-my-pi/pi-coding-agent",
|
|
38
|
+
"@oh-my-pi/pi-coding-agent/session/messages",
|
|
39
|
+
] as const;
|
|
40
|
+
// Pure loader-driven resolver: first candidate whose module exports a
|
|
41
|
+
// convertToLlm function wins, else null (caller keeps identity).
|
|
42
|
+
export const resolveConvertToLlm = (
|
|
43
|
+
load: (id: string) => any,
|
|
44
|
+
): ((messages: any[]) => any[]) | null => {
|
|
45
|
+
for (const id of CONVERT_TO_LLM_CANDIDATES) {
|
|
46
|
+
try {
|
|
47
|
+
const mod = load(id);
|
|
48
|
+
if (mod && typeof mod.convertToLlm === "function") return mod.convertToLlm;
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
};
|
|
13
53
|
let convertToLlm: (messages: any[]) => any[] = (m) => m;
|
|
14
54
|
try {
|
|
15
55
|
const req = createRequire(import.meta.url);
|
|
16
|
-
|
|
17
|
-
if (mod?.convertToLlm) convertToLlm = mod.convertToLlm;
|
|
56
|
+
convertToLlm = resolveConvertToLlm((id) => req(id)) ?? convertToLlm;
|
|
18
57
|
} catch {}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
58
|
+
// Test-only override for the module-level binding (mirrors
|
|
59
|
+
// clearCompactionHistoryForTests): lets suites pin convertToLlm wiring without
|
|
60
|
+
// stubbing node module resolution. Null resets to the identity fallback.
|
|
61
|
+
export const __setConvertToLlmForTests = (fn: ((messages: Array<unknown>) => Array<unknown>) | null): void => {
|
|
62
|
+
convertToLlm = fn ?? ((m) => m);
|
|
63
|
+
};
|
|
64
|
+
// Host-kind detection: omp and pi expose incompatible ctx.compact shapes
|
|
65
|
+
// (omp: (string|CompactOptions)=>Promise<void> with instructions on the
|
|
66
|
+
// string; pi: (CompactOptions)=>void with instructions only via
|
|
67
|
+
// options.customInstructions). Three layers, first hit wins:
|
|
68
|
+
// 1. Explicit test override (__setHostKindForTests).
|
|
69
|
+
// 2. Observable ctx shape — works in bundled runtimes where module
|
|
70
|
+
// resolution misses: pi's getSystemPrompt() returns a string, omp's
|
|
71
|
+
// returns string[]. Pure getters, safe to call.
|
|
72
|
+
// 3. Module scope — works in dev/source runtimes: @earendil-works is
|
|
73
|
+
// pi-exclusive (same mechanism as the convertToLlm shim above).
|
|
74
|
+
// Default "omp" preserves the legacy string-form call when host-free.
|
|
75
|
+
const HOST_KIND_CANDIDATES = ["@earendil-works/pi-coding-agent", "@oh-my-pi/pi-coding-agent"] as const;
|
|
76
|
+
export type VccHostKind = "pi" | "omp";
|
|
77
|
+
export type VccCompactForm = "object" | "string";
|
|
78
|
+
// Pure loader-driven resolver: first resolvable scope wins (@earendil-works
|
|
79
|
+
// first, mirroring CONVERT_TO_LLM_CANDIDATES), else "omp".
|
|
80
|
+
export const resolveHostKind = (load: (id: string) => unknown): VccHostKind => {
|
|
81
|
+
for (const id of HOST_KIND_CANDIDATES) {
|
|
82
|
+
try {
|
|
83
|
+
if (load(id)) return id.startsWith("@earendil-works") ? "pi" : "omp";
|
|
84
|
+
} catch {}
|
|
24
85
|
}
|
|
86
|
+
return "omp";
|
|
87
|
+
};
|
|
88
|
+
let defaultHostKind: VccHostKind = "omp";
|
|
89
|
+
try {
|
|
90
|
+
defaultHostKind = resolveHostKind((id) => createRequire(import.meta.url)(id));
|
|
25
91
|
} catch {}
|
|
92
|
+
let hostKindOverride: VccHostKind | null = null;
|
|
93
|
+
export const getHostKind = (): VccHostKind => hostKindOverride ?? defaultHostKind;
|
|
94
|
+
// Test-only override (mirrors __setConvertToLlmForTests). Null restores the
|
|
95
|
+
// detected default.
|
|
96
|
+
export const __setHostKindForTests = (kind: VccHostKind | null): void => {
|
|
97
|
+
hostKindOverride = kind;
|
|
98
|
+
};
|
|
99
|
+
// Layered compact-form decision for a live ctx. getSystemPrompt is read off
|
|
100
|
+
// the calling ctx (command or event); absent (host-free mocks) falls through
|
|
101
|
+
// to module scope, then the legacy default.
|
|
102
|
+
export const resolveCompactForm = (
|
|
103
|
+
load: (id: string) => unknown,
|
|
104
|
+
getSystemPrompt?: () => unknown,
|
|
105
|
+
): VccCompactForm => {
|
|
106
|
+
if (hostKindOverride) return hostKindOverride === "pi" ? "object" : "string";
|
|
107
|
+
try {
|
|
108
|
+
const sp = getSystemPrompt?.();
|
|
109
|
+
if (typeof sp === "string") return "object";
|
|
110
|
+
if (Array.isArray(sp)) return "string";
|
|
111
|
+
} catch {}
|
|
112
|
+
return resolveHostKind(load) === "pi" ? "object" : "string";
|
|
113
|
+
};
|
|
114
|
+
export const getCompactForm = (getSystemPrompt?: () => unknown): VccCompactForm => {
|
|
115
|
+
let load: (id: string) => unknown = () => {
|
|
116
|
+
throw new Error("no loader");
|
|
117
|
+
};
|
|
118
|
+
try {
|
|
119
|
+
const req = createRequire(import.meta.url);
|
|
120
|
+
load = (id) => req(id);
|
|
121
|
+
} catch {}
|
|
122
|
+
return resolveCompactForm(load, getSystemPrompt);
|
|
123
|
+
};
|
|
26
124
|
|
|
27
125
|
export { PI_VCC_COMPACT_INSTRUCTION } from "./core/compact-args";
|
|
28
126
|
export const OMP_VCC_COMPACT_INSTRUCTION = "__omp_vcc__";
|
|
@@ -106,33 +204,60 @@ export const evaluateGrowthGuard = (prefixChars: number, netNewSummaryChars: num
|
|
|
106
204
|
let lastStats: CompactionStats | null = null;
|
|
107
205
|
let lastCompactWasPiVcc = false;
|
|
108
206
|
let pendingFollowUpPrompt: string | null = null;
|
|
109
|
-
let pendingAutoContinueTimer:
|
|
207
|
+
let pendingAutoContinueTimer: unknown = null;
|
|
110
208
|
let globalHistory: CompactionStats[] = [];
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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>();
|
|
117
228
|
const perPiKeys = new Set<any>();
|
|
118
|
-
// Guard eager chainShakeHint to avoid recursion: tracks pis currently chaining.
|
|
119
229
|
const pendingChainShake = new WeakSet<object>();
|
|
120
|
-
const getPerPi = (pi: any) => {
|
|
230
|
+
const getPerPi = (pi: any): PerPiState | null => {
|
|
121
231
|
if (!pi || typeof pi !== "object") return null;
|
|
122
|
-
let
|
|
123
|
-
if (!
|
|
124
|
-
|
|
125
|
-
|
|
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;
|
|
126
251
|
};
|
|
127
252
|
const setLastStats = (pi: any, v: CompactionStats | null) => {
|
|
128
253
|
if (v && v.timestamp == null) v.timestamp = Date.now();
|
|
129
254
|
lastStats = v;
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
132
|
-
|
|
255
|
+
const state = getPerPi(pi);
|
|
256
|
+
if (state) {
|
|
257
|
+
state.lastStats = v;
|
|
133
258
|
if (v) {
|
|
134
|
-
|
|
135
|
-
if (
|
|
259
|
+
state.statsHistory.push(v);
|
|
260
|
+
if (state.statsHistory.length > 50) state.statsHistory.shift();
|
|
136
261
|
}
|
|
137
262
|
}
|
|
138
263
|
if (v) {
|
|
@@ -140,26 +265,103 @@ const setLastStats = (pi: any, v: CompactionStats | null) => {
|
|
|
140
265
|
if (globalHistory.length > 50) globalHistory.shift();
|
|
141
266
|
}
|
|
142
267
|
};
|
|
143
|
-
const setLastCompactWasPiVcc = (pi: any, v: boolean) => {
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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;
|
|
150
342
|
pendingAutoContinueTimer = null;
|
|
151
|
-
|
|
343
|
+
lastCompactWasPiVcc = false;
|
|
344
|
+
pendingChainShake.delete(pi);
|
|
152
345
|
};
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
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 {
|
|
157
354
|
pendingAutoContinueTimer = null;
|
|
158
|
-
|
|
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;
|
|
159
362
|
try { triggerInvisibleContinue(pi); } catch {}
|
|
160
|
-
}, 0);
|
|
161
|
-
pendingAutoContinueTimer = timer;
|
|
162
|
-
if (s) s.pendingAutoContinueTimer = timer;
|
|
363
|
+
}, 0, "auto-continue");
|
|
364
|
+
if (state) state.pendingAutoContinueTimer = timer;
|
|
163
365
|
};
|
|
164
366
|
// the LLM context with a user-visible continue prompt. triggerInvisibleContinue
|
|
165
367
|
// sends a custom message marked with a dedicated customType (content:[],
|
|
@@ -244,22 +446,19 @@ export const clearCompactionHistoryForTests = () => {
|
|
|
244
446
|
lastStats = null;
|
|
245
447
|
lastCompactWasPiVcc = false;
|
|
246
448
|
pendingFollowUpPrompt = null;
|
|
247
|
-
|
|
449
|
+
clearTimerHandle(undefined, pendingAutoContinueTimer);
|
|
248
450
|
pendingAutoContinueTimer = null;
|
|
249
451
|
for (const pi of perPiKeys) {
|
|
250
|
-
const
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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;
|
|
258
461
|
}
|
|
259
|
-
// Remove strong ref so pi can be GC'd and WeakMap entry cleared; fresh
|
|
260
|
-
// getPerPi(pi) will recreate if this pi is reused, but tests create fresh
|
|
261
|
-
// pi objects each time, so clearing prevents unbounded Set growth across
|
|
262
|
-
// the 377-test suite.
|
|
263
462
|
perPi.delete(pi);
|
|
264
463
|
}
|
|
265
464
|
perPiKeys.clear();
|
|
@@ -321,17 +520,189 @@ const readCompactionEventContext = (event: unknown): { reason?: CompactionReason
|
|
|
321
520
|
: undefined;
|
|
322
521
|
return { reason, willRetry: raw.willRetry === true };
|
|
323
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
|
+
};
|
|
324
650
|
|
|
325
|
-
|
|
326
|
-
|
|
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
|
+
};
|
|
690
|
+
|
|
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 = () => {
|
|
327
699
|
try {
|
|
328
|
-
ctx?.ui?.notify?.(
|
|
329
|
-
formatCompactionStats(stats),
|
|
330
|
-
"info",
|
|
331
|
-
);
|
|
700
|
+
ctx?.ui?.notify?.(formatCompactionStats(stats), "info");
|
|
332
701
|
} catch {}
|
|
333
|
-
}
|
|
334
|
-
|
|
702
|
+
};
|
|
703
|
+
if (hasManagedContext) scheduleManaged(pi, ctx, notify, 500, "stats");
|
|
704
|
+
else setTimeout(notify, 500);
|
|
705
|
+
}
|
|
335
706
|
|
|
336
707
|
const parseCompactionInstructions = (customInstructions?: string): {
|
|
337
708
|
isPiVcc: boolean;
|
|
@@ -376,6 +747,22 @@ const dbg = (settings: PiVccSettings, data: Record<string, unknown>) => {
|
|
|
376
747
|
try { writeFileSync("/tmp/omp-vcc-debug.json", JSON.stringify(data, null, 2)); } catch {}
|
|
377
748
|
try { writeFileSync("/tmp/pi-vcc-debug.json", JSON.stringify(data, null, 2)); } catch {}
|
|
378
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
|
+
};
|
|
379
766
|
|
|
380
767
|
const previewContent = (content: unknown): string => {
|
|
381
768
|
if (typeof content === "string") return content.slice(0, 300);
|
|
@@ -419,6 +806,9 @@ interface EntryWithMessage {
|
|
|
419
806
|
entry: { id: string; type: string };
|
|
420
807
|
message: { role: string; content: unknown };
|
|
421
808
|
}
|
|
809
|
+
const selectedEntryId = (entry: { id?: unknown }): string | undefined =>
|
|
810
|
+
typeof entry.id === "string" && entry.id.length > 0 ? entry.id : undefined;
|
|
811
|
+
|
|
422
812
|
|
|
423
813
|
// Convert a non-message entry that carries LLM-context text (custom_message /
|
|
424
814
|
// branch_summary) into its agent-message form, mirroring pi-core's
|
|
@@ -462,6 +852,7 @@ export type OwnCutResult =
|
|
|
462
852
|
requestedKeepUserTurns: number;
|
|
463
853
|
keepFallbackToCompactAll: boolean;
|
|
464
854
|
budgetCut?: BudgetCutKind;
|
|
855
|
+
selectedIds: Array<string | undefined>;
|
|
465
856
|
}
|
|
466
857
|
| { ok: false; reason: OwnCutCancelReason };
|
|
467
858
|
|
|
@@ -546,6 +937,7 @@ export function buildOwnCut(branchEntries: any[], keepUserTurns = 1, explicitKee
|
|
|
546
937
|
const compactAll = (keepFallbackToCompactAll: boolean) => ({
|
|
547
938
|
ok: true as const,
|
|
548
939
|
messages: liveMessages.map((e) => e.message),
|
|
940
|
+
selectedIds: liveMessages.map((e) => selectedEntryId(e.entry)),
|
|
549
941
|
firstKeptEntryId: "",
|
|
550
942
|
compactAll: true,
|
|
551
943
|
keptUserTurns: 0,
|
|
@@ -572,6 +964,7 @@ export function buildOwnCut(branchEntries: any[], keepUserTurns = 1, explicitKee
|
|
|
572
964
|
return {
|
|
573
965
|
ok: true,
|
|
574
966
|
messages: liveMessages.slice(0, firstUserIdx).map((e) => e.message),
|
|
967
|
+
selectedIds: liveMessages.slice(0, firstUserIdx).map((e) => selectedEntryId(e.entry)),
|
|
575
968
|
firstKeptEntryId: liveMessages[firstUserIdx].entry.id,
|
|
576
969
|
compactAll: false,
|
|
577
970
|
keptUserTurns: userIndices.length,
|
|
@@ -587,6 +980,7 @@ export function buildOwnCut(branchEntries: any[], keepUserTurns = 1, explicitKee
|
|
|
587
980
|
return {
|
|
588
981
|
ok: true,
|
|
589
982
|
messages: liveMessages.slice(0, cutIdx).map((e) => e.message),
|
|
983
|
+
selectedIds: liveMessages.slice(0, cutIdx).map((e) => selectedEntryId(e.entry)),
|
|
590
984
|
firstKeptEntryId: liveMessages[cutIdx].entry.id,
|
|
591
985
|
compactAll: false,
|
|
592
986
|
keptUserTurns: userIndices.length - targetUserIdx,
|
|
@@ -608,7 +1002,7 @@ export const findBudgetCutIndex = (
|
|
|
608
1002
|
let acc = 0;
|
|
609
1003
|
let crossed = -1;
|
|
610
1004
|
for (let i = live.length - 1; i >= 0; i--) {
|
|
611
|
-
acc +=
|
|
1005
|
+
acc += estimateScriptAwareMessageContentTokens(live[i].message.content);
|
|
612
1006
|
if (acc >= maxTokens) {
|
|
613
1007
|
crossed = i;
|
|
614
1008
|
break;
|
|
@@ -635,6 +1029,7 @@ export const applyTailBudget = (
|
|
|
635
1029
|
const budgetResult = (idx: number, budgetCut: BudgetCutKind): OwnCutResult => ({
|
|
636
1030
|
ok: true,
|
|
637
1031
|
messages: live.slice(0, idx).map((m) => m.message),
|
|
1032
|
+
selectedIds: live.slice(0, idx).map((m) => selectedEntryId(m.entry)),
|
|
638
1033
|
firstKeptEntryId: live[idx].entry.id,
|
|
639
1034
|
compactAll: false,
|
|
640
1035
|
keptUserTurns: live.slice(idx).filter((m) => m.message.role === "user").length,
|
|
@@ -658,7 +1053,7 @@ export const applyTailBudget = (
|
|
|
658
1053
|
const tailStart = cut.messages.length; // equals the cut index in the live window
|
|
659
1054
|
let tailTokens = 0;
|
|
660
1055
|
for (let i = tailStart; i < live.length; i++) {
|
|
661
|
-
tailTokens +=
|
|
1056
|
+
tailTokens += estimateScriptAwareMessageContentTokens(live[i].message.content);
|
|
662
1057
|
}
|
|
663
1058
|
if (tailTokens <= maxTokens * factor) return cut;
|
|
664
1059
|
const idx = findBudgetCutIndex(live, maxTokens, opts.charsPerToken);
|
|
@@ -711,11 +1106,10 @@ const tailTokensForKeep = (branchEntries: any[], keepUserTurns: number, charsPer
|
|
|
711
1106
|
const live = collectLiveMessages(branchEntries);
|
|
712
1107
|
const keptIdx = live.findIndex((e) => e.entry.id === cut.firstKeptEntryId);
|
|
713
1108
|
if (keptIdx < 0) return null;
|
|
714
|
-
|
|
715
|
-
(sum: number, e) => sum +
|
|
1109
|
+
return live.slice(keptIdx).reduce(
|
|
1110
|
+
(sum: number, e) => sum + estimateScriptAwareMessageContentTokens(e.message?.content),
|
|
716
1111
|
0,
|
|
717
1112
|
);
|
|
718
|
-
return estimateTokensFromChars(chars, charsPerToken);
|
|
719
1113
|
};
|
|
720
1114
|
|
|
721
1115
|
/**
|
|
@@ -765,40 +1159,131 @@ const REASON_MESSAGES: Record<OwnCutCancelReason, string> = {
|
|
|
765
1159
|
export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
766
1160
|
// Filter our invisible-continue marker out of the LLM context payload so the
|
|
767
1161
|
// model just continues from the compaction summary (matched by customType ONLY).
|
|
768
|
-
pi.on("context", (event) => {
|
|
769
|
-
|
|
1162
|
+
pi.on("context", (event, ctx) => {
|
|
1163
|
+
let messages = event.messages;
|
|
1164
|
+
const filtered = event.messages.filter((message) => {
|
|
770
1165
|
if (message.role !== "custom") return true;
|
|
771
1166
|
return message.customType !== AUTO_CONTINUE_CUSTOM_TYPE && message.customType !== LEGACY_AUTO_CONTINUE_CUSTOM_TYPE;
|
|
772
1167
|
});
|
|
773
|
-
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
|
+
});
|
|
774
1229
|
});
|
|
775
1230
|
|
|
776
|
-
pi.on("before_agent_start", () => {
|
|
777
|
-
clearPendingAutoContinueForPi(pi);
|
|
1231
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
1232
|
+
clearPendingAutoContinueForPi(pi, ctx);
|
|
778
1233
|
});
|
|
779
1234
|
|
|
780
1235
|
pi.on("session_before_compact", (event, ctx) => {
|
|
781
|
-
const
|
|
782
|
-
const
|
|
783
|
-
const
|
|
784
|
-
|
|
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;
|
|
785
1259
|
|
|
786
1260
|
// Always handle explicit /pi-vcc or /omp-vcc marker.
|
|
787
1261
|
// Otherwise, only handle when user opted in via settings.
|
|
788
1262
|
const { isPiVcc, keepUserTurns, keepUserTurnsExplicit, followUpPrompt } = parseCompactionInstructions(customInstructions);
|
|
789
|
-
setPendingFollowUpPrompt(pi, null);
|
|
790
1263
|
// Explicit host mode bypass: when the host signals an explicit compact mode
|
|
791
|
-
//
|
|
792
|
-
//
|
|
793
|
-
//
|
|
794
|
-
//
|
|
795
|
-
//
|
|
1264
|
+
// via an event field, let the host walker handle it even though
|
|
1265
|
+
// overrideDefaultCompaction is true. This enables sequential VCC →
|
|
1266
|
+
// snapcompact/shake combinations. No shipped host exposes such a field
|
|
1267
|
+
// today — omp carries the mode in the compact() options (never the event)
|
|
1268
|
+
// and pi has no modes (its /compact text is raw focus instructions, so
|
|
1269
|
+
// lone mode words must NEVER bypass: on pi `/compact shake` means
|
|
1270
|
+
// "focus on shake"). The branch stays as the contract for the optional
|
|
1271
|
+
// native patch / future hosts; unpatched, override:true serves explicit
|
|
1272
|
+
// omp modes via VCC (use override:false for native modes).
|
|
796
1273
|
const explicitMode = (event as any).compactMode ?? (event as any).explicitMode ?? (event as any).mode;
|
|
797
1274
|
if (!isPiVcc && typeof explicitMode === "string" && explicitMode) {
|
|
798
1275
|
const m = explicitMode.toLowerCase();
|
|
799
1276
|
if (m === "snapcompact" || m === "shake" || m === "soft" || m === "remote" || m === "handoff") return;
|
|
800
1277
|
}
|
|
1278
|
+
// Chain-shake yield: while a {mode:"shake"} chain is in flight (see
|
|
1279
|
+
// session_compact below), let the host run it — otherwise VCC would
|
|
1280
|
+
// swallow the modeless call into a second VCC pass. Sentinel compactions
|
|
1281
|
+
// still handled (isPiVcc path falls through below).
|
|
1282
|
+
if (!isPiVcc && pendingChainShake.has(pi as unknown as object)) return;
|
|
801
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;
|
|
802
1287
|
|
|
803
1288
|
const calibrationCut = buildOwnCut(branchEntries as any[], 0);
|
|
804
1289
|
const calibrationMessageChars = calibrationCut.ok
|
|
@@ -949,18 +1434,21 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
949
1434
|
setPendingFollowUpPrompt(pi, followUpPrompt);
|
|
950
1435
|
const agentMessages = ownCut.messages;
|
|
951
1436
|
const firstKeptEntryId = ownCut.firstKeptEntryId;
|
|
952
|
-
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;
|
|
953
1442
|
|
|
954
|
-
// Count kept messages and estimate tokens
|
|
1443
|
+
// Count kept messages and estimate tokens with the script-aware estimator.
|
|
955
1444
|
const keptIdx = (branchEntries as any[]).findIndex((e: any) => e.id === firstKeptEntryId);
|
|
956
1445
|
const keptEntries = keptIdx >= 0
|
|
957
1446
|
? (branchEntries as any[]).slice(keptIdx).filter((e: any) => e.type === "message")
|
|
958
1447
|
: [];
|
|
959
|
-
const
|
|
960
|
-
(sum: number,
|
|
1448
|
+
const keptTokensEst = keptEntries.reduce(
|
|
1449
|
+
(sum: number, entry: any) => sum + estimateScriptAwareMessageContentTokens(entry.message?.content),
|
|
961
1450
|
0,
|
|
962
1451
|
);
|
|
963
|
-
const keptTokensEst = estimateTokensFromChars(keptChars, tokenEstimate.charsPerToken);
|
|
964
1452
|
const config = settings;
|
|
965
1453
|
|
|
966
1454
|
// Ranked compaction: keep the highest-signal blocks under a token budget
|
|
@@ -982,8 +1470,9 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
982
1470
|
const RANKED_BRIEF_BUDGET_TOKENS = 1100;
|
|
983
1471
|
const RANKED_BRIEF_CEILING_TOKENS = 2000;
|
|
984
1472
|
const RANKED_BRIEF_TOKENS_PER_BLOCK = 15;
|
|
985
|
-
|
|
1473
|
+
let summary = compileRanked({
|
|
986
1474
|
messages,
|
|
1475
|
+
sourceIndices,
|
|
987
1476
|
previousSummary: preparation.previousSummary,
|
|
988
1477
|
fileOps: {
|
|
989
1478
|
readFiles: [...preparation.fileOps.read],
|
|
@@ -995,6 +1484,7 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
995
1484
|
briefCharsPerBlock: Math.round(RANKED_BRIEF_TOKENS_PER_BLOCK * tokenEstimate.charsPerToken),
|
|
996
1485
|
},
|
|
997
1486
|
});
|
|
1487
|
+
summary = injectBeforeRecallNote(summary, memoryBlock);
|
|
998
1488
|
|
|
999
1489
|
// Keep-all cut with an empty prefix and no previous summary yields nothing
|
|
1000
1490
|
// new to summarize. Never hand the host an empty summary — cancel and keep
|
|
@@ -1030,8 +1520,11 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
1030
1520
|
const guard = evaluateGrowthGuard(prefixChars, netNewSummaryChars);
|
|
1031
1521
|
const { netGrowthChars, toleranceChars } = guard;
|
|
1032
1522
|
if (guard.trip) {
|
|
1033
|
-
const prefixTok =
|
|
1034
|
-
|
|
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)));
|
|
1035
1528
|
dbg(settings, {
|
|
1036
1529
|
growthGuard: true,
|
|
1037
1530
|
cancelled: reason !== "overflow" && !willRetry,
|
|
@@ -1054,7 +1547,7 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
1054
1547
|
}
|
|
1055
1548
|
|
|
1056
1549
|
const tokensBefore = typeof preparation.tokensBefore === "number" ? preparation.tokensBefore : 0;
|
|
1057
|
-
const summaryTokensEst =
|
|
1550
|
+
const summaryTokensEst = estimateScriptAwareTokens(summary);
|
|
1058
1551
|
const tokensAfterEst = summaryTokensEst + keptTokensEst;
|
|
1059
1552
|
const tokensSavedEst = tokensBefore > 0 ? Math.max(0, tokensBefore - tokensAfterEst) : 0;
|
|
1060
1553
|
const savedPercentEst = tokensBefore > 0 && tokensSavedEst > 0 ? Math.round((tokensSavedEst / tokensBefore) * 100) : 0;
|
|
@@ -1091,6 +1584,8 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
1091
1584
|
preview: e.type === "message" ? previewContent(e.message?.content) : undefined,
|
|
1092
1585
|
}))
|
|
1093
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);
|
|
1094
1589
|
|
|
1095
1590
|
const KNOWN_SECTIONS = new Set(["Session Goal", "Files And Changes", "Commits", "Outstanding Context", "User Preferences"]);
|
|
1096
1591
|
const extractKnownSections = (text: string) =>
|
|
@@ -1121,13 +1616,100 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
1121
1616
|
savedPercentEst,
|
|
1122
1617
|
},
|
|
1123
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
|
+
});
|
|
1124
1704
|
|
|
1125
|
-
|
|
1705
|
+
|
|
1706
|
+
const details = appendDetails ?? {
|
|
1126
1707
|
compactor: "omp-vcc",
|
|
1127
1708
|
version: 2,
|
|
1128
1709
|
sections: extractKnownSections(summary),
|
|
1129
1710
|
sourceMessageCount: agentMessages.length,
|
|
1130
1711
|
previousSummaryUsed: Boolean(preparation.previousSummary),
|
|
1712
|
+
retainedToolOutputProjection: retainedProjection,
|
|
1131
1713
|
reason,
|
|
1132
1714
|
willRetry,
|
|
1133
1715
|
savings: {
|
|
@@ -1140,34 +1722,79 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
1140
1722
|
savedPercentEst,
|
|
1141
1723
|
},
|
|
1142
1724
|
};
|
|
1725
|
+
capturePreCompactionDisplay(pi, agentMessages, ownCut.selectedIds);
|
|
1143
1726
|
|
|
1144
1727
|
setLastCompactWasPiVcc(pi, isPiVcc);
|
|
1145
1728
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1729
|
+
const compaction = {
|
|
1730
|
+
summary,
|
|
1731
|
+
details,
|
|
1732
|
+
tokensBefore: preparation.tokensBefore,
|
|
1733
|
+
firstKeptEntryId,
|
|
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);
|
|
1153
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);
|
|
1154
1757
|
});
|
|
1155
1758
|
pi.on("session_compact", async (event, ctx) => {
|
|
1156
|
-
const
|
|
1157
|
-
|
|
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;
|
|
1158
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
|
+
}
|
|
1159
1785
|
setPendingFollowUpPrompt(pi, null);
|
|
1160
|
-
|
|
1786
|
+
if (!ownsCompaction) return;
|
|
1787
|
+
if (pendingDisplay && settings.showPreCompactionMessage) {
|
|
1788
|
+
try { ctx?.ui?.notify?.(`[Previous output — display only]\n${pendingDisplay.text}`, "info"); } catch {}
|
|
1789
|
+
}
|
|
1161
1790
|
const stats = per ? per.lastStats : lastStats;
|
|
1162
1791
|
if (!stats) return;
|
|
1163
|
-
// Enrich with authoritative tokensAfter from host if available (even for pi-vcc manual, before early return)
|
|
1164
|
-
const entry: any = (event as any).compactionEntry;
|
|
1165
1792
|
if (entry && typeof entry.tokensAfter === "number" && typeof entry.tokensBefore === "number") {
|
|
1166
1793
|
const before = entry.tokensBefore;
|
|
1167
1794
|
const after = entry.tokensAfter;
|
|
1168
1795
|
const saved = Math.max(0, before - after);
|
|
1169
1796
|
const percent = before > 0 && saved > 0 ? Math.round((saved / before) * 100) : 0;
|
|
1170
|
-
if (per
|
|
1797
|
+
if (per?.lastStats) {
|
|
1171
1798
|
per.lastStats.tokensAfter = after;
|
|
1172
1799
|
per.lastStats.tokensSaved = saved;
|
|
1173
1800
|
per.lastStats.savedPercent = percent;
|
|
@@ -1184,9 +1811,8 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
1184
1811
|
(stats as any).savedPercent = percent;
|
|
1185
1812
|
(stats as any).tokensBefore = before;
|
|
1186
1813
|
try {
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
dbg(cfg, {
|
|
1814
|
+
if (settings.debug) {
|
|
1815
|
+
dbg(settings, {
|
|
1190
1816
|
authoritativeSavings: { tokensBefore: before, tokensAfter: after, tokensSaved: saved, savedPercent: percent },
|
|
1191
1817
|
eventEntry: { id: entry.id, tokensBefore: entry.tokensBefore, tokensAfter: entry.tokensAfter },
|
|
1192
1818
|
});
|
|
@@ -1194,40 +1820,54 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
1194
1820
|
} catch {}
|
|
1195
1821
|
}
|
|
1196
1822
|
const isPiVccLast = per ? per.lastCompactWasPiVcc : lastCompactWasPiVcc;
|
|
1197
|
-
if (isPiVccLast)
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
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;
|
|
1203
1834
|
const isLargeCompaction = (stats.summarized > 10) || (stats.kept > 5) || (stats.keptTokensEst > 2000);
|
|
1204
|
-
const shouldContinueAfterAutoCompact =
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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;
|
|
1208
1841
|
try {
|
|
1209
|
-
const cfgChain = loadSettings(ctx);
|
|
1210
1842
|
const ctxMaybe = ctx as unknown as Record<string, unknown>;
|
|
1211
1843
|
const compactFn = ctxMaybe["compact"];
|
|
1212
|
-
|
|
1844
|
+
const promptOf = ctxMaybe["getSystemPrompt"] as ((this: unknown) => unknown) | undefined;
|
|
1845
|
+
const chainForm = getCompactForm(() => promptOf?.call(ctx));
|
|
1846
|
+
if (settings.chainShakeHint && chainForm === "string" && typeof compactFn === "function" && !pendingChainShake.has(pi as unknown as object) && !willRetry) {
|
|
1213
1847
|
pendingChainShake.add(pi as unknown as object);
|
|
1214
|
-
const
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
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");
|
|
1223
1862
|
}
|
|
1224
1863
|
} catch {}
|
|
1225
1864
|
if (followUpPrompt) {
|
|
1226
1865
|
try {
|
|
1227
|
-
|
|
1866
|
+
const sent = (pi as any).sendUserMessage?.(followUpPrompt) as Promise<void> | undefined;
|
|
1867
|
+
if (sent && typeof sent.catch === "function") sent.catch(() => {});
|
|
1228
1868
|
} catch {}
|
|
1229
1869
|
} else if (shouldContinueAfterAutoCompact) {
|
|
1230
|
-
scheduleAutoContinueForPi(pi);
|
|
1870
|
+
scheduleAutoContinueForPi(pi, ctx);
|
|
1231
1871
|
}
|
|
1232
1872
|
});
|
|
1233
1873
|
};
|
|
@@ -1306,16 +1946,18 @@ export const formatVccConfigCard = (view: VccConfigView): string => {
|
|
|
1306
1946
|
: view.readPath === view.path
|
|
1307
1947
|
? `Source: file ${view.readPath}`
|
|
1308
1948
|
: `Source: fallback file ${view.readPath}`;
|
|
1309
|
-
const lines = (Object.keys(DEFAULT_SETTINGS) as (keyof PiVccSettings)[]).map(
|
|
1310
|
-
|
|
1311
|
-
|
|
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
|
+
});
|
|
1312
1954
|
return [header, status, ...lines].join("\n");
|
|
1313
1955
|
};
|
|
1314
1956
|
|
|
1315
1957
|
export const registerVccConfigCommand = (pi: any) => {
|
|
1316
1958
|
const handler = async (_args: string, ctx: any) => {
|
|
1317
1959
|
// args deliberately ignored — always show the effective config
|
|
1318
|
-
const view =
|
|
1960
|
+
const view = await loadSettingsWithSourcesAsync(ctx);
|
|
1319
1961
|
const output = formatVccConfigCard(view);
|
|
1320
1962
|
const piAny = pi as unknown as { sendMessage?: (msg: unknown, opts?: unknown) => void };
|
|
1321
1963
|
try { piAny.sendMessage?.({ customType: "vcc-config", content: output, display: true }, { triggerTurn: false }); } catch {}
|