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
@@ -3,171 +3,221 @@
3
3
  *
4
4
  * Upstream: https://github.com/sting8k/pi-vcc (src/commands/pi-vcc.ts)
5
5
  * Modified by pi-vcc-om:
6
- * - Flushes pending OM state (observations/reflections/dropped) when noAutoCompact is active
6
+ * - Flushes pending OM state (observations/reflections/dropped) when manual mode is active
7
7
  * before triggering compaction, so the compaction summary includes all accumulated memory.
8
8
  */
9
9
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
10
  import type { Runtime } from "../om/runtime.js";
11
- import { PI_VCC_COMPACT_INSTRUCTION, notifyMigrationReminder, formatCompactionStats } from "../hooks/before-compact";
12
- import { saveUnifiedConfig, configPath } from "../core/unified-config.js";
13
- import { readPendingState, clearPendingState, hasPendingData } from "../om/pending.js";
14
- import { createConfigureOverlay } from "../om/configure-overlay.js";
15
11
  import {
16
- OM_OBSERVATIONS_DROPPED,
17
- OM_OBSERVATIONS_RECORDED,
18
- OM_REFLECTIONS_RECORDED,
12
+ PI_VCC_COMPACT_INSTRUCTION,
13
+ notifyMigrationReminder,
14
+ formatCompactionStats,
15
+ } from "../hooks/before-compact";
16
+ import {
17
+ readPendingState,
18
+ clearPendingState,
19
+ hasPendingData,
20
+ } from "../om/pending.js";
21
+ import {
22
+ OM_OBSERVATIONS_DROPPED,
23
+ OM_OBSERVATIONS_RECORDED,
24
+ OM_REFLECTIONS_RECORDED,
19
25
  } from "../om/ledger/index.js";
20
26
  import { handleCleanup } from "./cleanup.js";
27
+ import {
28
+ openBlackholeSettings,
29
+ config,
30
+ GLOBAL_CONFIG_DIR,
31
+ } from "../pi-base/blackhole-settings.js";
21
32
 
22
33
  export const registerPiVccCommand = (pi: ExtensionAPI, runtime: Runtime) => {
23
- const prefixMatch = (value: string, prefix: string): boolean => {
24
- return value.toLowerCase().startsWith(prefix.toLowerCase());
25
- };
26
-
27
- pi.registerCommand("blackhole", {
28
- description:
29
- "Compact conversation — structured summary (with observational memory when enabled). " +
30
- "Subcommands: [configure] settings overlay, [cleanup] remove orphaned files, " +
31
- "[om-off] / [om-on] disable / re-enable memory.",
32
- getArgumentCompletions: (prefix: string) => {
33
- const subcommands = [
34
- { value: "configure", label: "Open configuration overlay to edit settings [configure]" },
35
- { value: "cleanup", label: "Find and remove orphaned pending files [cleanup]" },
36
- { value: "om-off", label: "Disable observational memory [om-off]" },
37
- { value: "om-on", label: "Enable observational memory [om-on]" },
38
- ];
39
- if (!prefix) return subcommands;
40
- return subcommands.filter((s) => prefixMatch(s.value, prefix));
41
- },
42
- handler: async (args, ctx) => {
43
- const sessionId = ctx.sessionManager.getSessionId();
34
+ const prefixMatch = (value: string, prefix: string): boolean => {
35
+ return value.toLowerCase().startsWith(prefix.toLowerCase());
36
+ };
44
37
 
45
- // Handle subcommands
46
- const trimmed = (typeof args === "string" ? args : "").trim();
47
- if (trimmed === "configure") {
48
- // Open the config overlay
49
- const result = await ctx.ui.custom<{ saved: boolean; path: string; error?: string } | undefined>(
50
- (tui, theme, _kb, done) => createConfigureOverlay(configPath(), theme, tui, done),
51
- { overlay: true },
52
- );
53
- if (result) {
54
- if (result.error) {
55
- ctx.ui.notify(result.error, "warning");
56
- } else if (result.saved) {
57
- runtime.reloadConfig(ctx.cwd, (msg) => ctx.ui?.notify?.(msg, "warning"));
58
- ctx.ui.notify("Configuration saved.", "info");
59
- } else {
60
- ctx.ui.notify("Failed to save configuration the config file may be read-only (e.g., managed by Nix).", "warning");
61
- }
62
- }
63
- return;
64
- }
65
- if (trimmed === "cleanup") {
66
- await handleCleanup(ctx);
67
- return;
68
- }
69
- if (trimmed === "om-off") {
70
- const saved = saveUnifiedConfig({ memory: false });
71
- runtime.config.memory = false;
72
- if (saved) {
73
- ctx.ui.notify("Observational memory disabled. Use /blackhole om-on to re-enable.", "info");
74
- } else {
75
- ctx.ui.notify(
76
- "Failed to save config — the config file may be read-only (e.g., managed by Nix). " +
77
- "Runtime state updated for this session only.",
78
- "warning",
79
- );
80
- }
81
- return;
82
- }
83
- if (trimmed === "om-on") {
84
- const saved = saveUnifiedConfig({ memory: true });
85
- runtime.config.memory = true;
86
- if (saved) {
87
- ctx.ui.notify("Observational memory enabled.", "info");
88
- } else {
89
- ctx.ui.notify(
90
- "Failed to save config — the config file may be read-only (e.g., managed by Nix). " +
91
- "Runtime state updated for this session only.",
92
- "warning",
93
- );
94
- }
95
- return;
96
- }
38
+ pi.registerCommand("blackhole", {
39
+ description:
40
+ "Manual compact with structural summary. Subcommands: [settings] config overlay, " +
41
+ "[cleanup] remove orphaned files, [om-off]/[om-on] disable/enable observational memory.",
42
+ getArgumentCompletions: (prefix: string) => {
43
+ const subcommands = [
44
+ {
45
+ value: "settings",
46
+ label: "Open configuration overlay [settings]",
47
+ },
48
+ {
49
+ value: "cleanup",
50
+ label: "Remove orphaned pending files [cleanup]",
51
+ },
52
+ { value: "om-off", label: "Disable observational memory [om-off]" },
53
+ { value: "om-on", label: "Enable observational memory [om-on]" },
54
+ ];
55
+ if (!prefix) return subcommands;
56
+ // "configure" is an accepted alias for "settings" (routed by the
57
+ // handler); surface the settings entry when the user types either.
58
+ return subcommands.filter(
59
+ (s) =>
60
+ prefixMatch(s.value, prefix) ||
61
+ (s.value === "settings" && prefixMatch("configure", prefix)),
62
+ );
63
+ },
64
+ handler: async (args, ctx) => {
65
+ const sessionId = ctx.sessionManager.getSessionId();
97
66
 
98
- // Warn if input starts with a known subcommand but isn't an exact match.
99
- // Prevents "/blackhole configure foo" from silently becoming a follow-up.
100
- const SUBCOMMAND_NAMES = ["configure", "cleanup", "om-off", "om-on"];
101
- const nearMiss = SUBCOMMAND_NAMES.find(
102
- name => trimmed.toLowerCase().startsWith(name.toLowerCase()) && trimmed.length > name.length
103
- );
104
- if (nearMiss) {
105
- ctx.ui.notify(`/blackhole ${nearMiss} accepts no arguments. Did you mean \"/blackhole ${nearMiss}\"?`, "warning");
106
- return;
107
- }
67
+ // Handle subcommands
68
+ const trimmed = (typeof args === "string" ? args : "").trim();
69
+ if (trimmed === "configure" || trimmed === "settings") {
70
+ // Open the config overlay ("configure" kept as a hidden alias)
71
+ await openBlackholeSettings(ctx);
72
+ return;
73
+ }
74
+ if (trimmed === "cleanup") {
75
+ await handleCleanup(ctx);
76
+ return;
77
+ }
78
+ if (trimmed === "om-off") {
79
+ try {
80
+ config.save(
81
+ { ...config.load(ctx.cwd, GLOBAL_CONFIG_DIR), memory: false },
82
+ "global",
83
+ ctx.cwd,
84
+ GLOBAL_CONFIG_DIR,
85
+ );
86
+ runtime.config = config.loadWithWarnings(
87
+ ctx.cwd,
88
+ GLOBAL_CONFIG_DIR,
89
+ ).config;
90
+ ctx.ui.notify(
91
+ "Observational memory disabled. Use /blackhole om-on to re-enable.",
92
+ "info",
93
+ );
94
+ } catch {
95
+ ctx.ui.notify(
96
+ "Failed to save config — the config file may be read-only (e.g., managed by Nix). " +
97
+ "Runtime state updated for this session only.",
98
+ "warning",
99
+ );
100
+ }
101
+ return;
102
+ }
103
+ if (trimmed === "om-on") {
104
+ try {
105
+ config.save(
106
+ { ...config.load(ctx.cwd, GLOBAL_CONFIG_DIR), memory: true },
107
+ "global",
108
+ ctx.cwd,
109
+ GLOBAL_CONFIG_DIR,
110
+ );
111
+ runtime.config = config.loadWithWarnings(
112
+ ctx.cwd,
113
+ GLOBAL_CONFIG_DIR,
114
+ ).config;
115
+ ctx.ui.notify("Observational memory enabled.", "info");
116
+ } catch {
117
+ ctx.ui.notify(
118
+ "Failed to save config — the config file may be read-only (e.g., managed by Nix). " +
119
+ "Runtime state updated for this session only.",
120
+ "warning",
121
+ );
122
+ }
123
+ return;
124
+ } // Warn if input starts with a known subcommand but isn't an exact match.
125
+ // Prevents "/blackhole configure foo" from silently becoming a follow-up.
126
+ const SUBCOMMAND_NAMES = [
127
+ "configure",
128
+ "settings",
129
+ "cleanup",
130
+ "om-off",
131
+ "om-on",
132
+ ];
133
+ const nearMiss = SUBCOMMAND_NAMES.find(
134
+ (name) =>
135
+ trimmed.toLowerCase().startsWith(name.toLowerCase()) &&
136
+ trimmed.length > name.length,
137
+ );
138
+ if (nearMiss) {
139
+ ctx.ui.notify(
140
+ `/blackhole ${nearMiss} accepts no arguments. Did you mean \"/blackhole ${nearMiss}\"?`,
141
+ "warning",
142
+ );
143
+ return;
144
+ }
108
145
 
109
- // Extract follow-up prompt: everything after the subcommand check
110
- // that isn't a known subcommand is treated as follow-up text.
111
- const followUpPrompt = trimmed ? trimmed : null;
146
+ // Extract follow-up prompt: everything after the subcommand check
147
+ // that isn't a known subcommand is treated as follow-up text.
148
+ const followUpPrompt = trimmed ? trimmed : null;
112
149
 
113
- // If compaction is manual (or legacy noAutoCompact): flush pending OM entries
114
- // into the branch before compacting so the summary includes accumulated memory.
115
- if (runtime.config.compaction === "manual" && hasPendingData(sessionId)) {
116
- const pending = readPendingState(sessionId);
117
- // Write all accumulated observation batches (or latest single batch
118
- // as fallback for legacy pending.json without batch arrays).
119
- const obsBatches = pending.observationBatches?.length
120
- ? pending.observationBatches
121
- : (pending.observation ? [pending.observation] : []);
122
- for (const batch of obsBatches) {
123
- pi.appendEntry(OM_OBSERVATIONS_RECORDED, batch.data);
124
- }
125
- // Write all accumulated reflection batches (or latest single batch
126
- // as fallback for legacy pending.json without batch arrays).
127
- const reflBatches = pending.reflectionBatches?.length
128
- ? pending.reflectionBatches
129
- : (pending.reflection ? [pending.reflection] : []);
130
- for (const batch of reflBatches) {
131
- pi.appendEntry(OM_REFLECTIONS_RECORDED, batch.data);
132
- }
133
- // Write all accumulated dropper batches (or latest single batch
134
- // as fallback for legacy pending.json without batch arrays).
135
- const dropBatches = pending.droppedBatches?.length
136
- ? pending.droppedBatches
137
- : (pending.dropped ? [pending.dropped] : []);
138
- for (const batch of dropBatches) {
139
- pi.appendEntry(OM_OBSERVATIONS_DROPPED, batch.data);
140
- }
141
- clearPendingState(sessionId);
142
- ctx.ui.notify("Observational memory: pending entries flushed", "info");
143
- }
150
+ // If compaction is manual (or legacy noAutoCompact): flush pending OM entries
151
+ // into the branch before compacting so the summary includes accumulated memory.
152
+ if (runtime.config.compaction === "manual" && hasPendingData(sessionId)) {
153
+ const pending = readPendingState(sessionId);
154
+ // Write all accumulated observation batches (or latest single batch
155
+ // as fallback for legacy pending.json without batch arrays).
156
+ const obsBatches = pending.observationBatches?.length
157
+ ? pending.observationBatches
158
+ : pending.observation
159
+ ? [pending.observation]
160
+ : [];
161
+ for (const batch of obsBatches) {
162
+ pi.appendEntry(OM_OBSERVATIONS_RECORDED, batch.data);
163
+ }
164
+ // Write all accumulated reflection batches (or latest single batch
165
+ // as fallback for legacy pending.json without batch arrays).
166
+ const reflBatches = pending.reflectionBatches?.length
167
+ ? pending.reflectionBatches
168
+ : pending.reflection
169
+ ? [pending.reflection]
170
+ : [];
171
+ for (const batch of reflBatches) {
172
+ pi.appendEntry(OM_REFLECTIONS_RECORDED, batch.data);
173
+ }
174
+ // Write all accumulated dropper batches (or latest single batch
175
+ // as fallback for legacy pending.json without batch arrays).
176
+ const dropBatches = pending.droppedBatches?.length
177
+ ? pending.droppedBatches
178
+ : pending.dropped
179
+ ? [pending.dropped]
180
+ : [];
181
+ for (const batch of dropBatches) {
182
+ pi.appendEntry(OM_OBSERVATIONS_DROPPED, batch.data);
183
+ }
184
+ clearPendingState(sessionId);
185
+ ctx.ui.notify("Observational memory: pending entries flushed", "info");
186
+ }
144
187
 
145
- ctx.compact({
146
- customInstructions: PI_VCC_COMPACT_INSTRUCTION,
147
- onComplete: () => {
148
- const stats = runtime.compactionStats;
149
- if (stats) {
150
- ctx.ui.notify(formatCompactionStats(stats), "info");
151
- } else {
152
- ctx.ui.notify("Compacted with blackhole", "info");
153
- }
154
- notifyMigrationReminder(sessionId, (msg, level) => ctx.ui.notify(msg, level as any));
188
+ ctx.compact({
189
+ customInstructions: PI_VCC_COMPACT_INSTRUCTION,
190
+ onComplete: () => {
191
+ const stats = runtime.compactionStats;
192
+ if (stats) {
193
+ ctx.ui.notify(formatCompactionStats(stats), "info");
194
+ } else {
195
+ ctx.ui.notify("Compacted with blackhole", "info");
196
+ }
197
+ notifyMigrationReminder(sessionId, (msg, level) =>
198
+ ctx.ui.notify(msg, level as any),
199
+ );
155
200
 
156
- // Fire follow-up prompt after compaction completes
157
- if (followUpPrompt) {
158
- try {
159
- void Promise.resolve(pi.sendUserMessage(followUpPrompt)).catch(() => {});
160
- } catch {}
161
- }
162
- },
163
- onError: (err) => {
164
- if (err.message === "Compaction cancelled" || err.message === "Already compacted") {
165
- ctx.ui.notify("Nothing to compact", "warning");
166
- } else {
167
- ctx.ui.notify(`Compaction failed: ${err.message}`, "error");
168
- }
169
- },
170
- });
171
- },
172
- });
201
+ // Fire follow-up prompt after compaction completes
202
+ if (followUpPrompt) {
203
+ try {
204
+ void Promise.resolve(pi.sendUserMessage(followUpPrompt)).catch(
205
+ () => {},
206
+ );
207
+ } catch {}
208
+ }
209
+ },
210
+ onError: (err) => {
211
+ if (
212
+ err.message === "Compaction cancelled" ||
213
+ err.message === "Already compacted"
214
+ ) {
215
+ ctx.ui.notify("Nothing to compact", "warning");
216
+ } else {
217
+ ctx.ui.notify(`Compaction failed: ${err.message}`, "error");
218
+ }
219
+ },
220
+ });
221
+ },
222
+ });
173
223
  };
@@ -7,13 +7,16 @@
7
7
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
8
  import { loadAllMessages } from "../core/load-messages.js";
9
9
  import { searchEntries, getTouchedFiles } from "../core/search-entries.js";
10
- import { formatRecallOutput, formatTouchedOutput } from "../core/format-recall.js";
10
+ import {
11
+ formatRecallOutput,
12
+ formatTouchedOutput,
13
+ } from "../core/format-recall.js";
11
14
  import { getActiveLineageEntryIds } from "../core/lineage.js";
12
15
  import { parseRecallScope } from "../core/recall-scope.js";
13
16
  import {
14
- findObservationsForEntryIds,
15
- findReflectionsForEntryIds,
16
- formatRelatedObservations,
17
+ findObservationsForEntryIds,
18
+ findReflectionsForEntryIds,
19
+ formatRelatedObservations,
17
20
  } from "../om/reverse-recall.js";
18
21
  import type { Entry } from "../om/ledger/recall.js";
19
22
 
@@ -21,106 +24,134 @@ const PAGE_SIZE = 5;
21
24
  const DEFAULT_RECENT = 25;
22
25
 
23
26
  async function augmentWithObservations(
24
- output: string,
25
- rendered: { id: string }[],
26
- ctx: any,
27
+ output: string,
28
+ rendered: { id: string }[],
29
+ ctx: any,
27
30
  ): Promise<string> {
28
- const ids = rendered.map((e) => e.id).filter(Boolean);
29
- if (ids.length === 0) return output;
30
- try {
31
- const branchEntries = ctx.sessionManager.getBranch() as Entry[];
32
- const obs = findObservationsForEntryIds(branchEntries, ids);
33
- const refs = findReflectionsForEntryIds(branchEntries, ids);
34
- if (obs.length > 0 || refs.length > 0) {
35
- return output + "\n\n" + formatRelatedObservations(obs, refs);
36
- }
37
- } catch { /* branch may not be available */ }
38
- return output;
31
+ const ids = rendered.map((e) => e.id).filter(Boolean);
32
+ if (ids.length === 0) return output;
33
+ try {
34
+ const branchEntries = ctx.sessionManager.getBranch() as Entry[];
35
+ const obs = findObservationsForEntryIds(branchEntries, ids);
36
+ const refs = findReflectionsForEntryIds(branchEntries, ids);
37
+ if (obs.length > 0 || refs.length > 0) {
38
+ return output + "\n\n" + formatRelatedObservations(obs, refs);
39
+ }
40
+ } catch {
41
+ /* branch may not be available */
42
+ }
43
+ return output;
39
44
  }
40
45
 
41
46
  export const registerVccRecallCommand = (pi: ExtensionAPI) => {
42
- pi.registerCommand("blackhole-recall", {
43
- description:
44
- "Search session history. Defaults to active lineage. Usage: /blackhole-recall <query> [page:N] [scope:all] [mode:file|touched]",
45
- handler: async (args: string, ctx) => {
46
- const sessionFile = ctx.sessionManager.getSessionFile();
47
- if (!sessionFile) {
48
- ctx.ui.notify("No session file available.", "error");
49
- return;
50
- }
47
+ pi.registerCommand("blackhole-recall", {
48
+ description:
49
+ "Search session history. Defaults to active lineage. Usage: /blackhole-recall <query> [page:N] [scope:all] [mode:file|touched]",
50
+ handler: async (args: string, ctx) => {
51
+ const sessionFile = ctx.sessionManager.getSessionFile();
52
+ if (!sessionFile) {
53
+ ctx.ui.notify("No session file available.", "error");
54
+ return;
55
+ }
51
56
 
52
- const raw = args.trim();
53
- const parsed = parseRecallScope(raw);
54
- const lineageEntryIds =
55
- parsed.scope === "lineage"
56
- ? getActiveLineageEntryIds(ctx.sessionManager)
57
- : undefined;
58
- const mode = parsed.mode;
57
+ const raw = args.trim();
58
+ const parsed = parseRecallScope(raw);
59
+ const lineageEntryIds =
60
+ parsed.scope === "lineage"
61
+ ? getActiveLineageEntryIds(ctx.sessionManager)
62
+ : undefined;
63
+ const mode = parsed.mode;
59
64
 
60
- if (mode === "touched") {
61
- const pageMatch = raw.match(/\bpage:(\d+)\b/i);
62
- const page = pageMatch ? Math.max(1, parseInt(pageMatch[1], 10)) : 1;
63
- const { rendered, rawMessages } = loadAllMessages(sessionFile, false, lineageEntryIds);
64
- const touched = getTouchedFiles(rawMessages, rendered);
65
- const text = formatTouchedOutput(touched, page);
66
- pi.sendMessage(
67
- { customType: "blackhole-recall", content: text, display: true },
68
- { triggerTurn: true },
69
- );
70
- return;
71
- }
65
+ if (mode === "touched") {
66
+ const pageMatch = raw.match(/\bpage:(\d+)\b/i);
67
+ const page = pageMatch ? Math.max(1, parseInt(pageMatch[1], 10)) : 1;
68
+ const { rendered, rawMessages } = loadAllMessages(
69
+ sessionFile,
70
+ false,
71
+ lineageEntryIds,
72
+ );
73
+ const touched = getTouchedFiles(rawMessages, rendered);
74
+ const text = formatTouchedOutput(touched, page);
75
+ pi.sendMessage(
76
+ { customType: "blackhole-recall", content: text, display: true },
77
+ { triggerTurn: true },
78
+ );
79
+ return;
80
+ }
72
81
 
73
- if (!parsed.text) {
74
- // No query: show recent entries
75
- const { rendered } = loadAllMessages(sessionFile, false, lineageEntryIds);
76
- const recent = rendered.slice(-DEFAULT_RECENT);
77
- const base = (parsed.scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(recent);
78
- const output = await augmentWithObservations(base, recent, ctx);
79
- pi.sendMessage(
80
- { customType: "blackhole-recall", content: output, display: true },
81
- { triggerTurn: true },
82
- );
83
- return;
84
- }
82
+ if (!parsed.text) {
83
+ // No query: show recent entries
84
+ const { rendered } = loadAllMessages(
85
+ sessionFile,
86
+ false,
87
+ lineageEntryIds,
88
+ );
89
+ const recent = rendered.slice(-DEFAULT_RECENT);
90
+ const base =
91
+ (parsed.scope === "all" ? "Scope: all\n\n" : "") +
92
+ formatRecallOutput(recent);
93
+ const output = await augmentWithObservations(base, recent, ctx);
94
+ pi.sendMessage(
95
+ { customType: "blackhole-recall", content: output, display: true },
96
+ { triggerTurn: true },
97
+ );
98
+ return;
99
+ }
85
100
 
86
- // Parse page:N from args
87
- const pageMatch = parsed.text.match(/\bpage:(\d+)\b/i);
88
- const page = pageMatch ? Math.max(1, parseInt(pageMatch[1], 10)) : 1;
89
- const query = parsed.text.replace(/\bpage:\d+\b/i, "").trim();
101
+ // Parse page:N from args
102
+ const pageMatch = parsed.text.match(/\bpage:(\d+)\b/i);
103
+ const page = pageMatch ? Math.max(1, parseInt(pageMatch[1], 10)) : 1;
104
+ const query = parsed.text.replace(/\bpage:\d+\b/i, "").trim();
90
105
 
91
- if (!query) {
92
- const { rendered } = loadAllMessages(sessionFile, false, lineageEntryIds);
93
- const recent = rendered.slice(-DEFAULT_RECENT);
94
- const base = (parsed.scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(recent);
95
- const output = await augmentWithObservations(base, recent, ctx);
96
- pi.sendMessage(
97
- { customType: "blackhole-recall", content: output, display: true },
98
- { triggerTurn: true },
99
- );
100
- return;
101
- }
106
+ if (!query) {
107
+ const { rendered } = loadAllMessages(
108
+ sessionFile,
109
+ false,
110
+ lineageEntryIds,
111
+ );
112
+ const recent = rendered.slice(-DEFAULT_RECENT);
113
+ const base =
114
+ (parsed.scope === "all" ? "Scope: all\n\n" : "") +
115
+ formatRecallOutput(recent);
116
+ const output = await augmentWithObservations(base, recent, ctx);
117
+ pi.sendMessage(
118
+ { customType: "blackhole-recall", content: output, display: true },
119
+ { triggerTurn: true },
120
+ );
121
+ return;
122
+ }
102
123
 
103
- const { rendered, rawMessages } = loadAllMessages(sessionFile, false, lineageEntryIds);
104
- const allResults = searchEntries(rendered, rawMessages, query, undefined, mode);
124
+ const { rendered, rawMessages } = loadAllMessages(
125
+ sessionFile,
126
+ false,
127
+ lineageEntryIds,
128
+ );
129
+ const allResults = searchEntries(
130
+ rendered,
131
+ rawMessages,
132
+ query,
133
+ undefined,
134
+ mode,
135
+ );
105
136
 
106
- const start = (page - 1) * PAGE_SIZE;
107
- const pageResults = allResults.slice(start, start + PAGE_SIZE);
108
- const totalPages = Math.ceil(allResults.length / PAGE_SIZE);
109
- const scopeSuffix = parsed.scope === "all" ? " (scope: all)" : "";
110
- const header =
111
- totalPages > 1
112
- ? `Page ${page}/${totalPages} (${allResults.length} total matches${scopeSuffix})`
113
- : `${allResults.length} matches${scopeSuffix}`;
114
- const footer =
115
- page < totalPages
116
- ? `\n--- /blackhole-recall ${query}${parsed.scope === "all" ? " scope:all" : ""} page:${page + 1} ---`
117
- : "";
118
- const base = formatRecallOutput(pageResults, query, header) + footer;
119
- const output = await augmentWithObservations(base, pageResults, ctx);
120
- pi.sendMessage(
121
- { customType: "blackhole-recall", content: output, display: true },
122
- { triggerTurn: true },
123
- );
124
- },
125
- });
137
+ const start = (page - 1) * PAGE_SIZE;
138
+ const pageResults = allResults.slice(start, start + PAGE_SIZE);
139
+ const totalPages = Math.ceil(allResults.length / PAGE_SIZE);
140
+ const scopeSuffix = parsed.scope === "all" ? " (scope: all)" : "";
141
+ const header =
142
+ totalPages > 1
143
+ ? `Page ${page}/${totalPages} (${allResults.length} total matches${scopeSuffix})`
144
+ : `${allResults.length} matches${scopeSuffix}`;
145
+ const footer =
146
+ page < totalPages
147
+ ? `\n--- /blackhole-recall ${query}${parsed.scope === "all" ? " scope:all" : ""} page:${page + 1} ---`
148
+ : "";
149
+ const base = formatRecallOutput(pageResults, query, header) + footer;
150
+ const output = await augmentWithObservations(base, pageResults, ctx);
151
+ pi.sendMessage(
152
+ { customType: "blackhole-recall", content: output, display: true },
153
+ { triggerTurn: true },
154
+ );
155
+ },
156
+ });
126
157
  };