@aeondave/pi-skill-optimizer 3.0.0

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/src/index.ts ADDED
@@ -0,0 +1,1362 @@
1
+ import type { ExtensionAPI, ExtensionContext, Skill as PiSkill } from "@earendil-works/pi-coding-agent";
2
+ import type { UserMessage } from "@earendil-works/pi-ai";
3
+ import { Type } from "typebox";
4
+ import {
5
+ ensureGlobalConfigTemplate,
6
+ getConfig,
7
+ getConfigPaths,
8
+ getOutputConfig,
9
+ getProfilePaths,
10
+ getStatsFilePath,
11
+ getUsageConfig,
12
+ getUsageFilePath,
13
+ isDisabled,
14
+ } from "./config.ts";
15
+ import {
16
+ batchSkillsByWeight,
17
+ generateProfileInBatches,
18
+ interpretBatchResponse,
19
+ isRetryableBatchException,
20
+ responseText,
21
+ } from "./generate.ts";
22
+ import { deduplicateToolResultHistory, type HistoryArtifact } from "./history.ts";
23
+ import {
24
+ buildInitCatalogKey,
25
+ buildInitNoticeKey,
26
+ buildInitReadiness,
27
+ INIT_READINESS_ENTRY_TYPE,
28
+ readInitNoticeKeys,
29
+ type InitNoticeStatus,
30
+ type InitReadiness,
31
+ type InitReadinessInput,
32
+ } from "./init-readiness.ts";
33
+ import { buildInitProfileWrites, needsProfileScopeRepair } from "./init-profile.ts";
34
+ import { INIT_PROFILE_PROMPT, INIT_PROFILE_VERSION } from "./init-prompt.ts";
35
+ import { optimizePayload } from "./optimize.ts";
36
+ import {
37
+ buildExtractPrompt,
38
+ hasMinimumSavings,
39
+ isExcludedCommand,
40
+ isRtkSource,
41
+ reduceJsonArrayColumnar,
42
+ reduceOutput,
43
+ shouldReduceToolResult,
44
+ utf8ByteLength,
45
+ validateExtractedOutput,
46
+ type ExtractRejectionReason,
47
+ } from "./output.ts";
48
+ import {
49
+ cleanupTemporaryOutputs,
50
+ ConcurrentFileUpdateError,
51
+ fileExists,
52
+ loadStatsSnapshot,
53
+ loadUsageFile,
54
+ pruneUsageFile,
55
+ readStoredProfile,
56
+ resolveTemporaryOutput,
57
+ saveStatsDeltas,
58
+ saveTemporaryOutput,
59
+ saveUsageDelta,
60
+ writeProfileFiles,
61
+ type SavedStats,
62
+ type StoredProfile,
63
+ } from "./persistence.ts";
64
+ import {
65
+ computeFinalHashes,
66
+ diffSkills,
67
+ EMPTY_PROFILE,
68
+ mergeIncrementalProfile,
69
+ mergeProfiles,
70
+ pruneProfileNames,
71
+ type SkillOptimizerProfile,
72
+ } from "./profile.ts";
73
+ import { sliceOutputLines } from "./output-recovery.ts";
74
+ import { createSkillRegistry, type SkillRegistry } from "./skill-loader.ts";
75
+ import { auditSkillDescriptions } from "./skills.ts";
76
+ import {
77
+ addExtractionTelemetry,
78
+ addProviderCacheTelemetry,
79
+ addSavings,
80
+ EMPTY_EXTRACTION_TELEMETRY,
81
+ EMPTY_PROVIDER_CACHE_TELEMETRY,
82
+ EMPTY_SAVINGS,
83
+ totalSavings,
84
+ type ExtractionTelemetry,
85
+ type ProviderCacheTelemetry,
86
+ type SavingsByArea,
87
+ } from "./stats.ts";
88
+ import {
89
+ buildToolDiscoveryCatalog,
90
+ planToolActivation,
91
+ searchToolDiscoveryCatalog,
92
+ type ToolDiscoveryCatalog,
93
+ } from "./tools.ts";
94
+ import {
95
+ buildUsagePrior,
96
+ collectSkillUsageEvidence,
97
+ mergeUsageStats,
98
+ pruneUsageStats,
99
+ recordSkillUsage as updateSkillUsage,
100
+ type SkillUsageStats,
101
+ type UsagePruneOptions,
102
+ } from "./usage.ts";
103
+
104
+ const STATUS_KEY = "skill-optimizer";
105
+ const INIT_VERSION = INIT_PROFILE_VERSION;
106
+ const INIT_BATCH_MAX_SKILLS = 80;
107
+ const INIT_BATCH_MAX_UTF8_BYTES = 32 * 1024;
108
+ const INIT_BATCH_MAX_ATTEMPTS = 2;
109
+ const FLUSH_INTERVAL_MS = 15_000;
110
+ const LOAD_RETRY_MS = 1_000;
111
+ const MAX_SEEN_EVIDENCE = 4_096;
112
+ const MAX_HISTORY_ARTIFACTS = 128;
113
+ const PROTECTED_RUNTIME_TOOLS = ["skill_search", "tool_search", "retrieve_output"] as const;
114
+
115
+ type ConfigRecord = ReturnType<typeof getConfig>;
116
+
117
+ const EMPTY_STORED_PROFILE: StoredProfile = {
118
+ exists: false,
119
+ profile: EMPTY_PROFILE,
120
+ hashes: {},
121
+ initVersion: 0,
122
+ revision: null,
123
+ };
124
+
125
+ interface RuntimeSnapshot {
126
+ cwd: string;
127
+ config: ConfigRecord;
128
+ profile: SkillOptimizerProfile;
129
+ globalStoredProfile: StoredProfile;
130
+ projectStoredProfile?: StoredProfile;
131
+ usage: SkillUsageStats;
132
+ profileKey: string;
133
+ usagePath: string;
134
+ initReadiness?: { catalogKey: string; value: InitReadiness };
135
+ }
136
+
137
+ interface PendingStats {
138
+ savings: SavingsByArea;
139
+ extraction: ExtractionTelemetry;
140
+ cache: ProviderCacheTelemetry;
141
+ }
142
+
143
+ function emptyPendingStats(): PendingStats {
144
+ return {
145
+ savings: { ...EMPTY_SAVINGS },
146
+ extraction: { ...EMPTY_EXTRACTION_TELEMETRY },
147
+ cache: { ...EMPTY_PROVIDER_CACHE_TELEMETRY },
148
+ };
149
+ }
150
+
151
+ function approxK(chars: number): string {
152
+ return `${Math.round(chars / 400) / 10}k`;
153
+ }
154
+
155
+ function setStatus(ctx: ExtensionContext, text: string | undefined): void {
156
+ try {
157
+ ctx.ui.setStatus(STATUS_KEY, text);
158
+ } catch {
159
+ // Cosmetic only.
160
+ }
161
+ }
162
+
163
+ function profileSummary(profile: SkillOptimizerProfile): string {
164
+ return `${profile.critical.length} critical, ${Object.keys(profile.queries).length} query sets, ${Object.keys(profile.clusters).length} clusters, ${Object.keys(profile.activation ?? {}).length} activation tags`;
165
+ }
166
+
167
+ function initReadinessSummary(readiness: InitReadiness): string {
168
+ switch (readiness.status) {
169
+ case "empty": return "not initialized";
170
+ case "manual": return "manual profile";
171
+ case "outdated": return "generated profile is outdated";
172
+ case "incomplete": return "generated profile is incomplete";
173
+ case "current": return "current";
174
+ }
175
+ }
176
+
177
+ function usagePruneOptions(snapshot: RuntimeSnapshot): UsagePruneOptions {
178
+ const usage = getUsageConfig(snapshot.config);
179
+ return {
180
+ ...usage,
181
+ protectedNames: [
182
+ ...snapshot.profile.critical,
183
+ ...snapshot.config.alwaysSkills,
184
+ ],
185
+ };
186
+ }
187
+
188
+ function skillInputs(skills: readonly PiSkill[]): PiSkill[] {
189
+ const seen = new Set<string>();
190
+ return skills.filter((skill) => {
191
+ if (skill.disableModelInvocation || seen.has(skill.name)) return false;
192
+ seen.add(skill.name);
193
+ return true;
194
+ });
195
+ }
196
+
197
+ function makeSkillRegistry(skills: readonly PiSkill[]): SkillRegistry {
198
+ return createSkillRegistry(skillInputs(skills).map((skill) => ({
199
+ name: skill.name,
200
+ description: skill.description,
201
+ filePath: skill.filePath,
202
+ baseDir: skill.baseDir,
203
+ })));
204
+ }
205
+
206
+ function resolveOutputModel(ctx: ExtensionContext, spec: string): unknown {
207
+ if (!spec.trim()) return undefined;
208
+ try {
209
+ const registry = ctx.modelRegistry as { find?: (provider: string, id: string) => unknown };
210
+ if (typeof registry.find !== "function") return undefined;
211
+ if (spec.includes("/")) {
212
+ const separator = spec.indexOf("/");
213
+ return registry.find(spec.slice(0, separator), spec.slice(separator + 1));
214
+ }
215
+ const provider = ctx.model?.provider;
216
+ return provider ? registry.find(provider, spec) : undefined;
217
+ } catch {
218
+ return undefined;
219
+ }
220
+ }
221
+
222
+ async function tryExtractOutput(
223
+ ctx: ExtensionContext,
224
+ spec: string,
225
+ request: string,
226
+ command: string,
227
+ text: string,
228
+ ): Promise<{ text: string; telemetry: ProviderCacheTelemetry } | undefined> {
229
+ try {
230
+ const model = resolveOutputModel(ctx, spec);
231
+ if (!model) return undefined;
232
+ const prompt = buildExtractPrompt(request, command, text);
233
+ const message: UserMessage = {
234
+ role: "user",
235
+ content: [{ type: "text", text: prompt.user }],
236
+ timestamp: Date.now(),
237
+ };
238
+ const response = await ctx.modelRegistry.complete(
239
+ model as NonNullable<ExtensionContext["model"]>,
240
+ { systemPrompt: prompt.system, messages: [message] },
241
+ );
242
+ const extracted = responseText(response).trim();
243
+ return {
244
+ text: extracted,
245
+ telemetry: providerCacheDelta(response.usage),
246
+ };
247
+ } catch {
248
+ return undefined;
249
+ }
250
+ }
251
+
252
+ function providerCacheDelta(usage: {
253
+ input?: unknown;
254
+ output?: unknown;
255
+ cacheRead?: unknown;
256
+ cacheWrite?: unknown;
257
+ cost?: { total?: unknown };
258
+ }): ProviderCacheTelemetry {
259
+ const nonNegative = (value: unknown): number =>
260
+ typeof value === "number" && Number.isFinite(value) && value > 0
261
+ ? Math.floor(value)
262
+ : 0;
263
+ return {
264
+ requests: 1,
265
+ input: nonNegative(usage.input),
266
+ output: nonNegative(usage.output),
267
+ cacheRead: nonNegative(usage.cacheRead),
268
+ cacheWrite: nonNegative(usage.cacheWrite),
269
+ totalCost:
270
+ typeof usage.cost?.total === "number" && Number.isFinite(usage.cost.total)
271
+ ? Math.max(0, usage.cost.total)
272
+ : 0,
273
+ };
274
+ }
275
+
276
+ const SKILL_SEARCH_PARAMS = Type.Object({
277
+ action: Type.Union([
278
+ Type.Literal("search"),
279
+ Type.Literal("load"),
280
+ Type.Literal("resource"),
281
+ Type.Literal("location"),
282
+ ]),
283
+ query: Type.Optional(Type.String()),
284
+ name: Type.Optional(Type.String()),
285
+ path: Type.Optional(Type.String()),
286
+ cursor: Type.Optional(Type.String()),
287
+ pageSize: Type.Optional(Type.Integer({ minimum: 1, maximum: 50 })),
288
+ });
289
+
290
+ const TOOL_SEARCH_PARAMS = Type.Object({
291
+ query: Type.String(),
292
+ cursor: Type.Optional(Type.String()),
293
+ pageSize: Type.Optional(Type.Integer({ minimum: 1, maximum: 50 })),
294
+ });
295
+
296
+ const RETRIEVE_OUTPUT_PARAMS = Type.Object({
297
+ id: Type.String({ description: "Opaque sko: or htr: handle" }),
298
+ startLine: Type.Optional(Type.Integer({ minimum: 1 })),
299
+ lineCount: Type.Optional(Type.Integer({ minimum: 1, maximum: 1_000 })),
300
+ });
301
+
302
+ export default function skillOptimizer(pi: ExtensionAPI) {
303
+ let snapshot: RuntimeSnapshot | undefined;
304
+ let cachedConfig: { cwd: string; value: ConfigRecord } | undefined;
305
+ let retryAfter = 0;
306
+ let retryNoticeAt = 0;
307
+ let registry: SkillRegistry | undefined;
308
+ let toolCatalog: ToolDiscoveryCatalog | undefined;
309
+ let permittedToolNames: Set<string> | undefined;
310
+ const deactivatedToolNames = new Set<string>();
311
+ const retainedToolNames = new Set<string>();
312
+ let latestPrompt = "";
313
+ let lastSelected: string[] = [];
314
+ let lastRemovedChars = 0;
315
+ let lastBudget = { usedChars: 0, requestedChars: 0, nameOnlyCount: 0 };
316
+ let rtkPresence: boolean | undefined;
317
+ let hasStatus = false;
318
+ let lastStatusText: string | undefined;
319
+ const usedTools = new Set<string>();
320
+ const seenEvidence = new Set<string>();
321
+ const seenProviderMessages = new Set<string>();
322
+ const historyArtifacts = new Map<string, HistoryArtifact>();
323
+ const historyOrder: string[] = [];
324
+ const shownInitNoticeKeys = new Set<string>();
325
+
326
+ let sessionSaved: SavingsByArea = { ...EMPTY_SAVINGS };
327
+ let sessionExtraction: ExtractionTelemetry = { ...EMPTY_EXTRACTION_TELEMETRY };
328
+ let sessionCache: ProviderCacheTelemetry = { ...EMPTY_PROVIDER_CACHE_TELEMETRY };
329
+ const pendingStats = new Map<string, PendingStats>();
330
+ const pendingUsage = new Map<string, SkillUsageStats>();
331
+ const statsBase = new Map<string, SavedStats>();
332
+ let lastFlushAt = 0;
333
+
334
+ const setSessionStatus = (ctx: ExtensionContext, text: string | undefined): void => {
335
+ if (hasStatus && lastStatusText === text) return;
336
+ setStatus(ctx, text);
337
+ hasStatus = true;
338
+ lastStatusText = text;
339
+ };
340
+
341
+ const reportInitProgress = (
342
+ ctx: ExtensionContext,
343
+ message: string,
344
+ type: "info" | "warning" | "error" = "info",
345
+ notify = true,
346
+ ): void => {
347
+ if (notify) ctx.ui.notify(message, type);
348
+ setSessionStatus(ctx, message);
349
+ };
350
+
351
+ const stateFallback = (ctx: ExtensionContext, config: ConfigRecord): RuntimeSnapshot => {
352
+ const profilePaths = getProfilePaths(ctx.cwd);
353
+ return {
354
+ cwd: ctx.cwd,
355
+ config,
356
+ profile: EMPTY_PROFILE,
357
+ globalStoredProfile: EMPTY_STORED_PROFILE,
358
+ usage: {},
359
+ profileKey: `${profilePaths.global}::${profilePaths.project}`,
360
+ usagePath: getUsageFilePath(ctx.cwd),
361
+ };
362
+ };
363
+
364
+ const getRuntimeConfig = (ctx: ExtensionContext, refresh = false): ConfigRecord => {
365
+ if (refresh || cachedConfig?.cwd !== ctx.cwd) {
366
+ cachedConfig = { cwd: ctx.cwd, value: getConfig(ctx.cwd) };
367
+ }
368
+ if (snapshot?.cwd === ctx.cwd) snapshot.config = cachedConfig.value;
369
+ return cachedConfig.value;
370
+ };
371
+
372
+ const ensureSnapshot = (ctx: ExtensionContext, force = false): RuntimeSnapshot => {
373
+ if (!force && snapshot?.cwd === ctx.cwd && retryAfter === 0) return snapshot;
374
+ if (!force && Date.now() < retryAfter && snapshot?.cwd === ctx.cwd) return snapshot;
375
+ const config = getRuntimeConfig(ctx);
376
+ try {
377
+ const profilePaths = getProfilePaths(ctx.cwd);
378
+ const globalStoredProfile = readStoredProfile(profilePaths.global);
379
+ const projectStoredProfile = profilePaths.project !== profilePaths.global
380
+ ? readStoredProfile(profilePaths.project)
381
+ : undefined;
382
+ const profile = projectStoredProfile
383
+ ? mergeProfiles(globalStoredProfile.profile, projectStoredProfile.profile)
384
+ : globalStoredProfile.profile;
385
+ const usagePath = getUsageFilePath(ctx.cwd);
386
+ const candidate: RuntimeSnapshot = {
387
+ cwd: ctx.cwd,
388
+ config,
389
+ profile,
390
+ globalStoredProfile,
391
+ ...(projectStoredProfile ? { projectStoredProfile } : {}),
392
+ usage: loadUsageFile(usagePath),
393
+ profileKey: `${profilePaths.global}::${profilePaths.project}`,
394
+ usagePath,
395
+ };
396
+ const originalUsageSize = Object.keys(candidate.usage).length;
397
+ const pruned = pruneUsageStats(candidate.usage, usagePruneOptions(candidate));
398
+ candidate.usage = pruned;
399
+ if (Object.keys(pruned).length < originalUsageSize) {
400
+ candidate.usage = pruneUsageFile(usagePath, usagePruneOptions(candidate));
401
+ }
402
+ // Explicit profile refreshes must retain usage not yet committed by this process.
403
+ const pending = pendingUsage.get(usagePath);
404
+ if (pending) candidate.usage = mergeUsageStats(candidate.usage, pending);
405
+ snapshot = candidate;
406
+ retryAfter = 0;
407
+ return candidate;
408
+ } catch (error) {
409
+ retryAfter = Date.now() + LOAD_RETRY_MS;
410
+ snapshot = snapshot?.cwd === ctx.cwd ? snapshot : stateFallback(ctx, config);
411
+ if (Date.now() - retryNoticeAt > 5_000) {
412
+ retryNoticeAt = Date.now();
413
+ ctx.ui.notify(`skill-optimizer: state snapshot failed; retrying (${(error as Error).message})`, "warning");
414
+ }
415
+ return snapshot;
416
+ }
417
+ };
418
+
419
+ const getInitReadiness = (state: RuntimeSnapshot, skills: readonly PiSkill[]): InitReadiness => {
420
+ const hasProjectScope = state.projectStoredProfile !== undefined;
421
+ const globalSkills = hasProjectScope
422
+ ? skills.filter((skill) => skill.sourceInfo.scope !== "project")
423
+ : skills;
424
+ const projectSkills = hasProjectScope
425
+ ? skills.filter((skill) => skill.sourceInfo.scope === "project")
426
+ : undefined;
427
+ const input: InitReadinessInput = {
428
+ scopeKey: state.profileKey,
429
+ initVersion: INIT_VERSION,
430
+ global: { stored: state.globalStoredProfile, skills: globalSkills },
431
+ ...(state.projectStoredProfile && projectSkills
432
+ ? { project: { stored: state.projectStoredProfile, skills: projectSkills } }
433
+ : {}),
434
+ };
435
+ const catalogKey = buildInitCatalogKey(input);
436
+ if (state.initReadiness?.catalogKey === catalogKey) return state.initReadiness.value;
437
+ const readiness = buildInitReadiness(input, catalogKey);
438
+ state.initReadiness = { catalogKey: readiness.catalogKey, value: readiness };
439
+ return readiness;
440
+ };
441
+
442
+ const maybeShowInitNotice = (
443
+ ctx: ExtensionContext,
444
+ state: RuntimeSnapshot,
445
+ skills: readonly PiSkill[],
446
+ ): void => {
447
+ if (!ctx.hasUI || skills.length === 0 || retryAfter !== 0) return;
448
+ const readiness = getInitReadiness(state, skills);
449
+ if (readiness.status !== "empty" && readiness.status !== "outdated" && readiness.status !== "incomplete") return;
450
+ const status: InitNoticeStatus = readiness.status;
451
+ const key = buildInitNoticeKey(state.profileKey, INIT_VERSION, status);
452
+ if (shownInitNoticeKeys.has(key)) return;
453
+ shownInitNoticeKeys.add(key);
454
+ try {
455
+ pi.appendEntry(INIT_READINESS_ENTRY_TYPE, { schema: 1, key });
456
+ } catch {
457
+ // The notice is useful even when session metadata cannot be persisted.
458
+ }
459
+ try {
460
+ ctx.ui.notify("Optional setup: run `/skill-optimizer init` to improve skill matching (may incur model costs).", "info");
461
+ } catch {
462
+ // UI notification is cosmetic only.
463
+ }
464
+ };
465
+
466
+ const ensureStatsBase = (ctx: ExtensionContext): { path: string; base: SavedStats } => {
467
+ const path = getStatsFilePath(ctx.cwd);
468
+ let base = statsBase.get(path);
469
+ if (!base) {
470
+ try {
471
+ base = loadStatsSnapshot(path);
472
+ statsBase.set(path, base);
473
+ } catch {
474
+ base = {
475
+ savings: { ...EMPTY_SAVINGS },
476
+ extraction: { ...EMPTY_EXTRACTION_TELEMETRY },
477
+ cache: { ...EMPTY_PROVIDER_CACHE_TELEMETRY },
478
+ };
479
+ statsBase.set(path, base);
480
+ }
481
+ }
482
+ return { path, base };
483
+ };
484
+
485
+ const addPending = (
486
+ path: string,
487
+ savings: SavingsByArea = EMPTY_SAVINGS,
488
+ extraction: ExtractionTelemetry = EMPTY_EXTRACTION_TELEMETRY,
489
+ cache: ProviderCacheTelemetry = EMPTY_PROVIDER_CACHE_TELEMETRY,
490
+ ): void => {
491
+ const current = pendingStats.get(path) ?? emptyPendingStats();
492
+ pendingStats.set(path, {
493
+ savings: addSavings(current.savings, savings),
494
+ extraction: addExtractionTelemetry(current.extraction, extraction),
495
+ cache: addProviderCacheTelemetry(current.cache, cache),
496
+ });
497
+ };
498
+
499
+ const flushStats = (): void => {
500
+ for (const [path, delta] of pendingStats) {
501
+ try {
502
+ statsBase.set(path, saveStatsDeltas(path, delta.savings, delta.extraction, delta.cache));
503
+ pendingStats.delete(path);
504
+ } catch {
505
+ // Keep the complete delta for the next flush.
506
+ }
507
+ }
508
+ };
509
+
510
+ const flushUsage = (ctx?: ExtensionContext): void => {
511
+ for (const [path, delta] of pendingUsage) {
512
+ try {
513
+ const options = snapshot && snapshot.usagePath === path ? usagePruneOptions(snapshot) : undefined;
514
+ const saved = saveUsageDelta(path, delta, options);
515
+ pendingUsage.delete(path);
516
+ if (snapshot?.usagePath === path) snapshot.usage = saved;
517
+ } catch (error) {
518
+ ctx?.ui.notify(`skill-optimizer: usage flush failed (${(error as Error).message})`, "warning");
519
+ }
520
+ }
521
+ };
522
+
523
+ const maybeFlush = (ctx: ExtensionContext, force = false): void => {
524
+ if (!force && Date.now() - lastFlushAt < FLUSH_INTERVAL_MS) return;
525
+ lastFlushAt = Date.now();
526
+ flushUsage(ctx);
527
+ flushStats();
528
+ };
529
+
530
+ const recordSkillUsage = (ctx: ExtensionContext, names: readonly string[]): void => {
531
+ if (names.length === 0) return;
532
+ const state = ensureSnapshot(ctx);
533
+ const now = Date.now();
534
+ state.usage = updateSkillUsage(state.usage, names, now);
535
+ const delta = updateSkillUsage(pendingUsage.get(state.usagePath) ?? {}, names, now);
536
+ pendingUsage.set(state.usagePath, delta);
537
+ };
538
+
539
+ const updateStatus = (ctx: ExtensionContext): void => {
540
+ const denominator = sessionCache.input + sessionCache.cacheRead + sessionCache.cacheWrite;
541
+ const cache = denominator > 0 ? `${Math.round(sessionCache.cacheRead * 100 / denominator)}% cache` : "cache n/a";
542
+ const saved = totalSavings(sessionSaved);
543
+ setSessionStatus(ctx, `AUTO | -${approxK(saved)} tok | ${cache}`);
544
+ };
545
+
546
+ const storeHistoryArtifact = (artifact: HistoryArtifact): void => {
547
+ if (!historyArtifacts.has(artifact.id)) historyOrder.push(artifact.id);
548
+ historyArtifacts.set(artifact.id, artifact);
549
+ while (historyOrder.length > MAX_HISTORY_ARTIFACTS) {
550
+ const id = historyOrder.shift();
551
+ if (id) historyArtifacts.delete(id);
552
+ }
553
+ };
554
+
555
+ const rtkExtensionPresent = (): boolean => {
556
+ if (rtkPresence !== undefined) return rtkPresence;
557
+ try {
558
+ rtkPresence = pi.getCommands().some((command) =>
559
+ isRtkSource(command.name, command.sourceInfo?.path ?? "", command.sourceInfo?.source ?? ""),
560
+ );
561
+ } catch {
562
+ rtkPresence = false;
563
+ }
564
+ return rtkPresence;
565
+ };
566
+
567
+ const rebuildToolCatalog = (state: RuntimeSnapshot): ToolDiscoveryCatalog => {
568
+ permittedToolNames ??= new Set([
569
+ ...pi.getActiveTools().map((name) => name.toLowerCase()),
570
+ ...PROTECTED_RUNTIME_TOOLS,
571
+ ]);
572
+ const discoverable = pi.getAllTools().filter((tool) =>
573
+ permittedToolNames?.has(tool.name.toLowerCase()),
574
+ );
575
+ toolCatalog = buildToolDiscoveryCatalog(discoverable, {
576
+ protect: [...PROTECTED_RUNTIME_TOOLS, ...state.config.alwaysTools],
577
+ });
578
+ return toolCatalog;
579
+ };
580
+
581
+ const activateAdditively = (names: readonly string[]): string[] => {
582
+ const requested = new Set(names.map((name) => name.toLowerCase()));
583
+ const current = new Set(pi.getActiveTools().map((name) => name.toLowerCase()));
584
+ const added: string[] = [];
585
+ for (const tool of pi.getAllTools()) {
586
+ if (!permittedToolNames?.has(tool.name.toLowerCase())) continue;
587
+ if (!requested.has(tool.name.toLowerCase())) continue;
588
+ retainedToolNames.add(tool.name.toLowerCase());
589
+ if (current.has(tool.name.toLowerCase())) continue;
590
+ current.add(tool.name.toLowerCase());
591
+ deactivatedToolNames.delete(tool.name.toLowerCase());
592
+ added.push(tool.name);
593
+ }
594
+ const ordered = pi.getAllTools()
595
+ .map((tool) => tool.name)
596
+ .filter((name) => permittedToolNames?.has(name.toLowerCase()) && current.has(name.toLowerCase()));
597
+ pi.setActiveTools(ordered);
598
+ return added;
599
+ };
600
+
601
+ const restorePermittedTools = (): void => {
602
+ if (deactivatedToolNames.size === 0) {
603
+ toolCatalog = undefined;
604
+ return;
605
+ }
606
+ const active = new Set(pi.getActiveTools().map((name) => name.toLowerCase()));
607
+ for (const name of deactivatedToolNames) active.add(name);
608
+ pi.setActiveTools(
609
+ pi.getAllTools()
610
+ .map((tool) => tool.name)
611
+ .filter((name) => active.has(name.toLowerCase())),
612
+ );
613
+ deactivatedToolNames.clear();
614
+ toolCatalog = undefined;
615
+ };
616
+
617
+ pi.registerTool({
618
+ name: "skill_search",
619
+ label: "Skill Search",
620
+ description: "Search the local skill catalog or load one exact registered skill/resource on demand.",
621
+ promptSnippet: "Search and load full skill instructions on demand",
622
+ parameters: SKILL_SEARCH_PARAMS,
623
+ async execute(_id, params, _signal, _update, ctx) {
624
+ if (!registry) throw new Error("Skill registry is not initialized.");
625
+ const state = ensureSnapshot(ctx);
626
+ if (params.action === "search") {
627
+ if (!params.query) throw new TypeError("query is required for search");
628
+ const page = registry.search(params.query, {
629
+ cursor: params.cursor,
630
+ pageSize: params.pageSize,
631
+ profile: state.profile,
632
+ usagePrior: buildUsagePrior(state.usage, {
633
+ halfLifeDays: state.config.usageHalfLifeDays,
634
+ }),
635
+ never: state.config.excludeSkills,
636
+ fuzzyCandidateLimit: state.config.fuzzyCandidateLimit,
637
+ });
638
+ return { content: [{ type: "text", text: JSON.stringify(page) }], details: { action: "search", total: page.total } };
639
+ }
640
+ if (!params.name) throw new TypeError("name is required");
641
+ if (params.action === "load") {
642
+ const resource = registry.loadExact(params.name);
643
+ recordSkillUsage(ctx, [resource.name]);
644
+ return { content: [{ type: "text", text: resource.content }], details: { action: "load", name: resource.name } };
645
+ }
646
+ if (params.action === "resource") {
647
+ if (!params.path) throw new TypeError("path is required for resource");
648
+ const resource = registry.loadResource(params.name, params.path);
649
+ recordSkillUsage(ctx, [resource.name]);
650
+ return { content: [{ type: "text", text: resource.content }], details: { action: "resource", name: resource.name, relativePath: resource.relativePath } };
651
+ }
652
+ const location = registry.locateExact(params.name);
653
+ recordSkillUsage(ctx, [location.name]);
654
+ return { content: [{ type: "text", text: location.location }], details: { action: "location", name: location.name } };
655
+ },
656
+ });
657
+
658
+ pi.registerTool({
659
+ name: "tool_search",
660
+ label: "Tool Search",
661
+ description: "Search inactive tools locally. Returned tools are activated additively without disabling current tools.",
662
+ promptSnippet: "Search and activate additional tools on demand",
663
+ parameters: TOOL_SEARCH_PARAMS,
664
+ async execute(_id, params, _signal, _update, ctx) {
665
+ const state = ensureSnapshot(ctx);
666
+ const catalog = toolCatalog ?? rebuildToolCatalog(state);
667
+ const page = searchToolDiscoveryCatalog(catalog, params.query, {
668
+ cursor: params.cursor,
669
+ pageSize: Math.min(
670
+ params.pageSize ?? state.config.toolSearchPageSize,
671
+ state.config.toolSearchPageSize,
672
+ ),
673
+ });
674
+ const activated = activateAdditively(page.results.map((result) => result.name));
675
+ return {
676
+ content: [{ type: "text", text: JSON.stringify({ ...page, activated }) }],
677
+ details: { activated, confidence: page.confidence, failOpen: page.failOpen },
678
+ };
679
+ },
680
+ });
681
+
682
+ pi.registerTool({
683
+ name: "retrieve_output",
684
+ label: "Retrieve Output",
685
+ description: "Retrieve exact archived output by an opaque sko: or htr: handle.",
686
+ promptSnippet: "Retrieve exact content hidden behind an output handle",
687
+ parameters: RETRIEVE_OUTPUT_PARAMS,
688
+ async execute(_id, params, _signal, _update, ctx) {
689
+ const output = getOutputConfig(getRuntimeConfig(ctx));
690
+ const archived = params.id.startsWith("sko:")
691
+ ? resolveTemporaryOutput(params.id, { ttlMs: output.archiveTtlMs })
692
+ : historyArtifacts.get(params.id)?.text;
693
+ if (archived === undefined) {
694
+ throw new Error(`Unknown or expired output handle: ${params.id}`);
695
+ }
696
+ const recovered = sliceOutputLines(archived, params.startLine, params.lineCount);
697
+ return {
698
+ content: [{ type: "text", text: recovered.text }],
699
+ details: {
700
+ id: params.id,
701
+ found: true,
702
+ startLine: recovered.startLine,
703
+ lineCount: recovered.lineCount,
704
+ totalLines: recovered.totalLines,
705
+ },
706
+ };
707
+ },
708
+ });
709
+
710
+ const initializeToolSelection = (state: RuntimeSnapshot, query: string): void => {
711
+ const catalog = rebuildToolCatalog(state);
712
+ const retained = new Set([...usedTools, ...retainedToolNames]);
713
+ const plan = planToolActivation(catalog, query, {
714
+ topK: state.config.toolPrefetchMax,
715
+ usedNames: retained,
716
+ });
717
+ const protectedNames = new Set(PROTECTED_RUNTIME_TOOLS.map((name) => name.toLowerCase()));
718
+ const active = new Set(plan.activeNames.map((name) => name.toLowerCase()));
719
+ for (const name of protectedNames) active.add(name);
720
+ deactivatedToolNames.clear();
721
+ for (const name of permittedToolNames ?? []) {
722
+ if (!active.has(name)) deactivatedToolNames.add(name);
723
+ }
724
+ pi.setActiveTools(pi.getAllTools().map((tool) => tool.name).filter((name) =>
725
+ permittedToolNames?.has(name.toLowerCase()) && active.has(name.toLowerCase()),
726
+ ));
727
+ };
728
+
729
+ const initProfile = async (ctx: Parameters<Parameters<ExtensionAPI["registerCommand"]>[1]["handler"]>[1]): Promise<void> => {
730
+ if (!ctx.model) {
731
+ reportInitProgress(ctx, "skill-optimizer: init failed: no model selected", "error");
732
+ return;
733
+ }
734
+ const model = ctx.model;
735
+ const skills = skillInputs(ctx.getSystemPromptOptions().skills ?? []);
736
+ if (skills.length === 0) {
737
+ reportInitProgress(ctx, "skill-optimizer: init failed: no model-invocable skills found", "warning");
738
+ return;
739
+ }
740
+ const paths = getProfilePaths(ctx.cwd);
741
+ let stored: ReturnType<typeof readStoredProfile>;
742
+ let projectStored: ReturnType<typeof readStoredProfile> | undefined;
743
+ try {
744
+ stored = readStoredProfile(paths.global);
745
+ projectStored = paths.project !== paths.global ? readStoredProfile(paths.project) : undefined;
746
+ } catch (error) {
747
+ reportInitProgress(ctx, `skill-optimizer: init failed: profile read failed (${(error as Error).message})`, "error");
748
+ return;
749
+ }
750
+ const baseProfile = projectStored?.exists ? mergeProfiles(stored.profile, projectStored.profile) : stored.profile;
751
+ const storedHashes = projectStored?.exists ? { ...stored.hashes, ...projectStored.hashes } : stored.hashes;
752
+ const forceFull = !stored.exists
753
+ || stored.initVersion !== INIT_VERSION
754
+ || (!!projectStored?.exists && projectStored.initVersion !== INIT_VERSION);
755
+ const refs = skills.map((skill) => ({ name: skill.name, description: skill.description }));
756
+ const { changed, removed, hashes } = diffSkills(refs, forceFull ? {} : storedHashes);
757
+ const projectNames = new Set(skills
758
+ .filter((skill) => skill.sourceInfo.scope === "project")
759
+ .map((skill) => skill.name));
760
+ const scopeRepair = needsProfileScopeRepair({
761
+ paths,
762
+ projectNames,
763
+ global: stored,
764
+ project: projectStored,
765
+ });
766
+ if (!forceFull && changed.length === 0 && removed.length === 0 && !scopeRepair) {
767
+ reportInitProgress(ctx, `skill-optimizer: init done: profile already current (${skills.length} skills)`);
768
+ return;
769
+ }
770
+
771
+ const batchLimits = {
772
+ maxSkills: INIT_BATCH_MAX_SKILLS,
773
+ maxUtf8Bytes: INIT_BATCH_MAX_UTF8_BYTES,
774
+ maxAttempts: INIT_BATCH_MAX_ATTEMPTS,
775
+ };
776
+ const changedNames = new Set(changed);
777
+ const targetSkills = refs.filter((skill) => changedNames.has(skill.name));
778
+ const batches = targetSkills.length > 0 ? batchSkillsByWeight(targetSkills, batchLimits) : [];
779
+ const invalidatedNames = new Set([...removed, ...changed]);
780
+ const prunedBase = pruneProfileNames(forceFull ? EMPTY_PROFILE : baseProfile, invalidatedNames);
781
+ let partial = EMPTY_PROFILE;
782
+ let appliedChanged: string[] = [];
783
+ let savedCount = 0;
784
+ let checkpointedProfile: SkillOptimizerProfile | undefined;
785
+ let checkpointedApplied = new Set<string>();
786
+
787
+ const persist = (nextPartial: SkillOptimizerProfile, applied: readonly string[]): SkillOptimizerProfile => {
788
+ const appliedSet = new Set(applied);
789
+ const profile = mergeIncrementalProfile(prunedBase, nextPartial, applied);
790
+ const pending = changed.filter((name) => !appliedSet.has(name));
791
+ writeProfileFiles(buildInitProfileWrites({
792
+ paths,
793
+ profile,
794
+ hashes: computeFinalHashes(hashes, pending),
795
+ skillCount: skills.length,
796
+ projectNames,
797
+ globalRevision: stored.revision,
798
+ projectRevision: projectStored?.revision ?? null,
799
+ projectExists: !!projectStored?.exists,
800
+ }), INIT_VERSION);
801
+ stored = readStoredProfile(paths.global);
802
+ if (projectStored !== undefined) projectStored = readStoredProfile(paths.project);
803
+ snapshot = undefined;
804
+ retryAfter = 0;
805
+ savedCount = appliedSet.size;
806
+ checkpointedProfile = profile;
807
+ checkpointedApplied = appliedSet;
808
+ return profile;
809
+ };
810
+
811
+ const persistError = (error: unknown): string => error instanceof ConcurrentFileUpdateError
812
+ ? "profile changed concurrently; run init again"
813
+ : (error as Error).message;
814
+
815
+ reportInitProgress(
816
+ ctx,
817
+ `skill-optimizer: init starting (${skills.length} skills, ${batches.length} batch${batches.length === 1 ? "" : "es"}${removed.length > 0 ? `, ${removed.length} removed` : ""})`,
818
+ );
819
+
820
+ if (!forceFull && removed.length > 0) {
821
+ try {
822
+ persist(EMPTY_PROFILE, []);
823
+ } catch (error) {
824
+ reportInitProgress(ctx, `skill-optimizer: init failed: profile not saved (${persistError(error)})`, "error");
825
+ return;
826
+ }
827
+ reportInitProgress(ctx, `skill-optimizer: init saved ${removed.length} removal(s)`, "info", false);
828
+ }
829
+
830
+ if (changed.length > 0) {
831
+ const systemPrompt = INIT_PROFILE_PROMPT;
832
+ let result: Awaited<ReturnType<typeof generateProfileInBatches>>;
833
+ let lastBatchFailure: string | undefined;
834
+ try {
835
+ result = await generateProfileInBatches(
836
+ targetSkills,
837
+ batchLimits,
838
+ async (batch, index, total, attempt) => {
839
+ const label = `${index + 1}/${total}`;
840
+ reportInitProgress(
841
+ ctx,
842
+ `skill-optimizer: init ${label} starting (${batch.length} skills${attempt > 1 ? `, attempt ${attempt}` : ""})`,
843
+ "info",
844
+ false,
845
+ );
846
+ const message: UserMessage = {
847
+ role: "user",
848
+ content: [{
849
+ type: "text",
850
+ text: JSON.stringify({
851
+ skills: batch.map((skill) => ({
852
+ name: skill.name,
853
+ description: skill.description,
854
+ })),
855
+ }),
856
+ }],
857
+ timestamp: Date.now(),
858
+ };
859
+ try {
860
+ const response = await ctx.modelRegistry.complete(
861
+ model,
862
+ { systemPrompt, messages: [message] },
863
+ );
864
+ const outcome = interpretBatchResponse(response);
865
+ if (outcome.status === "failed") {
866
+ return outcome;
867
+ }
868
+ return {
869
+ profile: mergeIncrementalProfile(EMPTY_PROFILE, outcome.profile, batch.map((skill) => skill.name)),
870
+ processedSkills: outcome.processedSkills,
871
+ };
872
+ } catch (error) {
873
+ return {
874
+ status: "failed",
875
+ reason: `failed (${(error as Error).message})`,
876
+ retryable: isRetryableBatchException(error),
877
+ };
878
+ }
879
+ },
880
+ async (event) => {
881
+ if (event.type === "rejected") {
882
+ const willRetry = event.retryable && event.attempt < batchLimits.maxAttempts;
883
+ lastBatchFailure = `${event.index + 1}/${event.total} attempt ${event.attempt}: ${event.reason}`;
884
+ reportInitProgress(
885
+ ctx,
886
+ `skill-optimizer: init ${event.index + 1}/${event.total} attempt ${event.attempt} ${event.reason}${willRetry ? "; retrying" : ""}`,
887
+ "warning",
888
+ !willRetry,
889
+ );
890
+ return;
891
+ }
892
+ const applied = changed.filter((name) => event.applied.has(name));
893
+ persist(event.partial, applied);
894
+ appliedChanged = applied;
895
+ partial = event.partial;
896
+ reportInitProgress(
897
+ ctx,
898
+ `skill-optimizer: init ${event.index + 1}/${event.total} saved (${event.batchNames.length} skills, ${applied.length} written)`,
899
+ "info",
900
+ false,
901
+ );
902
+ },
903
+ );
904
+ } catch (error) {
905
+ reportInitProgress(
906
+ ctx,
907
+ `skill-optimizer: init failed: profile not saved (${persistError(error)})${savedCount > 0 ? `; ${savedCount} skills already written` : ""}`,
908
+ "error",
909
+ );
910
+ return;
911
+ }
912
+ if (!result) {
913
+ let profile = checkpointedProfile;
914
+ if (!profile) {
915
+ try {
916
+ profile = persist(EMPTY_PROFILE, []);
917
+ } catch (error) {
918
+ reportInitProgress(
919
+ ctx,
920
+ `skill-optimizer: init failed: profile not saved (${persistError(error)})`,
921
+ "error",
922
+ );
923
+ return;
924
+ }
925
+ }
926
+ reportInitProgress(
927
+ ctx,
928
+ `skill-optimizer: init failed: every batch failed (${lastBatchFailure ?? "unknown batch failure"}); ${changed.length} changed skill(s) left inactive for retry; ${profileSummary(profile)}`,
929
+ "error",
930
+ );
931
+ return;
932
+ }
933
+ appliedChanged = changed.filter((name) => result.applied.has(name));
934
+ partial = result.partial;
935
+ }
936
+
937
+ let profile: SkillOptimizerProfile;
938
+ const finalApplied = new Set(appliedChanged);
939
+ const checkpointIsFinal = checkpointedProfile !== undefined
940
+ && checkpointedApplied.size === finalApplied.size
941
+ && [...finalApplied].every((name) => checkpointedApplied.has(name));
942
+ if (checkpointIsFinal && checkpointedProfile) {
943
+ profile = checkpointedProfile;
944
+ } else {
945
+ try {
946
+ profile = persist(partial, appliedChanged);
947
+ } catch (error) {
948
+ reportInitProgress(
949
+ ctx,
950
+ `skill-optimizer: init failed: profile not saved (${persistError(error)})${savedCount > 0 ? `; ${savedCount} skills already written` : ""}`,
951
+ "error",
952
+ );
953
+ return;
954
+ }
955
+ }
956
+ const appliedChangedSet = new Set(appliedChanged);
957
+ const failedChanged = changed.filter((name) => !appliedChangedSet.has(name));
958
+ const newCount = appliedChanged.filter((name) => !(name in storedHashes)).length;
959
+ reportInitProgress(
960
+ ctx,
961
+ `skill-optimizer: init done v${INIT_VERSION} +${newCount}, ~${appliedChanged.length - newCount}, -${removed.length}, retry ${failedChanged.length}; ${profileSummary(profile)}`,
962
+ failedChanged.length > 0 ? "warning" : "info",
963
+ );
964
+ };
965
+
966
+ pi.on("session_start", (_event, ctx) => {
967
+ hasStatus = false;
968
+ lastStatusText = undefined;
969
+ shownInitNoticeKeys.clear();
970
+ try {
971
+ for (const key of readInitNoticeKeys(ctx.sessionManager.getBranch())) shownInitNoticeKeys.add(key);
972
+ } catch {
973
+ // Dedupe recovery is best-effort; the marker is session metadata only.
974
+ }
975
+ restorePermittedTools();
976
+ snapshot = undefined;
977
+ cachedConfig = undefined;
978
+ retryAfter = 0;
979
+ registry = undefined;
980
+ toolCatalog = undefined;
981
+ permittedToolNames = undefined;
982
+ deactivatedToolNames.clear();
983
+ retainedToolNames.clear();
984
+ latestPrompt = "";
985
+ lastSelected = [];
986
+ rtkPresence = undefined;
987
+ usedTools.clear();
988
+ for (const entry of ctx.sessionManager.getBranch()) {
989
+ const message = (entry as { message?: { role?: unknown; toolName?: unknown } }).message;
990
+ if (
991
+ message?.role === "toolResult"
992
+ && typeof message.toolName === "string"
993
+ ) {
994
+ usedTools.add(message.toolName.toLowerCase());
995
+ }
996
+ }
997
+ seenEvidence.clear();
998
+ seenProviderMessages.clear();
999
+ historyArtifacts.clear();
1000
+ historyOrder.length = 0;
1001
+ sessionSaved = { ...EMPTY_SAVINGS };
1002
+ sessionExtraction = { ...EMPTY_EXTRACTION_TELEMETRY };
1003
+ sessionCache = { ...EMPTY_PROVIDER_CACHE_TELEMETRY };
1004
+ lastFlushAt = 0;
1005
+ const config = getRuntimeConfig(ctx, true);
1006
+ cleanupTemporaryOutputs({ ttlMs: getOutputConfig(config).archiveTtlMs });
1007
+ try {
1008
+ const created = ensureGlobalConfigTemplate();
1009
+ if (created) ctx.ui.notify(`skill-optimizer: wrote default config to ${created}`, "info");
1010
+ } catch {
1011
+ // Convenience only.
1012
+ }
1013
+ });
1014
+
1015
+ pi.on("before_agent_start", (event, ctx) => {
1016
+ latestPrompt = event.prompt;
1017
+ if (isDisabled(ctx.cwd)) {
1018
+ restorePermittedTools();
1019
+ return;
1020
+ }
1021
+ const config = getRuntimeConfig(ctx, true);
1022
+ const providers = config.providers.length > 0 ? config.providers : undefined;
1023
+ if (providers && (!ctx.model || !providers.includes(ctx.model.provider))) {
1024
+ restorePermittedTools();
1025
+ return;
1026
+ }
1027
+ const state = ensureSnapshot(ctx);
1028
+ try {
1029
+ const skills = skillInputs(event.systemPromptOptions.skills ?? []);
1030
+ registry = makeSkillRegistry(skills);
1031
+ initializeToolSelection(state, event.prompt);
1032
+ maybeShowInitNotice(ctx, state, skills);
1033
+ updateStatus(ctx);
1034
+ return;
1035
+ } catch (error) {
1036
+ ctx.ui.notify(`skill-optimizer: AUTO prefetch failed open (${(error as Error).message})`, "warning");
1037
+ return;
1038
+ }
1039
+ });
1040
+
1041
+ pi.on("before_provider_request", (event, ctx) => {
1042
+ if (isDisabled(ctx.cwd)) return;
1043
+ const config = getRuntimeConfig(ctx);
1044
+ const providers = config.providers.length > 0 ? config.providers : undefined;
1045
+ if (providers && ctx.model && !providers.includes(ctx.model.provider)) return;
1046
+ const state = ensureSnapshot(ctx);
1047
+ const transformed = optimizePayload(event.payload, {
1048
+ ...state.config,
1049
+ profile: state.profile,
1050
+ usagePrior: buildUsagePrior(state.usage, {
1051
+ halfLifeDays: state.config.usageHalfLifeDays,
1052
+ }),
1053
+ });
1054
+ lastRemovedChars = transformed.removedChars;
1055
+ lastSelected = transformed.selected;
1056
+ lastBudget = {
1057
+ usedChars: transformed.budget.usedChars,
1058
+ requestedChars: transformed.budget.requestedChars,
1059
+ nameOnlyCount: transformed.budget.nameOnlyCount,
1060
+ };
1061
+ if (transformed.removedChars > 0) {
1062
+ sessionSaved.skills += transformed.removedChars;
1063
+ addPending(getStatsFilePath(ctx.cwd), { skills: transformed.removedChars, tools: 0, output: 0 });
1064
+ }
1065
+ if (registry) {
1066
+ const evidence = collectSkillUsageEvidence(event.payload, registry.skills.map((skill) => skill.name)).slice(-MAX_SEEN_EVIDENCE);
1067
+ const names: string[] = [];
1068
+ for (const item of evidence) {
1069
+ if (seenEvidence.has(item.key)) continue;
1070
+ seenEvidence.add(item.key);
1071
+ names.push(item.name);
1072
+ while (seenEvidence.size > MAX_SEEN_EVIDENCE) {
1073
+ const oldest = seenEvidence.values().next().value as string | undefined;
1074
+ if (oldest) seenEvidence.delete(oldest);
1075
+ }
1076
+ }
1077
+ recordSkillUsage(ctx, names);
1078
+ }
1079
+ maybeFlush(ctx);
1080
+ updateStatus(ctx);
1081
+ return transformed.next === event.payload ? undefined : transformed.next;
1082
+ });
1083
+
1084
+ pi.on("context", (event, ctx) => {
1085
+ if (isDisabled(ctx.cwd)) return;
1086
+ const config = getRuntimeConfig(ctx);
1087
+ const providers = config.providers.length > 0 ? config.providers : undefined;
1088
+ if (providers && (!ctx.model || !providers.includes(ctx.model.provider))) return;
1089
+ const state = ensureSnapshot(ctx);
1090
+ const positions: number[] = [];
1091
+ const results: Array<{ content: unknown; isError?: boolean }> = [];
1092
+ event.messages.forEach((message, index) => {
1093
+ if ((message as { role?: unknown }).role !== "toolResult") return;
1094
+ positions.push(index);
1095
+ results.push(message as unknown as { content: unknown; isError?: boolean });
1096
+ });
1097
+ const deduplicated = deduplicateToolResultHistory(results, {
1098
+ minBytes: state.config.historyDedupMinBytes,
1099
+ });
1100
+ if (deduplicated.next === results) return;
1101
+ for (const artifact of deduplicated.artifacts) storeHistoryArtifact(artifact);
1102
+ const messages = event.messages.slice();
1103
+ deduplicated.next.forEach((message, index) => {
1104
+ messages[positions[index]] = message as typeof messages[number];
1105
+ });
1106
+ return { messages };
1107
+ });
1108
+
1109
+ pi.on("message_end", (event, ctx) => {
1110
+ const message = event.message as {
1111
+ role?: unknown;
1112
+ responseId?: unknown;
1113
+ provider?: unknown;
1114
+ model?: unknown;
1115
+ timestamp?: unknown;
1116
+ usage?: {
1117
+ input?: unknown;
1118
+ output?: unknown;
1119
+ cacheRead?: unknown;
1120
+ cacheWrite?: unknown;
1121
+ cost?: { total?: unknown };
1122
+ };
1123
+ };
1124
+ if (message.role !== "assistant" || !message.usage) return;
1125
+ if (isDisabled(ctx.cwd)) return;
1126
+ const config = getRuntimeConfig(ctx);
1127
+ const providers = config.providers.length > 0 ? config.providers : undefined;
1128
+ const provider =
1129
+ typeof message.provider === "string" ? message.provider : ctx.model?.provider;
1130
+ if (providers && (!provider || !providers.includes(provider))) return;
1131
+ const signature = typeof message.responseId === "string"
1132
+ ? message.responseId
1133
+ : JSON.stringify([message.provider, message.model, message.timestamp, message.usage]);
1134
+ if (seenProviderMessages.has(signature)) return;
1135
+ seenProviderMessages.add(signature);
1136
+ while (seenProviderMessages.size > MAX_SEEN_EVIDENCE) {
1137
+ const oldest = seenProviderMessages.values().next().value as string | undefined;
1138
+ if (oldest) seenProviderMessages.delete(oldest);
1139
+ }
1140
+ const delta = providerCacheDelta(message.usage);
1141
+ sessionCache = addProviderCacheTelemetry(sessionCache, delta);
1142
+ addPending(getStatsFilePath(ctx.cwd), EMPTY_SAVINGS, EMPTY_EXTRACTION_TELEMETRY, delta);
1143
+ maybeFlush(ctx);
1144
+ updateStatus(ctx);
1145
+ });
1146
+
1147
+ pi.on("tool_result", async (event, ctx) => {
1148
+ usedTools.add(event.toolName.toLowerCase());
1149
+ if (PROTECTED_RUNTIME_TOOLS.includes(event.toolName as typeof PROTECTED_RUNTIME_TOOLS[number])) return;
1150
+ if (isDisabled(ctx.cwd) || event.isError) return;
1151
+ const config = getRuntimeConfig(ctx);
1152
+ const providers = config.providers.length > 0 ? config.providers : undefined;
1153
+ if (providers && (!ctx.model || !providers.includes(ctx.model.provider))) return;
1154
+ const output = getOutputConfig(config);
1155
+ if (output.mode === "off") return;
1156
+ const details = event.details && typeof event.details === "object"
1157
+ ? event.details as Record<string, unknown>
1158
+ : {};
1159
+ const rtkHandled = details.rtkHandled === true || details.reducedBy === "rtk";
1160
+ const command = typeof event.input.command === "string" ? event.input.command : "";
1161
+ let changed = false;
1162
+ const content = [];
1163
+ for (const block of event.content) {
1164
+ if (block.type !== "text") {
1165
+ content.push(block);
1166
+ continue;
1167
+ }
1168
+ const text = block.text;
1169
+ if (!shouldReduceToolResult({
1170
+ toolName: event.toolName,
1171
+ outputTools: output.tools,
1172
+ rtkHandled,
1173
+ text,
1174
+ })) {
1175
+ content.push(block);
1176
+ continue;
1177
+ }
1178
+ if (text.split(/\r?\n/).length <= output.maxLines && utf8ByteLength(text) <= output.maxBytes) {
1179
+ content.push(block);
1180
+ continue;
1181
+ }
1182
+
1183
+ let body: string | undefined;
1184
+ let strategy = "";
1185
+ let extractionOutcome: Exclude<keyof ExtractionTelemetry, "attempts"> | undefined;
1186
+ const columnar = reduceJsonArrayColumnar(text, {
1187
+ minSavingsBytes: output.minSavingsBytes,
1188
+ minSavingsRatio: output.minSavingsRatio,
1189
+ });
1190
+ if (columnar.reduced) {
1191
+ body = columnar.text;
1192
+ strategy = "columnar";
1193
+ } else if (
1194
+ output.mode === "extract"
1195
+ && output.model.trim().length > 0
1196
+ && utf8ByteLength(text) >= output.extractMinBytes
1197
+ && !isExcludedCommand(command, output.extractExclude)
1198
+ ) {
1199
+ const extraction = await tryExtractOutput(ctx, output.model, latestPrompt, command, text);
1200
+ if (extraction) {
1201
+ sessionCache = addProviderCacheTelemetry(sessionCache, extraction.telemetry);
1202
+ addPending(
1203
+ getStatsFilePath(ctx.cwd),
1204
+ EMPTY_SAVINGS,
1205
+ EMPTY_EXTRACTION_TELEMETRY,
1206
+ extraction.telemetry,
1207
+ );
1208
+ }
1209
+ if (!extraction?.text) {
1210
+ const smart = reduceOutput(text, {
1211
+ maxLines: output.maxLines,
1212
+ maxBytes: output.maxBytes,
1213
+ minSavingsBytes: output.minSavingsBytes,
1214
+ minSavingsRatio: output.minSavingsRatio,
1215
+ });
1216
+ if (smart.reduced) body = smart.text;
1217
+ strategy = smart.reduced ? "smart" : "original";
1218
+ extractionOutcome = "fallbackError";
1219
+ } else {
1220
+ const validated = validateExtractedOutput(text, extraction.text, {
1221
+ smartOptions: {
1222
+ maxLines: output.maxLines,
1223
+ maxBytes: output.maxBytes,
1224
+ minSavingsBytes: output.minSavingsBytes,
1225
+ minSavingsRatio: output.minSavingsRatio,
1226
+ },
1227
+ minSavingsBytes: output.minSavingsBytes,
1228
+ minSavingsRatio: output.minSavingsRatio,
1229
+ });
1230
+ if (validated.reduced) body = validated.text;
1231
+ strategy = validated.strategy;
1232
+ if (validated.strategy === "extract") extractionOutcome = "accepted";
1233
+ else {
1234
+ const reason: ExtractRejectionReason | undefined = validated.rejectionReason;
1235
+ extractionOutcome = reason === "insufficient-benefit"
1236
+ ? "fallbackSavings"
1237
+ : reason === "empty-extraction"
1238
+ ? "fallbackError"
1239
+ : "fallbackEvidence";
1240
+ }
1241
+ }
1242
+ } else {
1243
+ const smart = reduceOutput(text, {
1244
+ maxLines: output.maxLines,
1245
+ maxBytes: output.maxBytes,
1246
+ minSavingsBytes: output.minSavingsBytes,
1247
+ minSavingsRatio: output.minSavingsRatio,
1248
+ });
1249
+ if (smart.reduced) body = smart.text;
1250
+ strategy = smart.reduced ? "smart" : "original";
1251
+ }
1252
+
1253
+ if (extractionOutcome) {
1254
+ const telemetry = { ...EMPTY_EXTRACTION_TELEMETRY, attempts: 1 };
1255
+ telemetry[extractionOutcome] = 1;
1256
+ sessionExtraction = addExtractionTelemetry(sessionExtraction, telemetry);
1257
+ addPending(getStatsFilePath(ctx.cwd), EMPTY_SAVINGS, telemetry);
1258
+ }
1259
+ if (!body) {
1260
+ content.push(block);
1261
+ continue;
1262
+ }
1263
+ const placeholder = `sko:${"0".repeat(43)}`;
1264
+ const candidate = `${body}\n[skill-optimizer:${strategy}; full=${placeholder}]`;
1265
+ const fromBytes = utf8ByteLength(text);
1266
+ const toBytes = utf8ByteLength(candidate);
1267
+ if (!hasMinimumSavings(fromBytes, toBytes, output.minSavingsBytes, output.minSavingsRatio)) {
1268
+ content.push(block);
1269
+ continue;
1270
+ }
1271
+ const archive = saveTemporaryOutput(text, { ttlMs: output.archiveTtlMs });
1272
+ if (!archive) {
1273
+ content.push(block);
1274
+ continue;
1275
+ }
1276
+ const rendered = `${body}\n[skill-optimizer:${strategy}; full=${archive}]`;
1277
+ changed = true;
1278
+ const removed = Math.max(0, text.length - rendered.length);
1279
+ sessionSaved.output += removed;
1280
+ addPending(getStatsFilePath(ctx.cwd), { skills: 0, tools: 0, output: removed });
1281
+ content.push({ ...block, text: rendered });
1282
+ }
1283
+ maybeFlush(ctx);
1284
+ updateStatus(ctx);
1285
+ return changed ? { content } : undefined;
1286
+ });
1287
+
1288
+ pi.on("session_shutdown", (_event, ctx) => {
1289
+ maybeFlush(ctx, true);
1290
+ cleanupTemporaryOutputs({ ttlMs: getOutputConfig(getRuntimeConfig(ctx)).archiveTtlMs });
1291
+ restorePermittedTools();
1292
+ historyArtifacts.clear();
1293
+ historyOrder.length = 0;
1294
+ setSessionStatus(ctx, undefined);
1295
+ });
1296
+
1297
+ pi.registerCommand("skill-optimizer", {
1298
+ description: "AUTO optimizer status; subcommands: init, audit",
1299
+ handler: async (args, ctx) => {
1300
+ getRuntimeConfig(ctx, true);
1301
+ const command = args.trim().toLowerCase();
1302
+ if (command === "init") {
1303
+ try {
1304
+ await initProfile(ctx);
1305
+ } finally {
1306
+ updateStatus(ctx);
1307
+ }
1308
+ return;
1309
+ }
1310
+ const skills = skillInputs(ctx.getSystemPromptOptions().skills ?? []);
1311
+ if (command === "audit") {
1312
+ const report = auditSkillDescriptions(skills.map((skill) => ({
1313
+ name: skill.name,
1314
+ description: skill.description,
1315
+ location: "",
1316
+ })));
1317
+ const issues = report.issues.slice(0, 30).map((issue) =>
1318
+ ` ${issue.name}: ${issue.code}${issue.relatedSkill ? ` (like ${issue.relatedSkill})` : ""}`);
1319
+ ctx.ui.notify([
1320
+ `skill-optimizer audit: ${report.issueCount} issue(s) across ${report.skillCount} skills`,
1321
+ ` long ${report.counts.too_long}, empty ${report.counts.near_empty}, routing ${report.counts.missing_routing}, duplicate ${report.counts.duplicate_description}`,
1322
+ ` estimated source reduction: ${report.estimatedReducibleChars} chars`,
1323
+ ...issues,
1324
+ ...(report.issues.length > issues.length ? [` ... ${report.issues.length - issues.length} more`] : []),
1325
+ ].join("\n"), report.issueCount > 0 ? "warning" : "info");
1326
+ return;
1327
+ }
1328
+
1329
+ const state = ensureSnapshot(ctx, true);
1330
+ const readiness = getInitReadiness(state, skills);
1331
+ maybeFlush(ctx, true);
1332
+ const { path: statsPath, base } = ensureStatsBase(ctx);
1333
+ const saved = addSavings(base.savings, pendingStats.get(statsPath)?.savings ?? EMPTY_SAVINGS);
1334
+ const cache = addProviderCacheTelemetry(base.cache, pendingStats.get(statsPath)?.cache ?? EMPTY_PROVIDER_CACHE_TELEMETRY);
1335
+ const prior = Object.entries(buildUsagePrior(state.usage, {
1336
+ halfLifeDays: state.config.usageHalfLifeDays,
1337
+ })).slice(0, 8).map(([name]) => name);
1338
+ const configPaths = getConfigPaths(ctx.cwd);
1339
+ const profilePaths = getProfilePaths(ctx.cwd);
1340
+ const lines = [
1341
+ "pi-skill-optimizer",
1342
+ ` enabled: ${isDisabled(ctx.cwd) ? "no" : "yes"}`,
1343
+ " skills mode: AUTO (stable TSV + skill_search resolver)",
1344
+ ` catalog budget:${lastBudget.usedChars}/${lastBudget.requestedChars} chars, ${lastBudget.nameOnlyCount} name-only`,
1345
+ ` tools: ${pi.getActiveTools().length}/${pi.getAllTools().length} active; tool_search additive`,
1346
+ ` output: ${getOutputConfig(state.config).mode}; RTK ${rtkExtensionPresent() ? "detected per result" : "not detected"}`,
1347
+ ` scope: ${state.config.providers.join(", ") || "all providers"}`,
1348
+ ` init readiness: ${retryAfter !== 0 ? "unavailable (profile could not be read)" : readiness.skillCount === 0 ? "not available (no model-invocable skills)" : initReadinessSummary(readiness)}`,
1349
+ ` profile: ${profileSummary(state.profile)}`,
1350
+ ` critical: ${state.profile.critical.join(", ") || "none"}`,
1351
+ ` usage prior: ${prior.join(", ") || "none"} (${Object.keys(state.usage).length} tracked)`,
1352
+ ` cache lifetime: requests ${cache.requests}, input ${cache.input}, output ${cache.output}, read ${cache.cacheRead}, write ${cache.cacheWrite}, cost ${cache.totalCost.toFixed(4)}`,
1353
+ ` saved session: ${totalSavings(sessionSaved)} chars (~${approxK(totalSavings(sessionSaved))} tok)`,
1354
+ ` saved lifetime: ${totalSavings(saved)} chars (~${approxK(totalSavings(saved))} tok)`,
1355
+ ` last request: ${lastRemovedChars} chars; prefetch ${lastSelected.join(", ") || "none"}`,
1356
+ ` config: ${fileExists(configPaths.project) ? configPaths.project : configPaths.global}`,
1357
+ ` profile files: ${profilePaths.global}${fileExists(profilePaths.project) ? `, ${profilePaths.project}` : ""}`,
1358
+ ];
1359
+ ctx.ui.notify(lines.join("\n"), "info");
1360
+ },
1361
+ });
1362
+ }