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.
Files changed (87) hide show
  1. package/README.md +15 -0
  2. package/dist/index.js +11660 -0
  3. package/dist/index.js.map +1 -0
  4. package/example-config.json +1 -1
  5. package/index.ts +37 -63
  6. package/package.json +21 -9
  7. package/src/commands/cleanup.ts +279 -240
  8. package/src/commands/memory.ts +236 -184
  9. package/src/commands/pi-vcc.ts +202 -152
  10. package/src/commands/vcc-recall.ts +126 -95
  11. package/src/core/brief.ts +167 -33
  12. package/src/core/build-sections.ts +8 -2
  13. package/src/core/config-env.ts +117 -0
  14. package/src/core/content.ts +31 -7
  15. package/src/core/drill-down.ts +41 -11
  16. package/src/core/filter-noise.ts +9 -3
  17. package/src/core/format-recall.ts +15 -6
  18. package/src/core/format.ts +14 -4
  19. package/src/core/lineage.ts +9 -3
  20. package/src/core/load-messages.ts +24 -5
  21. package/src/core/normalize.ts +38 -14
  22. package/src/core/recall-scope.ts +11 -3
  23. package/src/core/render-entries.ts +22 -6
  24. package/src/core/sanitize.ts +5 -1
  25. package/src/core/search-entries.ts +111 -19
  26. package/src/core/settings.ts +1 -3
  27. package/src/core/summarize.ts +42 -21
  28. package/src/core/unified-config.ts +549 -411
  29. package/src/extract/commits.ts +4 -2
  30. package/src/extract/files.ts +10 -5
  31. package/src/extract/goals.ts +7 -2
  32. package/src/hooks/before-compact.ts +210 -88
  33. package/src/om/agents/dropper/agent.ts +380 -265
  34. package/src/om/agents/dropper/coverage.ts +102 -82
  35. package/src/om/agents/observer/agent.ts +242 -206
  36. package/src/om/agents/reflector/agent.ts +212 -153
  37. package/src/om/cleanup.ts +239 -218
  38. package/src/om/clipboard.ts +59 -51
  39. package/src/om/compaction-trigger.ts +448 -333
  40. package/src/om/config.ts +13 -6
  41. package/src/om/configure-overlay.ts +518 -355
  42. package/src/om/consolidation.ts +1460 -953
  43. package/src/om/cooldown.ts +75 -65
  44. package/src/om/debug-log.ts +86 -68
  45. package/src/om/ids.ts +1 -1
  46. package/src/om/ledger/fold.ts +89 -78
  47. package/src/om/ledger/progress.ts +181 -153
  48. package/src/om/ledger/projection.ts +248 -185
  49. package/src/om/ledger/recall.ts +247 -196
  50. package/src/om/ledger/render-summary.ts +79 -50
  51. package/src/om/ledger/types.ts +146 -117
  52. package/src/om/model-budget.ts +23 -13
  53. package/src/om/pending.ts +243 -179
  54. package/src/om/provider-stream.ts +52 -7
  55. package/src/om/retryable-error.ts +12 -16
  56. package/src/om/reverse-recall.ts +97 -91
  57. package/src/om/runtime.ts +474 -375
  58. package/src/om/serialize.ts +190 -166
  59. package/src/om/status-overlay.ts +246 -195
  60. package/src/om/tokens.ts +28 -21
  61. package/src/pi-base/blackhole-settings.ts +437 -0
  62. package/src/pi-base/config-manager.ts +440 -0
  63. package/src/pi-base/config.ts +469 -0
  64. package/src/pi-base/env.ts +43 -0
  65. package/src/pi-base/paths.ts +47 -0
  66. package/src/pi-base/settings/body.ts +1648 -0
  67. package/src/pi-base/settings/fields/action.ts +43 -0
  68. package/src/pi-base/settings/fields/boolean.ts +47 -0
  69. package/src/pi-base/settings/fields/custom.ts +72 -0
  70. package/src/pi-base/settings/fields/enum.ts +310 -0
  71. package/src/pi-base/settings/fields/index.ts +46 -0
  72. package/src/pi-base/settings/fields/model.ts +452 -0
  73. package/src/pi-base/settings/fields/string.ts +527 -0
  74. package/src/pi-base/settings/fields/text.ts +115 -0
  75. package/src/pi-base/settings/frame.ts +197 -0
  76. package/src/pi-base/settings/index.ts +77 -0
  77. package/src/pi-base/settings/inline-edit.ts +313 -0
  78. package/src/pi-base/settings/modal.ts +152 -0
  79. package/src/pi-base/settings/types.ts +500 -0
  80. package/src/pi-base/settings/validate-field.ts +113 -0
  81. package/src/pi-base/shell.ts +117 -0
  82. package/src/pi-base/types.ts +6 -0
  83. package/src/pi-base/ui.ts +32 -0
  84. package/src/tools/recall.ts +347 -225
  85. package/src/types.ts +20 -3
  86. package/tsup.config.ts +23 -0
  87. package/vitest.config.ts +15 -15
@@ -5,7 +5,8 @@ interface CommitInfo {
5
5
  message: string;
6
6
  }
7
7
 
8
- const COMMIT_MSG_RE = /git\s+commit[^\n]*?-m\s+(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'|\$?'((?:[^'\\]|\\.)*)')/;
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 = (b.args && typeof b.args.command === "string") ? b.args.command : "";
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
 
@@ -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", "Write", "edit", "write", "edit_file", "write_file",
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((p) => p.startsWith("/") || /^[A-Za-z]:\//.test(p));
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));
@@ -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 = /^(ok|yes|no|sure|yeah|yep|go|hi|hey|thx|thanks|ok\b.*|y|n|k)\s*[.!?]*$/i;
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(...lines.slice(0, 6).map((l) => clip(l, FIRST_MSG_CLIP) + indexSuffix(b.sourceIndex)));
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 { buildCompactionProjection, renderSummary } from "../om/ledger/index.js";
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
- sessionId: string,
33
- notify: (msg: string, level: string) => void,
35
+ sessionId: string,
36
+ notify: (msg: string, level: string) => void,
34
37
  ): void {
35
- const count = migrationNotifyCount.get(sessionId) ?? 0;
36
- if (count >= 2) return;
37
- if (!configFileNeedsMigration()) return;
38
- migrationNotifyCount.set(sessionId, count + 1);
39
- notify(
40
- "blackhole: Use `/blackhole configure` to save your updated configuration.",
41
- "info",
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(`tail kept ${stats.keptUserTurns}/${stats.totalUserTurns} user turns`);
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 { writeFileSync("/tmp/pi-blackhole-debug.json", JSON.stringify(data, null, 2)); } catch {}
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 = !!lastKeptId && branchEntries.some((e: any) => e.id === lastKeptId);
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((e: any) => e.id === piFirstKeptEntryId);
174
+ const cutInBranch = branchEntries.findIndex(
175
+ (e: any) => e.id === piFirstKeptEntryId,
176
+ );
167
177
  if (cutInBranch >= 0) {
168
- const liveCutIdx = liveMessages.findIndex((lm) => lm.entry.id === piFirstKeptEntryId);
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 (lastUserIdx > 0 && liveMessages[lastUserIdx].message.role !== "user") {
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) => i > cutInBranch && e.type === "message" && e.message,
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.slice(0, resolvedLiveIdx).map((e) => e.message),
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 (lastUserIdx > 0 && liveMessages[lastUserIdx].message.role !== "user") {
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) return { ok: false, reason: "no_live_messages" };
233
- if (liveMessages.length <= 2) return { ok: false, reason: "too_few_live_messages" };
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: "blackhole: Too few live messages — Pi's default logic preserves visible context. Set tailBehavior to \"minimal\" in config to force compaction with fewer 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 = (pi: ExtensionAPI, omRuntime: Runtime) => {
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) => ctx.ui?.notify?.(msg, "warning"));
275
- const trace = (ev: string, d?: Record<string, unknown>) => debugLog(ev, d, omRuntime.config.debugLog === true);
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
- noAutoCompact: omRuntime.config.noAutoCompact,
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", { reason: "compactionEngine_pi_default" });
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 (omRuntime.config.compaction === undefined && omRuntime.config.compactionEngine === undefined) {
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", { reason: "overrideDefaultCompaction=false and not /blackhole" });
349
+ trace("before_compact.return_early", {
350
+ reason: "overrideDefaultCompaction=false and not /blackhole",
351
+ });
313
352
  return;
314
353
  }
315
354
 
316
- if (omRuntime.config.noAutoCompact && !isPiVcc) {
317
- trace("before_compact.cancel", { reason: "noAutoCompact and not /blackhole" });
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].reverse().find((e: any) => e.type === "compaction");
341
- const lastCompIdx = lastComp ? (branchEntries as any[]).indexOf(lastComp) : -1;
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)?.firstKeptEntryId;
393
+ const lastKeptId: string | undefined = (lastComp as any)
394
+ ?.firstKeptEntryId;
345
395
  const hasPriorCompaction = lastCompIdx >= 0;
346
- const hasValidKeptId = !!lastKeptId && (branchEntries as any[]).some((e: any) => e.id === lastKeptId);
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[]>((acc, r, i) => (r === "user" ? (acc.push(i), acc) : acc), []);
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((e: any) => e.type === "message").length,
373
- compactions: (branchEntries as any[]).filter((e: any) => e.type === "compaction").length,
374
- entriesAfterLastCompaction: lastCompIdx >= 0 ? branchEntries.length - lastCompIdx - 1 : null,
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: liveRoles.length <= 30
382
- ? liveRoles
383
- : [...liveRoles.slice(0, 10), "...", ...liveRoles.slice(-10)],
441
+ roleSequence:
442
+ liveRoles.length <= 30
443
+ ? liveRoles
444
+ : [...liveRoles.slice(0, 10), "...", ...liveRoles.slice(-10)],
384
445
  },
385
- lastCompaction: lastComp ? {
386
- hasFirstKeptEntryId: !!(lastComp as any).firstKeptEntryId,
387
- foundInBranch: (lastComp as any).firstKeptEntryId
388
- ? (branchEntries as any[]).some((e: any) => e.id === (lastComp as any).firstKeptEntryId)
389
- : null,
390
- } : null,
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: e.type === "message" ? e.message?.content != null : undefined,
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((e: any) => e.id === firstKeptEntryId);
418
- const keptEntries = keptIdx >= 0
419
- ? (branchEntries as any[]).slice(keptIdx).filter((e: any) => e.type === "message")
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)) return sum + c.reduce((s: number, p: any) => {
425
- if (p.text) return s + p.text.length;
426
- if (p.type === "toolCall") return s + (p.name?.length ?? 0) + (typeof p.input === "string" ? p.input.length : JSON.stringify(p.input ?? "").length);
427
- if (p.type === "toolResult") return s + (typeof p.content === "string" ? p.content.length : JSON.stringify(p.content ?? "").length);
428
- return s;
429
- }, 0);
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((e: any) => e.type === "message" && e.message?.role === "user").length;
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[]).slice(keptIdx).filter((e: any) => e.type === "message" && e.message?.role === "user").length;
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: [...preparation.fileOps.written, ...preparation.fileOps.edited],
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 = cutIdx >= 0
462
- ? branchEntries.slice(Math.max(0, cutIdx - 3), Math.min(branchEntries.length, cutIdx + 3)).map((e: any) => ({
463
- id: e.id,
464
- type: e.type,
465
- role: e.type === "message" ? e.message?.role : undefined,
466
- preview: e.type === "message" ? previewContent(e.message?.content) : undefined,
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) => ({ role: m.role, preview: previewContent(m.content) })),
474
- messagesPreviewTail: agentMessages.slice(-3).map((m: any) => ({ role: m.role, preview: previewContent(m.content) })),
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", { summaryLength: summary.length, messageCount: agentMessages.length });
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", { memoryEnabled: omRuntime.config.memory !== false });
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
- branchEntries as any[],
503
- firstKeptEntryId,
504
- {
505
- observationsPoolMaxTokens: omRuntime.config.observationsPoolMaxTokens,
506
- fullFoldAlways: omRuntime.config.fullFoldAlways,
507
- },
508
- );
509
- omContent = renderSummary(projection.reflections, projection.observations);
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) => ctx?.ui?.notify?.(msg, level as any));
656
+ notifyMigrationReminder(sessionId, (msg, level) =>
657
+ ctx?.ui?.notify?.(msg, level as any),
658
+ );
537
659
  } catch {}
538
660
  }, 500);
539
661
  });