pi-condense 2.4.1 → 2.4.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,11 @@ Published to npm as [`pi-condense`](https://www.npmjs.com/package/pi-condense) (
7
7
  Pushing a `vX.Y.Z` tag triggers `.github/workflows/release.yml`, which runs the tests and
8
8
  publishes via OIDC trusted publishing. See `.agents/skills/release/SKILL.md`.
9
9
 
10
+ ## [2.4.2] - 2026-08-04
11
+
12
+ - **No-op renders skip context serialization.** `pruneMessages` (`src/pruner.ts`) computed `sizeMessages` (a full `JSON.stringify` over the entire message array) unconditionally on every `context` render, including no-ops where the result is never read (`index.ts` consumes `beforeChars`/`afterChars` only under `if (result.pruned)`). It now computes both sizes lazily in the pruned branch and returns a `{ beforeChars: 0, afterChars: 0 }` sentinel on a no-op, so a render that prunes nothing does zero `JSON.stringify` over the array. CPU/GC only - zero token cost, no wire or return-shape change. Also corrects a stale fast-path comment in the `context` handler (`index.ts`): index/registry emptiness alone does not imply a no-op, because error-purge and thinking-strip prune independently.
13
+ - **Bumped `engines.node` to `>=22.19.0`** to match the host pi runtime (`@earendil-works/pi-coding-agent@0.83.0`); drops advertised support for Node 20/21, which cannot run current pi. Bumped the `@sinclair/typebox` devDependency floor to `^0.34.52` (freshness only; the caret already resolved there).
14
+
10
15
  ## [2.4.1] - 2026-07-30
11
16
 
12
17
  - **Fix 421 Misdirected Request for GitHub Copilot business/enterprise seats.** The summarizer called pi-ai's `stream()` with the static model definition, whose shipped `baseUrl` pins the individual Copilot host (`api.individual.githubcopilot.com`); business/enterprise tokens are rejected there with `421 Misdirected Request`, so any `github-copilot/*` `summarizerModel` failed on those seats. `runOnce` now mirrors the main agent loop (pi's `model-runtime`): it resolves provider auth via `ctx.modelRegistry.getProviderAuth(model.provider)` and, when the resolved auth carries a seat-specific `baseUrl`, rebases the model onto it before streaming. Also bumps `@earendil-works/*` devDependencies to `^0.83.0` (needed for `getProviderAuth` on the extension-facing `ModelRegistry`) and imports `stream` from `@earendil-works/pi-ai/compat` (its export moved off the root in current pi-ai).
package/index.ts CHANGED
@@ -818,9 +818,11 @@ export default function (pi: ExtensionAPI) {
818
818
  let changed = false;
819
819
 
820
820
  // pruneMessages is the single source of truth for "is there work to do".
821
- // It fast-paths (returns the original array reference) when both the
822
- // tool-call index and chain registry are empty, so calling it
823
- // unconditionally is safe and avoids a split gate here.
821
+ // It returns the original array reference (pruned: false) only when none of
822
+ // the four phases changed anything; index/registry emptiness alone does not
823
+ // imply a no-op, since error-purge (phase 2) and thinking-strip (phase 4)
824
+ // prune independently of them. Calling it unconditionally is safe and avoids
825
+ // a split gate here.
824
826
  const result = pruneMessages(
825
827
  messages,
826
828
  indexer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-condense",
3
- "version": "2.4.1",
3
+ "version": "2.4.2",
4
4
  "description": "Pi coding-agent extension that summarizes completed tool-call batches, replaces raw outputs with short stubs, compresses closed tool-call chains, and recovers any original on demand via context_tree_query.",
5
5
  "author": "Jacek Juraszek",
6
6
  "license": "MIT",
@@ -27,7 +27,7 @@
27
27
  "ai"
28
28
  ],
29
29
  "engines": {
30
- "node": ">=20.3.0"
30
+ "node": ">=22.19.0"
31
31
  },
32
32
  "files": [
33
33
  "index.ts",
@@ -60,6 +60,6 @@
60
60
  "@earendil-works/pi-ai": "^0.83.0",
61
61
  "@earendil-works/pi-coding-agent": "^0.83.0",
62
62
  "@earendil-works/pi-tui": "^0.83.0",
63
- "@sinclair/typebox": "^0.34.49"
63
+ "@sinclair/typebox": "^0.34.52"
64
64
  }
65
65
  }
@@ -591,14 +591,17 @@ describe("sizeMessages", () => {
591
591
  });
592
592
 
593
593
  describe("pruneMessages beforeChars/afterChars", () => {
594
- it("no-op fast path: beforeChars === afterChars === sizeMessages(input) and pruned false", () => {
594
+ it("no-op returns {0,0} sentinel and pruned false (no serialization on no-op path)", () => {
595
595
  const indexer = makeMockIndexer();
596
+ // Non-empty input: a reverted fix that recomputes sizeMessages(messages)
597
+ // on the unconditional path would yield a nonzero beforeChars here and fail.
596
598
  const messages = [{ role: "user", content: "hello", timestamp: 1 }];
597
599
  const result = pruneMessages(messages, indexer);
598
600
  expect(result.pruned).toBe(false);
599
- const expected = sizeMessages(messages);
600
- expect(result.beforeChars).toBe(expected);
601
- expect(result.afterChars).toBe(expected);
601
+ expect(result.beforeChars).toBe(0);
602
+ expect(result.afterChars).toBe(0);
603
+ // No-op returns the original array reference unchanged.
604
+ expect(result.messages).toBe(messages);
602
605
  });
603
606
 
604
607
  it("pruning path: beforeChars > afterChars when stubs shrink content", () => {
package/src/pruner.ts CHANGED
@@ -55,6 +55,12 @@ export function sizeMessages(messages: any[]): number {
55
55
  * - `pruned: false` — nothing matched; the returned `messages` is the
56
56
  * **original input array reference** so the caller can cheaply skip
57
57
  * the reconstruction path.
58
+ * - `beforeChars` / `afterChars` — serialized context size (`sizeMessages`)
59
+ * before and after pruning when `pruned` is true. When `pruned` is false
60
+ * both are `0`: a no-op sentinel, not a measurement — the size is never
61
+ * computed on the no-op path (zero `JSON.stringify` over the array), and
62
+ * the only consumer (`index.ts` live-reclaim) reads them solely under
63
+ * `if (result.pruned)`.
58
64
  *
59
65
  * AssistantMessage tool-call blocks (which carry the IDs) are kept
60
66
  * unchanged so the model can still reference them by id when calling
@@ -69,7 +75,6 @@ export function pruneMessages(
69
75
  protection?: ProtectionConfig,
70
76
  recoveryGraceTurns: number = 0,
71
77
  ): { messages: any[]; pruned: boolean; beforeChars: number; afterChars: number } {
72
- const beforeChars = sizeMessages(messages);
73
78
  // Phase 1: stub-replace summarized tool results
74
79
  let pruned = false;
75
80
  const inGrace = inGraceRecoveryToolCallIds(messages, recoveryGraceTurns);
@@ -158,5 +163,7 @@ export function pruneMessages(
158
163
  }
159
164
  }
160
165
 
161
- return { messages: current, pruned, beforeChars, afterChars: pruned ? sizeMessages(current) : beforeChars };
166
+ return pruned
167
+ ? { messages: current, pruned, beforeChars: sizeMessages(messages), afterChars: sizeMessages(current) }
168
+ : { messages, pruned, beforeChars: 0, afterChars: 0 };
162
169
  }