pi-blackhole 0.3.3 → 0.3.5
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 +13 -13
- package/example-config.json +19 -27
- package/package.json +1 -1
- package/src/commands/pi-vcc.ts +3 -9
- package/src/core/brief.ts +18 -13
- package/src/core/build-sections.ts +1 -2
- package/src/core/content.ts +8 -5
- package/src/core/drill-down.ts +2 -9
- package/src/core/filter-noise.ts +7 -10
- package/src/core/format.ts +25 -25
- package/src/core/load-messages.ts +71 -2
- package/src/core/normalize.ts +10 -4
- package/src/core/render-entries.ts +10 -2
- package/src/core/search-entries.ts +9 -1
- package/src/core/settings.ts +3 -34
- package/src/core/summarize.ts +33 -13
- package/src/core/tool-args.ts +4 -1
- package/src/core/unified-config.ts +33 -26
- package/src/extract/commits.ts +3 -2
- package/src/extract/files.ts +6 -4
- package/src/hooks/before-compact.ts +7 -3
- package/src/om/agents/dropper/agent.ts +2 -7
- package/src/om/agents/observer/agent.ts +11 -13
- package/src/om/agents/reflector/agent.ts +2 -7
- package/src/om/compaction-trigger.ts +1 -8
- package/src/om/configure-overlay.ts +20 -14
- package/src/om/consolidation.ts +57 -18
- package/src/om/cooldown.ts +24 -11
- package/src/om/debug-log.ts +79 -11
- package/src/om/key-matcher.ts +6 -66
- package/src/om/ledger/fold.ts +8 -0
- package/src/om/ledger/render-summary.ts +16 -15
- package/src/om/ledger/types.ts +1 -1
- package/src/om/pending.ts +38 -5
- package/src/om/provider-stream.ts +17 -0
- package/src/om/retryable-error.ts +26 -0
- package/src/om/reverse-recall.ts +9 -1
- package/src/om/runtime.ts +69 -18
- package/src/om/status-overlay.ts +6 -5
- package/src/sections.ts +0 -3
- package/src/tools/recall.ts +7 -2
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared retryable-error regex and detection function.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from compaction-trigger.ts and cooldown.ts which had diverging
|
|
5
|
+
* copies of the same logic. This is the single source of truth.
|
|
6
|
+
*
|
|
7
|
+
* Now also re-exports Pi's context-overflow detection from @earendil-works/pi-ai,
|
|
8
|
+
* avoiding the need to duplicate 20+ provider-specific overflow patterns.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Regex matching retryable API error messages. */
|
|
12
|
+
export const RETRYABLE_ERROR_RE =
|
|
13
|
+
/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i;
|
|
14
|
+
|
|
15
|
+
/** Check whether an error string or Error indicates a retryable error. */
|
|
16
|
+
export function isRetryableError(error: unknown): boolean {
|
|
17
|
+
const message = error instanceof Error ? error.message : String(error || "");
|
|
18
|
+
return RETRYABLE_ERROR_RE.test(message);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Re-export Pi's context-overflow detection so blackhole callers use one
|
|
23
|
+
* authoritative source instead of maintaining their own provider-specific patterns.
|
|
24
|
+
* @see @earendil-works/pi-ai/dist/utils/overflow.d.ts
|
|
25
|
+
*/
|
|
26
|
+
export { isContextOverflow } from "@earendil-works/pi-ai";
|
package/src/om/reverse-recall.ts
CHANGED
|
@@ -82,7 +82,7 @@ export function findReflectionsForEntryIds(
|
|
|
82
82
|
|
|
83
83
|
export function formatRelatedObservations(
|
|
84
84
|
observations: RelatedObservation[],
|
|
85
|
-
|
|
85
|
+
reflections: RelatedReflection[],
|
|
86
86
|
): string {
|
|
87
87
|
const parts: string[] = [];
|
|
88
88
|
|
|
@@ -97,6 +97,14 @@ export function formatRelatedObservations(
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
if (reflections.length > 0) {
|
|
101
|
+
if (parts.length > 0) parts.push("");
|
|
102
|
+
parts.push("Related reflections:");
|
|
103
|
+
for (const ref of reflections) {
|
|
104
|
+
parts.push(` [${ref.memoryId}] ${ref.content}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
100
108
|
return parts.join("\n");
|
|
101
109
|
}
|
|
102
110
|
|
package/src/om/runtime.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - markConsolidationError sets 30s retry gate for failed runs.
|
|
11
11
|
*/
|
|
12
12
|
import { type Config, type ConfiguredModel, DEFAULTS, loadConfig } from "./config.js";
|
|
13
|
-
import { isCooldownActive, recordCooldown, expireCooldowns, modelKey } from "./cooldown.js";
|
|
13
|
+
import { isCooldownActive, getCooldownEntry, recordCooldown, expireCooldowns, modelKey } from "./cooldown.js";
|
|
14
14
|
|
|
15
15
|
export type ResolveResult =
|
|
16
16
|
| { ok: true; model: any; apiKey: string; headers?: Record<string, string>; cooldownApplied?: boolean }
|
|
@@ -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;
|
|
@@ -87,13 +94,17 @@ export class Runtime {
|
|
|
87
94
|
* Tries the candidate list in order:
|
|
88
95
|
* 1. Primary stage model → 2. Stage fallbacks → 3. Base config.model → 4. Session model.
|
|
89
96
|
*
|
|
97
|
+
* Session model fallback can be disabled via config.sessionFallback: false.
|
|
98
|
+
* When disabled, returns { ok: false } instead of using the session model,
|
|
99
|
+
* allowing the stage to be skipped entirely when all configured OM models fail.
|
|
100
|
+
*
|
|
90
101
|
* Skips models that are currently in a cooldown window.
|
|
91
102
|
* On retryable error (after the agent runs), the model that failed is cooled down
|
|
92
103
|
* and the next candidate is tried. The caller must call `recordRetryableError`
|
|
93
104
|
* after the API attempt to mark the failed model.
|
|
94
105
|
*
|
|
95
106
|
* Returns `ok: true` with the resolved model, or `ok: false` with a reason
|
|
96
|
-
* if all candidates (including session model) are exhausted or unavailable.
|
|
107
|
+
* if all candidates (including session model, if enabled) are exhausted or unavailable.
|
|
97
108
|
*/
|
|
98
109
|
async resolveModel(ctx: ResolveCtx): Promise<ResolveResult> {
|
|
99
110
|
const candidates = this.buildCandidateList(ctx.stageModel, ctx.stageFallbacks);
|
|
@@ -101,10 +112,25 @@ export class Runtime {
|
|
|
101
112
|
|
|
102
113
|
// Try configured candidates
|
|
103
114
|
for (const candidate of candidates) {
|
|
115
|
+
const key = modelKey(candidate);
|
|
116
|
+
|
|
117
|
+
// In-memory skip: model failed earlier in this stage with cooldownHours 0
|
|
118
|
+
if (this.failedInCycle.has(key)) {
|
|
119
|
+
if (ctx.hasUI && ctx.ui) {
|
|
120
|
+
ctx.ui.notify(
|
|
121
|
+
`Observational memory: ${stageName} skipping ${key} (failed this cycle, cooldown disabled)`,
|
|
122
|
+
"info",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
104
128
|
if (isCooldownActive(candidate)) {
|
|
105
129
|
if (ctx.hasUI && ctx.ui) {
|
|
130
|
+
const entry = getCooldownEntry(candidate);
|
|
131
|
+
const reason = entry ? `: ${entry.reason}` : "";
|
|
106
132
|
ctx.ui.notify(
|
|
107
|
-
`Observational memory: ${stageName} skipping ${
|
|
133
|
+
`Observational memory: ${stageName} skipping ${key} (cooldown${reason})`,
|
|
108
134
|
"info",
|
|
109
135
|
);
|
|
110
136
|
}
|
|
@@ -142,25 +168,40 @@ export class Runtime {
|
|
|
142
168
|
};
|
|
143
169
|
}
|
|
144
170
|
|
|
145
|
-
// Fall back to session model
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
171
|
+
// Fall back to session model (if enabled)
|
|
172
|
+
if (this.config.sessionFallback !== false) {
|
|
173
|
+
const sessionModel = ctx.model;
|
|
174
|
+
if (!sessionModel) {
|
|
175
|
+
return { ok: false, reason: `no model available for ${stageName} (all candidates exhausted, no session model)` };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(sessionModel);
|
|
179
|
+
if (!auth.ok || !auth.apiKey) {
|
|
180
|
+
const provider = (sessionModel as { provider?: string }).provider ?? "unknown";
|
|
181
|
+
return { ok: false, reason: `no API key for session model provider "${provider}"` };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
ok: true,
|
|
186
|
+
model: sessionModel,
|
|
187
|
+
apiKey: auth.apiKey as string,
|
|
188
|
+
headers: auth.headers as Record<string, string> | undefined,
|
|
189
|
+
cooldownApplied: false,
|
|
190
|
+
};
|
|
149
191
|
}
|
|
150
192
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
193
|
+
// All configured candidates exhausted and session fallback disabled —
|
|
194
|
+
// skip the stage entirely. Info-level to match cooldown-disabled pattern.
|
|
195
|
+
// Set resolveFailureNotified so the consolidation layer doesn't duplicate.
|
|
196
|
+
if (ctx.hasUI && ctx.ui) {
|
|
197
|
+
ctx.ui.notify(
|
|
198
|
+
`Observational memory: ${stageName} skipped — all candidates failed (sessionFallback disabled, won't use main model)`,
|
|
199
|
+
"info",
|
|
200
|
+
);
|
|
155
201
|
}
|
|
202
|
+
this.resolveFailureNotified = true;
|
|
156
203
|
|
|
157
|
-
return {
|
|
158
|
-
ok: true,
|
|
159
|
-
model: sessionModel,
|
|
160
|
-
apiKey: auth.apiKey as string,
|
|
161
|
-
headers: auth.headers as Record<string, string> | undefined,
|
|
162
|
-
cooldownApplied: false,
|
|
163
|
-
};
|
|
204
|
+
return { ok: false, reason: `no model available for ${stageName} (all candidates exhausted, sessionFallback disabled)` };
|
|
164
205
|
}
|
|
165
206
|
|
|
166
207
|
/**
|
|
@@ -179,9 +220,19 @@ export class Runtime {
|
|
|
179
220
|
/**
|
|
180
221
|
* Record a retryable error for a model. The model must be one of the candidates
|
|
181
222
|
* (not the session model). If it's the session model we don't cool it down.
|
|
223
|
+
*
|
|
224
|
+
* When cooldownHours is explicitly 0, the model is tracked in-memory for the
|
|
225
|
+
* current consolidation stage (no disk writes). Otherwise a persisted cooldown
|
|
226
|
+
* is recorded.
|
|
182
227
|
*/
|
|
183
228
|
recordRetryableError(modelConfig: ConfiguredModel | undefined, error: unknown, stage: ConsolidationPhase): void {
|
|
184
229
|
if (!modelConfig) return;
|
|
230
|
+
if (modelConfig.cooldownHours === 0) {
|
|
231
|
+
// In-memory only: skip this model for the rest of this stage.
|
|
232
|
+
// No disk writes, no persistent cooldown.
|
|
233
|
+
this.failedInCycle.add(modelKey(modelConfig));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
185
236
|
const reason = error instanceof Error ? error.message : String(error || "unknown error");
|
|
186
237
|
recordCooldown(modelConfig, reason, stage);
|
|
187
238
|
}
|
package/src/om/status-overlay.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* Esc to close.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { visibleWidth } from "./key-matcher.js";
|
|
11
|
+
import { matchesKey } from "@earendil-works/pi-tui";
|
|
11
12
|
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
13
14
|
// Theme shape (duck-typed from what pi provides)
|
|
@@ -117,12 +118,12 @@ export function createStatusOverlay(
|
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
function handleInput(data: string): void {
|
|
120
|
-
if (
|
|
121
|
+
if (matchesKey(data, "escape")) {
|
|
121
122
|
done({ action: "close" });
|
|
122
123
|
return;
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
if (
|
|
126
|
+
if (matchesKey(data, "enter") || matchesKey(data, "space")) {
|
|
126
127
|
if (navIsAction(selectedIndex)) {
|
|
127
128
|
const actionIdx = selectedIndex - selectableConfigCount - selectablePipelineCount - selectableErrorCount;
|
|
128
129
|
const action = actionItems[actionIdx]!;
|
|
@@ -132,12 +133,12 @@ export function createStatusOverlay(
|
|
|
132
133
|
return;
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
if (
|
|
136
|
+
if (matchesKey(data, "up")) {
|
|
136
137
|
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
137
138
|
invalidate();
|
|
138
139
|
return;
|
|
139
140
|
}
|
|
140
|
-
if (
|
|
141
|
+
if (matchesKey(data, "down")) {
|
|
141
142
|
selectedIndex = Math.min(totalSelectable - 1, selectedIndex + 1);
|
|
142
143
|
invalidate();
|
|
143
144
|
return;
|
package/src/sections.ts
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* Upstream: https://github.com/sting8k/pi-vcc (src/sections.ts)
|
|
5
5
|
* Unmodified.
|
|
6
6
|
*/
|
|
7
|
-
import type { TranscriptEntry } from "./core/brief";
|
|
8
7
|
|
|
9
8
|
export interface SectionData {
|
|
10
9
|
sessionGoal: string[];
|
|
@@ -13,6 +12,4 @@ export interface SectionData {
|
|
|
13
12
|
commits: string[];
|
|
14
13
|
userPreferences: string[];
|
|
15
14
|
briefTranscript: string;
|
|
16
|
-
/** Structured transcript entries (verbose object format) */
|
|
17
|
-
transcriptEntries: TranscriptEntry[];
|
|
18
15
|
}
|
package/src/tools/recall.ts
CHANGED
|
@@ -138,7 +138,11 @@ async function vccRecall(params: { query?: string; expand?: number[]; page?: num
|
|
|
138
138
|
|
|
139
139
|
// Merge expanded entries into full result set BEFORE pagination
|
|
140
140
|
// so pagination counts and positioning stay consistent
|
|
141
|
+
let appendedExpandCount = 0;
|
|
141
142
|
if (expandedFullEntries) {
|
|
143
|
+
// Track expand-only entries that don't match the query so the header can distinguish them
|
|
144
|
+
const existingIndices = new Set(allResults.map((r) => r.index));
|
|
145
|
+
appendedExpandCount = expandedFullEntries.filter((fe) => !existingIndices.has(fe.index)).length;
|
|
142
146
|
allResults = mergeExpandedIntoSearchResults(allResults, expandedFullEntries);
|
|
143
147
|
}
|
|
144
148
|
|
|
@@ -148,9 +152,10 @@ async function vccRecall(params: { query?: string; expand?: number[]; page?: num
|
|
|
148
152
|
const pageResults: SearchHit[] = allResults.slice(start, start + PAGE_SIZE);
|
|
149
153
|
const totalPages = Math.ceil(allResults.length / PAGE_SIZE);
|
|
150
154
|
const scopeSuffix = scope === "all" ? " (scope: all)" : "";
|
|
155
|
+
const matchCount = allResults.length - appendedExpandCount;
|
|
151
156
|
const header = totalPages > 1
|
|
152
|
-
? `Page ${page}/${totalPages} (${
|
|
153
|
-
: `${
|
|
157
|
+
? `Page ${page}/${totalPages} (${matchCount} matches${appendedExpandCount > 0 ? ` + ${appendedExpandCount} expanded` : ""}${scopeSuffix})`
|
|
158
|
+
: `${matchCount} matches${appendedExpandCount > 0 ? ` (+ ${appendedExpandCount} expanded)` : ""}${scopeSuffix}`;
|
|
154
159
|
const footer = page < totalPages
|
|
155
160
|
? `\n--- Use page:${page + 1}${scope === "all" ? " with scope:'all'" : ""} for more results ---`
|
|
156
161
|
: "";
|