@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 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. Countdown 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 countdown.
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
- Run `/provider-compaction-status` to see observer/capture eligibility, estimated cache lifetime, the last selected path, elapsed time, reported input/cache/output tokens, and estimated cost (including available companion corrections). Optional outcome notifications show measured usage after a committed compaction; hypothetical savings are not presented as measured. Failed/cancelled expiry attempts remain visible in runtime status; if no checkpoint is saved, their usage cannot be attached to compaction totals through Pi's extension API. Portable fallback also appends a `pi-provider-compaction:diagnostic` session entry containing only a fixed reason string. It is not included in model context. Diagnostics never include request content, credentials, or backend error text.
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 diagnostics require the extension to be loaded when the attempt happens; they cannot explain earlier compactions retroactively. A committed native checkpoint does not by itself prove that a later request replayed it.
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; outcomeNotifications: boolean }
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, outcomeNotifications: false, cacheRetention: undefined };
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
- compactionStatus = "preparing compaction";
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
- lastOutcome = { path: attempt ? "expiry cancelled" : "cancelled", elapsedMs: Date.now() - began, usage, reason };
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 === 3)
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, outcomeNotifications: false, cacheRetention: undefined };
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 captureRejected = (reason: string) => { captureStatus = reason; };
181
- const fallback = (reason: string) => { fallbackReason = reason; compactionStatus = `portable: ${reason}`; };
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)) { captureRejected("unsupported API"); return; }
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)) { captureRejected("unsupported route overrides"); return; }
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) { captureRejected("provider unavailable"); return payload; }
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)) { captureStatus = candidate ? "eligible cached request captured" : "cached request ineligible"; return payload; }
314
- const prepared = capturePreparedRequest(model, context, canonical, payload, captureRejected);
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
- compactionStatus = "preparing compaction";
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
- lastOutcome = { path: attempt ? "expiry cancelled" : "cancelled", elapsedMs: Date.now() - began, usage, reason };
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 === 3) compactionStatus = "native checkpoint committed";
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; compactionStatus = "compaction failed or cancelled"; });
515
+ pi.on("session_compact_failed", () => { pending = undefined; snapshot = undefined; cache = undefined; });
542
516
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nijaru/pi-provider-compaction",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "type": "module",
5
5
  "description": "Provider-native context compaction adapters for Pi",
6
6
  "keywords": [