pi-blackhole 0.4.2 → 0.4.3
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 +15 -0
- package/dist/index.js +11660 -0
- package/dist/index.js.map +1 -0
- package/example-config.json +1 -1
- package/index.ts +37 -63
- package/package.json +21 -9
- package/src/commands/cleanup.ts +279 -240
- package/src/commands/memory.ts +236 -184
- package/src/commands/pi-vcc.ts +202 -152
- package/src/commands/vcc-recall.ts +126 -95
- package/src/core/brief.ts +167 -33
- package/src/core/build-sections.ts +8 -2
- package/src/core/config-env.ts +117 -0
- package/src/core/content.ts +31 -7
- package/src/core/drill-down.ts +41 -11
- package/src/core/filter-noise.ts +9 -3
- package/src/core/format-recall.ts +15 -6
- package/src/core/format.ts +14 -4
- package/src/core/lineage.ts +9 -3
- package/src/core/load-messages.ts +24 -5
- package/src/core/normalize.ts +38 -14
- package/src/core/recall-scope.ts +11 -3
- package/src/core/render-entries.ts +22 -6
- package/src/core/sanitize.ts +5 -1
- package/src/core/search-entries.ts +111 -19
- package/src/core/settings.ts +1 -3
- package/src/core/summarize.ts +42 -21
- package/src/core/unified-config.ts +549 -411
- package/src/extract/commits.ts +4 -2
- package/src/extract/files.ts +10 -5
- package/src/extract/goals.ts +7 -2
- package/src/hooks/before-compact.ts +210 -88
- package/src/om/agents/dropper/agent.ts +380 -265
- package/src/om/agents/dropper/coverage.ts +102 -82
- package/src/om/agents/observer/agent.ts +242 -206
- package/src/om/agents/reflector/agent.ts +212 -153
- package/src/om/cleanup.ts +239 -218
- package/src/om/clipboard.ts +59 -51
- package/src/om/compaction-trigger.ts +448 -333
- package/src/om/config.ts +13 -6
- package/src/om/configure-overlay.ts +518 -355
- package/src/om/consolidation.ts +1460 -953
- package/src/om/cooldown.ts +75 -65
- package/src/om/debug-log.ts +86 -68
- package/src/om/ids.ts +1 -1
- package/src/om/ledger/fold.ts +89 -78
- package/src/om/ledger/progress.ts +181 -153
- package/src/om/ledger/projection.ts +248 -185
- package/src/om/ledger/recall.ts +247 -196
- package/src/om/ledger/render-summary.ts +79 -50
- package/src/om/ledger/types.ts +146 -117
- package/src/om/model-budget.ts +23 -13
- package/src/om/pending.ts +243 -179
- package/src/om/provider-stream.ts +52 -7
- package/src/om/retryable-error.ts +12 -16
- package/src/om/reverse-recall.ts +97 -91
- package/src/om/runtime.ts +474 -375
- package/src/om/serialize.ts +190 -166
- package/src/om/status-overlay.ts +246 -195
- package/src/om/tokens.ts +28 -21
- package/src/pi-base/blackhole-settings.ts +437 -0
- package/src/pi-base/config-manager.ts +440 -0
- package/src/pi-base/config.ts +469 -0
- package/src/pi-base/env.ts +43 -0
- package/src/pi-base/paths.ts +47 -0
- package/src/pi-base/settings/body.ts +1648 -0
- package/src/pi-base/settings/fields/action.ts +43 -0
- package/src/pi-base/settings/fields/boolean.ts +47 -0
- package/src/pi-base/settings/fields/custom.ts +72 -0
- package/src/pi-base/settings/fields/enum.ts +310 -0
- package/src/pi-base/settings/fields/index.ts +46 -0
- package/src/pi-base/settings/fields/model.ts +452 -0
- package/src/pi-base/settings/fields/string.ts +527 -0
- package/src/pi-base/settings/fields/text.ts +115 -0
- package/src/pi-base/settings/frame.ts +197 -0
- package/src/pi-base/settings/index.ts +77 -0
- package/src/pi-base/settings/inline-edit.ts +313 -0
- package/src/pi-base/settings/modal.ts +152 -0
- package/src/pi-base/settings/types.ts +500 -0
- package/src/pi-base/settings/validate-field.ts +113 -0
- package/src/pi-base/shell.ts +117 -0
- package/src/pi-base/types.ts +6 -0
- package/src/pi-base/ui.ts +32 -0
- package/src/tools/recall.ts +347 -225
- package/src/types.ts +20 -3
- package/tsup.config.ts +23 -0
- package/vitest.config.ts +15 -15
package/src/extract/commits.ts
CHANGED
|
@@ -5,7 +5,8 @@ interface CommitInfo {
|
|
|
5
5
|
message: string;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
const COMMIT_MSG_RE =
|
|
8
|
+
const COMMIT_MSG_RE =
|
|
9
|
+
/git\s+commit[^\n]*?-m\s+(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'|\$?'((?:[^'\\]|\\.)*)')/;
|
|
9
10
|
// Match short hash from git output — only as fallback after bracket/range patterns fail.
|
|
10
11
|
// Requires 8+ hex chars to reduce false positives from random hex in tool output.
|
|
11
12
|
const HASH_RE = /\b([0-9a-f]{8,12})\b/;
|
|
@@ -61,7 +62,8 @@ export const extractCommits = (blocks: NormalizedBlock[]): CommitInfo[] => {
|
|
|
61
62
|
|
|
62
63
|
// ── Case 1: tool_call (agent calls bash tool) ──
|
|
63
64
|
if (b.kind === "tool_call" && b.name === "bash") {
|
|
64
|
-
const cmd =
|
|
65
|
+
const cmd =
|
|
66
|
+
b.args && typeof b.args.command === "string" ? b.args.command : "";
|
|
65
67
|
const message = tryExtractMessage(cmd);
|
|
66
68
|
if (!message) continue;
|
|
67
69
|
|
package/src/extract/files.ts
CHANGED
|
@@ -7,12 +7,15 @@ interface FileActivity {
|
|
|
7
7
|
created: Set<string>;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
const FILE_READ_TOOLS = new Set([
|
|
11
|
-
"Read", "read_file", "View",
|
|
12
|
-
]);
|
|
10
|
+
const FILE_READ_TOOLS = new Set(["Read", "read_file", "View"]);
|
|
13
11
|
|
|
14
12
|
const FILE_WRITE_TOOLS = new Set([
|
|
15
|
-
"Edit",
|
|
13
|
+
"Edit",
|
|
14
|
+
"Write",
|
|
15
|
+
"edit",
|
|
16
|
+
"write",
|
|
17
|
+
"edit_file",
|
|
18
|
+
"write_file",
|
|
16
19
|
"MultiEdit",
|
|
17
20
|
]);
|
|
18
21
|
|
|
@@ -27,7 +30,9 @@ const FILE_CREATE_TOOLS = new Set<string>();
|
|
|
27
30
|
const longestCommonDirPrefix = (paths: string[]): string => {
|
|
28
31
|
// Normalize backslashes (Windows) to forward slashes for uniform comparison
|
|
29
32
|
const normalized = paths.map((p) => p.replace(/\\/g, "/"));
|
|
30
|
-
const abs = normalized.filter(
|
|
33
|
+
const abs = normalized.filter(
|
|
34
|
+
(p) => p.startsWith("/") || /^[A-Za-z]:\//.test(p),
|
|
35
|
+
);
|
|
31
36
|
if (abs.length < 2) return "";
|
|
32
37
|
const split = abs.map((p) => p.split("/"));
|
|
33
38
|
const min = Math.min(...split.map((s) => s.length));
|
package/src/extract/goals.ts
CHANGED
|
@@ -8,7 +8,8 @@ const SCOPE_CHANGE_RE =
|
|
|
8
8
|
const TASK_RE =
|
|
9
9
|
/\b(fix|implement|add|create|build|refactor|debug|investigate|update|remove|delete|migrate|deploy|test|write|set up)\b/i;
|
|
10
10
|
|
|
11
|
-
const NOISE_SHORT_RE =
|
|
11
|
+
const NOISE_SHORT_RE =
|
|
12
|
+
/^(ok|yes|no|sure|yeah|yep|go|hi|hey|thx|thanks|ok\b.*|y|n|k)\s*[.!?]*$/i;
|
|
12
13
|
|
|
13
14
|
// Reject lines that are clearly not user goals (pasted output, code, paths, tool dumps)
|
|
14
15
|
// or meta-prompt boilerplate (command templates like `/issues` that start with "For each issue:"
|
|
@@ -64,7 +65,11 @@ export const extractGoals = (blocks: NormalizedBlock[]): string[] => {
|
|
|
64
65
|
if (lines.length === 0) continue;
|
|
65
66
|
|
|
66
67
|
if (goals.length === 0) {
|
|
67
|
-
goals.push(
|
|
68
|
+
goals.push(
|
|
69
|
+
...lines
|
|
70
|
+
.slice(0, 6)
|
|
71
|
+
.map((l) => clip(l, FIRST_MSG_CLIP) + indexSuffix(b.sourceIndex)),
|
|
72
|
+
);
|
|
68
73
|
continue;
|
|
69
74
|
}
|
|
70
75
|
|
|
@@ -12,7 +12,10 @@ import { convertToLlm } from "@earendil-works/pi-coding-agent";
|
|
|
12
12
|
import { writeFileSync } from "fs";
|
|
13
13
|
import { compile } from "../core/summarize";
|
|
14
14
|
import type { PiVccCompactionDetails } from "../details";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
buildCompactionProjection,
|
|
17
|
+
renderSummary,
|
|
18
|
+
} from "../om/ledger/index.js";
|
|
16
19
|
import type { Runtime } from "../om/runtime.js";
|
|
17
20
|
import { debugLog } from "../om/debug-log.js";
|
|
18
21
|
import { configFileNeedsMigration } from "../core/unified-config.js";
|
|
@@ -29,20 +32,19 @@ const migrationNotifyCount = new Map<string, number>();
|
|
|
29
32
|
* At most 2 notifications per session. Call after compaction completes.
|
|
30
33
|
*/
|
|
31
34
|
export function notifyMigrationReminder(
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
sessionId: string,
|
|
36
|
+
notify: (msg: string, level: string) => void,
|
|
34
37
|
): void {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
const count = migrationNotifyCount.get(sessionId) ?? 0;
|
|
39
|
+
if (count >= 2) return;
|
|
40
|
+
if (!configFileNeedsMigration()) return;
|
|
41
|
+
migrationNotifyCount.set(sessionId, count + 1);
|
|
42
|
+
notify(
|
|
43
|
+
"blackhole: Use `/blackhole configure` to save your updated configuration.",
|
|
44
|
+
"info",
|
|
45
|
+
);
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
|
|
46
48
|
const formatTokens = (n: number): string => {
|
|
47
49
|
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
48
50
|
return String(n);
|
|
@@ -69,7 +71,9 @@ export interface CompactionStats {
|
|
|
69
71
|
*/
|
|
70
72
|
export const formatCompactionStats = (stats: CompactionStats): string => {
|
|
71
73
|
const parts: string[] = [`${stats.summarized} source entries processed`];
|
|
72
|
-
parts.push(
|
|
74
|
+
parts.push(
|
|
75
|
+
`tail kept ${stats.keptUserTurns}/${stats.totalUserTurns} user turns`,
|
|
76
|
+
);
|
|
73
77
|
if (stats.smartKeepAdjusted) {
|
|
74
78
|
parts.push(`smart keep:${stats.smartFromKeep}→${stats.keptUserTurns}`);
|
|
75
79
|
}
|
|
@@ -81,7 +85,12 @@ export const formatCompactionStats = (stats: CompactionStats): string => {
|
|
|
81
85
|
|
|
82
86
|
const dbg = (debug: boolean, data: Record<string, unknown>) => {
|
|
83
87
|
if (!debug) return;
|
|
84
|
-
try {
|
|
88
|
+
try {
|
|
89
|
+
writeFileSync(
|
|
90
|
+
"/tmp/pi-blackhole-debug.json",
|
|
91
|
+
JSON.stringify(data, null, 2),
|
|
92
|
+
);
|
|
93
|
+
} catch {}
|
|
85
94
|
};
|
|
86
95
|
|
|
87
96
|
const previewContent = (content: unknown): string => {
|
|
@@ -106,9 +115,7 @@ interface EntryWithMessage {
|
|
|
106
115
|
message: { role: string; content: unknown };
|
|
107
116
|
}
|
|
108
117
|
|
|
109
|
-
export type OwnCutCancelReason =
|
|
110
|
-
| "no_live_messages"
|
|
111
|
-
| "too_few_live_messages";
|
|
118
|
+
export type OwnCutCancelReason = "no_live_messages" | "too_few_live_messages";
|
|
112
119
|
|
|
113
120
|
export type OwnCutResult =
|
|
114
121
|
| { ok: true; messages: any[]; firstKeptEntryId: string; compactAll: boolean }
|
|
@@ -136,7 +143,8 @@ export function buildOwnCut(
|
|
|
136
143
|
// compact-all) OR set to an id that no longer exists in the branch. In both cases,
|
|
137
144
|
// start collecting from right after the last compaction entry.
|
|
138
145
|
const hasPriorCompaction = lastCompactionIdx >= 0;
|
|
139
|
-
const hasValidKeptId =
|
|
146
|
+
const hasValidKeptId =
|
|
147
|
+
!!lastKeptId && branchEntries.some((e: any) => e.id === lastKeptId);
|
|
140
148
|
const orphanRecovery = hasPriorCompaction && !hasValidKeptId;
|
|
141
149
|
|
|
142
150
|
// Collect live messages
|
|
@@ -163,9 +171,13 @@ export function buildOwnCut(
|
|
|
163
171
|
|
|
164
172
|
// ── Pi's cut path: use Pi's firstKeptEntryId instead of last-user cut ──
|
|
165
173
|
if (tailBehavior === "pi-default" && piFirstKeptEntryId) {
|
|
166
|
-
const cutInBranch = branchEntries.findIndex(
|
|
174
|
+
const cutInBranch = branchEntries.findIndex(
|
|
175
|
+
(e: any) => e.id === piFirstKeptEntryId,
|
|
176
|
+
);
|
|
167
177
|
if (cutInBranch >= 0) {
|
|
168
|
-
const liveCutIdx = liveMessages.findIndex(
|
|
178
|
+
const liveCutIdx = liveMessages.findIndex(
|
|
179
|
+
(lm) => lm.entry.id === piFirstKeptEntryId,
|
|
180
|
+
);
|
|
169
181
|
if (liveCutIdx > 0) {
|
|
170
182
|
return {
|
|
171
183
|
ok: true,
|
|
@@ -180,7 +192,10 @@ export function buildOwnCut(
|
|
|
180
192
|
// everything for a fresh page). If minimal path would aggressively cut
|
|
181
193
|
// (multiple user messages), cancel to respect Pi's guidance.
|
|
182
194
|
let lastUserIdx = liveMessages.length - 1;
|
|
183
|
-
while (
|
|
195
|
+
while (
|
|
196
|
+
lastUserIdx > 0 &&
|
|
197
|
+
liveMessages[lastUserIdx].message.role !== "user"
|
|
198
|
+
) {
|
|
184
199
|
lastUserIdx--;
|
|
185
200
|
}
|
|
186
201
|
if (lastUserIdx > 0) {
|
|
@@ -194,7 +209,8 @@ export function buildOwnCut(
|
|
|
194
209
|
// type:"compaction"). Resolve to the next message entry after pi's cut point.
|
|
195
210
|
if (liveCutIdx < 0) {
|
|
196
211
|
const nextMsgEntry = branchEntries.find(
|
|
197
|
-
(e: any, i: number) =>
|
|
212
|
+
(e: any, i: number) =>
|
|
213
|
+
i > cutInBranch && e.type === "message" && e.message,
|
|
198
214
|
);
|
|
199
215
|
if (nextMsgEntry) {
|
|
200
216
|
const resolvedId: string = nextMsgEntry.id;
|
|
@@ -204,14 +220,19 @@ export function buildOwnCut(
|
|
|
204
220
|
if (resolvedLiveIdx > 0) {
|
|
205
221
|
return {
|
|
206
222
|
ok: true,
|
|
207
|
-
messages: liveMessages
|
|
223
|
+
messages: liveMessages
|
|
224
|
+
.slice(0, resolvedLiveIdx)
|
|
225
|
+
.map((e) => e.message),
|
|
208
226
|
firstKeptEntryId: resolvedId,
|
|
209
227
|
compactAll: false,
|
|
210
228
|
};
|
|
211
229
|
}
|
|
212
230
|
if (resolvedLiveIdx === 0) {
|
|
213
231
|
let lastUserIdx = liveMessages.length - 1;
|
|
214
|
-
while (
|
|
232
|
+
while (
|
|
233
|
+
lastUserIdx > 0 &&
|
|
234
|
+
liveMessages[lastUserIdx].message.role !== "user"
|
|
235
|
+
) {
|
|
215
236
|
lastUserIdx--;
|
|
216
237
|
}
|
|
217
238
|
if (lastUserIdx > 0) {
|
|
@@ -229,8 +250,10 @@ export function buildOwnCut(
|
|
|
229
250
|
// piFirstKeptEntryId not found in branch → fall through to minimal / orphan recovery
|
|
230
251
|
}
|
|
231
252
|
|
|
232
|
-
if (liveMessages.length === 0)
|
|
233
|
-
|
|
253
|
+
if (liveMessages.length === 0)
|
|
254
|
+
return { ok: false, reason: "no_live_messages" };
|
|
255
|
+
if (liveMessages.length <= 2)
|
|
256
|
+
return { ok: false, reason: "too_few_live_messages" };
|
|
234
257
|
|
|
235
258
|
// Summarize all messages, keep only the last user message as context
|
|
236
259
|
let cutIdx = liveMessages.length - 1;
|
|
@@ -265,20 +288,29 @@ export function buildOwnCut(
|
|
|
265
288
|
|
|
266
289
|
const REASON_MESSAGES: Record<OwnCutCancelReason, string> = {
|
|
267
290
|
no_live_messages: "blackhole: Nothing to compact (no live messages)",
|
|
268
|
-
too_few_live_messages:
|
|
291
|
+
too_few_live_messages:
|
|
292
|
+
'blackhole: Too few live messages — Pi\'s default logic preserves visible context. Set tailBehavior to "minimal" in config to force compaction with fewer messages.',
|
|
269
293
|
};
|
|
270
294
|
|
|
271
|
-
export const registerBeforeCompactHook = (
|
|
295
|
+
export const registerBeforeCompactHook = (
|
|
296
|
+
pi: ExtensionAPI,
|
|
297
|
+
omRuntime: Runtime,
|
|
298
|
+
) => {
|
|
272
299
|
pi.on("session_before_compact", (event, ctx) => {
|
|
273
300
|
const { preparation, branchEntries, customInstructions } = event;
|
|
274
|
-
omRuntime.ensureConfig(ctx.cwd ?? process.cwd(), (msg) =>
|
|
275
|
-
|
|
301
|
+
omRuntime.ensureConfig(ctx.cwd ?? process.cwd(), (msg) =>
|
|
302
|
+
ctx.ui?.notify?.(msg, "warning"),
|
|
303
|
+
);
|
|
304
|
+
const trace = (ev: string, d?: Record<string, unknown>) =>
|
|
305
|
+
debugLog(ev, d, omRuntime.config.debugLog === true);
|
|
276
306
|
|
|
277
307
|
trace("before_compact.enter", {
|
|
278
308
|
customInstructions,
|
|
279
309
|
isPiVcc: customInstructions === PI_VCC_COMPACT_INSTRUCTION,
|
|
280
310
|
overrideDefaultCompaction: omRuntime.config.overrideDefaultCompaction,
|
|
281
|
-
|
|
311
|
+
manualMode:
|
|
312
|
+
omRuntime.config.compaction === "manual" ||
|
|
313
|
+
omRuntime.config.noAutoCompact === true,
|
|
282
314
|
branchLength: branchEntries.length,
|
|
283
315
|
hasPreviousSummary: !!preparation.previousSummary,
|
|
284
316
|
});
|
|
@@ -296,7 +328,9 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
296
328
|
|
|
297
329
|
// compactionEngine "pi-default" means let Pi handle auto-triggered compactions
|
|
298
330
|
if (omRuntime.config.compactionEngine === "pi-default" && !isPiVcc) {
|
|
299
|
-
trace("before_compact.return_early", {
|
|
331
|
+
trace("before_compact.return_early", {
|
|
332
|
+
reason: "compactionEngine_pi_default",
|
|
333
|
+
});
|
|
300
334
|
return;
|
|
301
335
|
}
|
|
302
336
|
|
|
@@ -307,14 +341,25 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
307
341
|
}
|
|
308
342
|
|
|
309
343
|
// LEGACY: old config key guards — only apply when new keys are absent (unmigrated config)
|
|
310
|
-
if (
|
|
344
|
+
if (
|
|
345
|
+
omRuntime.config.compaction === undefined &&
|
|
346
|
+
omRuntime.config.compactionEngine === undefined
|
|
347
|
+
) {
|
|
311
348
|
if (!isPiVcc && !omRuntime.config.overrideDefaultCompaction) {
|
|
312
|
-
trace("before_compact.return_early", {
|
|
349
|
+
trace("before_compact.return_early", {
|
|
350
|
+
reason: "overrideDefaultCompaction=false and not /blackhole",
|
|
351
|
+
});
|
|
313
352
|
return;
|
|
314
353
|
}
|
|
315
354
|
|
|
316
|
-
if (
|
|
317
|
-
|
|
355
|
+
if (
|
|
356
|
+
(omRuntime.config.compaction === "manual" ||
|
|
357
|
+
omRuntime.config.noAutoCompact) &&
|
|
358
|
+
!isPiVcc
|
|
359
|
+
) {
|
|
360
|
+
trace("before_compact.cancel", {
|
|
361
|
+
reason: "manual mode and not /blackhole",
|
|
362
|
+
});
|
|
318
363
|
return { cancel: true };
|
|
319
364
|
}
|
|
320
365
|
}
|
|
@@ -337,13 +382,20 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
337
382
|
effectiveTailBehavior,
|
|
338
383
|
);
|
|
339
384
|
if (!ownCut.ok) {
|
|
340
|
-
const lastComp = [...branchEntries]
|
|
341
|
-
|
|
385
|
+
const lastComp = [...branchEntries]
|
|
386
|
+
.reverse()
|
|
387
|
+
.find((e: any) => e.type === "compaction");
|
|
388
|
+
const lastCompIdx = lastComp
|
|
389
|
+
? (branchEntries as any[]).indexOf(lastComp)
|
|
390
|
+
: -1;
|
|
342
391
|
|
|
343
392
|
// Recompute liveMessages view (same logic as buildOwnCut) for diagnostic
|
|
344
|
-
const lastKeptId: string | undefined = (lastComp as any)
|
|
393
|
+
const lastKeptId: string | undefined = (lastComp as any)
|
|
394
|
+
?.firstKeptEntryId;
|
|
345
395
|
const hasPriorCompaction = lastCompIdx >= 0;
|
|
346
|
-
const hasValidKeptId =
|
|
396
|
+
const hasValidKeptId =
|
|
397
|
+
!!lastKeptId &&
|
|
398
|
+
(branchEntries as any[]).some((e: any) => e.id === lastKeptId);
|
|
347
399
|
const diagOrphan = hasPriorCompaction && !hasValidKeptId;
|
|
348
400
|
const liveRoles: string[] = [];
|
|
349
401
|
if (diagOrphan) {
|
|
@@ -361,7 +413,10 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
361
413
|
if (e.type === "message" && e.message) liveRoles.push(e.message.role);
|
|
362
414
|
}
|
|
363
415
|
}
|
|
364
|
-
const userIndices = liveRoles.reduce<number[]>(
|
|
416
|
+
const userIndices = liveRoles.reduce<number[]>(
|
|
417
|
+
(acc, r, i) => (r === "user" ? (acc.push(i), acc) : acc),
|
|
418
|
+
[],
|
|
419
|
+
);
|
|
365
420
|
|
|
366
421
|
dbg(omRuntime.config.debug, {
|
|
367
422
|
cancelled: true,
|
|
@@ -369,29 +424,40 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
369
424
|
isPiVcc,
|
|
370
425
|
counts: {
|
|
371
426
|
total: branchEntries.length,
|
|
372
|
-
messages: (branchEntries as any[]).filter(
|
|
373
|
-
|
|
374
|
-
|
|
427
|
+
messages: (branchEntries as any[]).filter(
|
|
428
|
+
(e: any) => e.type === "message",
|
|
429
|
+
).length,
|
|
430
|
+
compactions: (branchEntries as any[]).filter(
|
|
431
|
+
(e: any) => e.type === "compaction",
|
|
432
|
+
).length,
|
|
433
|
+
entriesAfterLastCompaction:
|
|
434
|
+
lastCompIdx >= 0 ? branchEntries.length - lastCompIdx - 1 : null,
|
|
375
435
|
},
|
|
376
436
|
liveMessages: {
|
|
377
437
|
count: liveRoles.length,
|
|
378
438
|
userCount: userIndices.length,
|
|
379
439
|
firstUserIdx: userIndices[0] ?? null,
|
|
380
440
|
lastUserIdx: userIndices[userIndices.length - 1] ?? null,
|
|
381
|
-
roleSequence:
|
|
382
|
-
|
|
383
|
-
|
|
441
|
+
roleSequence:
|
|
442
|
+
liveRoles.length <= 30
|
|
443
|
+
? liveRoles
|
|
444
|
+
: [...liveRoles.slice(0, 10), "...", ...liveRoles.slice(-10)],
|
|
384
445
|
},
|
|
385
|
-
lastCompaction: lastComp
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
446
|
+
lastCompaction: lastComp
|
|
447
|
+
? {
|
|
448
|
+
hasFirstKeptEntryId: !!(lastComp as any).firstKeptEntryId,
|
|
449
|
+
foundInBranch: (lastComp as any).firstKeptEntryId
|
|
450
|
+
? (branchEntries as any[]).some(
|
|
451
|
+
(e: any) => e.id === (lastComp as any).firstKeptEntryId,
|
|
452
|
+
)
|
|
453
|
+
: null,
|
|
454
|
+
}
|
|
455
|
+
: null,
|
|
391
456
|
tail: (branchEntries as any[]).slice(-5).map((e: any) => ({
|
|
392
457
|
type: e.type,
|
|
393
458
|
role: e.type === "message" ? e.message?.role : undefined,
|
|
394
|
-
hasContent:
|
|
459
|
+
hasContent:
|
|
460
|
+
e.type === "message" ? e.message?.content != null : undefined,
|
|
395
461
|
})),
|
|
396
462
|
});
|
|
397
463
|
|
|
@@ -414,25 +480,53 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
414
480
|
const messages = convertToLlm(agentMessages);
|
|
415
481
|
|
|
416
482
|
// Count kept messages and estimate tokens
|
|
417
|
-
const keptIdx = (branchEntries as any[]).findIndex(
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
483
|
+
const keptIdx = (branchEntries as any[]).findIndex(
|
|
484
|
+
(e: any) => e.id === firstKeptEntryId,
|
|
485
|
+
);
|
|
486
|
+
const keptEntries =
|
|
487
|
+
keptIdx >= 0
|
|
488
|
+
? (branchEntries as any[])
|
|
489
|
+
.slice(keptIdx)
|
|
490
|
+
.filter((e: any) => e.type === "message")
|
|
491
|
+
: [];
|
|
421
492
|
const keptChars = keptEntries.reduce((sum: number, e: any) => {
|
|
422
493
|
const c = e.message?.content;
|
|
423
494
|
if (typeof c === "string") return sum + c.length;
|
|
424
|
-
if (Array.isArray(c))
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
495
|
+
if (Array.isArray(c))
|
|
496
|
+
return (
|
|
497
|
+
sum +
|
|
498
|
+
c.reduce((s: number, p: any) => {
|
|
499
|
+
if (p.text) return s + p.text.length;
|
|
500
|
+
if (p.type === "toolCall")
|
|
501
|
+
return (
|
|
502
|
+
s +
|
|
503
|
+
(p.name?.length ?? 0) +
|
|
504
|
+
(typeof p.input === "string"
|
|
505
|
+
? p.input.length
|
|
506
|
+
: JSON.stringify(p.input ?? "").length)
|
|
507
|
+
);
|
|
508
|
+
if (p.type === "toolResult")
|
|
509
|
+
return (
|
|
510
|
+
s +
|
|
511
|
+
(typeof p.content === "string"
|
|
512
|
+
? p.content.length
|
|
513
|
+
: JSON.stringify(p.content ?? "").length)
|
|
514
|
+
);
|
|
515
|
+
return s;
|
|
516
|
+
}, 0)
|
|
517
|
+
);
|
|
430
518
|
return sum;
|
|
431
519
|
}, 0);
|
|
432
|
-
const totalUserTurns = (branchEntries as any[]).filter(
|
|
520
|
+
const totalUserTurns = (branchEntries as any[]).filter(
|
|
521
|
+
(e: any) => e.type === "message" && e.message?.role === "user",
|
|
522
|
+
).length;
|
|
433
523
|
const keptUserTurns = ownCut.compactAll
|
|
434
524
|
? 0
|
|
435
|
-
: (branchEntries as any[])
|
|
525
|
+
: (branchEntries as any[])
|
|
526
|
+
.slice(keptIdx)
|
|
527
|
+
.filter(
|
|
528
|
+
(e: any) => e.type === "message" && e.message?.role === "user",
|
|
529
|
+
).length;
|
|
436
530
|
omRuntime.compactionStats = {
|
|
437
531
|
summarized: agentMessages.length,
|
|
438
532
|
kept: keptEntries.length,
|
|
@@ -452,26 +546,44 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
452
546
|
previousSummary: preparation.previousSummary,
|
|
453
547
|
fileOps: {
|
|
454
548
|
readFiles: [...preparation.fileOps.read],
|
|
455
|
-
modifiedFiles: [
|
|
549
|
+
modifiedFiles: [
|
|
550
|
+
...preparation.fileOps.written,
|
|
551
|
+
...preparation.fileOps.edited,
|
|
552
|
+
],
|
|
456
553
|
},
|
|
457
554
|
});
|
|
458
555
|
|
|
459
556
|
const branchIds = branchEntries.map((e: any) => e.id);
|
|
460
557
|
const cutIdx = branchIds.indexOf(firstKeptEntryId);
|
|
461
|
-
const cutWindow =
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
558
|
+
const cutWindow =
|
|
559
|
+
cutIdx >= 0
|
|
560
|
+
? branchEntries
|
|
561
|
+
.slice(
|
|
562
|
+
Math.max(0, cutIdx - 3),
|
|
563
|
+
Math.min(branchEntries.length, cutIdx + 3),
|
|
564
|
+
)
|
|
565
|
+
.map((e: any) => ({
|
|
566
|
+
id: e.id,
|
|
567
|
+
type: e.type,
|
|
568
|
+
role: e.type === "message" ? e.message?.role : undefined,
|
|
569
|
+
preview:
|
|
570
|
+
e.type === "message"
|
|
571
|
+
? previewContent(e.message?.content)
|
|
572
|
+
: undefined,
|
|
573
|
+
}))
|
|
574
|
+
: [];
|
|
469
575
|
|
|
470
576
|
dbg(omRuntime.config.debug, {
|
|
471
577
|
usedOwnCut: true,
|
|
472
578
|
messagesToSummarize: agentMessages.length,
|
|
473
|
-
messagesPreviewHead: agentMessages.slice(0, 3).map((m: any) => ({
|
|
474
|
-
|
|
579
|
+
messagesPreviewHead: agentMessages.slice(0, 3).map((m: any) => ({
|
|
580
|
+
role: m.role,
|
|
581
|
+
preview: previewContent(m.content),
|
|
582
|
+
})),
|
|
583
|
+
messagesPreviewTail: agentMessages.slice(-3).map((m: any) => ({
|
|
584
|
+
role: m.role,
|
|
585
|
+
preview: previewContent(m.content),
|
|
586
|
+
})),
|
|
475
587
|
convertedMessages: messages.length,
|
|
476
588
|
firstKeptEntryId,
|
|
477
589
|
cutWindow,
|
|
@@ -481,7 +593,10 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
481
593
|
sections: [...summary.matchAll(/^\[(.+?)\]/gm)].map((m) => m[1]),
|
|
482
594
|
});
|
|
483
595
|
|
|
484
|
-
trace("before_compact.summary_generated", {
|
|
596
|
+
trace("before_compact.summary_generated", {
|
|
597
|
+
summaryLength: summary.length,
|
|
598
|
+
messageCount: agentMessages.length,
|
|
599
|
+
});
|
|
485
600
|
|
|
486
601
|
const details: PiVccCompactionDetails = {
|
|
487
602
|
compactor: "blackhole",
|
|
@@ -496,17 +611,22 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
496
611
|
// ── Inject observational-memory content ───────────────────────────
|
|
497
612
|
let omContent: string;
|
|
498
613
|
let omDetails: Record<string, unknown> | undefined;
|
|
499
|
-
trace("before_compact.om_injection", {
|
|
614
|
+
trace("before_compact.om_injection", {
|
|
615
|
+
memoryEnabled: omRuntime.config.memory !== false,
|
|
616
|
+
});
|
|
500
617
|
if (omRuntime.config.memory !== false) {
|
|
501
618
|
const projection = buildCompactionProjection(
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
omContent = renderSummary(
|
|
619
|
+
branchEntries as any[],
|
|
620
|
+
firstKeptEntryId,
|
|
621
|
+
{
|
|
622
|
+
observationsPoolMaxTokens: omRuntime.config.observationsPoolMaxTokens,
|
|
623
|
+
fullFoldAlways: omRuntime.config.fullFoldAlways,
|
|
624
|
+
},
|
|
625
|
+
);
|
|
626
|
+
omContent = renderSummary(
|
|
627
|
+
projection.reflections,
|
|
628
|
+
projection.observations,
|
|
629
|
+
);
|
|
510
630
|
omDetails = projection.details;
|
|
511
631
|
} else {
|
|
512
632
|
omContent = renderSummary([], []);
|
|
@@ -533,7 +653,9 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
533
653
|
setTimeout(() => {
|
|
534
654
|
try {
|
|
535
655
|
ctx?.ui?.notify?.(formatCompactionStats(stats), "info");
|
|
536
|
-
notifyMigrationReminder(sessionId, (msg, level) =>
|
|
656
|
+
notifyMigrationReminder(sessionId, (msg, level) =>
|
|
657
|
+
ctx?.ui?.notify?.(msg, level as any),
|
|
658
|
+
);
|
|
537
659
|
} catch {}
|
|
538
660
|
}, 500);
|
|
539
661
|
});
|