pi-blackhole 0.3.2 → 0.3.4
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 +63 -62
- package/package.json +1 -1
- package/src/commands/memory.ts +5 -5
- package/src/commands/pi-vcc.ts +44 -7
- package/src/core/settings.ts +17 -2
- package/src/core/unified-config.ts +184 -20
- package/src/hooks/before-compact.ts +148 -9
- package/src/om/compaction-trigger.ts +26 -12
- package/src/om/configure-overlay.ts +383 -0
- package/src/om/consolidation.ts +64 -6
- package/src/om/cooldown.ts +10 -0
- package/src/om/key-matcher.ts +38 -0
- package/src/om/model-budget.ts +22 -0
- package/src/om/runtime.ts +31 -1
- package/src/om/status-overlay.ts +243 -0
package/src/om/runtime.ts
CHANGED
|
@@ -45,6 +45,13 @@ export class Runtime {
|
|
|
45
45
|
consolidationInFlight = false;
|
|
46
46
|
consolidationPromise: Promise<void> | null = null;
|
|
47
47
|
consolidationPhase: ConsolidationPhase | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Models that failed in the current consolidation stage (in-memory only).
|
|
50
|
+
* Used when cooldownHours is 0 — avoids disk writes while still letting
|
|
51
|
+
* the retry loop advance past the failed model within this stage.
|
|
52
|
+
* Cleared between stages at the pipeline level.
|
|
53
|
+
*/
|
|
54
|
+
failedInCycle: Set<string> = new Set();
|
|
48
55
|
compactInFlight = false;
|
|
49
56
|
compactHookInFlight = false;
|
|
50
57
|
resolveFailureNotified = false;
|
|
@@ -101,10 +108,23 @@ export class Runtime {
|
|
|
101
108
|
|
|
102
109
|
// Try configured candidates
|
|
103
110
|
for (const candidate of candidates) {
|
|
111
|
+
const key = modelKey(candidate);
|
|
112
|
+
|
|
113
|
+
// In-memory skip: model failed earlier in this stage with cooldownHours 0
|
|
114
|
+
if (this.failedInCycle.has(key)) {
|
|
115
|
+
if (ctx.hasUI && ctx.ui) {
|
|
116
|
+
ctx.ui.notify(
|
|
117
|
+
`Observational memory: ${stageName} skipping ${key} (failed this cycle, cooldown disabled)`,
|
|
118
|
+
"info",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
104
124
|
if (isCooldownActive(candidate)) {
|
|
105
125
|
if (ctx.hasUI && ctx.ui) {
|
|
106
126
|
ctx.ui.notify(
|
|
107
|
-
`Observational memory: ${stageName} skipping ${
|
|
127
|
+
`Observational memory: ${stageName} skipping ${key} (cooldown active)`,
|
|
108
128
|
"info",
|
|
109
129
|
);
|
|
110
130
|
}
|
|
@@ -179,9 +199,19 @@ export class Runtime {
|
|
|
179
199
|
/**
|
|
180
200
|
* Record a retryable error for a model. The model must be one of the candidates
|
|
181
201
|
* (not the session model). If it's the session model we don't cool it down.
|
|
202
|
+
*
|
|
203
|
+
* When cooldownHours is explicitly 0, the model is tracked in-memory for the
|
|
204
|
+
* current consolidation stage (no disk writes). Otherwise a persisted cooldown
|
|
205
|
+
* is recorded.
|
|
182
206
|
*/
|
|
183
207
|
recordRetryableError(modelConfig: ConfiguredModel | undefined, error: unknown, stage: ConsolidationPhase): void {
|
|
184
208
|
if (!modelConfig) return;
|
|
209
|
+
if (modelConfig.cooldownHours === 0) {
|
|
210
|
+
// In-memory only: skip this model for the rest of this stage.
|
|
211
|
+
// No disk writes, no persistent cooldown.
|
|
212
|
+
this.failedInCycle.add(modelKey(modelConfig));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
185
215
|
const reason = error instanceof Error ? error.message : String(error || "unknown error");
|
|
186
216
|
recordCooldown(modelConfig, reason, stage);
|
|
187
217
|
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status Overlay — shows current compaction config, OM state, and pipeline status.
|
|
3
|
+
*
|
|
4
|
+
* Used by `/blackhole-memory` to display runtime state.
|
|
5
|
+
* Opens as a floating overlay via ctx.ui.custom({ overlay: true }).
|
|
6
|
+
* Press Enter/Space on "Open configure overlay" to open config overlay.
|
|
7
|
+
* Esc to close.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { visibleWidth } from "./key-matcher.js";
|
|
11
|
+
import { matchesKey } from "@earendil-works/pi-tui";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Theme shape (duck-typed from what pi provides)
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
type ThemeShim = {
|
|
18
|
+
fg: (style: string, text: string) => string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const EMPTY_THEME: ThemeShim = {
|
|
22
|
+
fg: (_s: string, t: string) => t,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Status display
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
export interface StatusInfo {
|
|
30
|
+
compaction: "auto" | "manual" | "off";
|
|
31
|
+
compactionEngine: "blackhole" | "pi-default";
|
|
32
|
+
tailBehavior: "pi-default" | "minimal";
|
|
33
|
+
memory: boolean;
|
|
34
|
+
compactAfterTokens: number;
|
|
35
|
+
consolidationInFlight: boolean;
|
|
36
|
+
compactInFlight: boolean;
|
|
37
|
+
lastObserverError?: string;
|
|
38
|
+
lastReflectorError?: string;
|
|
39
|
+
lastDropperError?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface StatusResult {
|
|
43
|
+
action: "configure" | "close";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create a StatusOverlay instance to pass to ctx.ui.custom().
|
|
48
|
+
*
|
|
49
|
+
* Usage:
|
|
50
|
+
* ```ts
|
|
51
|
+
* const result = await ctx.ui.custom<StatusResult | undefined>(
|
|
52
|
+
* (tui, theme, _kb, done) => createStatusOverlay(info, theme, tui, done),
|
|
53
|
+
* { overlay: true },
|
|
54
|
+
* );
|
|
55
|
+
* if (result?.action === "configure") {
|
|
56
|
+
* // open configure overlay
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function createStatusOverlay(
|
|
61
|
+
info: StatusInfo,
|
|
62
|
+
theme: unknown,
|
|
63
|
+
tui: { requestRender: () => void },
|
|
64
|
+
done: (result: StatusResult | undefined) => void,
|
|
65
|
+
): { render(width: number): string[]; handleInput(data: string): void; invalidate(): void; dispose(): void } {
|
|
66
|
+
const th: ThemeShim = (theme && typeof (theme as Record<string, unknown>).fg === "function")
|
|
67
|
+
? theme as ThemeShim
|
|
68
|
+
: EMPTY_THEME;
|
|
69
|
+
|
|
70
|
+
const fg = (style: string, text: string) => th.fg(style, text);
|
|
71
|
+
const dim = (s: string) => fg("dim", s);
|
|
72
|
+
const accent = (s: string) => fg("accent", s);
|
|
73
|
+
const success = (s: string) => fg("success", s);
|
|
74
|
+
const warning = (s: string) => fg("warning", s);
|
|
75
|
+
const error = (s: string) => fg("error", s);
|
|
76
|
+
const muted = (s: string) => fg("muted", s);
|
|
77
|
+
|
|
78
|
+
const configItems: { label: string; value: string }[] = [
|
|
79
|
+
{ label: "Compaction", value: info.compaction },
|
|
80
|
+
{ label: "Engine", value: info.compactionEngine },
|
|
81
|
+
{ label: "Tail behavior", value: info.tailBehavior },
|
|
82
|
+
{ label: "Memory", value: info.memory ? "Enabled" : "Disabled" },
|
|
83
|
+
{ label: "Threshold", value: `${info.compactAfterTokens.toLocaleString()} tok` },
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const pipelineItems: { label: string; value: string }[] = [
|
|
87
|
+
{ label: "Compaction in flight", value: info.compactInFlight ? "yes" : "no" },
|
|
88
|
+
{ label: "Consolidation in flight", value: info.consolidationInFlight ? "yes" : "no" },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const errorItems: { label: string; value: string }[] = [];
|
|
92
|
+
if (info.lastObserverError) errorItems.push({ label: "Observer", value: info.lastObserverError });
|
|
93
|
+
if (info.lastReflectorError) errorItems.push({ label: "Reflector", value: info.lastReflectorError });
|
|
94
|
+
if (info.lastDropperError) errorItems.push({ label: "Dropper", value: info.lastDropperError });
|
|
95
|
+
|
|
96
|
+
const actionItems = [
|
|
97
|
+
{ label: "Open configure overlay", value: "configure" as const },
|
|
98
|
+
{ label: "Close", value: "close" as const },
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
// Contiguous selectable item model: no section headers in nav indices
|
|
102
|
+
const selectableConfigCount = configItems.length;
|
|
103
|
+
const selectablePipelineCount = pipelineItems.length;
|
|
104
|
+
const selectableErrorCount = errorItems.length;
|
|
105
|
+
const selectableActionCount = actionItems.length;
|
|
106
|
+
const totalSelectable = selectableConfigCount + selectablePipelineCount + selectableErrorCount + selectableActionCount;
|
|
107
|
+
|
|
108
|
+
// Render helpers: map a contiguous nav index to the display positions
|
|
109
|
+
// (section headers are inserted at render time, not in the nav model)
|
|
110
|
+
const navIsAction = (idx: number) => idx >= selectableConfigCount + selectablePipelineCount + selectableErrorCount;
|
|
111
|
+
|
|
112
|
+
let selectedIndex = 0;
|
|
113
|
+
let cachedLines: string[] | undefined;
|
|
114
|
+
|
|
115
|
+
function invalidate(): void {
|
|
116
|
+
cachedLines = undefined;
|
|
117
|
+
try { tui.requestRender(); } catch { /* no-op */ }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function handleInput(data: string): void {
|
|
121
|
+
if (matchesKey(data, "escape")) {
|
|
122
|
+
done({ action: "close" });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (matchesKey(data, "enter") || matchesKey(data, "space")) {
|
|
127
|
+
if (navIsAction(selectedIndex)) {
|
|
128
|
+
const actionIdx = selectedIndex - selectableConfigCount - selectablePipelineCount - selectableErrorCount;
|
|
129
|
+
const action = actionItems[actionIdx]!;
|
|
130
|
+
done({ action: action.value });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (matchesKey(data, "up")) {
|
|
137
|
+
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
138
|
+
invalidate();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (matchesKey(data, "down")) {
|
|
142
|
+
selectedIndex = Math.min(totalSelectable - 1, selectedIndex + 1);
|
|
143
|
+
invalidate();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function render(width: number): string[] {
|
|
149
|
+
if (cachedLines) return cachedLines;
|
|
150
|
+
|
|
151
|
+
const w = Math.max(2, Math.min(width - 2, 74));
|
|
152
|
+
const innerW = w - 4;
|
|
153
|
+
|
|
154
|
+
const pad = (s: string, len: number) => {
|
|
155
|
+
const vis = visibleWidth(s);
|
|
156
|
+
return s + " ".repeat(Math.max(0, len - vis));
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const line = (content: string) => `│ ${pad(content, innerW)} │`;
|
|
160
|
+
const border = (s: string) => fg("border", s);
|
|
161
|
+
|
|
162
|
+
const lines: string[] = [];
|
|
163
|
+
|
|
164
|
+
// Top border + header
|
|
165
|
+
lines.push(fg("border", `╭${"─".repeat(w - 2)}╮`));
|
|
166
|
+
lines.push(fg("border", `│ ${accent("Blackhole Status")}${" ".repeat(Math.max(0, innerW + 1 - 16))}│`));
|
|
167
|
+
lines.push(fg("border", `├${"─".repeat(w - 2)}┤`));
|
|
168
|
+
|
|
169
|
+
// Config section
|
|
170
|
+
lines.push(border(line(` ${dim("─── Compaction Config ───")}`)));
|
|
171
|
+
for (let i = 0; i < configItems.length; i++) {
|
|
172
|
+
const item = configItems[i]!;
|
|
173
|
+
const isSelected = selectedIndex === i;
|
|
174
|
+
const prefix = isSelected ? accent(" ›") : " ";
|
|
175
|
+
const label = isSelected ? accent(`${item.label}:`) : `${item.label}:`;
|
|
176
|
+
let value: string;
|
|
177
|
+
switch (item.label) {
|
|
178
|
+
case "Compaction":
|
|
179
|
+
value = item.value === "auto" ? accent(item.value) : item.value === "off" ? muted(item.value) : dim(item.value);
|
|
180
|
+
break;
|
|
181
|
+
case "Engine":
|
|
182
|
+
value = item.value === "blackhole" ? success(item.value) : dim(item.value);
|
|
183
|
+
break;
|
|
184
|
+
case "Memory":
|
|
185
|
+
value = item.value === "Enabled" ? success("Enabled") : muted("Disabled");
|
|
186
|
+
break;
|
|
187
|
+
default:
|
|
188
|
+
value = dim(item.value);
|
|
189
|
+
}
|
|
190
|
+
lines.push(border(line(`${prefix} ${pad(label, 16)} ${value}`)));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Pipeline section
|
|
194
|
+
if (pipelineItems.length > 0) {
|
|
195
|
+
lines.push(border(line(` ${dim("─── Pipeline ───")}`)));
|
|
196
|
+
for (let i = 0; i < pipelineItems.length; i++) {
|
|
197
|
+
const item = pipelineItems[i]!;
|
|
198
|
+
const isSelected = selectedIndex === selectableConfigCount + i;
|
|
199
|
+
const prefix = isSelected ? accent(" ›") : " ";
|
|
200
|
+
const label = isSelected ? accent(`${item.label}:`) : `${item.label}:`;
|
|
201
|
+
const value = item.value === "yes" ? warning("yes") : dim("no");
|
|
202
|
+
lines.push(border(line(`${prefix} ${pad(label, 16)} ${value}`)));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Errors section
|
|
207
|
+
if (errorItems.length > 0) {
|
|
208
|
+
lines.push(border(line(` ${dim("─── Last Errors ───")}`)));
|
|
209
|
+
for (let i = 0; i < errorItems.length; i++) {
|
|
210
|
+
const item = errorItems[i]!;
|
|
211
|
+
const isSelected = selectedIndex === selectableConfigCount + selectablePipelineCount + i;
|
|
212
|
+
const prefix = isSelected ? accent(" ›") : " ";
|
|
213
|
+
const label = isSelected ? accent(`${item.label}:`) : `${item.label}:`;
|
|
214
|
+
const truncated = item.value.length > 40
|
|
215
|
+
? item.value.slice(0, 37) + "..."
|
|
216
|
+
: item.value;
|
|
217
|
+
lines.push(border(line(`${prefix} ${pad(label, 16)} ${error(truncated)}`)));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Separator
|
|
222
|
+
lines.push(border(line(` ${dim("─── Actions ───")}`)));
|
|
223
|
+
|
|
224
|
+
// Action items
|
|
225
|
+
for (let i = 0; i < actionItems.length; i++) {
|
|
226
|
+
const item = actionItems[i]!;
|
|
227
|
+
const isSelected = selectedIndex === selectableConfigCount + selectablePipelineCount + selectableErrorCount + i;
|
|
228
|
+
const prefix = isSelected ? accent(" ▶") : " ";
|
|
229
|
+
const text = isSelected ? accent(item.label) : dim(item.label);
|
|
230
|
+
lines.push(border(line(`${prefix} ${text}`)));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Bottom hints
|
|
234
|
+
lines.push(border(line("")));
|
|
235
|
+
lines.push(border(dim(" ↑↓ navigate Enter/Space select Esc close ")));
|
|
236
|
+
lines.push(fg("border", `╰${"─".repeat(w - 2)}╯`));
|
|
237
|
+
|
|
238
|
+
cachedLines = lines;
|
|
239
|
+
return lines;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return { render, handleInput, invalidate, dispose: () => {} };
|
|
243
|
+
}
|