bermudis-pi-goodies 0.27.1 → 0.27.2

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.
Files changed (3) hide show
  1. package/README.md +8 -5
  2. package/package.json +1 -1
  3. package/side.ts +97 -5
package/README.md CHANGED
@@ -26,7 +26,7 @@ extensions. One entry point, thirteen independent features.
26
26
  First install, pinned to a published version:
27
27
 
28
28
  ```bash
29
- pi install npm:bermudis-pi-goodies@0.27.1
29
+ pi install npm:bermudis-pi-goodies@0.27.2
30
30
  ```
31
31
 
32
32
  Picking up a new release on an existing install (`pi install` is not the
@@ -438,10 +438,13 @@ Details worth knowing:
438
438
  path. (`deliverAs: "nextTurn"` would only queue in memory and die with
439
439
  the process.)
440
440
  - The footer's context-usage estimate reflects the raw session branch, not
441
- the lens output, so it over-reports while a side session is active — by
442
- the full uncompacted main history at worst, shrinking after compaction
443
- (the lens renders compaction summaries into the quote rather than
444
- resending summarized-away turns). Compaction summaries that cover side
441
+ the lens output, so it can over-report while a side session is active.
442
+ Threshold auto-compaction is vetoed when the **lensed side request**
443
+ fits the side model's window (including system/tool overhead and the
444
+ configured token reserve), rather than trusting the main model's usage.
445
+ The quoted main transcript still counts toward that window. Manual
446
+ `/compact` and actual context-overflow recovery remain available.
447
+ Compaction summaries that cover side
445
448
  turns are quoted with an explicit who-is-who caveat instead of passing as
446
449
  the consultant's own history.
447
450
  - Both quote directions carry an explicit untrusted-evidence preamble:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bermudis-pi-goodies",
3
- "version": "0.27.1",
3
+ "version": "0.27.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/bermudi/agent-extensions.git",
package/side.ts CHANGED
@@ -28,11 +28,10 @@
28
28
  * limb that is ever resumed delivers only newer turns. v1 always starts a
29
29
  * fresh side limb, but the bookkeeping keeps that future-safe.
30
30
  *
31
- * Known limitation: pi's footer context-usage estimate (and its pre-send
32
- * compaction threshold) reflect the raw session branch, not the lens
33
- * output — the footer over-reports while a side session is active, and
34
- * premature auto-compaction is possible on large side consults (the lens
35
- * is compaction-aware and survives it; the main limb is never touched).
31
+ * Pi's footer context-usage estimate reflects the raw session branch, not
32
+ * the lens output, so the footer over-reports during side sessions. A
33
+ * session_before_compact guard vetoes threshold compaction when the lensed
34
+ * request fits; manual compaction and actual overflow recovery still run.
36
35
  * The side badge carries pi's own estimator applied to the lensed messages
37
36
  * ("· ~46k lensed") as the honest counterpoint to the inflated figure.
38
37
  */
@@ -44,11 +43,14 @@ import type {
44
43
  ExtensionCommandContext,
45
44
  ExtensionContext,
46
45
  SessionEntry,
46
+ ToolInfo,
47
47
  } from "@earendil-works/pi-coding-agent";
48
48
  import {
49
49
  ModelSelectorComponent,
50
+ calculateContextTokens,
50
51
  estimateTokens,
51
52
  sessionEntryToContextMessages,
53
+ shouldCompact,
52
54
  } from "@earendil-works/pi-coding-agent";
53
55
  import { describeError, extractTextParts } from "./json-file.ts";
54
56
  import { buildTrajectory, renderTrajectory } from "./copy-trajectory.ts";
@@ -321,6 +323,50 @@ export function estimateLensedContextTokens(
321
323
  return messages.reduce((sum, message) => sum + estimateTokens(message), 0);
322
324
  }
323
325
 
326
+ /**
327
+ * Conservative side-request budget. Main-model usage cannot leak into this
328
+ * estimate: its assistant turns have already become quoted user text.
329
+ * Include system/tool overhead before the first side response, and trust
330
+ * valid SIDE usage as a floor (it includes provider-visible overhead).
331
+ */
332
+ export function estimateSideCompactionTokens(
333
+ messages: ContextEvent["messages"],
334
+ systemPrompt: string,
335
+ tools: readonly Pick<ToolInfo, "name" | "description" | "parameters">[],
336
+ rawBranch: readonly SessionEntry[],
337
+ ): number {
338
+ // Kept responses still carry pre-compaction usage. Match by source
339
+ // message identity, not timestamp: branch order is the provenance Pi uses.
340
+ const lastInvalidation = rawBranch.findLastIndex(
341
+ (entry) => entry.type === "compaction" || entry.type === "context_edit",
342
+ );
343
+ const validUsageMessages = new Set(
344
+ rawBranch
345
+ .slice(lastInvalidation + 1)
346
+ .flatMap((entry) => sessionEntryToContextMessages(entry)),
347
+ );
348
+ const estimated =
349
+ messages.reduce((sum, message) => sum + estimateTokens(message), 0) +
350
+ Math.ceil(systemPrompt.length / 4) +
351
+ Math.ceil(JSON.stringify(tools).length / 4);
352
+ let trailing = 0;
353
+ for (let i = messages.length - 1; i >= 0; i--) {
354
+ const message = messages[i]!;
355
+ if (
356
+ message.role === "assistant" &&
357
+ validUsageMessages.has(message) &&
358
+ message.stopReason !== "error" &&
359
+ message.stopReason !== "aborted" &&
360
+ message.usage
361
+ ) {
362
+ const usage = calculateContextTokens(message.usage);
363
+ if (usage > 0) return Math.max(estimated, usage + trailing);
364
+ }
365
+ trailing += estimateTokens(message);
366
+ }
367
+ return estimated;
368
+ }
369
+
324
370
  // ---------------------------------------------------------------------------
325
371
  // Handoff: what the main agent is given at exit
326
372
  // ---------------------------------------------------------------------------
@@ -627,6 +673,52 @@ let modelRegistryRef: ExtensionContext["modelRegistry"] | undefined;
627
673
  let lastBadgeCtx: ExtensionContext | undefined;
628
674
 
629
675
  export default function side(pi: ExtensionAPI): void {
676
+ // Pi checks the raw projection BEFORE emitting `context`. Recheck the
677
+ // same threshold against our lens at its supported cancellation boundary.
678
+ // Never veto manual compaction or a real provider overflow.
679
+ pi.on("session_before_compact", (event, ctx) => {
680
+ if (event.reason !== "threshold" || !ctx.model) return;
681
+ const raw = ctx.sessionManager.getBranch();
682
+ const markerIdx = findSideBoundary(raw);
683
+ const marker = raw[markerIdx];
684
+ if (marker?.type !== "custom" || !parseSideMarkerData(marker.data)) return;
685
+ const messages = buildLensMessages(
686
+ ctx.sessionManager.buildContextEntries(),
687
+ raw,
688
+ markerIdx,
689
+ [],
690
+ );
691
+ if (!messages || ctx.model.contextWindow <= 0) return;
692
+ const activeTools = new Set(pi.getActiveTools());
693
+ const tokens = estimateSideCompactionTokens(
694
+ messages,
695
+ ctx.getSystemPrompt(),
696
+ pi
697
+ .getAllTools()
698
+ .filter((tool) => activeTools.has(tool.name))
699
+ .map(({ name, description, parameters }) => ({
700
+ name,
701
+ description,
702
+ parameters,
703
+ })),
704
+ raw,
705
+ );
706
+ const needed = shouldCompact(
707
+ tokens,
708
+ ctx.model.contextWindow,
709
+ event.preparation.settings,
710
+ );
711
+ logGoodiesEvent({
712
+ type: "side_compaction_check",
713
+ raw_tokens: event.preparation.tokensBefore,
714
+ side_tokens: tokens,
715
+ context_window: ctx.model.contextWindow,
716
+ reserve_tokens: event.preparation.settings.reserveTokens,
717
+ cancelled: !needed,
718
+ });
719
+ if (!needed) return { cancel: true };
720
+ });
721
+
630
722
  // Lens: rewrite side-agent requests so the main trajectory arrives as an
631
723
  // attributed quote instead of the side model's own history. Detection uses
632
724
  // the raw branch (the marker is always an ancestor there); content uses