pi-condense 2.15.0 → 2.15.1

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,12 @@ 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.15.1] - 2026-11-10
11
+
12
+ ### Fixed
13
+
14
+ - `/pruner on` runs the reload rearm probe when it flips pruning from off to on, so tool results produced while pruning was disabled, or recovered by a reload that happened while it was disabled, reach the next `turn_end` gate. Before, only `session_start` and `session_tree` probed, and enabling later left that work unflushed until a fresh batch arrived. (#29)
15
+
10
16
  ## [2.15.0] - 2026-11-09
11
17
 
12
18
  ### Changed
package/PRUNING.md CHANGED
@@ -1074,9 +1074,9 @@ See `doc/specs/2026-08-12-single-chain-observability-trigger-repair.md` for the
1074
1074
 
1075
1075
  ### Reload rearm
1076
1076
 
1077
- A reload (`session_start` or `session_tree`) rescans the branch for completed-but-unflushed tool-call batches past the frontier (the same rescan `flushPending` itself uses, no queue reconstruction). If that rescan finds recoverable work, a transient in-memory flag arms: the next `turn_end` evaluates the budget/delta trigger even on a turn that contributes no new tool results itself, instead of silently requiring a fresh batch to reach the gate. No flush runs at reload time - arming only changes what an *existing* trigger sees on the next turn.
1077
+ A reload (`session_start` or `session_tree`), and `/pruner on` when it flips pruning from off to on, rescans the branch for completed-but-unflushed tool-call batches past the frontier (the same rescan `flushPending` itself uses, no queue reconstruction). If that rescan finds recoverable work, a transient in-memory flag arms: the next `turn_end` evaluates the budget/delta trigger even on a turn that contributes no new tool results itself, instead of silently requiring a fresh batch to reach the gate. No flush runs at reload time - arming only changes what an *existing* trigger sees on the next turn.
1078
1078
 
1079
- The flag clears the moment any flush attempt runs (any outcome) and is re-evaluated on the next reload. A reload followed by total idleness - no further turns at all - gets visibility only: the footer, `/pruner status`, and the `agent_end` pending notice (`prune: recovered pending (reload)`) all reflect the recoverable work, but nothing flushes automatically. This is by design - no immediate flush at boot (print-mode sessions may die under it; a boot-time compact was not requested by the user).
1079
+ The flag clears the moment any flush attempt runs (any outcome) and is re-evaluated on the next reload or enable. `turn_end` captures nothing while pruning is disabled, so the `/pruner on` probe is what puts tool results from a disabled stretch back in front of the gate; a `/pruner on` while already enabled does not probe. A reload followed by total idleness - no further turns at all - gets visibility only: the footer, `/pruner status`, and the `agent_end` pending notice (`prune: recovered pending (reload)`) all reflect the recoverable work, but nothing flushes automatically. This is by design - no immediate flush at boot (print-mode sessions may die under it; a boot-time compact was not requested by the user).
1080
1080
 
1081
1081
  ---
1082
1082
 
package/index.ts CHANGED
@@ -180,9 +180,9 @@ export default function (pi: ExtensionAPI) {
180
180
  // ── Helper: capture + trim + group pending batches (no LLM work) ──────────
181
181
  // Exposed to commands.ts via registerCommands so /pruner now can preview the
182
182
  // queue before opening the multi-row progress overlay.
183
- // `rethrow` is for the reload rearm probe only (session_start/session_tree):
184
- // it needs to observe a rescan failure so it can console.error and leave
185
- // rearmedPending false, per spec. Every other caller (turn_end capture path,
183
+ // `rethrow` is for probeRearm only: it needs to observe a rescan failure so
184
+ // it can console.error and leave rearmedPending false, per spec. Every
185
+ // other caller (turn_end capture path,
186
186
  // flushPending, /pruner commands) keeps the existing swallow-and-fall-back
187
187
  // behavior so a transient getBranch failure there never blocks the turn.
188
188
  const capturePendingBatches = (ctx: any, opts?: { rethrow?: boolean }): CapturedBatch[] => {
@@ -200,6 +200,20 @@ export default function (pi: ExtensionAPI) {
200
200
  return groupBatchesByMode(batches, currentConfig.value.batchingMode);
201
201
  };
202
202
 
203
+ // Arms the one-shot rearm flag when the persisted branch holds un-pruned
204
+ // tool results the in-memory queue does not know about: after a reload, a
205
+ // branch switch, or a stretch with pruning disabled (turn_end captures
206
+ // nothing while disabled). A disabled pruner never arms.
207
+ const probeRearm = (ctx: any): void => {
208
+ rearmedPending = false;
209
+ if (!currentConfig.value.enabled) return;
210
+ try {
211
+ rearmedPending = capturePendingBatches(ctx, { rethrow: true }).length > 0;
212
+ } catch (err) {
213
+ console.error("pi-condense: reload rearm probe failed", err);
214
+ }
215
+ };
216
+
203
217
  // Summarizes + indexes all pending batches.
204
218
  // When options.onProgress is provided batches are processed sequentially
205
219
  // (one LLM call each) so the caller can update per-row UI. Otherwise all
@@ -817,14 +831,7 @@ export default function (pi: ExtensionAPI) {
817
831
  // Clear any batches queued before the session reload
818
832
  pendingBatches.length = 0;
819
833
  previousFraction = null;
820
- rearmedPending = false;
821
- if (currentConfig.value.enabled) {
822
- try {
823
- rearmedPending = capturePendingBatches(ctx, { rethrow: true }).length > 0;
824
- } catch (err) {
825
- console.error("pi-condense: reload rearm probe failed", err);
826
- }
827
- }
834
+ probeRearm(ctx);
828
835
 
829
836
  // Update footer status
830
837
  setPruneStatusWidget(ctx, currentConfig.value, statsAccum.getLiveReclaim(), diagnostics.counts());
@@ -859,14 +866,7 @@ export default function (pi: ExtensionAPI) {
859
866
  // Pending batches belong to the old branch — discard them
860
867
  pendingBatches.length = 0;
861
868
  previousFraction = null;
862
- rearmedPending = false;
863
- if (currentConfig.value.enabled) {
864
- try {
865
- rearmedPending = capturePendingBatches(ctx, { rethrow: true }).length > 0;
866
- } catch (err) {
867
- console.error("pi-condense: reload rearm probe failed", err);
868
- }
869
- }
869
+ probeRearm(ctx);
870
870
 
871
871
  setPruneStatusWidget(ctx, currentConfig.value, statsAccum.getLiveReclaim(), diagnostics.counts());
872
872
  });
@@ -1138,5 +1138,6 @@ export default function (pi: ExtensionAPI) {
1138
1138
  () => diagnostics.counts(),
1139
1139
  (ctx: any) => computeMetricsSnapshot(ctx) ?? EMPTY_METRICS_SNAPSHOT,
1140
1140
  () => rearmedPending,
1141
+ probeRearm,
1141
1142
  );
1142
1143
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-condense",
3
- "version": "2.15.0",
3
+ "version": "2.15.1",
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",
@@ -73,6 +73,7 @@ function setupPrunerCommand(overrides: {
73
73
  undefined,
74
74
  overrides.getContextMetrics,
75
75
  overrides.getRearmed,
76
+ undefined,
76
77
  overrides.save,
77
78
  );
78
79
 
package/src/commands.ts CHANGED
@@ -477,6 +477,7 @@ export function registerCommands(
477
477
  getDiagnosticCounts?: () => Record<DiagnosticKind, number>,
478
478
  getContextMetrics?: (ctx: ExtensionCommandContext) => ContextMetricsSnapshot,
479
479
  getRearmed?: () => boolean,
480
+ probeRearm?: (ctx: ExtensionCommandContext) => void,
480
481
  save: (config: ContextPruneConfig) => Promise<void> = saveConfig,
481
482
  ): void {
482
483
  // Register the /pruner command
@@ -856,8 +857,12 @@ export function registerCommands(
856
857
 
857
858
  // ── /pruner on ──
858
859
  case "on": {
860
+ const wasEnabled = currentConfig.value.enabled;
859
861
  currentConfig.value = { ...currentConfig.value, enabled: true };
860
862
  void persistConfig((m, t) => ctx.ui.notify(m, t), currentConfig.value, save);
863
+ // Tool results produced while disabled were never queued; the probe
864
+ // puts them back in front of the next gate check.
865
+ if (!wasEnabled) probeRearm?.(ctx);
861
866
  ctx.ui.notify("Context pruning enabled.");
862
867
  setPruneStatusWidget(ctx, currentConfig.value, getLiveReclaim(), getDiagnosticCounts?.());
863
868
  break;
@@ -214,12 +214,13 @@ function bootExtension(
214
214
  budgetTurnDelta?: number | null;
215
215
  frontierGapThresholdTokens?: number | null;
216
216
  showPruneStatusLine?: boolean;
217
+ enabled?: boolean;
217
218
  } = {},
218
219
  ) {
219
220
  const agentDir = mkdtempSync(join(tmpdir(), "pi-condense-rearm-"));
220
221
  process.env.PI_CODING_AGENT_DIR = agentDir;
221
222
  const contextPruneSettings: any = {
222
- enabled: true,
223
+ enabled: options.enabled ?? true,
223
224
  pruneOn: "agent-message",
224
225
  batchingMode: "agent-message",
225
226
  autoBudgetThreshold: options.autoBudgetThreshold === undefined ? 0.5 : options.autoBudgetThreshold,
@@ -557,6 +558,24 @@ describe("reload rearm (issue #6)", () => {
557
558
  expect(statusCalls).toContain("\u2502 prune: recovered pending (reload)");
558
559
  });
559
560
 
561
+ it("/pruner on rearms the gate so work recovered from a disabled stretch flushes", async () => {
562
+ const { handlers, commands, ctx, appended } = await boot({ enabled: false });
563
+ await handlers.get("session_start")!({}, ctx);
564
+ const callsBefore = summarizerCalls;
565
+
566
+ const budgetTurn = { toolResults: [], message: { role: "assistant", content: [{ type: "text", text: "hi" }] }, turnIndex: 2 };
567
+ await handlers.get("turn_end")!(budgetTurn, ctx);
568
+ expect(summarizerCalls).toBe(callsBefore);
569
+
570
+ await commands.get("pruner")!("on", ctx);
571
+ await handlers.get("turn_end")!(budgetTurn, ctx);
572
+
573
+ expect(summarizerCalls).toBe(callsBefore + 1);
574
+ const fm = appended.filter((e) => e.type === "context-prune-flush-metrics");
575
+ expect(fm.length).toBe(1);
576
+ expect((fm[0].data as any).trigger).toBe("rearmed");
577
+ });
578
+
560
579
  it("reports a rescan failure to console.error and leaves rearmedPending false, without failing session_start (G4)", async () => {
561
580
  // Spec (Component 2, Rescan failure): "if the reload rearm probe throws,
562
581
  // console.error ... and leave rearmedPending = false - reload must never