@nijaru/pi-provider-compaction 0.0.10 → 0.0.12
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/README.md +4 -5
- package/cached-summary.ts +1 -2
- package/dist/index.js +17 -69
- package/index.ts +20 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,15 +39,14 @@ The summary request extends the captured logical prompt, rather than serializing
|
|
|
39
39
|
```json
|
|
40
40
|
{
|
|
41
41
|
"providerCompaction": {
|
|
42
|
-
"cachedSummaries": true
|
|
43
|
-
"outcomeNotifications": false
|
|
42
|
+
"cachedSummaries": true
|
|
44
43
|
}
|
|
45
44
|
}
|
|
46
45
|
```
|
|
47
46
|
|
|
48
47
|
These are the defaults on hosts exposing Pi's `getSettings` API; older hosts retain native compaction with cached summaries disabled. Optional `cacheRetention` accepts `"short"`, `"long"`, or `"none"`; omission inherits Pi/provider behavior. Retention is applied per request, not through process-wide environment changes. A longer tier can increase cache-write charges and only works where the adapter/backend supports it.
|
|
49
48
|
|
|
50
|
-
A cache hit is best effort, not a promised saving.
|
|
49
|
+
A cache hit is best effort, not a promised saving. Expiry estimates use the model's declared `promptCache` lifetime for the active tier, measured conservatively from request dispatch. Missing lifetimes remain unknown: DeepSeek, for example, [documents best-effort eviction over hours to days](https://api-docs.deepseek.com/guides/kv_cache/), not a predictable expiry. Pi's successful warmer requests refresh the observed cycle and estimated expiry.
|
|
51
50
|
|
|
52
51
|
With [pi-compactor's opt-in idle expiry policy](https://github.com/nijaru/pi-compactor#cache-aware-idle-compaction), compaction can run before an estimated expiry. Both extensions share a live ownership/input guard, checked before paid summary and native-checkpoint stages regardless of extension order. A failed or revoked cached-only attempt cancels instead of starting an ordinary summary. It does not start a continuation.
|
|
53
52
|
|
|
@@ -77,9 +76,9 @@ Legacy v1/v2 whole-history checkpoints remain inspectable but are never reinterp
|
|
|
77
76
|
|
|
78
77
|
## Diagnose portable fallback
|
|
79
78
|
|
|
80
|
-
|
|
79
|
+
Committed compaction entries retain the selected path, elapsed time, reported usage, and estimated cost. Portable fallback appends a `pi-provider-compaction:diagnostic` session entry with a fixed reason. Cancelled attempts with reported usage retain that usage in the same diagnostic record; without a saved checkpoint, Pi's extension API cannot include it in compaction totals. Diagnostics stay out of model context and never include request content, credentials, or backend error text.
|
|
81
80
|
|
|
82
|
-
These
|
|
81
|
+
These records describe attempts made while the extension was loaded; they cannot explain earlier compactions retroactively. A committed native checkpoint does not by itself prove that a later request replayed it.
|
|
83
82
|
|
|
84
83
|
## Generic compaction-model override
|
|
85
84
|
|
package/cached-summary.ts
CHANGED
|
@@ -17,13 +17,12 @@ export interface CacheState {
|
|
|
17
17
|
cachedTokens: number;
|
|
18
18
|
inputTokens: number;
|
|
19
19
|
}
|
|
20
|
-
export interface CacheConfig { cachedSummaries: boolean; cacheRetention?: CacheRetention
|
|
20
|
+
export interface CacheConfig { cachedSummaries: boolean; cacheRetention?: CacheRetention }
|
|
21
21
|
export function cacheConfig(settings: unknown): CacheConfig {
|
|
22
22
|
const raw = isObject(settings) && isObject(settings.providerCompaction) ? settings.providerCompaction : {};
|
|
23
23
|
return {
|
|
24
24
|
cachedSummaries: raw.cachedSummaries !== false,
|
|
25
25
|
cacheRetention: raw.cacheRetention === "long" || raw.cacheRetention === "short" || raw.cacheRetention === "none" ? raw.cacheRetention : undefined,
|
|
26
|
-
outcomeNotifications: raw.outcomeNotifications === true,
|
|
27
26
|
};
|
|
28
27
|
}
|
|
29
28
|
export function supportsCachedSummary(model: Model<any> | undefined): boolean {
|
package/dist/index.js
CHANGED
|
@@ -1591,8 +1591,7 @@ function cacheConfig(settings) {
|
|
|
1591
1591
|
const raw = isObject(settings) && isObject(settings.providerCompaction) ? settings.providerCompaction : {};
|
|
1592
1592
|
return {
|
|
1593
1593
|
cachedSummaries: raw.cachedSummaries !== false,
|
|
1594
|
-
cacheRetention: raw.cacheRetention === "long" || raw.cacheRetention === "short" || raw.cacheRetention === "none" ? raw.cacheRetention : undefined
|
|
1595
|
-
outcomeNotifications: raw.outcomeNotifications === true
|
|
1594
|
+
cacheRetention: raw.cacheRetention === "long" || raw.cacheRetention === "short" || raw.cacheRetention === "none" ? raw.cacheRetention : undefined
|
|
1596
1595
|
};
|
|
1597
1596
|
}
|
|
1598
1597
|
function supportsCachedSummary(model) {
|
|
@@ -2257,7 +2256,7 @@ function supportedRouteOptions(model, options) {
|
|
|
2257
2256
|
return !["AZURE_OPENAI_BASE_URL", "AZURE_OPENAI_RESOURCE_NAME", "AZURE_OPENAI_API_VERSION", "AZURE_OPENAI_DEPLOYMENT_NAME_MAP"].some((key) => process.env[key] !== undefined && options.env?.[key] === undefined);
|
|
2258
2257
|
}
|
|
2259
2258
|
function pi_provider_compaction_default(pi) {
|
|
2260
|
-
const configuration = () => typeof pi.getSettings === "function" ? cacheConfig(pi.getSettings()) : { cachedSummaries: false,
|
|
2259
|
+
const configuration = () => typeof pi.getSettings === "function" ? cacheConfig(pi.getSettings()) : { cachedSummaries: false, cacheRetention: undefined };
|
|
2261
2260
|
const secret = randomBytes(32);
|
|
2262
2261
|
const routeDigest = (value) => createHmac("sha256", secret).update(JSON.stringify(value)).digest("hex");
|
|
2263
2262
|
let runtimeId = randomUUID2();
|
|
@@ -2274,30 +2273,10 @@ function pi_provider_compaction_default(pi) {
|
|
|
2274
2273
|
let unregisterCleanup;
|
|
2275
2274
|
let generation = 0;
|
|
2276
2275
|
let requestSequence = 0;
|
|
2277
|
-
let captureStatus = "no request observed";
|
|
2278
|
-
let compactionStatus = "not attempted in this runtime";
|
|
2279
2276
|
let fallbackReason;
|
|
2280
|
-
const captureRejected = (reason) => {
|
|
2281
|
-
captureStatus = reason;
|
|
2282
|
-
};
|
|
2283
2277
|
const fallback = (reason) => {
|
|
2284
2278
|
fallbackReason = reason;
|
|
2285
|
-
compactionStatus = `portable: ${reason}`;
|
|
2286
2279
|
};
|
|
2287
|
-
pi.registerCommand("provider-compaction-status", {
|
|
2288
|
-
description: "Show cache/native eligibility and measured compaction usage",
|
|
2289
|
-
handler: async (_args, context) => {
|
|
2290
|
-
const state = cacheState(context);
|
|
2291
|
-
const remaining = state.expiresAt === undefined ? "unknown lifetime" : `estimated ${Math.max(0, Math.ceil((state.expiresAt - Date.now()) / 1000))}s remaining (model declaration; best effort)`;
|
|
2292
|
-
const usage = lastOutcome?.usage;
|
|
2293
|
-
context.ui.notify(`Provider compaction
|
|
2294
|
-
Observer: ${observer?.isInstalled() ? "installed" : "unavailable for current registration"}
|
|
2295
|
-
Capture: ${captureStatus}
|
|
2296
|
-
Cache: ${state.reason}; ${remaining}
|
|
2297
|
-
Last attempt: ${compactionStatus}${lastOutcome ? `
|
|
2298
|
-
Outcome: ${lastOutcome.path}, ${lastOutcome.elapsedMs}ms${usage ? `; input ${usage.input}, cache read ${usage.cacheRead}, write ${usage.cacheWrite}, output ${usage.output}; estimated cost $${usage.cost.total.toFixed(6)}` : "; usage unavailable"}${lastOutcome.reason ? `; ${lastOutcome.reason}` : ""}` : ""}`, "info");
|
|
2299
|
-
}
|
|
2300
|
-
});
|
|
2301
2280
|
const invalidate = () => {
|
|
2302
2281
|
generation++;
|
|
2303
2282
|
runtimeId = randomUUID2();
|
|
@@ -2306,7 +2285,6 @@ Outcome: ${lastOutcome.path}, ${lastOutcome.elapsedMs}ms${usage ? `; input ${usa
|
|
|
2306
2285
|
active = undefined;
|
|
2307
2286
|
pending = undefined;
|
|
2308
2287
|
expiryAttempt = undefined;
|
|
2309
|
-
captureStatus = "invalidated by lifecycle/history/policy change";
|
|
2310
2288
|
};
|
|
2311
2289
|
const retire = () => {
|
|
2312
2290
|
usageObservers.clear();
|
|
@@ -2396,23 +2374,12 @@ Outcome: ${lastOutcome.path}, ${lastOutcome.elapsedMs}ms${usage ? `; input ${usa
|
|
|
2396
2374
|
return;
|
|
2397
2375
|
if (compactionRequest)
|
|
2398
2376
|
return;
|
|
2399
|
-
if (!supportsNativeCompaction(model) && !supportsCachedSummary(model))
|
|
2400
|
-
captureRejected("unsupported API");
|
|
2377
|
+
if (!supportsNativeCompaction(model) && !supportsCachedSummary(model))
|
|
2401
2378
|
return;
|
|
2402
|
-
|
|
2403
|
-
if (!options.onPayload) {
|
|
2404
|
-
if (captureStatus !== "eligible prepared request captured")
|
|
2405
|
-
captureRejected("no payload callback");
|
|
2379
|
+
if (!options.onPayload || generic(owner))
|
|
2406
2380
|
return;
|
|
2407
|
-
|
|
2408
|
-
if (generic(owner)) {
|
|
2409
|
-
captureRejected("generic compaction model takes precedence");
|
|
2410
|
-
return;
|
|
2411
|
-
}
|
|
2412
|
-
if (!supportedRouteOptions(model, options)) {
|
|
2413
|
-
captureRejected("unsupported route overrides");
|
|
2381
|
+
if (!supportedRouteOptions(model, options))
|
|
2414
2382
|
return;
|
|
2415
|
-
}
|
|
2416
2383
|
const ticket = generation;
|
|
2417
2384
|
const startedAt = Date.now();
|
|
2418
2385
|
const sequence = ++requestSequence;
|
|
@@ -2443,14 +2410,11 @@ Outcome: ${lastOutcome.path}, ${lastOutcome.elapsedMs}ms${usage ? `; input ${usa
|
|
|
2443
2410
|
if (!auth.ok || generic(owner) || auth.apiKey !== options.apiKey || (auth.baseUrl ?? model.baseUrl) !== model.baseUrl || !equal(normalizedHeaders(auth.headers), normalizedHeaders(options.headers)) || !equal(auth.env ?? {}, options.env ?? {})) {
|
|
2444
2411
|
active = undefined;
|
|
2445
2412
|
cache = undefined;
|
|
2446
|
-
captureRejected("resolved auth/route differs from prepared request, or generic policy changed");
|
|
2447
2413
|
return payload;
|
|
2448
2414
|
}
|
|
2449
2415
|
const provider = owner.modelRegistry.getProvider(model.provider);
|
|
2450
|
-
if (!provider)
|
|
2451
|
-
captureRejected("provider unavailable");
|
|
2416
|
+
if (!provider)
|
|
2452
2417
|
return payload;
|
|
2453
|
-
}
|
|
2454
2418
|
const routeHash = route(model, options);
|
|
2455
2419
|
if (config.cachedSummaries && supportsCachedSummary(model)) {
|
|
2456
2420
|
const warming = options.maxTokens === 1 && cache && cache.authHash === routeDigest(auth) && sameCachedPrompt(cache.payload, payload) && equal(cache.context, context);
|
|
@@ -2469,11 +2433,9 @@ Outcome: ${lastOutcome.path}, ${lastOutcome.elapsedMs}ms${usage ? `; input ${usa
|
|
|
2469
2433
|
if (!warming)
|
|
2470
2434
|
cache = undefined;
|
|
2471
2435
|
}
|
|
2472
|
-
if (!supportsNativeCompaction(model))
|
|
2473
|
-
captureStatus = candidate ? "eligible cached request captured" : "cached request ineligible";
|
|
2436
|
+
if (!supportsNativeCompaction(model))
|
|
2474
2437
|
return payload;
|
|
2475
|
-
|
|
2476
|
-
const prepared = capturePreparedRequest(model, context, canonical, payload, captureRejected);
|
|
2438
|
+
const prepared = capturePreparedRequest(model, context, canonical, payload);
|
|
2477
2439
|
if (!prepared) {
|
|
2478
2440
|
active = undefined;
|
|
2479
2441
|
return payload;
|
|
@@ -2502,7 +2464,6 @@ Outcome: ${lastOutcome.path}, ${lastOutcome.elapsedMs}ms${usage ? `; input ${usa
|
|
|
2502
2464
|
active = undefined;
|
|
2503
2465
|
} else
|
|
2504
2466
|
active = undefined;
|
|
2505
|
-
captureStatus = "eligible prepared request captured";
|
|
2506
2467
|
snapshot = {
|
|
2507
2468
|
prepared,
|
|
2508
2469
|
model: structuredClone(model),
|
|
@@ -2540,7 +2501,6 @@ Outcome: ${lastOutcome.path}, ${lastOutcome.elapsedMs}ms${usage ? `; input ${usa
|
|
|
2540
2501
|
if (!supportsNativeCompaction(context.model) && !supportsCachedSummary(context.model))
|
|
2541
2502
|
return;
|
|
2542
2503
|
observer = installObserver(pi, context, observe);
|
|
2543
|
-
captureStatus = observer ? "awaiting prepared request" : "unsupported provider registration";
|
|
2544
2504
|
};
|
|
2545
2505
|
pi.on("session_start", (_event, context) => {
|
|
2546
2506
|
retire();
|
|
@@ -2581,8 +2541,8 @@ Outcome: ${lastOutcome.path}, ${lastOutcome.elapsedMs}ms${usage ? `; input ${usa
|
|
|
2581
2541
|
removeInvalidation();
|
|
2582
2542
|
});
|
|
2583
2543
|
pi.on("session_before_compact", async (event, context) => {
|
|
2584
|
-
const began = Date.now();
|
|
2585
|
-
|
|
2544
|
+
const began = Date.now(), ticket = generation;
|
|
2545
|
+
const sessionId = context.sessionManager.getSessionId();
|
|
2586
2546
|
fallbackReason = undefined;
|
|
2587
2547
|
const attempt = expiryAttempt;
|
|
2588
2548
|
const capture = snapshot;
|
|
@@ -2591,7 +2551,12 @@ Outcome: ${lastOutcome.path}, ${lastOutcome.elapsedMs}ms${usage ? `; input ${usa
|
|
|
2591
2551
|
cache = undefined;
|
|
2592
2552
|
pending = undefined;
|
|
2593
2553
|
const cancelled = (usage, reason = "compaction ownership or input changed") => {
|
|
2594
|
-
|
|
2554
|
+
if (usage && ticket === generation && ctx) {
|
|
2555
|
+
try {
|
|
2556
|
+
if (ctx.sessionManager.getSessionId() === sessionId)
|
|
2557
|
+
pi.appendEntry("pi-provider-compaction:diagnostic", { status: `${attempt ? "expiry cancelled" : "cancelled"}: ${reason}`, usage });
|
|
2558
|
+
} catch {}
|
|
2559
|
+
}
|
|
2595
2560
|
return { cancel: true };
|
|
2596
2561
|
};
|
|
2597
2562
|
const reject = (reason) => {
|
|
@@ -2605,7 +2570,6 @@ Outcome: ${lastOutcome.path}, ${lastOutcome.elapsedMs}ms${usage ? `; input ${usa
|
|
|
2605
2570
|
}
|
|
2606
2571
|
if (!capture && !cached || !context.model || generic(context) || event.signal.aborted)
|
|
2607
2572
|
return reject("no eligible snapshot, explicit model override, or aborted attempt");
|
|
2608
|
-
const ticket = generation;
|
|
2609
2573
|
const branch = context.sessionManager.getBranch();
|
|
2610
2574
|
const branchHash = digest(branch);
|
|
2611
2575
|
const source = cached ?? capture;
|
|
@@ -2630,7 +2594,6 @@ Outcome: ${lastOutcome.path}, ${lastOutcome.elapsedMs}ms${usage ? `; input ${usa
|
|
|
2630
2594
|
let summaryPath = "standard";
|
|
2631
2595
|
let summaryReason;
|
|
2632
2596
|
if (cached) {
|
|
2633
|
-
compactionStatus = "generating cached portable summary";
|
|
2634
2597
|
compactionRequest = true;
|
|
2635
2598
|
try {
|
|
2636
2599
|
const result = await cachedSummary(cached, context, event.preparation, event.customInstructions, event.signal, current);
|
|
@@ -2650,7 +2613,6 @@ Outcome: ${lastOutcome.path}, ${lastOutcome.elapsedMs}ms${usage ? `; input ${usa
|
|
|
2650
2613
|
return cancelled(attemptedUsage, summaryReason ?? "warm cache unavailable");
|
|
2651
2614
|
if (!input && !attemptedUsage)
|
|
2652
2615
|
return reject(summaryReason ?? fallbackReason ?? "native coverage unavailable");
|
|
2653
|
-
compactionStatus = "generating portable summary";
|
|
2654
2616
|
try {
|
|
2655
2617
|
portable = await compact(event.preparation, source.model, auth.apiKey, auth.headers, event.customInstructions, event.signal, context.thinkingLevel, source.provider.streamSimple.bind(source.provider), auth.env, undefined, undefined, source.sessionId);
|
|
2656
2618
|
portable = withFileTracking(portable, event.preparation);
|
|
@@ -2669,7 +2631,6 @@ Outcome: ${lastOutcome.path}, ${lastOutcome.elapsedMs}ms${usage ? `; input ${usa
|
|
|
2669
2631
|
return { compaction: result };
|
|
2670
2632
|
};
|
|
2671
2633
|
if (!capture || !protocol || !input) {
|
|
2672
|
-
compactionStatus = `${summaryPath} portable summary prepared`;
|
|
2673
2634
|
return outcome(summaryPath, portable, summaryReason);
|
|
2674
2635
|
}
|
|
2675
2636
|
try {
|
|
@@ -2680,7 +2641,6 @@ Outcome: ${lastOutcome.path}, ${lastOutcome.elapsedMs}ms${usage ? `; input ${usa
|
|
|
2680
2641
|
fallback("auth changed after portable generation");
|
|
2681
2642
|
return outcome(summaryPath, portable, "native auth changed");
|
|
2682
2643
|
}
|
|
2683
|
-
compactionStatus = "requesting native checkpoint";
|
|
2684
2644
|
let native;
|
|
2685
2645
|
compactionRequest = true;
|
|
2686
2646
|
try {
|
|
@@ -2742,19 +2702,8 @@ Outcome: ${lastOutcome.path}, ${lastOutcome.elapsedMs}ms${usage ? `; input ${usa
|
|
|
2742
2702
|
pi.on("session_compact", (event, context) => {
|
|
2743
2703
|
const recorded = isObject(event.compactionEntry.details) ? event.compactionEntry.details.providerCompaction : undefined;
|
|
2744
2704
|
const owned = isObject(recorded) && equal(recorded, lastOutcome);
|
|
2745
|
-
if (readNativeCompactionDetails(event.compactionEntry.details)?.version
|
|
2746
|
-
compactionStatus = "native checkpoint committed";
|
|
2747
|
-
else if (fallbackReason)
|
|
2748
|
-
compactionStatus = `portable: ${fallbackReason}`;
|
|
2749
|
-
else if (owned)
|
|
2750
|
-
compactionStatus = `${lastOutcome.path} portable summary committed`;
|
|
2751
|
-
else
|
|
2705
|
+
if (!owned && !fallbackReason && readNativeCompactionDetails(event.compactionEntry.details)?.version !== 3)
|
|
2752
2706
|
fallback("another handler supplied the committed result");
|
|
2753
|
-
if (owned) {
|
|
2754
|
-
if (configuration().outcomeNotifications && lastOutcome)
|
|
2755
|
-
context.ui.notify(`Compacted via ${lastOutcome.path} in ${(lastOutcome.elapsedMs / 1000).toFixed(1)}s${lastOutcome.usage ? `; cache read ${lastOutcome.usage.cacheRead}, write ${lastOutcome.usage.cacheWrite}; estimated cost $${lastOutcome.usage.cost.total.toFixed(6)}` : ""}`, "info");
|
|
2756
|
-
} else
|
|
2757
|
-
lastOutcome = { path: "Pi/other handler", elapsedMs: 0, usage: event.compactionEntry.usage, reason: compactionStatus };
|
|
2758
2707
|
if (fallbackReason)
|
|
2759
2708
|
pi.appendEntry("pi-provider-compaction:diagnostic", { status: `portable: ${fallbackReason}` });
|
|
2760
2709
|
active = undefined;
|
|
@@ -2783,7 +2732,6 @@ Outcome: ${lastOutcome.path}, ${lastOutcome.elapsedMs}ms${usage ? `; input ${usa
|
|
|
2783
2732
|
pending = undefined;
|
|
2784
2733
|
snapshot = undefined;
|
|
2785
2734
|
cache = undefined;
|
|
2786
|
-
compactionStatus = "compaction failed or cancelled";
|
|
2787
2735
|
});
|
|
2788
2736
|
}
|
|
2789
2737
|
export {
|
package/index.ts
CHANGED
|
@@ -157,7 +157,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
157
157
|
// hook/privacy policy, even if the visible summary text happens to match.
|
|
158
158
|
// Older hosts in the native adapter's supported range lack getSettings.
|
|
159
159
|
// Keep their existing native path; cache preferences require that host API.
|
|
160
|
-
const configuration = () => typeof pi.getSettings === "function" ? cacheConfig(pi.getSettings()) : { cachedSummaries: false,
|
|
160
|
+
const configuration = () => typeof pi.getSettings === "function" ? cacheConfig(pi.getSettings()) : { cachedSummaries: false, cacheRetention: undefined };
|
|
161
161
|
const secret = randomBytes(32);
|
|
162
162
|
const routeDigest = (value: unknown) => createHmac("sha256", secret).update(JSON.stringify(value)).digest("hex");
|
|
163
163
|
let runtimeId = randomUUID();
|
|
@@ -174,21 +174,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
174
174
|
let unregisterCleanup: (() => void) | undefined;
|
|
175
175
|
let generation = 0;
|
|
176
176
|
let requestSequence = 0;
|
|
177
|
-
let captureStatus = "no request observed";
|
|
178
|
-
let compactionStatus = "not attempted in this runtime";
|
|
179
177
|
let fallbackReason: string | undefined;
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
pi.registerCommand("provider-compaction-status", {
|
|
183
|
-
description: "Show cache/native eligibility and measured compaction usage",
|
|
184
|
-
handler: async (_args, context) => {
|
|
185
|
-
const state = cacheState(context);
|
|
186
|
-
const remaining = state.expiresAt === undefined ? "unknown lifetime" : `estimated ${Math.max(0, Math.ceil((state.expiresAt - Date.now()) / 1000))}s remaining (model declaration; best effort)`;
|
|
187
|
-
const usage = lastOutcome?.usage;
|
|
188
|
-
context.ui.notify(`Provider compaction\nObserver: ${observer?.isInstalled() ? "installed" : "unavailable for current registration"}\nCapture: ${captureStatus}\nCache: ${state.reason}; ${remaining}\nLast attempt: ${compactionStatus}${lastOutcome ? `\nOutcome: ${lastOutcome.path}, ${lastOutcome.elapsedMs}ms${usage ? `; input ${usage.input}, cache read ${usage.cacheRead}, write ${usage.cacheWrite}, output ${usage.output}; estimated cost $${usage.cost.total.toFixed(6)}` : "; usage unavailable"}${lastOutcome.reason ? `; ${lastOutcome.reason}` : ""}` : ""}`, "info");
|
|
189
|
-
},
|
|
190
|
-
});
|
|
191
|
-
const invalidate = () => { generation++; runtimeId = randomUUID(); snapshot = undefined; cache = undefined; active = undefined; pending = undefined; expiryAttempt = undefined; captureStatus = "invalidated by lifecycle/history/policy change"; };
|
|
178
|
+
const fallback = (reason: string) => { fallbackReason = reason; };
|
|
179
|
+
const invalidate = () => { generation++; runtimeId = randomUUID(); snapshot = undefined; cache = undefined; active = undefined; pending = undefined; expiryAttempt = undefined; };
|
|
192
180
|
const retire = () => {
|
|
193
181
|
usageObservers.clear();
|
|
194
182
|
invalidate();
|
|
@@ -260,17 +248,11 @@ export default function (pi: ExtensionAPI): void {
|
|
|
260
248
|
}
|
|
261
249
|
if (options.sessionId !== manager.getSessionId()) return;
|
|
262
250
|
if (compactionRequest) return;
|
|
263
|
-
if (!supportsNativeCompaction(model) && !supportsCachedSummary(model))
|
|
264
|
-
if (!options.onPayload)
|
|
265
|
-
// The portable-summary request re-enters this observer without a payload
|
|
266
|
-
// callback. Do not overwrite the main request's capture diagnostic.
|
|
267
|
-
if (captureStatus !== "eligible prepared request captured") captureRejected("no payload callback");
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
if (generic(owner)) { captureRejected("generic compaction model takes precedence"); return; }
|
|
251
|
+
if (!supportsNativeCompaction(model) && !supportsCachedSummary(model)) return;
|
|
252
|
+
if (!options.onPayload || generic(owner)) return;
|
|
271
253
|
// The Azure adapter also accepts API-specific overrides and process-env
|
|
272
254
|
// fallbacks. Those must not silently select a route absent from our evidence.
|
|
273
|
-
if (!supportedRouteOptions(model, options))
|
|
255
|
+
if (!supportedRouteOptions(model, options)) return;
|
|
274
256
|
const ticket = generation;
|
|
275
257
|
const startedAt = Date.now();
|
|
276
258
|
const sequence = ++requestSequence;
|
|
@@ -296,11 +278,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
296
278
|
const normalizedHeaders = (headers: ProviderHeaders | undefined) => Object.entries(headers ?? {}).map(([name, value]) => [name.toLowerCase(), value]).sort(([a], [b]) => String(a).localeCompare(String(b)));
|
|
297
279
|
if (!auth.ok || generic(owner) || auth.apiKey !== options.apiKey || (auth.baseUrl ?? model.baseUrl) !== model.baseUrl || !equal(normalizedHeaders(auth.headers), normalizedHeaders(options.headers)) || !equal(auth.env ?? {}, options.env ?? {})) {
|
|
298
280
|
active = undefined; cache = undefined;
|
|
299
|
-
captureRejected("resolved auth/route differs from prepared request, or generic policy changed");
|
|
300
281
|
return payload;
|
|
301
282
|
}
|
|
302
283
|
const provider = owner.modelRegistry.getProvider(model.provider);
|
|
303
|
-
if (!provider)
|
|
284
|
+
if (!provider) return payload;
|
|
304
285
|
const routeHash = route(model, options);
|
|
305
286
|
if (config.cachedSummaries && supportsCachedSummary(model)) {
|
|
306
287
|
const warming = options.maxTokens === 1 && cache && cache.authHash === routeDigest(auth) && sameCachedPrompt(cache.payload, payload) && equal(cache.context, context);
|
|
@@ -310,8 +291,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
310
291
|
}, payload);
|
|
311
292
|
if (!warming) cache = undefined;
|
|
312
293
|
}
|
|
313
|
-
if (!supportsNativeCompaction(model))
|
|
314
|
-
const prepared = capturePreparedRequest(model, context, canonical, payload
|
|
294
|
+
if (!supportsNativeCompaction(model)) return payload;
|
|
295
|
+
const prepared = capturePreparedRequest(model, context, canonical, payload);
|
|
315
296
|
if (!prepared) { active = undefined; return payload; }
|
|
316
297
|
const policyHash = digest(requestPolicy(model, prepared));
|
|
317
298
|
const latest = latestCompaction(branch);
|
|
@@ -334,7 +315,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
334
315
|
} else active = undefined;
|
|
335
316
|
} else active = undefined;
|
|
336
317
|
} else active = undefined;
|
|
337
|
-
captureStatus = "eligible prepared request captured";
|
|
338
318
|
snapshot = {
|
|
339
319
|
prepared, model: structuredClone(model), provider,
|
|
340
320
|
options: { apiKey: options.apiKey, headers: structuredClone(options.headers), env: structuredClone(options.env), fetch: options.fetch, ...(typeof (options as { serviceTier?: unknown }).serviceTier === "string" ? { serviceTier: (options as { serviceTier: string }).serviceTier } : {}) },
|
|
@@ -359,7 +339,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
359
339
|
if (observer) { invalidate(); observer.remove(); observer = undefined; }
|
|
360
340
|
if (!supportsNativeCompaction(context.model) && !supportsCachedSummary(context.model)) return;
|
|
361
341
|
observer = installObserver(pi, context, observe);
|
|
362
|
-
captureStatus = observer ? "awaiting prepared request" : "unsupported provider registration";
|
|
363
342
|
};
|
|
364
343
|
pi.on("session_start", (_event, context) => {
|
|
365
344
|
retire();
|
|
@@ -386,15 +365,21 @@ export default function (pi: ExtensionAPI): void {
|
|
|
386
365
|
pi.on("session_shutdown", () => { if (typeof removeInvalidation === "function") removeInvalidation(); });
|
|
387
366
|
|
|
388
367
|
pi.on("session_before_compact", async (event, context) => {
|
|
389
|
-
const began = Date.now();
|
|
390
|
-
|
|
368
|
+
const began = Date.now(), ticket = generation;
|
|
369
|
+
const sessionId = context.sessionManager.getSessionId();
|
|
391
370
|
fallbackReason = undefined;
|
|
392
371
|
const attempt = expiryAttempt;
|
|
393
372
|
const capture = snapshot;
|
|
394
373
|
const cached = cacheState(context).available ? cache : undefined;
|
|
395
374
|
snapshot = undefined; cache = undefined; pending = undefined;
|
|
396
375
|
const cancelled = (usage?: Usage, reason = "compaction ownership or input changed") => {
|
|
397
|
-
|
|
376
|
+
// A cancelled attempt has no checkpoint to carry its known bill. Retain
|
|
377
|
+
// that usage in the existing session diagnostic, not a command-only cache.
|
|
378
|
+
if (usage && ticket === generation && ctx) {
|
|
379
|
+
try {
|
|
380
|
+
if (ctx.sessionManager.getSessionId() === sessionId) pi.appendEntry("pi-provider-compaction:diagnostic", { status: `${attempt ? "expiry cancelled" : "cancelled"}: ${reason}`, usage });
|
|
381
|
+
} catch { /* A disposed session cannot accept a diagnostic. */ }
|
|
382
|
+
}
|
|
398
383
|
return { cancel: true as const };
|
|
399
384
|
};
|
|
400
385
|
const reject = (reason: string) => {
|
|
@@ -408,7 +393,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
408
393
|
attempt.signal = event.signal;
|
|
409
394
|
}
|
|
410
395
|
if ((!capture && !cached) || !context.model || generic(context) || event.signal.aborted) return reject("no eligible snapshot, explicit model override, or aborted attempt");
|
|
411
|
-
const ticket = generation;
|
|
412
396
|
const branch = context.sessionManager.getBranch();
|
|
413
397
|
const branchHash = digest(branch);
|
|
414
398
|
const source = cached ?? capture!;
|
|
@@ -431,7 +415,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
431
415
|
let summaryPath = "standard";
|
|
432
416
|
let summaryReason: string | undefined;
|
|
433
417
|
if (cached) {
|
|
434
|
-
compactionStatus = "generating cached portable summary";
|
|
435
418
|
compactionRequest = true;
|
|
436
419
|
try {
|
|
437
420
|
const result = await cachedSummary(cached, context, event.preparation, event.customInstructions, event.signal, current);
|
|
@@ -446,7 +429,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
446
429
|
if (attempt) return cancelled(attemptedUsage, summaryReason ?? "warm cache unavailable");
|
|
447
430
|
// Without native coverage or a dispatched cached attempt, Pi can own fallback.
|
|
448
431
|
if (!input && !attemptedUsage) return reject(summaryReason ?? fallbackReason ?? "native coverage unavailable");
|
|
449
|
-
compactionStatus = "generating portable summary";
|
|
450
432
|
try {
|
|
451
433
|
portable = await compact(event.preparation, source.model, auth.apiKey, auth.headers as Record<string, string> | undefined, event.customInstructions, event.signal, context.thinkingLevel, source.provider.streamSimple.bind(source.provider), auth.env, undefined, undefined, source.sessionId);
|
|
452
434
|
portable = withFileTracking(portable, event.preparation);
|
|
@@ -463,14 +445,12 @@ export default function (pi: ExtensionAPI): void {
|
|
|
463
445
|
return { compaction: result! };
|
|
464
446
|
};
|
|
465
447
|
if (!capture || !protocol || !input) {
|
|
466
|
-
compactionStatus = `${summaryPath} portable summary prepared`;
|
|
467
448
|
return outcome(summaryPath, portable, summaryReason);
|
|
468
449
|
}
|
|
469
450
|
try {
|
|
470
451
|
const freshAuth = await context.modelRegistry.getApiKeyAndHeaders(context.model);
|
|
471
452
|
if (!current()) return cancelled(portable.usage);
|
|
472
453
|
if (!freshAuth.ok || routeDigest(freshAuth) !== capture.authHash) { fallback("auth changed after portable generation"); return outcome(summaryPath, portable, "native auth changed"); }
|
|
473
|
-
compactionStatus = "requesting native checkpoint";
|
|
474
454
|
let native: Awaited<ReturnType<typeof requestProviderCompaction>>;
|
|
475
455
|
compactionRequest = true;
|
|
476
456
|
try {
|
|
@@ -512,13 +492,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
512
492
|
pi.on("session_compact", (event, context) => {
|
|
513
493
|
const recorded = isObject(event.compactionEntry.details) ? event.compactionEntry.details.providerCompaction : undefined;
|
|
514
494
|
const owned = isObject(recorded) && equal(recorded, lastOutcome);
|
|
515
|
-
if (readNativeCompactionDetails(event.compactionEntry.details)?.version
|
|
516
|
-
else if (fallbackReason) compactionStatus = `portable: ${fallbackReason}`;
|
|
517
|
-
else if (owned) compactionStatus = `${lastOutcome!.path} portable summary committed`;
|
|
518
|
-
else fallback("another handler supplied the committed result");
|
|
519
|
-
if (owned) {
|
|
520
|
-
if (configuration().outcomeNotifications && lastOutcome) context.ui.notify(`Compacted via ${lastOutcome.path} in ${(lastOutcome.elapsedMs / 1000).toFixed(1)}s${lastOutcome.usage ? `; cache read ${lastOutcome.usage.cacheRead}, write ${lastOutcome.usage.cacheWrite}; estimated cost $${lastOutcome.usage.cost.total.toFixed(6)}` : ""}`, "info");
|
|
521
|
-
} else lastOutcome = { path: "Pi/other handler", elapsedMs: 0, usage: event.compactionEntry.usage, reason: compactionStatus };
|
|
495
|
+
if (!owned && !fallbackReason && readNativeCompactionDetails(event.compactionEntry.details)?.version !== 3) fallback("another handler supplied the committed result");
|
|
522
496
|
// Only static diagnostics are persisted, never request/auth data or error text.
|
|
523
497
|
if (fallbackReason) pi.appendEntry("pi-provider-compaction:diagnostic", { status: `portable: ${fallbackReason}` });
|
|
524
498
|
active = undefined; snapshot = undefined; cache = undefined;
|
|
@@ -538,5 +512,5 @@ export default function (pi: ExtensionAPI): void {
|
|
|
538
512
|
...(typeof candidate.capture.prepared.payload.model === "string" ? { payloadModel: candidate.capture.prepared.payload.model } : {}),
|
|
539
513
|
};
|
|
540
514
|
});
|
|
541
|
-
pi.on("session_compact_failed", () => { pending = undefined; snapshot = undefined; cache = undefined;
|
|
515
|
+
pi.on("session_compact_failed", () => { pending = undefined; snapshot = undefined; cache = undefined; });
|
|
542
516
|
}
|