pi-blackhole 0.3.9 → 0.4.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/README.md +7 -1
- package/example-config.json +2 -0
- package/index.ts +2 -2
- package/package.json +10 -10
- package/src/commands/cleanup.ts +319 -0
- package/src/commands/memory.ts +2 -2
- package/src/commands/pi-vcc.ts +37 -14
- package/src/core/format.ts +1 -1
- package/src/core/summarize.ts +30 -23
- package/src/core/unified-config.ts +53 -11
- package/src/extract/commits.ts +68 -25
- package/src/extract/goals.ts +13 -2
- package/src/hooks/before-compact.ts +50 -9
- package/src/om/agents/dropper/agent.ts +1 -1
- package/src/om/agents/observer/agent.ts +1 -1
- package/src/om/agents/reflector/agent.ts +1 -1
- package/src/om/cleanup.ts +367 -0
- package/src/om/compaction-trigger.ts +147 -51
- package/src/om/configure-overlay.ts +41 -5
- package/src/om/consolidation.ts +103 -27
- package/src/om/ledger/projection.ts +6 -1
- package/src/om/retryable-error.ts +14 -0
- package/src/om/runtime.ts +78 -34
|
@@ -62,6 +62,14 @@ export interface UnifiedConfig {
|
|
|
62
62
|
* "pi-default" — Pi's built-in summarization */
|
|
63
63
|
compactionEngine: "blackhole" | "pi-default";
|
|
64
64
|
|
|
65
|
+
/** Mid-run auto-compaction (turn_end trigger, fires while the agent is still
|
|
66
|
+
* executing tool loops — agent_end alone never fires during long runs).
|
|
67
|
+
* "resume" — compact at threshold and inject a resume message so the agent
|
|
68
|
+
* continues the task (default)
|
|
69
|
+
* "pause" — compact at threshold but stop; user continues manually
|
|
70
|
+
* "off" — only evaluate the threshold when the agent finishes a run */
|
|
71
|
+
midRunCompaction: "resume" | "pause" | "off";
|
|
72
|
+
|
|
65
73
|
/** How much recent transcript to keep visible after compaction.
|
|
66
74
|
* "pi-default" — use Pi's firstKeptEntryId (respects Pi's keepRecentTokens)
|
|
67
75
|
* "minimal" — keep only last user message (current agressive pi-vcc behavior)
|
|
@@ -76,6 +84,9 @@ export interface UnifiedConfig {
|
|
|
76
84
|
compactAfterTokens: number;
|
|
77
85
|
/** Observation pool token pressure for full fold. */
|
|
78
86
|
observationsPoolMaxTokens: number;
|
|
87
|
+
/** Treat every compaction as a full-fold boundary so early reflections/drops
|
|
88
|
+
* survive the first compaction in a fresh session. Default true. */
|
|
89
|
+
fullFoldAlways: boolean;
|
|
79
90
|
/** Target token budget for the observation pool (dropper aims here).
|
|
80
91
|
* Optional; defaults to half of observationsPoolMaxTokens when unset.
|
|
81
92
|
* Must be less than observationsPoolMaxTokens.
|
|
@@ -146,11 +157,13 @@ export const DEFAULTS: UnifiedConfig = {
|
|
|
146
157
|
compaction: "auto",
|
|
147
158
|
compactionEngine: "blackhole",
|
|
148
159
|
tailBehavior: "minimal",
|
|
160
|
+
midRunCompaction: "resume",
|
|
149
161
|
|
|
150
162
|
observeAfterTokens: 15_000,
|
|
151
163
|
reflectAfterTokens: 25_000,
|
|
152
164
|
compactAfterTokens: 81_000,
|
|
153
165
|
observationsPoolMaxTokens: 20_000,
|
|
166
|
+
fullFoldAlways: true,
|
|
154
167
|
observationsPoolTargetTokens: 10_000,
|
|
155
168
|
reflectorInputMaxTokens: 80_000,
|
|
156
169
|
dropperInputMaxTokens: 80_000,
|
|
@@ -171,6 +184,7 @@ const THINKING_LEVELS: readonly string[] = ["off", "minimal", "low", "medium", "
|
|
|
171
184
|
const COMPACTION_VALUES = ["auto", "manual", "off"] as const;
|
|
172
185
|
const COMPACTION_ENGINE_VALUES = ["blackhole", "pi-default"] as const;
|
|
173
186
|
const TAIL_BEHAVIOR_VALUES = ["pi-default", "minimal"] as const;
|
|
187
|
+
const MID_RUN_COMPACTION_VALUES = ["resume", "pause", "off"] as const;
|
|
174
188
|
|
|
175
189
|
function isCompaction(v: unknown): v is "auto" | "manual" | "off" {
|
|
176
190
|
return typeof v === "string" && (COMPACTION_VALUES as readonly string[]).includes(v);
|
|
@@ -181,6 +195,9 @@ function isCompactionEngine(v: unknown): v is "blackhole" | "pi-default" {
|
|
|
181
195
|
function isTailBehavior(v: unknown): v is "pi-default" | "minimal" {
|
|
182
196
|
return typeof v === "string" && (TAIL_BEHAVIOR_VALUES as readonly string[]).includes(v);
|
|
183
197
|
}
|
|
198
|
+
function isMidRunCompaction(v: unknown): v is "resume" | "pause" | "off" {
|
|
199
|
+
return typeof v === "string" && (MID_RUN_COMPACTION_VALUES as readonly string[]).includes(v);
|
|
200
|
+
}
|
|
184
201
|
|
|
185
202
|
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
186
203
|
return typeof v === "object" && v !== null;
|
|
@@ -230,6 +247,7 @@ function parseConfig(raw: Record<string, unknown>): Partial<UnifiedConfig> {
|
|
|
230
247
|
if (isCompaction(raw.compaction)) c.compaction = raw.compaction;
|
|
231
248
|
if (isCompactionEngine(raw.compactionEngine)) c.compactionEngine = raw.compactionEngine;
|
|
232
249
|
if (isTailBehavior(raw.tailBehavior)) c.tailBehavior = raw.tailBehavior;
|
|
250
|
+
if (isMidRunCompaction(raw.midRunCompaction)) c.midRunCompaction = raw.midRunCompaction;
|
|
233
251
|
|
|
234
252
|
// Booleans — pi-vcc
|
|
235
253
|
if (typeof raw.overrideDefaultCompaction === "boolean") c.overrideDefaultCompaction = raw.overrideDefaultCompaction;
|
|
@@ -240,6 +258,7 @@ function parseConfig(raw: Record<string, unknown>): Partial<UnifiedConfig> {
|
|
|
240
258
|
if (typeof raw.noAutoCompact === "boolean") c.noAutoCompact = raw.noAutoCompact;
|
|
241
259
|
if (typeof raw.passive === "boolean") c.passive = raw.passive;
|
|
242
260
|
if (typeof raw.memory === "boolean") c.memory = raw.memory;
|
|
261
|
+
if (typeof raw.fullFoldAlways === "boolean") c.fullFoldAlways = raw.fullFoldAlways;
|
|
243
262
|
if (typeof raw.debugLog === "boolean") c.debugLog = raw.debugLog;
|
|
244
263
|
|
|
245
264
|
// Numeric fields — use nonNegativeInt for observerPreambleMaxTokens (0 = auto)
|
|
@@ -319,35 +338,54 @@ function migrateOldKnobs(parsed: Record<string, unknown>): void {
|
|
|
319
338
|
|
|
320
339
|
// ── Load and save ────────────────────────────────────────────────────────────
|
|
321
340
|
|
|
322
|
-
function readJson(path: string): Record<string, unknown> | null {
|
|
323
|
-
if (!existsSync(path)) return null;
|
|
341
|
+
function readJson(path: string): { data: Record<string, unknown> | null; error: string | null } {
|
|
342
|
+
if (!existsSync(path)) return { data: null, error: null };
|
|
324
343
|
try {
|
|
325
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
326
|
-
} catch {
|
|
327
|
-
|
|
344
|
+
return { data: JSON.parse(readFileSync(path, "utf-8")), error: null };
|
|
345
|
+
} catch (e) {
|
|
346
|
+
const msg = `blackhole: config file at ${path} has invalid JSON: ${(e as Error).message}. Using defaults.`;
|
|
347
|
+
console.warn(msg);
|
|
348
|
+
return { data: null, error: msg };
|
|
328
349
|
}
|
|
329
350
|
}
|
|
330
351
|
|
|
352
|
+
/** Optional warning callback invoked when the primary config file has invalid JSON.
|
|
353
|
+
* Receives the warning message string. Used by callers with UI access to surface
|
|
354
|
+
* the error via ctx.ui.notify(message, "warning").
|
|
355
|
+
*/
|
|
356
|
+
type WarnFn = (message: string) => void;
|
|
357
|
+
|
|
331
358
|
/**
|
|
332
359
|
* Load unified configuration from ~/.pi/agent/pi-blackhole/pi-blackhole-config.json.
|
|
333
360
|
* Falls back to legacy sources if the unified file doesn't exist.
|
|
334
361
|
*/
|
|
335
|
-
export function loadUnifiedConfig(cwd: string): UnifiedConfig {
|
|
362
|
+
export function loadUnifiedConfig(cwd: string, onWarn?: WarnFn): UnifiedConfig {
|
|
336
363
|
const path = configPath();
|
|
337
|
-
let raw
|
|
364
|
+
let raw: Record<string, unknown> | null;
|
|
365
|
+
let primaryError: string | null = null;
|
|
366
|
+
const result = readJson(path);
|
|
367
|
+
raw = result.data;
|
|
368
|
+
primaryError = result.error;
|
|
369
|
+
if (primaryError && onWarn) onWarn(primaryError);
|
|
338
370
|
|
|
339
371
|
// Fallback to legacy sources if unified file doesn't exist
|
|
340
372
|
if (!raw) {
|
|
341
373
|
// Try legacy pi-vcc config
|
|
342
374
|
const piVccPath = join(getAgentDir(), "pi-vcc-config.json");
|
|
343
|
-
const
|
|
375
|
+
const piVccResult = readJson(piVccPath);
|
|
376
|
+
const piVccRaw = piVccResult.data;
|
|
377
|
+
if (piVccResult.error && onWarn) onWarn(piVccResult.error);
|
|
344
378
|
|
|
345
379
|
// Try legacy om config from settings.json
|
|
346
380
|
const settingsPath = join(getAgentDir(), "settings.json");
|
|
347
|
-
const
|
|
381
|
+
const settingsResult = readJson(settingsPath);
|
|
382
|
+
const settingsRaw = settingsResult.data;
|
|
383
|
+
if (settingsResult.error && onWarn) onWarn(settingsResult.error);
|
|
348
384
|
const omRaw = settingsRaw?.["pi-blackhole"] ?? settingsRaw?.["observational-memory"];
|
|
349
385
|
const projectSettingsPath = join(cwd, ".pi", "settings.json");
|
|
350
|
-
const
|
|
386
|
+
const projectResult = readJson(projectSettingsPath);
|
|
387
|
+
const projectRaw = projectResult.data;
|
|
388
|
+
if (projectResult.error && onWarn) onWarn(projectResult.error);
|
|
351
389
|
const projectOmRaw = projectRaw?.["pi-blackhole"] ?? projectRaw?.["observational-memory"];
|
|
352
390
|
|
|
353
391
|
// Merge legacy sources
|
|
@@ -449,7 +487,11 @@ export function saveUnifiedConfig(settings: Partial<UnifiedConfig>): boolean {
|
|
|
449
487
|
const path = configPath();
|
|
450
488
|
const dir = dirname(path);
|
|
451
489
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
452
|
-
const
|
|
490
|
+
const existingResult = readJson(path);
|
|
491
|
+
const existing = existingResult.data ?? {};
|
|
492
|
+
if (existingResult.error) {
|
|
493
|
+
console.warn("blackhole: overwriting corrupt config file at " + path);
|
|
494
|
+
}
|
|
453
495
|
const next = { ...existing, ...settings };
|
|
454
496
|
writeFileSync(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
455
497
|
return true;
|
package/src/extract/commits.ts
CHANGED
|
@@ -18,41 +18,84 @@ const firstLineOf = (text: string): string => {
|
|
|
18
18
|
const cleanMessage = (msg: string): string =>
|
|
19
19
|
msg.replace(/\\"/g, '"').replace(/\\'/g, "'").trim();
|
|
20
20
|
|
|
21
|
+
/** Extract commit hash from git output text (tool_result or bash output). */
|
|
22
|
+
const extractHashFromOutput = (text: string): string | undefined => {
|
|
23
|
+
const bracket = text.match(/\[\S+\s+([0-9a-f]{7,12})\]/);
|
|
24
|
+
if (bracket) return bracket[1];
|
|
25
|
+
const range = text.match(/\b([0-9a-f]{7,12})\.\.([0-9a-f]{7,12})\b/);
|
|
26
|
+
if (range) return range[2];
|
|
27
|
+
const plain = text.match(HASH_RE);
|
|
28
|
+
if (plain) return plain[1];
|
|
29
|
+
return undefined;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Try to extract a commit message from a git commit command string. */
|
|
33
|
+
const tryExtractMessage = (cmd: string): string | undefined => {
|
|
34
|
+
if (!/\bgit\s+commit\b/.test(cmd)) return undefined;
|
|
35
|
+
const m = cmd.match(COMMIT_MSG_RE);
|
|
36
|
+
if (!m) return undefined;
|
|
37
|
+
const message = firstLineOf(cleanMessage(m[1] ?? m[2] ?? m[3] ?? ""));
|
|
38
|
+
return message || undefined;
|
|
39
|
+
};
|
|
40
|
+
|
|
21
41
|
/**
|
|
22
|
-
* Extract git commits from bash tool calls
|
|
23
|
-
*
|
|
42
|
+
* Extract git commits from bash tool calls, bash execution messages,
|
|
43
|
+
* and user messages that wrap bash execution output (post-convertToLlm).
|
|
44
|
+
*
|
|
45
|
+
* Handles three block kinds:
|
|
46
|
+
* - tool_call (name: "bash") — agent tool call to the bash tool
|
|
47
|
+
* - bash — pi's internal bashExecution message
|
|
48
|
+
* - user — convertToLlm wraps bashExecution as "Ran `cmd`\n```\noutput\n```"
|
|
24
49
|
*/
|
|
25
50
|
export const extractCommits = (blocks: NormalizedBlock[]): CommitInfo[] => {
|
|
26
51
|
const commits: CommitInfo[] = [];
|
|
52
|
+
const addCommit = (hash: string | undefined, message: string) => {
|
|
53
|
+
const key = `${hash ?? ""}::${message}`;
|
|
54
|
+
if (!commits.some((c) => `${c.hash ?? ""}::${c.message}` === key)) {
|
|
55
|
+
commits.push({ hash, message });
|
|
56
|
+
}
|
|
57
|
+
};
|
|
27
58
|
|
|
28
59
|
for (let i = 0; i < blocks.length; i++) {
|
|
29
60
|
const b = blocks[i];
|
|
30
|
-
if (b.kind !== "tool_call" || b.name !== "bash") continue;
|
|
31
|
-
const cmd = typeof b.args.command === "string" ? b.args.command : "";
|
|
32
|
-
if (!/\bgit\s+commit\b/.test(cmd)) continue;
|
|
33
|
-
const m = cmd.match(COMMIT_MSG_RE);
|
|
34
|
-
if (!m) continue;
|
|
35
|
-
const message = firstLineOf(cleanMessage(m[1] ?? m[2] ?? m[3] ?? ""));
|
|
36
|
-
if (!message) continue;
|
|
37
61
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
62
|
+
// ── Case 1: tool_call (agent calls bash tool) ──
|
|
63
|
+
if (b.kind === "tool_call" && b.name === "bash") {
|
|
64
|
+
const cmd = (b.args && typeof b.args.command === "string") ? b.args.command : "";
|
|
65
|
+
const message = tryExtractMessage(cmd);
|
|
66
|
+
if (!message) continue;
|
|
67
|
+
|
|
68
|
+
let hash: string | undefined;
|
|
69
|
+
for (let j = i + 1; j < Math.min(blocks.length, i + 3); j++) {
|
|
70
|
+
const r = blocks[j];
|
|
71
|
+
if (r.kind !== "tool_result") continue;
|
|
72
|
+
hash = extractHashFromOutput(r.text);
|
|
73
|
+
if (hash) break;
|
|
74
|
+
}
|
|
75
|
+
addCommit(hash, message);
|
|
76
|
+
continue;
|
|
50
77
|
}
|
|
51
78
|
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
79
|
+
// ── Case 2: bash execution message ──
|
|
80
|
+
if (b.kind === "bash") {
|
|
81
|
+
const message = tryExtractMessage(b.command);
|
|
82
|
+
if (!message) continue;
|
|
83
|
+
const hash = extractHashFromOutput(b.output);
|
|
84
|
+
addCommit(hash, message);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Case 3: user message wrapping a bash execution (post-convertToLlm) ──
|
|
89
|
+
if (b.kind === "user") {
|
|
90
|
+
// Detect "Ran `git commit -m "..."`" pattern in user text
|
|
91
|
+
const ranCmd = b.text.match(/Ran\s+`((?:[^`\\]|\\.)*)`/);
|
|
92
|
+
if (!ranCmd) continue;
|
|
93
|
+
const message = tryExtractMessage(ranCmd[1]);
|
|
94
|
+
if (!message) continue;
|
|
95
|
+
// Extract output from the code block following the command
|
|
96
|
+
const codeBlock = b.text.match(/```\n([\s\S]*?)```/);
|
|
97
|
+
const hash = codeBlock ? extractHashFromOutput(codeBlock[1]) : undefined;
|
|
98
|
+
addCommit(hash, message);
|
|
56
99
|
}
|
|
57
100
|
}
|
|
58
101
|
|
package/src/extract/goals.ts
CHANGED
|
@@ -40,6 +40,11 @@ const isSubstantiveGoal = (text: string): boolean => {
|
|
|
40
40
|
return true;
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
+
const FIRST_MSG_CLIP = 80;
|
|
44
|
+
|
|
45
|
+
const indexSuffix = (sourceIndex?: number): string =>
|
|
46
|
+
sourceIndex != null ? ` (#${sourceIndex})` : "";
|
|
47
|
+
|
|
43
48
|
// Test scope-change / task intent only on the leading portion of a user block
|
|
44
49
|
// so that pasted outputs below the actual instruction do not trigger matches.
|
|
45
50
|
const LEADING_CHARS = 200;
|
|
@@ -47,6 +52,7 @@ const LEADING_CHARS = 200;
|
|
|
47
52
|
export const extractGoals = (blocks: NormalizedBlock[]): string[] => {
|
|
48
53
|
const goals: string[] = [];
|
|
49
54
|
let latestScopeChange: string[] | null = null;
|
|
55
|
+
let latestScopeIndex: number | undefined;
|
|
50
56
|
|
|
51
57
|
for (const b of blocks) {
|
|
52
58
|
if (b.kind !== "user") continue;
|
|
@@ -58,21 +64,26 @@ export const extractGoals = (blocks: NormalizedBlock[]): string[] => {
|
|
|
58
64
|
if (lines.length === 0) continue;
|
|
59
65
|
|
|
60
66
|
if (goals.length === 0) {
|
|
61
|
-
goals.push(...lines.slice(0, 6));
|
|
67
|
+
goals.push(...lines.slice(0, 6).map((l) => clip(l, FIRST_MSG_CLIP) + indexSuffix(b.sourceIndex)));
|
|
62
68
|
continue;
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
const leading = b.text.slice(0, LEADING_CHARS);
|
|
66
72
|
if (SCOPE_CHANGE_RE.test(leading)) {
|
|
67
73
|
latestScopeChange = lines.slice(0, 3).map((l) => clip(l, MAX_GOAL_CHARS));
|
|
74
|
+
latestScopeIndex = b.sourceIndex;
|
|
68
75
|
} else if (TASK_RE.test(leading) && lines[0].length > 15) {
|
|
69
76
|
latestScopeChange = lines.slice(0, 2).map((l) => clip(l, MAX_GOAL_CHARS));
|
|
77
|
+
latestScopeIndex = b.sourceIndex;
|
|
70
78
|
}
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
// Only emit the [Scope change] marker when we actually captured bullets.
|
|
74
82
|
if (latestScopeChange && latestScopeChange.length > 0) {
|
|
75
|
-
goals.push("[Scope change]"
|
|
83
|
+
goals.push("[Scope change]");
|
|
84
|
+
for (const line of latestScopeChange) {
|
|
85
|
+
goals.push(line + indexSuffix(latestScopeIndex));
|
|
86
|
+
}
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
return goals.slice(0, 8);
|
|
@@ -48,6 +48,37 @@ const formatTokens = (n: number): string => {
|
|
|
48
48
|
return String(n);
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
export interface CompactionStats {
|
|
52
|
+
summarized: number;
|
|
53
|
+
kept: number;
|
|
54
|
+
keptTokensEst: number;
|
|
55
|
+
compactAll: boolean;
|
|
56
|
+
totalUserTurns: number;
|
|
57
|
+
keptUserTurns: number;
|
|
58
|
+
requestedKeepUserTurns: number;
|
|
59
|
+
keepUserTurnsExplicit: boolean;
|
|
60
|
+
keepFallbackToCompactAll: boolean;
|
|
61
|
+
smartKeepAdjusted: boolean;
|
|
62
|
+
smartFromKeep: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Format compaction stats for user-visible notification.
|
|
67
|
+
* Example output:
|
|
68
|
+
* blackhole: 6 source entries processed; tail kept 1/4 user turns (~0.5k tok).
|
|
69
|
+
*/
|
|
70
|
+
export const formatCompactionStats = (stats: CompactionStats): string => {
|
|
71
|
+
const parts: string[] = [`${stats.summarized} source entries processed`];
|
|
72
|
+
parts.push(`tail kept ${stats.keptUserTurns}/${stats.totalUserTurns} user turns`);
|
|
73
|
+
if (stats.smartKeepAdjusted) {
|
|
74
|
+
parts.push(`smart keep:${stats.smartFromKeep}→${stats.keptUserTurns}`);
|
|
75
|
+
}
|
|
76
|
+
if (stats.keepFallbackToCompactAll) {
|
|
77
|
+
parts.push(`compact-all`);
|
|
78
|
+
}
|
|
79
|
+
return `blackhole: ${parts.join("; ")} (~${formatTokens(stats.keptTokensEst)} tok).`;
|
|
80
|
+
};
|
|
81
|
+
|
|
51
82
|
const dbg = (debug: boolean, data: Record<string, unknown>) => {
|
|
52
83
|
if (!debug) return;
|
|
53
84
|
try { writeFileSync("/tmp/pi-blackhole-debug.json", JSON.stringify(data, null, 2)); } catch {}
|
|
@@ -240,7 +271,7 @@ const REASON_MESSAGES: Record<OwnCutCancelReason, string> = {
|
|
|
240
271
|
export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime) => {
|
|
241
272
|
pi.on("session_before_compact", (event, ctx) => {
|
|
242
273
|
const { preparation, branchEntries, customInstructions } = event;
|
|
243
|
-
omRuntime.ensureConfig(ctx.cwd ?? process.cwd());
|
|
274
|
+
omRuntime.ensureConfig(ctx.cwd ?? process.cwd(), (msg) => ctx.ui?.notify?.(msg, "warning"));
|
|
244
275
|
const trace = (ev: string, d?: Record<string, unknown>) => debugLog(ev, d, omRuntime.config.debugLog === true);
|
|
245
276
|
|
|
246
277
|
trace("before_compact.enter", {
|
|
@@ -291,9 +322,7 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
291
322
|
// Determine effective tail behavior for buildOwnCut
|
|
292
323
|
// Both /blackhole and auto-triggered default to "minimal" (aggressive cut);
|
|
293
324
|
// users can opt into "pi-default" (gentler) by setting tailBehavior in config.
|
|
294
|
-
const effectiveTailBehavior =
|
|
295
|
-
? (omRuntime.config.tailBehavior ?? "minimal")
|
|
296
|
-
: (omRuntime.config.tailBehavior ?? "minimal");
|
|
325
|
+
const effectiveTailBehavior = omRuntime.config.tailBehavior ?? "minimal";
|
|
297
326
|
|
|
298
327
|
trace("before_compact.tail_behavior", {
|
|
299
328
|
effectiveTailBehavior,
|
|
@@ -400,10 +429,22 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
400
429
|
}, 0);
|
|
401
430
|
return sum;
|
|
402
431
|
}, 0);
|
|
432
|
+
const totalUserTurns = (branchEntries as any[]).filter((e: any) => e.type === "message" && e.message?.role === "user").length;
|
|
433
|
+
const keptUserTurns = ownCut.compactAll
|
|
434
|
+
? 0
|
|
435
|
+
: (branchEntries as any[]).slice(keptIdx).filter((e: any) => e.type === "message" && e.message?.role === "user").length;
|
|
403
436
|
omRuntime.compactionStats = {
|
|
404
437
|
summarized: agentMessages.length,
|
|
405
438
|
kept: keptEntries.length,
|
|
406
439
|
keptTokensEst: Math.round(keptChars / 4),
|
|
440
|
+
compactAll: ownCut.compactAll,
|
|
441
|
+
totalUserTurns,
|
|
442
|
+
keptUserTurns,
|
|
443
|
+
requestedKeepUserTurns: 1,
|
|
444
|
+
keepUserTurnsExplicit: false,
|
|
445
|
+
keepFallbackToCompactAll: ownCut.compactAll,
|
|
446
|
+
smartKeepAdjusted: false,
|
|
447
|
+
smartFromKeep: 1,
|
|
407
448
|
};
|
|
408
449
|
|
|
409
450
|
const summary = compile({
|
|
@@ -460,7 +501,10 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
460
501
|
const projection = buildCompactionProjection(
|
|
461
502
|
branchEntries as any[],
|
|
462
503
|
firstKeptEntryId,
|
|
463
|
-
{
|
|
504
|
+
{
|
|
505
|
+
observationsPoolMaxTokens: omRuntime.config.observationsPoolMaxTokens,
|
|
506
|
+
fullFoldAlways: omRuntime.config.fullFoldAlways,
|
|
507
|
+
},
|
|
464
508
|
);
|
|
465
509
|
omContent = renderSummary(projection.reflections, projection.observations);
|
|
466
510
|
omDetails = projection.details;
|
|
@@ -488,10 +532,7 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
488
532
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
489
533
|
setTimeout(() => {
|
|
490
534
|
try {
|
|
491
|
-
ctx?.ui?.notify?.(
|
|
492
|
-
`blackhole: ${stats.summarized} source entries processed; tail kept ${stats.kept} (~${formatTokens(stats.keptTokensEst)} tok).`,
|
|
493
|
-
"info",
|
|
494
|
-
);
|
|
535
|
+
ctx?.ui?.notify?.(formatCompactionStats(stats), "info");
|
|
495
536
|
notifyMigrationReminder(sessionId, (msg, level) => ctx?.ui?.notify?.(msg, level as any));
|
|
496
537
|
} catch {}
|
|
497
538
|
}, 500);
|
|
@@ -8,7 +8,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
10
|
import { createBridgeStreamFn } from "../../provider-stream.js";
|
|
11
|
-
import { streamSimple } from "@earendil-works/pi-ai";
|
|
11
|
+
import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
12
12
|
import { Type } from "typebox";
|
|
13
13
|
import type { Static } from "typebox";
|
|
14
14
|
import { debugLog } from "../../debug-log.js";
|
|
@@ -9,7 +9,7 @@
|
|
|
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
11
|
import { createBridgeStreamFn } from "../../provider-stream.js";
|
|
12
|
-
import { streamSimple } from "@earendil-works/pi-ai";
|
|
12
|
+
import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
13
13
|
import { Type } from "typebox";
|
|
14
14
|
import type { Static } from "typebox";
|
|
15
15
|
import { hashId } from "../../ids.js";
|
|
@@ -8,7 +8,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
10
|
import { createBridgeStreamFn } from "../../provider-stream.js";
|
|
11
|
-
import { streamSimple } from "@earendil-works/pi-ai";
|
|
11
|
+
import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
12
12
|
import { Type } from "typebox";
|
|
13
13
|
import type { Static } from "typebox";
|
|
14
14
|
import { hashId } from "../../ids.js";
|