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
package/src/core/settings.ts
CHANGED
|
@@ -1,40 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Scaffold the pi-blackhole config file on disk.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Modified by pi-vcc-om: loadSettings wraps loadUnifiedConfig.
|
|
4
|
+
* Only holds scaffoldSettings(); config loading/parsing happens in unified-config.ts.
|
|
6
5
|
*/
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
export interface PiVccSettings {
|
|
10
|
-
/** @deprecated Use compactionEngine instead. */
|
|
11
|
-
overrideDefaultCompaction: boolean;
|
|
12
|
-
/** @deprecated Use compaction instead. */
|
|
13
|
-
noAutoCompact: boolean;
|
|
14
|
-
/** @deprecated Use compaction + memory instead. */
|
|
15
|
-
passive: boolean;
|
|
16
|
-
/** Write debug snapshots to /tmp/pi-blackhole-debug.json. */
|
|
17
|
-
debug: boolean;
|
|
18
|
-
/** Unified compaction control: "auto" | "manual" | "off". */
|
|
19
|
-
compaction: "auto" | "manual" | "off";
|
|
20
|
-
/** Compaction engine: "blackhole" | "pi-default". */
|
|
21
|
-
compactionEngine: "blackhole" | "pi-default";
|
|
22
|
-
/** Visible tail behavior: "pi-default" | "minimal". */
|
|
23
|
-
tailBehavior: "pi-default" | "minimal";
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function loadSettings(): PiVccSettings {
|
|
27
|
-
const config = loadUnifiedConfig(process.cwd());
|
|
28
|
-
return {
|
|
29
|
-
overrideDefaultCompaction: config.overrideDefaultCompaction ?? false,
|
|
30
|
-
noAutoCompact: config.noAutoCompact ?? false,
|
|
31
|
-
passive: config.passive ?? false,
|
|
32
|
-
debug: config.debug,
|
|
33
|
-
compaction: config.compaction,
|
|
34
|
-
compactionEngine: config.compactionEngine,
|
|
35
|
-
tailBehavior: config.tailBehavior,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
6
|
+
import { scaffoldConfig } from "./unified-config.js";
|
|
38
7
|
|
|
39
8
|
export function scaffoldSettings(): void {
|
|
40
9
|
scaffoldConfig();
|
package/src/core/summarize.ts
CHANGED
|
@@ -27,11 +27,19 @@ const sectionOf = (text: string, header: string): string => {
|
|
|
27
27
|
const start = text.indexOf(tag);
|
|
28
28
|
if (start < 0) return "";
|
|
29
29
|
const after = text.slice(start);
|
|
30
|
-
// Find next section header
|
|
30
|
+
// Find next section header (must start at line boundary to avoid matching in content)
|
|
31
31
|
const nextSection = HEADER_NAMES
|
|
32
32
|
.filter((h) => h !== header)
|
|
33
|
-
.map((h) =>
|
|
34
|
-
|
|
33
|
+
.map((h) => {
|
|
34
|
+
// Escape the header name for regex safety
|
|
35
|
+
const escaped = h.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
36
|
+
const re = new RegExp(`(?:^|\\n)\\[${escaped}\\]`);
|
|
37
|
+
const m = after.match(re);
|
|
38
|
+
if (!m) return -1;
|
|
39
|
+
// m.index points to \n (or 0); advance past it to the [
|
|
40
|
+
return m.index! + (m[0].startsWith("\n") ? 1 : 0);
|
|
41
|
+
})
|
|
42
|
+
.filter((n) => n >= 0);
|
|
35
43
|
const nextSep = after.indexOf("\n\n---\n\n");
|
|
36
44
|
const candidates = [...nextSection, ...(nextSep > 0 ? [nextSep] : [])].sort((a, b) => a - b);
|
|
37
45
|
const end = candidates[0];
|
|
@@ -93,6 +101,8 @@ const mergeFileLines = (prev: string, fresh: string): string => {
|
|
|
93
101
|
|
|
94
102
|
// Dedup: if already in Modified, drop from Created (file existed before)
|
|
95
103
|
for (const p of merged.Modified) merged.Created.delete(p);
|
|
104
|
+
// Also remove Read entries that also appear in Modified (same file read+edited)
|
|
105
|
+
for (const p of merged.Modified) merged.Read.delete(p);
|
|
96
106
|
|
|
97
107
|
const cap = (set: Set<string>, limit: number) => {
|
|
98
108
|
const arr = [...set];
|
|
@@ -154,9 +164,7 @@ export const compile = (input: CompileInput): string => {
|
|
|
154
164
|
prev = prev ? stripOMContent(prev) : undefined;
|
|
155
165
|
const merged = prev ? mergePrevious(prev, fresh) : fresh;
|
|
156
166
|
if (!merged) return "";
|
|
157
|
-
return wrapLongLines(merged);
|
|
158
|
-
// RECALL_NOTE is now part of OM_FOOTER in render-summary.ts, appended
|
|
159
|
-
// by the before-compact hook after observations/reflections.
|
|
167
|
+
return wrapLongLines(merged + SEPARATOR + RECALL_NOTE);
|
|
160
168
|
};
|
|
161
169
|
|
|
162
170
|
const stripRecallNote = (text: string): string => {
|
|
@@ -168,19 +176,28 @@ const stripRecallNote = (text: string): string => {
|
|
|
168
176
|
};
|
|
169
177
|
|
|
170
178
|
/**
|
|
171
|
-
* Strip OM content
|
|
172
|
-
*
|
|
179
|
+
* Strip OM content and recall-guidance footers from a previous compaction summary.
|
|
180
|
+
*
|
|
181
|
+
* OM content (## Reflections / ## Observations + instructions) is appended after
|
|
182
|
+
* compile() by the before-compact hook, so it must be stripped from the previous
|
|
183
|
+
* summary to prevent compounding across compactions. The fresh OM projection is
|
|
184
|
+
* re-rendered each time.
|
|
173
185
|
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* compactions. The fresh OM projection is re-rendered each time.
|
|
186
|
+
* Also strips the basic recall-guidance footer that is always appended when OM
|
|
187
|
+
* is off or has no entries.
|
|
177
188
|
*/
|
|
178
189
|
const stripOMContent = (text: string): string => {
|
|
179
190
|
// Remove everything from "## Reflections" or "## Observations" onward,
|
|
180
191
|
// plus the instructions preamble that precedes them.
|
|
181
192
|
// The preamble starts with "These are condensed memories from earlier in this session."
|
|
182
|
-
|
|
183
|
-
const
|
|
193
|
+
// Use line-start anchoring to avoid matching inside conversation content
|
|
194
|
+
const reflMatch = text.match(/^## Reflections/m);
|
|
195
|
+
const reflIdx = reflMatch ? reflMatch.index! : -1;
|
|
196
|
+
const obsMatch = text.match(/^## Observations/m);
|
|
197
|
+
const obsIdx = obsMatch ? obsMatch.index! : -1;
|
|
198
|
+
|
|
199
|
+
// Also detect the basic recall-guidance footer (no observation preamble)
|
|
200
|
+
const basicFooterIdx = text.indexOf("Use `recall` with an id to retrieve original context, or `#N:path` drill-down");
|
|
184
201
|
|
|
185
202
|
// Find the start of OM content: either the instructions preamble or the first section header
|
|
186
203
|
let stripFrom = -1;
|
|
@@ -197,6 +214,9 @@ const stripOMContent = (text: string): string => {
|
|
|
197
214
|
} else if (minSectionIdx < Infinity) {
|
|
198
215
|
stripFrom = minSectionIdx;
|
|
199
216
|
}
|
|
217
|
+
} else if (basicFooterIdx >= 0) {
|
|
218
|
+
// Strip the basic recall-guidance footer (no observations/reflections present)
|
|
219
|
+
stripFrom = basicFooterIdx;
|
|
200
220
|
}
|
|
201
221
|
|
|
202
222
|
if (stripFrom < 0) return text;
|
package/src/core/tool-args.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
/** Canonical list of path-like argument keys across all Pi tools. */
|
|
2
|
+
export const PATH_KEYS = ["path", "file_path", "filePath", "file"] as const;
|
|
3
|
+
|
|
1
4
|
export const extractPath = (args: Record<string, unknown>): string | null => {
|
|
2
|
-
for (const key of
|
|
5
|
+
for (const key of PATH_KEYS) {
|
|
3
6
|
if (typeof args[key] === "string") return args[key] as string;
|
|
4
7
|
}
|
|
5
8
|
return null;
|
|
@@ -117,6 +117,10 @@ export interface UnifiedConfig {
|
|
|
117
117
|
/** Fallback models for dropper, tried in order after primary model fails. */
|
|
118
118
|
dropperFallbackModels?: OmModelConfig[];
|
|
119
119
|
|
|
120
|
+
/** When false, skip session model fallback when all OM model candidates are exhausted.
|
|
121
|
+
* Default true for backward compatibility. */
|
|
122
|
+
sessionFallback?: boolean;
|
|
123
|
+
|
|
120
124
|
/** @deprecated Use compaction instead. */
|
|
121
125
|
noAutoCompact?: boolean;
|
|
122
126
|
/** @deprecated Use compaction + memory instead. */
|
|
@@ -131,6 +135,7 @@ export interface UnifiedConfig {
|
|
|
131
135
|
|
|
132
136
|
export const DEFAULTS: UnifiedConfig = {
|
|
133
137
|
debug: false,
|
|
138
|
+
sessionFallback: true,
|
|
134
139
|
|
|
135
140
|
// New config surface
|
|
136
141
|
compaction: "auto",
|
|
@@ -187,6 +192,11 @@ function positiveInt(v: unknown): number | undefined {
|
|
|
187
192
|
return Number.isInteger(v) && typeof v === "number" && v > 0 ? v : undefined;
|
|
188
193
|
}
|
|
189
194
|
|
|
195
|
+
/** Like positiveInt but allows 0. Used for cooldownHours where 0 means "disabled". */
|
|
196
|
+
function nonNegativeInt(v: unknown): number | undefined {
|
|
197
|
+
return Number.isInteger(v) && typeof v === "number" && v >= 0 ? v : undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
190
200
|
function parseModel(v: unknown): OmModelConfig | undefined {
|
|
191
201
|
if (!isRecord(v)) return undefined;
|
|
192
202
|
const provider = nonEmptyString(v.provider);
|
|
@@ -194,7 +204,7 @@ function parseModel(v: unknown): OmModelConfig | undefined {
|
|
|
194
204
|
if (!provider || !id) return undefined;
|
|
195
205
|
const model: OmModelConfig = { provider, id };
|
|
196
206
|
if (isThinkingLevel(v.thinking)) model.thinking = v.thinking;
|
|
197
|
-
const cooldown =
|
|
207
|
+
const cooldown = nonNegativeInt(v.cooldownHours);
|
|
198
208
|
if (cooldown !== undefined) model.cooldownHours = cooldown;
|
|
199
209
|
const ctxWindow = positiveInt(v.contextWindow);
|
|
200
210
|
if (ctxWindow !== undefined) model.contextWindow = ctxWindow;
|
|
@@ -220,15 +230,18 @@ function parseConfig(raw: Record<string, unknown>): Partial<UnifiedConfig> {
|
|
|
220
230
|
if (typeof raw.debug === "boolean") c.debug = raw.debug;
|
|
221
231
|
|
|
222
232
|
// Booleans — om
|
|
233
|
+
if (typeof raw.sessionFallback === "boolean") c.sessionFallback = raw.sessionFallback;
|
|
223
234
|
if (typeof raw.noAutoCompact === "boolean") c.noAutoCompact = raw.noAutoCompact;
|
|
224
235
|
if (typeof raw.passive === "boolean") c.passive = raw.passive;
|
|
225
236
|
if (typeof raw.memory === "boolean") c.memory = raw.memory;
|
|
226
237
|
if (typeof raw.debugLog === "boolean") c.debugLog = raw.debugLog;
|
|
227
238
|
|
|
228
|
-
//
|
|
239
|
+
// Numeric fields — use nonNegativeInt for observerPreambleMaxTokens (0 = auto)
|
|
229
240
|
const numKeys = ["observeAfterTokens", "reflectAfterTokens", "compactAfterTokens", "observationsPoolMaxTokens", "observationsPoolTargetTokens", "reflectorInputMaxTokens", "dropperInputMaxTokens", "observerChunkMaxTokens", "observerPreambleMaxTokens", "agentMaxTurns"] as const;
|
|
230
241
|
for (const k of numKeys) {
|
|
231
|
-
|
|
242
|
+
// observerPreambleMaxTokens accepts 0 (auto-compute); everything else must be > 0
|
|
243
|
+
const validator = k === "observerPreambleMaxTokens" ? nonNegativeInt : positiveInt;
|
|
244
|
+
const v = validator(raw[k]);
|
|
232
245
|
if (v !== undefined) (c as Record<string, unknown>)[k] = v;
|
|
233
246
|
}
|
|
234
247
|
|
|
@@ -257,8 +270,9 @@ function parseConfig(raw: Record<string, unknown>): Partial<UnifiedConfig> {
|
|
|
257
270
|
|
|
258
271
|
/**
|
|
259
272
|
* Migrate legacy config knobs to new unified surface.
|
|
260
|
-
*
|
|
261
|
-
* Does NOT mutate the on-disk config file.
|
|
273
|
+
* Operates on the in-memory parsed object each time loadUnifiedConfig() is called;
|
|
274
|
+
* old keys are removed from this copy. Does NOT mutate the on-disk config file.
|
|
275
|
+
* Idempotent — safe to call repeatedly.
|
|
262
276
|
*/
|
|
263
277
|
function migrateOldKnobs(parsed: Record<string, unknown>): void {
|
|
264
278
|
// Only run if new keys are absent AND old keys are present
|
|
@@ -356,13 +370,23 @@ export function loadUnifiedConfig(cwd: string): UnifiedConfig {
|
|
|
356
370
|
|
|
357
371
|
// Env override — new compaction surface
|
|
358
372
|
const envCompaction = process.env.PI_BLACKHOLE_COMPACTION;
|
|
359
|
-
if (envCompaction !== undefined
|
|
360
|
-
|
|
373
|
+
if (envCompaction !== undefined) {
|
|
374
|
+
const trimmed = envCompaction.trim().toLowerCase();
|
|
375
|
+
if (isCompaction(trimmed)) {
|
|
376
|
+
parsed.compaction = trimmed as "auto" | "manual" | "off";
|
|
377
|
+
} else {
|
|
378
|
+
console.warn(`blackhole: invalid PI_BLACKHOLE_COMPACTION value "${envCompaction}"; ignoring`);
|
|
379
|
+
}
|
|
361
380
|
}
|
|
362
381
|
|
|
363
382
|
const envCompactionEngine = process.env.PI_BLACKHOLE_COMPACTION_ENGINE;
|
|
364
|
-
if (envCompactionEngine !== undefined
|
|
365
|
-
|
|
383
|
+
if (envCompactionEngine !== undefined) {
|
|
384
|
+
const trimmed = envCompactionEngine.trim().toLowerCase();
|
|
385
|
+
if (isCompactionEngine(trimmed)) {
|
|
386
|
+
parsed.compactionEngine = trimmed as "blackhole" | "pi-default";
|
|
387
|
+
} else {
|
|
388
|
+
console.warn(`blackhole: invalid PI_BLACKHOLE_COMPACTION_ENGINE value "${envCompactionEngine}"; ignoring`);
|
|
389
|
+
}
|
|
366
390
|
}
|
|
367
391
|
|
|
368
392
|
// Merge defaults then override
|
|
@@ -440,24 +464,7 @@ export function scaffoldConfig(): void {
|
|
|
440
464
|
}
|
|
441
465
|
}
|
|
442
466
|
|
|
443
|
-
// ── Toggle helpers ───────────────────────────────────────────────────────────
|
|
444
467
|
|
|
445
|
-
/** Cycle compaction: auto → manual → off → auto */
|
|
446
|
-
export function toggleCompaction(current: "auto" | "manual" | "off"): "auto" | "manual" | "off" {
|
|
447
|
-
const cycle: Array<"auto" | "manual" | "off"> = ["auto", "manual", "off"];
|
|
448
|
-
const idx = cycle.indexOf(current);
|
|
449
|
-
return cycle[(idx + 1) % cycle.length];
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/** Toggle compactionEngine: blackhole ↔ pi-default */
|
|
453
|
-
export function toggleCompactionEngine(current: "blackhole" | "pi-default"): "blackhole" | "pi-default" {
|
|
454
|
-
return current === "blackhole" ? "pi-default" : "blackhole";
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/** Toggle tailBehavior: pi-default ↔ minimal */
|
|
458
|
-
export function toggleTailBehavior(current: "pi-default" | "minimal"): "pi-default" | "minimal" {
|
|
459
|
-
return current === "pi-default" ? "minimal" : "pi-default";
|
|
460
|
-
}
|
|
461
468
|
|
|
462
469
|
// ── Migration detection ───────────────────────────────────────────────────────
|
|
463
470
|
|
package/src/extract/commits.ts
CHANGED
|
@@ -6,8 +6,9 @@ interface CommitInfo {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
const COMMIT_MSG_RE = /git\s+commit[^\n]*?-m\s+(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'|\$?'((?:[^'\\]|\\.)*)')/;
|
|
9
|
-
// Match short hash from git output
|
|
10
|
-
|
|
9
|
+
// Match short hash from git output — only as fallback after bracket/range patterns fail.
|
|
10
|
+
// Requires 8+ hex chars to reduce false positives from random hex in tool output.
|
|
11
|
+
const HASH_RE = /\b([0-9a-f]{8,12})\b/;
|
|
11
12
|
|
|
12
13
|
const firstLineOf = (text: string): string => {
|
|
13
14
|
const line = text.split(/\\n|\n/)[0] ?? "";
|
package/src/extract/files.ts
CHANGED
|
@@ -16,16 +16,18 @@ const FILE_WRITE_TOOLS = new Set([
|
|
|
16
16
|
"MultiEdit",
|
|
17
17
|
]);
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
// Pi never exposes a "createdFiles" field — Write operations are tracked as modified.
|
|
20
|
+
// FILE_CREATE_TOOLS kept as empty set for forward-compat if Pi adds a creation signal.
|
|
21
|
+
const FILE_CREATE_TOOLS = new Set<string>();
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Find the longest common directory prefix among absolute paths.
|
|
25
25
|
* Returns "" if fewer than 2 absolute paths or no meaningful common prefix.
|
|
26
26
|
*/
|
|
27
27
|
const longestCommonDirPrefix = (paths: string[]): string => {
|
|
28
|
-
|
|
28
|
+
// Normalize backslashes (Windows) to forward slashes for uniform comparison
|
|
29
|
+
const normalized = paths.map((p) => p.replace(/\\/g, "/"));
|
|
30
|
+
const abs = normalized.filter((p) => p.startsWith("/") || /^[A-Za-z]:\//.test(p));
|
|
29
31
|
if (abs.length < 2) return "";
|
|
30
32
|
const split = abs.map((p) => p.split("/"));
|
|
31
33
|
const min = Math.min(...split.map((s) => s.length));
|
|
@@ -289,8 +289,10 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
// Determine effective tail behavior for buildOwnCut
|
|
292
|
+
// Both /blackhole and auto-triggered default to "minimal" (aggressive cut);
|
|
293
|
+
// users can opt into "pi-default" (gentler) by setting tailBehavior in config.
|
|
292
294
|
const effectiveTailBehavior = isPiVcc
|
|
293
|
-
? (omRuntime.config.tailBehavior ?? "minimal")
|
|
295
|
+
? (omRuntime.config.tailBehavior ?? "minimal")
|
|
294
296
|
: (omRuntime.config.tailBehavior ?? "minimal");
|
|
295
297
|
|
|
296
298
|
trace("before_compact.tail_behavior", {
|
|
@@ -451,7 +453,7 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
451
453
|
omRuntime.compactWasPiVcc = isPiVcc;
|
|
452
454
|
|
|
453
455
|
// ── Inject observational-memory content ───────────────────────────
|
|
454
|
-
let omContent
|
|
456
|
+
let omContent: string;
|
|
455
457
|
let omDetails: Record<string, unknown> | undefined;
|
|
456
458
|
trace("before_compact.om_injection", { memoryEnabled: omRuntime.config.memory !== false });
|
|
457
459
|
if (omRuntime.config.memory !== false) {
|
|
@@ -462,11 +464,13 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
462
464
|
);
|
|
463
465
|
omContent = renderSummary(projection.reflections, projection.observations);
|
|
464
466
|
omDetails = projection.details;
|
|
467
|
+
} else {
|
|
468
|
+
omContent = renderSummary([], []);
|
|
465
469
|
}
|
|
466
470
|
|
|
467
471
|
return {
|
|
468
472
|
compaction: {
|
|
469
|
-
summary:
|
|
473
|
+
summary: summary + "\n\n" + omContent,
|
|
470
474
|
details: { ...details, "om.folded": omDetails },
|
|
471
475
|
tokensBefore: preparation.tokensBefore,
|
|
472
476
|
firstKeptEntryId,
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { agentLoop, type AgentContext, type AgentLoopConfig, type AgentTool } from "@earendil-works/pi-agent-core";
|
|
9
9
|
import type { Message, Model, ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
10
|
+
import { createBridgeStreamFn } from "../../provider-stream.js";
|
|
10
11
|
import { streamSimple } from "@earendil-works/pi-ai";
|
|
11
12
|
import { Type } from "typebox";
|
|
12
13
|
import type { Static } from "typebox";
|
|
@@ -287,13 +288,7 @@ export async function runDropper(args: RunDropperArgs): Promise<string[] | undef
|
|
|
287
288
|
|
|
288
289
|
const loop = args.agentLoop ?? agentLoop;
|
|
289
290
|
// ── Bridge stream function ──
|
|
290
|
-
const
|
|
291
|
-
const bridgeStreamFn = (model: any, ctx: any, opts: any) => {
|
|
292
|
-
const providerStreams: Map<string, Function> | undefined = (globalThis as any)[PROVIDER_STREAMS_KEY];
|
|
293
|
-
if (!providerStreams) return streamSimple(model, ctx, opts);
|
|
294
|
-
const customFn = model?.api ? providerStreams.get(model.api) : undefined;
|
|
295
|
-
return customFn ? customFn(model, ctx, opts) : streamSimple(model, ctx, opts);
|
|
296
|
-
};
|
|
291
|
+
const bridgeStreamFn = createBridgeStreamFn(streamSimple);
|
|
297
292
|
const streamFn = args.streamFn ?? bridgeStreamFn;
|
|
298
293
|
const stream = loop(prompts, context, config, signal, streamFn);
|
|
299
294
|
let agentError: string | undefined;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { agentLoop, type AgentContext, type AgentLoopConfig, type AgentTool } from "@earendil-works/pi-agent-core";
|
|
10
10
|
import type { Message, Model, ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
11
|
+
import { createBridgeStreamFn } from "../../provider-stream.js";
|
|
11
12
|
import { streamSimple } from "@earendil-works/pi-ai";
|
|
12
13
|
import { Type } from "typebox";
|
|
13
14
|
import type { Static } from "typebox";
|
|
@@ -85,13 +86,19 @@ export function normalizeSourceEntryIds(
|
|
|
85
86
|
const allowedOrder = new Map<string, number>();
|
|
86
87
|
for (let i = 0; i < allowedSourceEntryIds.length; i++) allowedOrder.set(allowedSourceEntryIds[i], i);
|
|
87
88
|
|
|
89
|
+
// Filter out invalid/unknown IDs instead of rejecting the entire batch.
|
|
90
|
+
// Matches the dropper's normalizeDropObservationIds pattern: one hallucinated
|
|
91
|
+
// ID from the LLM should not discard valid observations.
|
|
88
92
|
const seen = new Set<string>();
|
|
93
|
+
const valid: string[] = [];
|
|
89
94
|
for (const id of sourceEntryIds) {
|
|
90
|
-
if (!allowedOrder.has(id))
|
|
95
|
+
if (!allowedOrder.has(id)) continue;
|
|
96
|
+
if (seen.has(id)) continue;
|
|
91
97
|
seen.add(id);
|
|
98
|
+
valid.push(id);
|
|
92
99
|
}
|
|
93
|
-
if (
|
|
94
|
-
return
|
|
100
|
+
if (valid.length === 0) return undefined;
|
|
101
|
+
return valid.sort((a, b) => (allowedOrder.get(a) ?? 0) - (allowedOrder.get(b) ?? 0));
|
|
95
102
|
}
|
|
96
103
|
|
|
97
104
|
/** Result returned by runObserver when no observations are recorded. */
|
|
@@ -226,16 +233,7 @@ ${conversation}`;
|
|
|
226
233
|
// Consolidation agents run via jiti (moduleCache: false) which creates a separate
|
|
227
234
|
// pi-ai instance whose apiProviderRegistry lacks custom providers registered by
|
|
228
235
|
// other extensions (e.g., claude-bridge). The bridge looks up streamSimple functions
|
|
229
|
-
|
|
230
|
-
// If no custom provider is found, it falls back to the jiti-loaded streamSimple
|
|
231
|
-
// which handles all built-in providers correctly.
|
|
232
|
-
const PROVIDER_STREAMS_KEY = Symbol.for("pi-blackhole:provider-streams");
|
|
233
|
-
const bridgeStreamFn = (model: any, ctx: any, opts: any) => {
|
|
234
|
-
const providerStreams: Map<string, Function> | undefined = (globalThis as any)[PROVIDER_STREAMS_KEY];
|
|
235
|
-
if (!providerStreams) return streamSimple(model, ctx, opts);
|
|
236
|
-
const customFn = model?.api ? providerStreams.get(model.api) : undefined;
|
|
237
|
-
return customFn ? customFn(model, ctx, opts) : streamSimple(model, ctx, opts);
|
|
238
|
-
};
|
|
236
|
+
const bridgeStreamFn = createBridgeStreamFn(streamSimple);
|
|
239
237
|
const streamFn = args.streamFn ?? bridgeStreamFn;
|
|
240
238
|
const stream = loop(prompts, context, config, signal, streamFn);
|
|
241
239
|
let agentError: string | undefined;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { agentLoop, type AgentContext, type AgentLoopConfig, type AgentTool } from "@earendil-works/pi-agent-core";
|
|
9
9
|
import type { Message, Model, ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
10
|
+
import { createBridgeStreamFn } from "../../provider-stream.js";
|
|
10
11
|
import { streamSimple } from "@earendil-works/pi-ai";
|
|
11
12
|
import { Type } from "typebox";
|
|
12
13
|
import type { Static } from "typebox";
|
|
@@ -150,13 +151,7 @@ export async function runReflector(args: RunReflectorArgs): Promise<Reflection[]
|
|
|
150
151
|
|
|
151
152
|
const loop = args.agentLoop ?? agentLoop;
|
|
152
153
|
// ── Bridge stream function ──
|
|
153
|
-
const
|
|
154
|
-
const bridgeStreamFn = (model: any, ctx: any, opts: any) => {
|
|
155
|
-
const providerStreams: Map<string, Function> | undefined = (globalThis as any)[PROVIDER_STREAMS_KEY];
|
|
156
|
-
if (!providerStreams) return streamSimple(model, ctx, opts);
|
|
157
|
-
const customFn = model?.api ? providerStreams.get(model.api) : undefined;
|
|
158
|
-
return customFn ? customFn(model, ctx, opts) : streamSimple(model, ctx, opts);
|
|
159
|
-
};
|
|
154
|
+
const bridgeStreamFn = createBridgeStreamFn(streamSimple);
|
|
160
155
|
const streamFn = args.streamFn ?? bridgeStreamFn;
|
|
161
156
|
const stream = loop(prompts, context, config, signal, streamFn);
|
|
162
157
|
let agentError: string | undefined;
|
|
@@ -2,14 +2,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { rawTokensSinceLastCompaction, type Entry } from "./ledger/index.js";
|
|
3
3
|
import type { Runtime } from "./runtime.js";
|
|
4
4
|
import { debugLog } from "./debug-log.js";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Regex matching Pi's internal retryable error detection.
|
|
8
|
-
* When the last assistant message in agent_end has stopReason "error" matching this pattern,
|
|
9
|
-
* Pi will auto-retry — we must not trigger compaction between attempts.
|
|
10
|
-
*/
|
|
11
|
-
const RETRYABLE_ERROR_RE =
|
|
12
|
-
/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;
|
|
5
|
+
import { RETRYABLE_ERROR_RE } from "./retryable-error.js";
|
|
13
6
|
|
|
14
7
|
export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): void {
|
|
15
8
|
pi.on("agent_end", (event: any, ctx: any) => {
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* Navigation: ↑↓ Edit: Enter Save: Ctrl+S Cancel: Esc
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { visibleWidth } from "./key-matcher.js";
|
|
10
|
+
import { matchesKey, decodeKittyPrintable } from "@earendil-works/pi-tui";
|
|
11
|
+
import { DEFAULTS } from "../core/unified-config.js";
|
|
10
12
|
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
11
13
|
import { dirname } from "node:path";
|
|
12
14
|
|
|
@@ -48,6 +50,8 @@ const FIELDS: FieldDef[] = [
|
|
|
48
50
|
// ── Observational Memory ──
|
|
49
51
|
{ key: "memory", label: "Observational memory", type: "boolean", section: "Observational Memory",
|
|
50
52
|
helpText: "Enable OM workers (observer, reflector, dropper) and content injection" },
|
|
53
|
+
{ key: "sessionFallback", label: "Session model fallback", type: "boolean", section: "Observational Memory",
|
|
54
|
+
helpText: "off=skip stage when all OM models fail, instead of falling back to the main coding model" },
|
|
51
55
|
{ key: "observeAfterTokens", label: "Observer threshold", type: "number", section: "Observational Memory",
|
|
52
56
|
helpText: "Tokens accumulated since last observer run before triggering next observe" },
|
|
53
57
|
{ key: "reflectAfterTokens", label: "Reflect + dropper threshold", type: "number", section: "Observational Memory",
|
|
@@ -138,9 +142,10 @@ export function createConfigureOverlay(
|
|
|
138
142
|
raw = {};
|
|
139
143
|
}
|
|
140
144
|
|
|
145
|
+
const defaults = DEFAULTS as unknown as Record<string, unknown>;
|
|
141
146
|
const fields: FieldState[] = FIELDS.map((def) => ({
|
|
142
147
|
def,
|
|
143
|
-
value: formatValue(def, raw[def.key]),
|
|
148
|
+
value: formatValue(def, def.key in raw ? raw[def.key] : defaults[def.key]),
|
|
144
149
|
editing: false,
|
|
145
150
|
cursor: 0,
|
|
146
151
|
}));
|
|
@@ -191,18 +196,18 @@ export function createConfigureOverlay(
|
|
|
191
196
|
|
|
192
197
|
// While editing a number field
|
|
193
198
|
if (cur.editing) {
|
|
194
|
-
if (
|
|
199
|
+
if (matchesKey(data, "escape")) {
|
|
195
200
|
cur.value = formatValue(cur.def, raw[cur.def.key]);
|
|
196
201
|
cur.editing = false;
|
|
197
202
|
invalidate();
|
|
198
203
|
return;
|
|
199
204
|
}
|
|
200
|
-
if (
|
|
205
|
+
if (matchesKey(data, "enter") || matchesKey(data, "tab")) {
|
|
201
206
|
cur.editing = false;
|
|
202
207
|
invalidate();
|
|
203
208
|
return;
|
|
204
209
|
}
|
|
205
|
-
if (
|
|
210
|
+
if (matchesKey(data, "backspace")) {
|
|
206
211
|
if (cur.cursor > 0) {
|
|
207
212
|
cur.value = cur.value.slice(0, cur.cursor - 1) + cur.value.slice(cur.cursor);
|
|
208
213
|
cur.cursor--;
|
|
@@ -210,18 +215,19 @@ export function createConfigureOverlay(
|
|
|
210
215
|
}
|
|
211
216
|
return;
|
|
212
217
|
}
|
|
213
|
-
if (
|
|
218
|
+
if (matchesKey(data, "left")) {
|
|
214
219
|
cur.cursor = Math.max(0, cur.cursor - 1);
|
|
215
220
|
invalidate();
|
|
216
221
|
return;
|
|
217
222
|
}
|
|
218
|
-
if (
|
|
223
|
+
if (matchesKey(data, "right")) {
|
|
219
224
|
cur.cursor = Math.min(cur.value.length, cur.cursor + 1);
|
|
220
225
|
invalidate();
|
|
221
226
|
return;
|
|
222
227
|
}
|
|
223
|
-
|
|
224
|
-
|
|
228
|
+
const digit = decodeKittyPrintable(data) ?? data;
|
|
229
|
+
if (digit.length === 1 && digit >= "0" && digit <= "9") {
|
|
230
|
+
cur.value = cur.value.slice(0, cur.cursor) + digit + cur.value.slice(cur.cursor);
|
|
225
231
|
cur.cursor++;
|
|
226
232
|
invalidate();
|
|
227
233
|
}
|
|
@@ -231,20 +237,20 @@ export function createConfigureOverlay(
|
|
|
231
237
|
// Not editing — global navigation
|
|
232
238
|
|
|
233
239
|
// Ctrl+S → save and close
|
|
234
|
-
if (
|
|
240
|
+
if (matchesKey(data, "ctrl+s")) {
|
|
235
241
|
const saved = save();
|
|
236
242
|
done({ saved, path: configPath });
|
|
237
243
|
return;
|
|
238
244
|
}
|
|
239
245
|
|
|
240
246
|
// Esc → close without saving
|
|
241
|
-
if (
|
|
247
|
+
if (matchesKey(data, "escape")) {
|
|
242
248
|
done(undefined);
|
|
243
249
|
return;
|
|
244
250
|
}
|
|
245
251
|
|
|
246
252
|
// Enter/space → edit/toggle
|
|
247
|
-
if (
|
|
253
|
+
if (matchesKey(data, "enter") || matchesKey(data, "space")) {
|
|
248
254
|
switch (cur.def.type) {
|
|
249
255
|
case "boolean":
|
|
250
256
|
cur.value = cur.value === "on" ? "off" : "on";
|
|
@@ -268,12 +274,12 @@ export function createConfigureOverlay(
|
|
|
268
274
|
}
|
|
269
275
|
|
|
270
276
|
// ↑↓ navigation
|
|
271
|
-
if (
|
|
277
|
+
if (matchesKey(data, "up")) {
|
|
272
278
|
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
273
279
|
invalidate();
|
|
274
280
|
return;
|
|
275
281
|
}
|
|
276
|
-
if (
|
|
282
|
+
if (matchesKey(data, "down")) {
|
|
277
283
|
selectedIndex = Math.min(fields.length - 1, selectedIndex + 1);
|
|
278
284
|
invalidate();
|
|
279
285
|
return;
|