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
|
@@ -2,7 +2,30 @@
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { dirname, join } from "path";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
5
6
|
|
|
7
|
+
type PluginSettingsLoader = (pluginName: string, cwd: string) => Promise<Record<string, unknown>>;
|
|
8
|
+
let pluginSettingsLoader: PluginSettingsLoader | null | undefined;
|
|
9
|
+
const resolvePluginSettingsLoader = (): PluginSettingsLoader | null => {
|
|
10
|
+
if (pluginSettingsLoader !== undefined) return pluginSettingsLoader;
|
|
11
|
+
try {
|
|
12
|
+
const req = createRequire(import.meta.url);
|
|
13
|
+
for (const id of [
|
|
14
|
+
"@oh-my-pi/pi-coding-agent/extensibility/plugins",
|
|
15
|
+
"@earendil-works/pi-coding-agent/extensibility/plugins",
|
|
16
|
+
]) {
|
|
17
|
+
try {
|
|
18
|
+
const mod = req(id) as { getPluginSettings?: PluginSettingsLoader };
|
|
19
|
+
if (typeof mod.getPluginSettings === "function") {
|
|
20
|
+
pluginSettingsLoader = mod.getPluginSettings;
|
|
21
|
+
return pluginSettingsLoader;
|
|
22
|
+
}
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
} catch {}
|
|
26
|
+
pluginSettingsLoader = null;
|
|
27
|
+
return pluginSettingsLoader;
|
|
28
|
+
};
|
|
6
29
|
// omp-vcc: XDG-aware config path, mirrored from pi-vcc but under ~/.omp
|
|
7
30
|
// Priorities: $OMP_VCC_CONFIG_PATH > $PI_VCC_CONFIG_PATH (legacy) > ~/.omp/omp-vcc/config.json
|
|
8
31
|
// Also respects $PI_CODING_AGENT_DIR / $OMP_DIR if set (oh-my-pi base dir)
|
|
@@ -22,8 +45,8 @@ const fallbackReadPath = (): string | null => {
|
|
|
22
45
|
const candidates: string[] = [];
|
|
23
46
|
if (process.env.OMP_VCC_CONFIG_PATH) candidates.push(process.env.OMP_VCC_CONFIG_PATH);
|
|
24
47
|
if (process.env.PI_VCC_CONFIG_PATH) candidates.push(process.env.PI_VCC_CONFIG_PATH);
|
|
25
|
-
candidates.push(SETTINGS_PATH_DEFAULT);
|
|
26
48
|
if (!candidates.includes(legacyPiPath)) candidates.push(legacyPiPath);
|
|
49
|
+
if (!candidates.includes(SETTINGS_PATH_DEFAULT)) candidates.push(SETTINGS_PATH_DEFAULT);
|
|
27
50
|
for (const p of candidates) if (existsSync(p)) return p;
|
|
28
51
|
// No candidate exists — return primary for creation path (used by scaffold)
|
|
29
52
|
return null;
|
|
@@ -72,6 +95,18 @@ export interface PiVccSettings {
|
|
|
72
95
|
* CompactionEntry).
|
|
73
96
|
*/
|
|
74
97
|
chainShakeHint: boolean;
|
|
98
|
+
/** Use append-only segments with a mutable trailing summary. */
|
|
99
|
+
compactionSummaryMode: "rewrite" | "append";
|
|
100
|
+
/** Maximum provider-visible retained tool-output tokens; 0 disables projection. */
|
|
101
|
+
retainedToolOutputMaxTokens: number;
|
|
102
|
+
/** Emit a display-only notification for text dropped during compaction. */
|
|
103
|
+
showPreCompactionMessage: boolean;
|
|
104
|
+
/** Maximum model-facing recall response characters; 0 disables the cap. */
|
|
105
|
+
recallResponseMaxChars: number;
|
|
106
|
+
/** Query oh-my-pi's public native memory backend during compaction. */
|
|
107
|
+
nativeMemory: boolean;
|
|
108
|
+
/** Write bounded rotating JSONL metrics under the omp config directory. */
|
|
109
|
+
debugLog: boolean;
|
|
75
110
|
}
|
|
76
111
|
|
|
77
112
|
export const DEFAULT_SETTINGS: PiVccSettings = {
|
|
@@ -81,49 +116,204 @@ export const DEFAULT_SETTINGS: PiVccSettings = {
|
|
|
81
116
|
continueAfterThresholdCompact: true,
|
|
82
117
|
debug: false,
|
|
83
118
|
chainShakeHint: false,
|
|
119
|
+
compactionSummaryMode: "append",
|
|
120
|
+
retainedToolOutputMaxTokens: 20_000,
|
|
121
|
+
showPreCompactionMessage: true,
|
|
122
|
+
recallResponseMaxChars: 48_000,
|
|
123
|
+
nativeMemory: true,
|
|
124
|
+
debugLog: false,
|
|
84
125
|
};
|
|
85
126
|
|
|
86
|
-
|
|
127
|
+
export type JsonReadStatus =
|
|
128
|
+
| { kind: "missing" }
|
|
129
|
+
| { kind: "valid"; value: Record<string, unknown> }
|
|
130
|
+
| { kind: "invalid" };
|
|
131
|
+
|
|
132
|
+
export const readJsonStatus = (path: string): JsonReadStatus => {
|
|
133
|
+
let raw: string;
|
|
134
|
+
try {
|
|
135
|
+
raw = readFileSync(path, "utf-8");
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if ((err as { code?: string })?.code === "ENOENT") return { kind: "missing" };
|
|
138
|
+
return { kind: "invalid" };
|
|
139
|
+
}
|
|
87
140
|
try {
|
|
88
|
-
|
|
141
|
+
const value = JSON.parse(raw);
|
|
142
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { kind: "invalid" };
|
|
143
|
+
return { kind: "valid", value: value as Record<string, unknown> };
|
|
89
144
|
} catch {
|
|
90
|
-
return
|
|
145
|
+
return { kind: "invalid" };
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const asRecord = (value: unknown): Record<string, unknown> | undefined =>
|
|
150
|
+
value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
151
|
+
|
|
152
|
+
const warnedConfigPaths = new WeakMap<object, Set<string>>();
|
|
153
|
+
const warnedConfigPathsWithoutContext = new Set<string>();
|
|
154
|
+
const warnInvalidConfig = (ctx: unknown, path: string): void => {
|
|
155
|
+
const root = asRecord(ctx);
|
|
156
|
+
const owner = asRecord(root?.sessionManager) ?? (ctx && (typeof ctx === "object" || typeof ctx === "function") ? ctx as object : null);
|
|
157
|
+
const seen = owner ? warnedConfigPaths.get(owner) ?? new Set<string>() : warnedConfigPathsWithoutContext;
|
|
158
|
+
if (owner && !warnedConfigPaths.has(owner)) warnedConfigPaths.set(owner, seen);
|
|
159
|
+
if (seen.has(path)) return;
|
|
160
|
+
seen.add(path);
|
|
161
|
+
try {
|
|
162
|
+
const ui = asRecord(root?.ui);
|
|
163
|
+
if (typeof ui?.notify === "function") ui.notify.call(ui, `omp-vcc: config file ${path} is invalid; using defaults`, "warning");
|
|
164
|
+
} catch {}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const BOOLEAN_SETTING_KEYS: Array<keyof PiVccSettings> = [
|
|
168
|
+
"vccEnabled", "overrideDefaultCompaction", "smartKeepTail", "continueAfterThresholdCompact",
|
|
169
|
+
"debug", "chainShakeHint", "showPreCompactionMessage", "nativeMemory", "debugLog",
|
|
170
|
+
];
|
|
171
|
+
const isValidSettingValue = (key: keyof PiVccSettings, value: unknown): boolean => {
|
|
172
|
+
if (BOOLEAN_SETTING_KEYS.includes(key)) return typeof value === "boolean";
|
|
173
|
+
if (key === "compactionSummaryMode") return value === "rewrite" || value === "append";
|
|
174
|
+
if (key === "retainedToolOutputMaxTokens") return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 200_000;
|
|
175
|
+
if (key === "recallResponseMaxChars") return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 2_000_000;
|
|
176
|
+
return true;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const normalizeSettings = (value: Record<string, unknown> | undefined): PiVccSettings => {
|
|
180
|
+
const merged: PiVccSettings = { ...DEFAULT_SETTINGS, ...(value ?? {}) };
|
|
181
|
+
for (const key of BOOLEAN_SETTING_KEYS) {
|
|
182
|
+
if (typeof merged[key] !== "boolean") merged[key] = DEFAULT_SETTINGS[key];
|
|
183
|
+
}
|
|
184
|
+
if (merged.compactionSummaryMode !== "rewrite" && merged.compactionSummaryMode !== "append") {
|
|
185
|
+
merged.compactionSummaryMode = DEFAULT_SETTINGS.compactionSummaryMode;
|
|
186
|
+
}
|
|
187
|
+
const retained = merged.retainedToolOutputMaxTokens;
|
|
188
|
+
if (typeof retained !== "number" || !Number.isFinite(retained) || retained < 0 || retained > 200_000) {
|
|
189
|
+
merged.retainedToolOutputMaxTokens = DEFAULT_SETTINGS.retainedToolOutputMaxTokens;
|
|
190
|
+
}
|
|
191
|
+
const recall = merged.recallResponseMaxChars;
|
|
192
|
+
if (typeof recall !== "number" || !Number.isFinite(recall) || recall < 0 || recall > 2_000_000) {
|
|
193
|
+
merged.recallResponseMaxChars = DEFAULT_SETTINGS.recallResponseMaxChars;
|
|
194
|
+
}
|
|
195
|
+
return merged;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const tryGetSetting = (ctx: unknown, key: string): unknown => {
|
|
199
|
+
try {
|
|
200
|
+
const root = asRecord(ctx);
|
|
201
|
+
if (!root) return undefined;
|
|
202
|
+
for (const containerKey of ["settings", "config"] as const) {
|
|
203
|
+
const container = asRecord(root[containerKey]);
|
|
204
|
+
if (!container) continue;
|
|
205
|
+
const getter = container.get;
|
|
206
|
+
if (typeof getter === "function") return getter.call(container, key);
|
|
207
|
+
if (key in container) return container[key];
|
|
208
|
+
}
|
|
209
|
+
} catch {}
|
|
210
|
+
return undefined;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const contextOverlay = (ctx: unknown): Partial<PiVccSettings> => {
|
|
214
|
+
const overlay: Partial<PiVccSettings> = {};
|
|
215
|
+
for (const key of Object.keys(DEFAULT_SETTINGS) as (keyof PiVccSettings)[]) {
|
|
216
|
+
const value = tryGetSetting(ctx, `plugins.@zhulinchng/omp-vcc.${key}`)
|
|
217
|
+
?? tryGetSetting(ctx, `plugins.omp-vcc.${key}`)
|
|
218
|
+
?? tryGetSetting(ctx, `omp-vcc.${key}`)
|
|
219
|
+
?? tryGetSetting(ctx, key);
|
|
220
|
+
if (value !== undefined) Object.assign(overlay, { [key]: value });
|
|
221
|
+
}
|
|
222
|
+
return overlay;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
const readFileSettings = (ctx?: unknown): { values: PiVccSettings; parsed: Record<string, unknown> | null; readPath: string | null; filePresent: boolean; fileValid: boolean } => {
|
|
227
|
+
const primary = settingsPath();
|
|
228
|
+
const primaryStatus = readJsonStatus(primary);
|
|
229
|
+
let parsed: Record<string, unknown> | null = null;
|
|
230
|
+
let readPath: string | null = null;
|
|
231
|
+
let filePresent = primaryStatus.kind !== "missing";
|
|
232
|
+
let fileValid = false;
|
|
233
|
+
if (primaryStatus.kind === "valid") {
|
|
234
|
+
parsed = primaryStatus.value;
|
|
235
|
+
readPath = primary;
|
|
236
|
+
fileValid = true;
|
|
237
|
+
} else if (primaryStatus.kind === "invalid") {
|
|
238
|
+
readPath = primary;
|
|
239
|
+
warnInvalidConfig(ctx, primary);
|
|
240
|
+
} else {
|
|
241
|
+
const fb = fallbackReadPath();
|
|
242
|
+
if (fb && fb !== primary) {
|
|
243
|
+
const fallbackStatus = readJsonStatus(fb);
|
|
244
|
+
filePresent = true;
|
|
245
|
+
if (fallbackStatus.kind === "valid") {
|
|
246
|
+
parsed = fallbackStatus.value;
|
|
247
|
+
readPath = fb;
|
|
248
|
+
fileValid = true;
|
|
249
|
+
} else if (fallbackStatus.kind === "invalid") {
|
|
250
|
+
readPath = fb;
|
|
251
|
+
warnInvalidConfig(ctx, fb);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
91
254
|
}
|
|
255
|
+
return { values: normalizeSettings(parsed ?? undefined), parsed, readPath, filePresent, fileValid };
|
|
92
256
|
};
|
|
93
257
|
|
|
94
258
|
export function loadSettings(ctx?: unknown): PiVccSettings {
|
|
95
|
-
|
|
96
|
-
// via ctx.settings (omp manifest `omp.settings` / `pi.settings` UI surface),
|
|
97
|
-
// merge them on top of file so /settings toggles take effect without restart.
|
|
98
|
-
// Host shapes vary: ctx.settings.get(key), ctx.config.get(key), or plain map.
|
|
99
|
-
const tryGet = (key: string): unknown => {
|
|
100
|
-
try {
|
|
101
|
-
const c = ctx as any;
|
|
102
|
-
if (!c) return undefined;
|
|
103
|
-
if (c.settings?.get) return c.settings.get(key);
|
|
104
|
-
if (c.config?.get) return c.config.get(key);
|
|
105
|
-
if (c.settings && typeof c.settings === "object" && key in c.settings) return c.settings[key];
|
|
106
|
-
if (c.config && typeof c.config === "object" && key in c.config) return c.config[key];
|
|
107
|
-
} catch {}
|
|
108
|
-
return undefined;
|
|
109
|
-
};
|
|
110
|
-
const file = (() => {
|
|
111
|
-
const primary = settingsPath();
|
|
112
|
-
const parsed = readJson(primary) ?? (() => {
|
|
113
|
-
const fb = fallbackReadPath();
|
|
114
|
-
return fb && fb !== primary ? readJson(fb) : null;
|
|
115
|
-
})();
|
|
116
|
-
if (!parsed || typeof parsed !== "object") return { ...DEFAULT_SETTINGS };
|
|
117
|
-
return { ...DEFAULT_SETTINGS, ...(parsed as Partial<PiVccSettings>) };
|
|
118
|
-
})();
|
|
259
|
+
const file = readFileSettings(ctx).values;
|
|
119
260
|
if (!ctx) return file;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
261
|
+
const overlay = contextOverlay(ctx);
|
|
262
|
+
return Object.keys(overlay).length ? normalizeSettings({ ...file, ...overlay }) : file;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const pluginSettingsCwd = (ctx: unknown): string | undefined => {
|
|
266
|
+
const cwd = asRecord(ctx)?.cwd;
|
|
267
|
+
return typeof cwd === "string" && cwd.length > 0 ? cwd : undefined;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/** Load file settings plus the host's public plugin-settings overlay. */
|
|
271
|
+
export function loadSettingsWithPluginOverlay(ctx: unknown): PiVccSettings | Promise<PiVccSettings> {
|
|
272
|
+
const base = loadSettings(ctx);
|
|
273
|
+
const loader = resolvePluginSettingsLoader();
|
|
274
|
+
const cwd = pluginSettingsCwd(ctx);
|
|
275
|
+
if (!loader || !cwd) return base;
|
|
276
|
+
return Promise.resolve(loader("omp-vcc", cwd))
|
|
277
|
+
.then((overlay) => normalizeSettings({ ...base, ...overlay }))
|
|
278
|
+
.catch(() => base);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function loadSettingsWithSources(ctx?: unknown): VccConfigView {
|
|
282
|
+
const path = settingsPath();
|
|
283
|
+
const file = readFileSettings(ctx);
|
|
284
|
+
const values = file.values;
|
|
285
|
+
const sources = {} as Record<keyof PiVccSettings, VccSettingSource>;
|
|
286
|
+
for (const key of Object.keys(DEFAULT_SETTINGS) as (keyof PiVccSettings)[]) {
|
|
287
|
+
sources[key] = file.fileValid && file.parsed && key in file.parsed && isValidSettingValue(key, file.parsed[key])
|
|
288
|
+
? "file"
|
|
289
|
+
: "default";
|
|
125
290
|
}
|
|
126
|
-
|
|
291
|
+
if (ctx) {
|
|
292
|
+
const overlay = contextOverlay(ctx);
|
|
293
|
+
const merged = normalizeSettings({ ...values, ...overlay });
|
|
294
|
+
for (const key of Object.keys(DEFAULT_SETTINGS) as (keyof PiVccSettings)[]) {
|
|
295
|
+
values[key] = merged[key];
|
|
296
|
+
if (overlay[key] !== undefined) sources[key] = isValidSettingValue(key, overlay[key]) ? "overlay" : "default";
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return { path, readPath: file.readPath, filePresent: file.filePresent, fileValid: file.fileValid, values, sources };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function loadSettingsWithSourcesAsync(ctx: unknown): VccConfigView | Promise<VccConfigView> {
|
|
303
|
+
const view = loadSettingsWithSources(ctx);
|
|
304
|
+
const loader = resolvePluginSettingsLoader();
|
|
305
|
+
const cwd = pluginSettingsCwd(ctx);
|
|
306
|
+
if (!loader || !cwd) return view;
|
|
307
|
+
return Promise.resolve(loader("omp-vcc", cwd))
|
|
308
|
+
.then((overlay) => {
|
|
309
|
+
const merged = normalizeSettings({ ...view.values, ...overlay });
|
|
310
|
+
for (const key of Object.keys(DEFAULT_SETTINGS) as (keyof PiVccSettings)[]) {
|
|
311
|
+
view.values[key] = merged[key];
|
|
312
|
+
if (overlay[key] !== undefined) view.sources[key] = isValidSettingValue(key, overlay[key]) ? "overlay" : "default";
|
|
313
|
+
}
|
|
314
|
+
return view;
|
|
315
|
+
})
|
|
316
|
+
.catch(() => view);
|
|
127
317
|
}
|
|
128
318
|
|
|
129
319
|
/**
|
|
@@ -139,22 +329,26 @@ export function scaffoldSettings(): void {
|
|
|
139
329
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
140
330
|
|
|
141
331
|
if (!existsSync(path)) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
332
|
+
const fallback = fallbackReadPath();
|
|
333
|
+
if (fallback && fallback !== path) {
|
|
334
|
+
const status = readJsonStatus(fallback);
|
|
335
|
+
if (status.kind === "valid") {
|
|
336
|
+
writeFileSync(path, `${JSON.stringify(normalizeSettings(status.value), null, 2)}\n`);
|
|
337
|
+
} else if (status.kind === "invalid") {
|
|
338
|
+
return;
|
|
339
|
+
} else {
|
|
340
|
+
writeFileSync(path, `${JSON.stringify(DEFAULT_SETTINGS, null, 2)}\n`);
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
writeFileSync(path, `${JSON.stringify(DEFAULT_SETTINGS, null, 2)}\n`);
|
|
148
344
|
}
|
|
149
|
-
writeFileSync(path, `${JSON.stringify(DEFAULT_SETTINGS, null, 2)}\n`);
|
|
150
345
|
return;
|
|
151
346
|
}
|
|
152
347
|
|
|
153
|
-
const
|
|
154
|
-
if (
|
|
155
|
-
|
|
348
|
+
const status = readJsonStatus(path);
|
|
349
|
+
if (status.kind !== "valid") return;
|
|
156
350
|
let changed = false;
|
|
157
|
-
const next: Record<string, unknown> = { ...
|
|
351
|
+
const next: Record<string, unknown> = { ...status.value };
|
|
158
352
|
for (const [key, value] of Object.entries(DEFAULT_SETTINGS)) {
|
|
159
353
|
if (!(key in next)) {
|
|
160
354
|
next[key] = value;
|
|
@@ -189,57 +383,3 @@ export interface VccConfigView {
|
|
|
189
383
|
* happens to equal the default still counts as `file`. */
|
|
190
384
|
sources: Record<keyof PiVccSettings, VccSettingSource>;
|
|
191
385
|
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* `loadSettings` plus provenance for `/vcc-config`. Read-only: never creates or
|
|
195
|
-
* repairs files (`scaffoldSettings` owns that). Merge order mirrors `loadSettings`
|
|
196
|
-
* exactly — defaults, then the first readable candidate (primary, else the same
|
|
197
|
-
* `fallbackReadPath()` order), then the identical ctx overlay chain.
|
|
198
|
-
*/
|
|
199
|
-
export function loadSettingsWithSources(ctx?: unknown): VccConfigView {
|
|
200
|
-
const path = settingsPath();
|
|
201
|
-
const primaryParsed = readJson(path);
|
|
202
|
-
let parsed: Record<string, unknown> | null = primaryParsed;
|
|
203
|
-
let readPath: string | null = primaryParsed ? path : null;
|
|
204
|
-
if (!parsed) {
|
|
205
|
-
const fb = fallbackReadPath();
|
|
206
|
-
if (fb && fb !== path) {
|
|
207
|
-
const fbParsed = readJson(fb);
|
|
208
|
-
if (fbParsed) {
|
|
209
|
-
parsed = fbParsed;
|
|
210
|
-
readPath = fb;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
const candidateExists = fallbackReadPath() !== null;
|
|
215
|
-
const valid = !!parsed && typeof parsed === "object";
|
|
216
|
-
const values: PiVccSettings =
|
|
217
|
-
valid && parsed
|
|
218
|
-
? { ...DEFAULT_SETTINGS, ...(parsed as Partial<PiVccSettings>) }
|
|
219
|
-
: { ...DEFAULT_SETTINGS };
|
|
220
|
-
const sources = {} as Record<keyof PiVccSettings, VccSettingSource>;
|
|
221
|
-
for (const k of Object.keys(DEFAULT_SETTINGS) as (keyof PiVccSettings)[]) {
|
|
222
|
-
sources[k] = valid && parsed && k in parsed ? "file" : "default";
|
|
223
|
-
}
|
|
224
|
-
if (ctx) {
|
|
225
|
-
const tryGet = (key: string): unknown => {
|
|
226
|
-
try {
|
|
227
|
-
const c = ctx as any;
|
|
228
|
-
if (!c) return undefined;
|
|
229
|
-
if (c.settings?.get) return c.settings.get(key);
|
|
230
|
-
if (c.config?.get) return c.config.get(key);
|
|
231
|
-
if (c.settings && typeof c.settings === "object" && key in c.settings) return c.settings[key];
|
|
232
|
-
if (c.config && typeof c.config === "object" && key in c.config) return c.config[key];
|
|
233
|
-
} catch {}
|
|
234
|
-
return undefined;
|
|
235
|
-
};
|
|
236
|
-
for (const k of Object.keys(DEFAULT_SETTINGS) as (keyof PiVccSettings)[]) {
|
|
237
|
-
const v = tryGet(`plugins.@zhulinchng/omp-vcc.${k}`) ?? tryGet(`plugins.omp-vcc.${k}`) ?? tryGet(`omp-vcc.${k}`) ?? tryGet(k);
|
|
238
|
-
if (v !== undefined) {
|
|
239
|
-
(values as any)[k] = v;
|
|
240
|
-
sources[k] = "overlay";
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
return { path, readPath, filePresent: candidateExists, fileValid: valid, values, sources };
|
|
245
|
-
}
|
|
@@ -12,6 +12,7 @@ export interface CompileInput {
|
|
|
12
12
|
messages: Message[];
|
|
13
13
|
previousSummary?: string;
|
|
14
14
|
fileOps?: FileOps;
|
|
15
|
+
sourceIndices?: Array<number | undefined>;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export interface RankedCompileInput extends CompileInput {
|
|
@@ -236,11 +237,12 @@ const mergePrevious = (prev: string, fresh: string, options: { preserveFreshBrie
|
|
|
236
237
|
interface CompileWithBriefBlocksOptions {
|
|
237
238
|
briefBlocksFor?: (blocks: ReturnType<typeof normalize>) => ReturnType<typeof normalize>;
|
|
238
239
|
capFreshBrief?: boolean;
|
|
240
|
+
includeRecallNote?: boolean;
|
|
239
241
|
preserveFreshBriefOnMerge?: boolean;
|
|
240
242
|
}
|
|
241
243
|
|
|
242
244
|
const compileWithBriefBlocks = (input: CompileInput, options: CompileWithBriefBlocksOptions = {}): string => {
|
|
243
|
-
const blocks = filterNoise(normalize(input.messages));
|
|
245
|
+
const blocks = filterNoise(normalize(input.messages, input.sourceIndices));
|
|
244
246
|
const briefBlocks = options.briefBlocksFor?.(blocks);
|
|
245
247
|
const data = buildSections({ blocks, briefBlocks, fileOps: input.fileOps });
|
|
246
248
|
const fresh = formatSummary(data, { capBriefTranscript: options.capFreshBrief ?? true });
|
|
@@ -251,7 +253,7 @@ const compileWithBriefBlocks = (input: CompileInput, options: CompileWithBriefBl
|
|
|
251
253
|
: undefined;
|
|
252
254
|
const merged = prev ? mergePrevious(prev, fresh, { preserveFreshBrief: options.preserveFreshBriefOnMerge }) : fresh;
|
|
253
255
|
if (!merged) return "";
|
|
254
|
-
return wrapLongLines(merged) + SEPARATOR + RECALL_NOTE;
|
|
256
|
+
return wrapLongLines(merged) + (options.includeRecallNote === false ? "" : SEPARATOR + RECALL_NOTE);
|
|
255
257
|
};
|
|
256
258
|
|
|
257
259
|
export const compile = (input: CompileInput): string =>
|
|
@@ -279,3 +281,7 @@ const stripRecallNote = (text: string): string => {
|
|
|
279
281
|
if (!match || match.index < 0) return text;
|
|
280
282
|
return text.slice(0, match.index).replace(/\s*(?:\n\n---\n\n)?\s*$/, "").trimEnd();
|
|
281
283
|
};
|
|
284
|
+
|
|
285
|
+
/** Compile only the fresh selected window; prior summary and recall note are excluded. */
|
|
286
|
+
export const compileSegment = (input: CompileInput): string =>
|
|
287
|
+
compileWithBriefBlocks({ ...input, previousSummary: undefined }, { includeRecallNote: false });
|
|
@@ -139,6 +139,61 @@ export const estimateMessageContentTokens = (
|
|
|
139
139
|
charsPerToken = DEFAULT_CHARS_PER_TOKEN,
|
|
140
140
|
): number => estimateTokensFromChars(estimateMessageContentChars(content), charsPerToken);
|
|
141
141
|
|
|
142
|
+
const asRecord = (value: unknown): Record<string, unknown> | undefined =>
|
|
143
|
+
value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
144
|
+
|
|
145
|
+
const isCjkCodePoint = (codePoint: number): boolean =>
|
|
146
|
+
(codePoint >= 0x2e80 && codePoint <= 0x9fff)
|
|
147
|
+
|| (codePoint >= 0xac00 && codePoint <= 0xd7ff)
|
|
148
|
+
|| (codePoint >= 0x3000 && codePoint <= 0x303f);
|
|
149
|
+
|
|
150
|
+
/** Script-aware estimate for non-authoritative retained-tail and recall budgets. */
|
|
151
|
+
export const estimateScriptAwareTokens = (text: string): number => {
|
|
152
|
+
if (!text) return 0;
|
|
153
|
+
let cjk = 0;
|
|
154
|
+
let other = 0;
|
|
155
|
+
for (const character of text) {
|
|
156
|
+
if (isCjkCodePoint(character.codePointAt(0) ?? 0)) cjk++;
|
|
157
|
+
else other++;
|
|
158
|
+
}
|
|
159
|
+
return cjk + Math.ceil(other / 4);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const estimateScriptAwareMessageContentTokens = (content: unknown): number => {
|
|
163
|
+
if (typeof content === "string") return estimateScriptAwareTokens(content);
|
|
164
|
+
if (!Array.isArray(content)) return 0;
|
|
165
|
+
let total = 0;
|
|
166
|
+
for (const rawPart of content) {
|
|
167
|
+
const part = asRecord(rawPart);
|
|
168
|
+
if (!part) continue;
|
|
169
|
+
switch (part.type) {
|
|
170
|
+
case "text":
|
|
171
|
+
total += estimateScriptAwareTokens(typeof part.text === "string" ? part.text : "");
|
|
172
|
+
break;
|
|
173
|
+
case "thinking":
|
|
174
|
+
total += estimateScriptAwareTokens(typeof part.thinking === "string" ? part.thinking : "");
|
|
175
|
+
break;
|
|
176
|
+
case "toolCall": {
|
|
177
|
+
const args = part.arguments ?? part.input;
|
|
178
|
+
const argText = typeof args === "string" ? args : safeJsonStringify(args);
|
|
179
|
+
total += estimateScriptAwareTokens(`${typeof part.name === "string" ? part.name : ""}${argText}`);
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case "toolResult": {
|
|
183
|
+
const value = part.content;
|
|
184
|
+
total += estimateScriptAwareTokens(typeof value === "string" ? value : safeJsonStringify(value));
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
case "image":
|
|
188
|
+
total += IMAGE_CONTENT_CHARS / DEFAULT_CHARS_PER_TOKEN;
|
|
189
|
+
break;
|
|
190
|
+
default:
|
|
191
|
+
total += estimateScriptAwareTokens(typeof part.text === "string" ? part.text : "");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return Math.ceil(total);
|
|
195
|
+
};
|
|
196
|
+
|
|
142
197
|
export interface UsageStats {
|
|
143
198
|
messageCount: number;
|
|
144
199
|
byRole: Record<string, number>;
|