pi-condense 2.6.0 → 2.8.0
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 +19 -0
- package/PRUNING.md +32 -3
- package/README.md +12 -1
- package/index.ts +288 -109
- package/package.json +1 -1
- package/src/budget.test.ts +49 -2
- package/src/budget.ts +22 -8
- package/src/commands.test.ts +138 -4
- package/src/commands.ts +32 -11
- package/src/context-metrics.test.ts +335 -0
- package/src/context-metrics.ts +152 -0
- package/src/reload-rearm.integration.test.ts +647 -0
- package/src/summarizer-wiring.test.ts +2 -0
- package/src/types.ts +48 -6
package/src/budget.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import type { ContextUsage } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
|
+
// Ceiling on what the budget triggers treat as the context window. Advertised
|
|
4
|
+
// windows reach 1M, which makes any (0,1] fraction unreachable in a real session.
|
|
5
|
+
// The two triggers apply it in different shapes on purpose: the threshold is a
|
|
6
|
+
// LEVEL, so the cap bounds the level itself (min(CAP, threshold * window)); the
|
|
7
|
+
// delta is a GROWTH RATE, where a 300k ceiling could never bind, so the cap
|
|
8
|
+
// enters through the denominator instead (delta * min(window, CAP)).
|
|
9
|
+
export const MAX_BUDGET_WINDOW = 300_000;
|
|
10
|
+
|
|
3
11
|
/**
|
|
4
|
-
* True iff a budget-triggered flush should fire
|
|
5
|
-
*
|
|
6
|
-
* (a 0–100 value, null
|
|
7
|
-
* compaction — guarded here.
|
|
12
|
+
* True iff a budget-triggered flush should fire: at `threshold` of the model's
|
|
13
|
+
* window, or at MAX_BUDGET_WINDOW tokens, whichever comes first. Computes the
|
|
14
|
+
* level ourselves rather than using ContextUsage.percent (a 0–100 value, null
|
|
15
|
+
* when tokens is null). tokens is also null right after a compaction — guarded here.
|
|
8
16
|
*/
|
|
9
17
|
export function shouldBudgetFlush(
|
|
10
18
|
usage: ContextUsage | undefined,
|
|
@@ -12,17 +20,23 @@ export function shouldBudgetFlush(
|
|
|
12
20
|
): boolean {
|
|
13
21
|
if (threshold == null || threshold <= 0 || threshold > 1) return false;
|
|
14
22
|
if (!usage || usage.tokens == null || !(usage.contextWindow > 0)) return false;
|
|
15
|
-
return usage.tokens
|
|
23
|
+
return usage.tokens >= Math.min(MAX_BUDGET_WINDOW, threshold * usage.contextWindow);
|
|
16
24
|
}
|
|
17
25
|
|
|
18
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Usage fraction against the effective window (min(contextWindow, MAX_BUDGET_WINDOW)),
|
|
28
|
+
* or null when usage is missing / tokens null / window non-positive. NOT bounded by 1:
|
|
29
|
+
* 600k tokens on a 1M window returns 2.0. Deliberately unclamped — clamping would make
|
|
30
|
+
* shouldDeltaFlush saturate above the ceiling and stop re-arming.
|
|
31
|
+
*/
|
|
19
32
|
export function usageFraction(usage: ContextUsage | undefined): number | null {
|
|
20
33
|
if (!usage || usage.tokens == null || !(usage.contextWindow > 0)) return null;
|
|
21
|
-
return usage.tokens / usage.contextWindow;
|
|
34
|
+
return usage.tokens / Math.min(usage.contextWindow, MAX_BUDGET_WINDOW);
|
|
22
35
|
}
|
|
23
36
|
|
|
24
37
|
/**
|
|
25
|
-
* True iff this turn's usage fraction rose by at least `delta` versus the previous turn
|
|
38
|
+
* True iff this turn's usage fraction rose by at least `delta` versus the previous turn,
|
|
39
|
+
* i.e. growth of at least `delta * min(contextWindow, MAX_BUDGET_WINDOW)` tokens.
|
|
26
40
|
* Mirrors shouldBudgetFlush's guards. previousFraction === null (first turn or post-restart)
|
|
27
41
|
* never fires; the absolute autoBudgetThreshold covers that gap.
|
|
28
42
|
*/
|
package/src/commands.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import { pruneStatusText, setPruneStatusWidget } from "./commands.js";
|
|
3
|
-
import type { ContextPruneConfig } from "./types.js";
|
|
1
|
+
import { describe, it, expect, mock } from "bun:test";
|
|
2
|
+
import { pruneStatusText, setPruneStatusWidget, registerCommands } from "./commands.js";
|
|
3
|
+
import type { ContextPruneConfig, ContextMetricsSnapshot, SummarizerStats } from "./types.js";
|
|
4
|
+
import { DEFAULT_CONFIG } from "./types.js";
|
|
4
5
|
|
|
5
6
|
const cfg = (enabled: boolean): ContextPruneConfig => ({ enabled } as ContextPruneConfig);
|
|
6
7
|
const cfgVisible = (enabled: boolean): ContextPruneConfig =>
|
|
@@ -10,12 +11,119 @@ function captureStatus(
|
|
|
10
11
|
config: ContextPruneConfig,
|
|
11
12
|
value?: Parameters<typeof setPruneStatusWidget>[2],
|
|
12
13
|
diagnostics?: Parameters<typeof setPruneStatusWidget>[3],
|
|
14
|
+
metrics?: Parameters<typeof setPruneStatusWidget>[4],
|
|
13
15
|
): string | undefined {
|
|
14
16
|
let captured: string | undefined;
|
|
15
|
-
setPruneStatusWidget({ ui: { setStatus: (_id, text) => { captured = text; } } }, config, value, diagnostics);
|
|
17
|
+
setPruneStatusWidget({ ui: { setStatus: (_id, text) => { captured = text; } } }, config, value, diagnostics, metrics);
|
|
16
18
|
return captured;
|
|
17
19
|
}
|
|
18
20
|
|
|
21
|
+
// ── /pruner command handler harness (registerCommands) ──────────────────────
|
|
22
|
+
// Drives the real switch-statement handler registered by registerCommands,
|
|
23
|
+
// with all injected collaborators stubbed. This is the seam for exercising
|
|
24
|
+
// /pruner subcommands without booting the full index.ts extension.
|
|
25
|
+
function setupPrunerCommand(overrides: {
|
|
26
|
+
capturePendingBatches?: () => any[];
|
|
27
|
+
flushPending?: (ctx: any, options?: any) => Promise<any>;
|
|
28
|
+
getRearmed?: () => boolean;
|
|
29
|
+
getContextMetrics?: (ctx: any) => ContextMetricsSnapshot;
|
|
30
|
+
} = {}) {
|
|
31
|
+
let handler: (args: string, ctx: any) => Promise<void>;
|
|
32
|
+
const notifications: { message: string; type?: string }[] = [];
|
|
33
|
+
const flushCalls: any[] = [];
|
|
34
|
+
|
|
35
|
+
const flushPending =
|
|
36
|
+
overrides.flushPending ??
|
|
37
|
+
(async (_ctx: any, options?: any) => {
|
|
38
|
+
flushCalls.push(options);
|
|
39
|
+
return { ok: false, reason: "empty" };
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const pi: any = {
|
|
43
|
+
registerCommand(_name: string, spec: { handler: (args: string, ctx: any) => Promise<void> }) {
|
|
44
|
+
handler = spec.handler;
|
|
45
|
+
},
|
|
46
|
+
registerMessageRenderer() {},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const currentConfig = { value: { ...DEFAULT_CONFIG, enabled: true } };
|
|
50
|
+
|
|
51
|
+
registerCommands(
|
|
52
|
+
pi,
|
|
53
|
+
currentConfig,
|
|
54
|
+
flushPending,
|
|
55
|
+
overrides.capturePendingBatches ?? (() => []),
|
|
56
|
+
() => ({ callCount: 0, totalInputTokens: 0, totalOutputTokens: 0, totalCost: 0 } as SummarizerStats),
|
|
57
|
+
() => undefined,
|
|
58
|
+
{} as any,
|
|
59
|
+
async () => ({ compressedEntries: [], skipped: 0 }),
|
|
60
|
+
undefined,
|
|
61
|
+
overrides.getContextMetrics,
|
|
62
|
+
undefined,
|
|
63
|
+
overrides.getRearmed,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const ctx: any = {
|
|
67
|
+
ui: {
|
|
68
|
+
notify(message: string, type?: string) {
|
|
69
|
+
notifications.push({ message, type });
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
run: (args: string) => handler(args, ctx),
|
|
76
|
+
notifications,
|
|
77
|
+
flushCalls,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe("/pruner now (empty capture)", () => {
|
|
82
|
+
it("invokes flushPending even when nothing is pending, so a flush-metrics entry is recorded", async () => {
|
|
83
|
+
const harness = setupPrunerCommand({ capturePendingBatches: () => [] });
|
|
84
|
+
await harness.run("now");
|
|
85
|
+
|
|
86
|
+
expect(harness.flushCalls.length).toBe(1);
|
|
87
|
+
expect(harness.flushCalls[0]).toMatchObject({ previewedBatches: [], trigger: "manual" });
|
|
88
|
+
expect(harness.notifications[0]?.message).toBe("pruner: nothing pending — no batches to summarize");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("/pruner status context block", () => {
|
|
93
|
+
const metrics: ContextMetricsSnapshot = {
|
|
94
|
+
openCycleThinkingTokens: 12000,
|
|
95
|
+
largestChainSharePct: 62,
|
|
96
|
+
frontierGapTokens: 195000,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
it("renders the --- context --- block with all three metrics", async () => {
|
|
100
|
+
const harness = setupPrunerCommand({ getContextMetrics: () => metrics, getRearmed: () => false });
|
|
101
|
+
await harness.run("status");
|
|
102
|
+
|
|
103
|
+
const text = harness.notifications[0]?.message ?? "";
|
|
104
|
+
expect(text).toContain("--- context ---");
|
|
105
|
+
expect(text).toContain("thinking: 12.0k tokens (open segment)");
|
|
106
|
+
expect(text).toContain("chain share: 62%");
|
|
107
|
+
expect(text).toContain("frontier gap: 195.0k tokens");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("appends the rearmed: line only when getRearmed() is true", async () => {
|
|
111
|
+
const armed = setupPrunerCommand({ getContextMetrics: () => metrics, getRearmed: () => true });
|
|
112
|
+
await armed.run("status");
|
|
113
|
+
expect(armed.notifications[0]?.message).toContain("rearmed: yes");
|
|
114
|
+
|
|
115
|
+
const notArmed = setupPrunerCommand({ getContextMetrics: () => metrics, getRearmed: () => false });
|
|
116
|
+
await notArmed.run("status");
|
|
117
|
+
expect(notArmed.notifications[0]?.message).not.toContain("rearmed:");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("omits the context block entirely when getContextMetrics is unwired", async () => {
|
|
121
|
+
const harness = setupPrunerCommand();
|
|
122
|
+
await harness.run("status");
|
|
123
|
+
expect(harness.notifications[0]?.message).not.toContain("--- context ---");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
19
127
|
describe("pruneStatusText", () => {
|
|
20
128
|
it("disabled config -> 'prune: OFF'", () => {
|
|
21
129
|
expect(pruneStatusText(cfg(false))).toBe("prune: OFF");
|
|
@@ -94,3 +202,29 @@ describe("diagnostic counters on the status line", () => {
|
|
|
94
202
|
);
|
|
95
203
|
});
|
|
96
204
|
});
|
|
205
|
+
|
|
206
|
+
describe("context metrics suffix on the status line", () => {
|
|
207
|
+
const metrics: ContextMetricsSnapshot = {
|
|
208
|
+
openCycleThinkingTokens: 12000,
|
|
209
|
+
largestChainSharePct: 62,
|
|
210
|
+
frontierGapTokens: 195000,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
it("appends a compact think/gap/chain segment when frontierGapTokens > 0", () => {
|
|
214
|
+
const text = pruneStatusText(cfg(true), undefined, undefined, metrics);
|
|
215
|
+
expect(text).toContain("\u00b7 think 12.0k \u00b7 gap 195.0k \u00b7 chain 62%");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("omits the suffix when frontierGapTokens is 0", () => {
|
|
219
|
+
const withZeroGap = { ...metrics, frontierGapTokens: 0 };
|
|
220
|
+
expect(pruneStatusText(cfg(true), undefined, undefined, withZeroGap)).toBe(
|
|
221
|
+
pruneStatusText(cfg(true)),
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("composes after the diag suffix when both are present", () => {
|
|
226
|
+
const mixedDiag = { "unresolved-range": 2, "range-id-mismatch": 0, "orphan-sweep": 1 } as const;
|
|
227
|
+
const text = pruneStatusText(cfg(true), undefined, mixedDiag, metrics);
|
|
228
|
+
expect(text).toBe("prune: ON \u00b7 diag u2/o1 \u00b7 think 12.0k \u00b7 gap 195.0k \u00b7 chain 62%");
|
|
229
|
+
});
|
|
230
|
+
});
|
package/src/commands.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type ChainCompressionEntry,
|
|
7
7
|
type FlushOptions,
|
|
8
8
|
type DiagnosticKind,
|
|
9
|
+
type ContextMetricsSnapshot,
|
|
9
10
|
PRUNE_ON_MODES,
|
|
10
11
|
BATCHING_MODES,
|
|
11
12
|
STATUS_WIDGET_ID,
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
} from "./types.js";
|
|
24
25
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
25
26
|
import { saveConfig } from "./config.js";
|
|
27
|
+
import { MAX_BUDGET_WINDOW } from "./budget.js";
|
|
26
28
|
import { formatTokens, formatCost, formatCharProgress, formatCompactCount } from "./stats.js";
|
|
27
29
|
import { Container, Text, SettingsList, type SettingItem } from "@earendil-works/pi-tui";
|
|
28
30
|
import { DynamicBorder, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
@@ -62,6 +64,7 @@ export function pruneStatusText(
|
|
|
62
64
|
config: ContextPruneConfig,
|
|
63
65
|
reclaim?: LiveReclaim,
|
|
64
66
|
diagnostics?: Record<DiagnosticKind, number>,
|
|
67
|
+
metrics?: ContextMetricsSnapshot,
|
|
65
68
|
): string {
|
|
66
69
|
if (!config.enabled) return "prune: OFF";
|
|
67
70
|
const diag = diagnostics
|
|
@@ -72,11 +75,14 @@ export function pruneStatusText(
|
|
|
72
75
|
].filter(Boolean)
|
|
73
76
|
: [];
|
|
74
77
|
const suffix = diag.length > 0 ? ` \u00b7 diag ${diag.join("/")}` : "";
|
|
75
|
-
|
|
78
|
+
const metricsSuffix = metrics && metrics.frontierGapTokens > 0
|
|
79
|
+
? ` \u00b7 think ${formatCompactCount(metrics.openCycleThinkingTokens)} \u00b7 gap ${formatCompactCount(metrics.frontierGapTokens)} \u00b7 chain ${metrics.largestChainSharePct}%`
|
|
80
|
+
: "";
|
|
81
|
+
if (!reclaim || reclaim.beforeChars <= 0) return `prune: ON${suffix}${metricsSuffix}`;
|
|
76
82
|
const beforeTok = Math.round(reclaim.beforeChars / 4);
|
|
77
83
|
const afterTok = Math.round(reclaim.afterChars / 4);
|
|
78
84
|
const reduction = Math.max(0, Math.round((1 - afterTok / beforeTok) * 100));
|
|
79
|
-
return `prune: ON \u00b7 ${formatCompactCount(beforeTok)}->${formatCompactCount(afterTok)} (-${reduction}%)${suffix}`;
|
|
85
|
+
return `prune: ON \u00b7 ${formatCompactCount(beforeTok)}->${formatCompactCount(afterTok)} (-${reduction}%)${suffix}${metricsSuffix}`;
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
export function setPruneStatusWidget(
|
|
@@ -84,12 +90,13 @@ export function setPruneStatusWidget(
|
|
|
84
90
|
config: ContextPruneConfig,
|
|
85
91
|
value?: LiveReclaim | string,
|
|
86
92
|
diagnostics?: Record<DiagnosticKind, number>,
|
|
93
|
+
metrics?: ContextMetricsSnapshot,
|
|
87
94
|
): void {
|
|
88
95
|
if (!config.showPruneStatusLine) {
|
|
89
96
|
ctx.ui.setStatus(STATUS_WIDGET_ID, undefined);
|
|
90
97
|
return;
|
|
91
98
|
}
|
|
92
|
-
const text = typeof value === "string" ? value : pruneStatusText(config, value, diagnostics);
|
|
99
|
+
const text = typeof value === "string" ? value : pruneStatusText(config, value, diagnostics, metrics);
|
|
93
100
|
// Leading-only separator: the footer joins extension status segments with a
|
|
94
101
|
// single space, so a trailing divider collides with the next segment's leading
|
|
95
102
|
// one and renders doubled. One leading bar yields single dividers between
|
|
@@ -228,10 +235,12 @@ function maxTimeoutDescription(config: ContextPruneConfig): string {
|
|
|
228
235
|
}
|
|
229
236
|
|
|
230
237
|
function autoBudgetThresholdDescription(config: ContextPruneConfig): string {
|
|
238
|
+
const cap = `${MAX_BUDGET_WINDOW / 1000}k`;
|
|
231
239
|
if (config.autoBudgetThreshold == null) {
|
|
232
|
-
return `Token-budget auto-flush: force a prune when context usage reaches this share of the window, regardless of prune-on mode. Currently off. Pick a percentage to enable.`;
|
|
240
|
+
return `Token-budget auto-flush: force a prune when context usage reaches this share of the window (or ${cap} tokens, whichever comes first), regardless of prune-on mode. Currently off. Pick a percentage to enable.`;
|
|
233
241
|
}
|
|
234
|
-
|
|
242
|
+
const pct = Math.round(config.autoBudgetThreshold * 100);
|
|
243
|
+
return `Token-budget auto-flush: force a prune when context usage reaches ${pct}% of the window or ${cap} tokens, whichever comes first, regardless of prune-on mode. The ${cap} ceiling keeps this reachable on huge-window models. Set to Off to disable.`;
|
|
235
244
|
}
|
|
236
245
|
|
|
237
246
|
function protectedToolsDisplay(list: string[]): string {
|
|
@@ -470,6 +479,9 @@ export function registerCommands(
|
|
|
470
479
|
indexer: ToolCallIndexer,
|
|
471
480
|
compactChains: (ctx: ExtensionCommandContext) => Promise<{ compressedEntries: ChainCompressionEntry[]; skipped: number }>,
|
|
472
481
|
getDiagnosticCounts?: () => Record<DiagnosticKind, number>,
|
|
482
|
+
getContextMetrics?: (ctx: ExtensionCommandContext) => ContextMetricsSnapshot,
|
|
483
|
+
getCachedMetrics?: () => ContextMetricsSnapshot | undefined,
|
|
484
|
+
getRearmed?: () => boolean,
|
|
473
485
|
): void {
|
|
474
486
|
// Register the /pruner command
|
|
475
487
|
pi.registerCommand("pruner", {
|
|
@@ -816,7 +828,7 @@ export function registerCommands(
|
|
|
816
828
|
}
|
|
817
829
|
currentConfig.value = newConfig;
|
|
818
830
|
saveConfig(newConfig);
|
|
819
|
-
setPruneStatusWidget(ctx, newConfig, getLiveReclaim(), getDiagnosticCounts?.());
|
|
831
|
+
setPruneStatusWidget(ctx, newConfig, getLiveReclaim(), getDiagnosticCounts?.(), getCachedMetrics?.());
|
|
820
832
|
settingsList?.invalidate();
|
|
821
833
|
};
|
|
822
834
|
|
|
@@ -851,7 +863,7 @@ export function registerCommands(
|
|
|
851
863
|
currentConfig.value = { ...currentConfig.value, enabled: true };
|
|
852
864
|
saveConfig(currentConfig.value);
|
|
853
865
|
ctx.ui.notify("Context pruning enabled.");
|
|
854
|
-
setPruneStatusWidget(ctx, currentConfig.value, getLiveReclaim(), getDiagnosticCounts?.());
|
|
866
|
+
setPruneStatusWidget(ctx, currentConfig.value, getLiveReclaim(), getDiagnosticCounts?.(), getCachedMetrics?.());
|
|
855
867
|
break;
|
|
856
868
|
}
|
|
857
869
|
|
|
@@ -860,7 +872,7 @@ export function registerCommands(
|
|
|
860
872
|
currentConfig.value = { ...currentConfig.value, enabled: false };
|
|
861
873
|
saveConfig(currentConfig.value);
|
|
862
874
|
ctx.ui.notify("Context pruning disabled.");
|
|
863
|
-
setPruneStatusWidget(ctx, currentConfig.value, getLiveReclaim(), getDiagnosticCounts?.());
|
|
875
|
+
setPruneStatusWidget(ctx, currentConfig.value, getLiveReclaim(), getDiagnosticCounts?.(), getCachedMetrics?.());
|
|
864
876
|
break;
|
|
865
877
|
}
|
|
866
878
|
|
|
@@ -873,8 +885,12 @@ export function registerCommands(
|
|
|
873
885
|
? `\n --- summarizer ---\n calls: ${s.callCount}\n input: ${formatTokens(s.totalInputTokens)} tokens\n output: ${formatTokens(s.totalOutputTokens)} tokens\n cost: ${formatCost(s.totalCost)}`
|
|
874
886
|
: "\n (no summarizer calls yet)";
|
|
875
887
|
const fmtTimeout = (ms: number) => (ms === 0 ? "disabled" : `${Math.round(ms / 1000)}s`);
|
|
888
|
+
const m = getContextMetrics?.(ctx);
|
|
889
|
+
const contextLine = m
|
|
890
|
+
? `\n --- context ---\n thinking: ${formatTokens(m.openCycleThinkingTokens)} tokens (open segment)\n chain share: ${m.largestChainSharePct}%\n frontier gap: ${formatTokens(m.frontierGapTokens)} tokens${getRearmed?.() ? "\n rearmed: yes" : ""}`
|
|
891
|
+
: "";
|
|
876
892
|
ctx.ui.notify(
|
|
877
|
-
`pruner status:\n enabled: ${cfg.enabled}\n model: ${cfg.summarizerModel}\n thinking: ${summarizerThinkingLabel(cfg.summarizerThinking)} (${cfg.summarizerThinking})\n idle to: ${fmtTimeout(cfg.summarizerIdleTimeoutMs)}\n max to: ${fmtTimeout(cfg.summarizerMaxTimeoutMs)}\n trigger: ${mode}\n batching: ${batchingModeLabel(cfg.batchingMode)} (${cfg.batchingMode})\n dedup: ${cfg.dedupByContentHash ? "on" : "off"}\n status: ${cfg.showPruneStatusLine ? "on" : "off"}${statsLine}`,
|
|
893
|
+
`pruner status:\n enabled: ${cfg.enabled}\n model: ${cfg.summarizerModel}\n thinking: ${summarizerThinkingLabel(cfg.summarizerThinking)} (${cfg.summarizerThinking})\n idle to: ${fmtTimeout(cfg.summarizerIdleTimeoutMs)}\n max to: ${fmtTimeout(cfg.summarizerMaxTimeoutMs)}\n trigger: ${mode}\n batching: ${batchingModeLabel(cfg.batchingMode)} (${cfg.batchingMode})\n dedup: ${cfg.dedupByContentHash ? "on" : "off"}\n status: ${cfg.showPruneStatusLine ? "on" : "off"}${statsLine}${contextLine}`,
|
|
878
894
|
);
|
|
879
895
|
break;
|
|
880
896
|
}
|
|
@@ -979,7 +995,7 @@ export function registerCommands(
|
|
|
979
995
|
currentConfig.value = { ...currentConfig.value, pruneOn: modeArg as ContextPruneConfig["pruneOn"] };
|
|
980
996
|
}
|
|
981
997
|
saveConfig(currentConfig.value);
|
|
982
|
-
setPruneStatusWidget(ctx, currentConfig.value, getLiveReclaim(), getDiagnosticCounts?.());
|
|
998
|
+
setPruneStatusWidget(ctx, currentConfig.value, getLiveReclaim(), getDiagnosticCounts?.(), getCachedMetrics?.());
|
|
983
999
|
break;
|
|
984
1000
|
}
|
|
985
1001
|
|
|
@@ -1052,6 +1068,11 @@ export function registerCommands(
|
|
|
1052
1068
|
const batches = capturePendingBatches(ctx);
|
|
1053
1069
|
if (batches.length === 0) {
|
|
1054
1070
|
ctx.ui.notify("pruner: nothing pending — no batches to summarize", "info");
|
|
1071
|
+
// Still invoke flushPending so its finally-emitted flush-metrics entry
|
|
1072
|
+
// records this attempt (outcome "empty") — the incident's exact
|
|
1073
|
+
// undiagnosable "nothing pending" report is precisely what this log
|
|
1074
|
+
// exists to make diagnosable on recurrence.
|
|
1075
|
+
await flushPending(ctx, { previewedBatches: batches, trigger: "manual" });
|
|
1055
1076
|
break;
|
|
1056
1077
|
}
|
|
1057
1078
|
|
|
@@ -1076,7 +1097,7 @@ export function registerCommands(
|
|
|
1076
1097
|
|
|
1077
1098
|
// Remove the widget and restore the normal footer status.
|
|
1078
1099
|
clearWidget();
|
|
1079
|
-
setPruneStatusWidget(ctx, currentConfig.value, getLiveReclaim(), getDiagnosticCounts?.());
|
|
1100
|
+
setPruneStatusWidget(ctx, currentConfig.value, getLiveReclaim(), getDiagnosticCounts?.(), getCachedMetrics?.());
|
|
1080
1101
|
|
|
1081
1102
|
if (!result.ok) {
|
|
1082
1103
|
const suffix = "error" in result && result.error ? ` (${result.error})` : "";
|