pi-blackhole 0.2.0

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 (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +373 -0
  3. package/example-config.json +115 -0
  4. package/index.ts +39 -0
  5. package/package.json +55 -0
  6. package/src/commands/memory.ts +191 -0
  7. package/src/commands/pi-vcc.ts +94 -0
  8. package/src/commands/vcc-recall.ts +112 -0
  9. package/src/core/brief.ts +390 -0
  10. package/src/core/build-sections.ts +85 -0
  11. package/src/core/content.ts +60 -0
  12. package/src/core/filter-noise.ts +42 -0
  13. package/src/core/format-recall.ts +27 -0
  14. package/src/core/format.ts +76 -0
  15. package/src/core/lineage.ts +26 -0
  16. package/src/core/load-messages.ts +41 -0
  17. package/src/core/normalize.ts +79 -0
  18. package/src/core/recall-scope.ts +14 -0
  19. package/src/core/render-entries.ts +56 -0
  20. package/src/core/report.ts +237 -0
  21. package/src/core/sanitize.ts +5 -0
  22. package/src/core/search-entries.ts +227 -0
  23. package/src/core/settings.ts +34 -0
  24. package/src/core/skill-collapse.ts +35 -0
  25. package/src/core/summarize.ts +213 -0
  26. package/src/core/tool-args.ts +14 -0
  27. package/src/core/unified-config.ts +285 -0
  28. package/src/details.ts +13 -0
  29. package/src/extract/commits.ts +69 -0
  30. package/src/extract/files.ts +80 -0
  31. package/src/extract/goals.ts +79 -0
  32. package/src/extract/preferences.ts +55 -0
  33. package/src/hooks/before-compact.ts +345 -0
  34. package/src/om/agents/dropper/agent.ts +204 -0
  35. package/src/om/agents/dropper/prompts.ts +48 -0
  36. package/src/om/agents/observer/agent.ts +256 -0
  37. package/src/om/agents/observer/prompts.ts +119 -0
  38. package/src/om/agents/reflector/agent.ts +161 -0
  39. package/src/om/agents/reflector/prompts.ts +77 -0
  40. package/src/om/clipboard.ts +63 -0
  41. package/src/om/compaction-hook.ts +63 -0
  42. package/src/om/compaction-trigger.ts +92 -0
  43. package/src/om/config.ts +22 -0
  44. package/src/om/consolidation.ts +514 -0
  45. package/src/om/cooldown.ts +130 -0
  46. package/src/om/debug-log.ts +55 -0
  47. package/src/om/ids.ts +5 -0
  48. package/src/om/ledger/fold.ts +106 -0
  49. package/src/om/ledger/index.ts +6 -0
  50. package/src/om/ledger/progress.ts +225 -0
  51. package/src/om/ledger/projection.ts +237 -0
  52. package/src/om/ledger/recall.ts +243 -0
  53. package/src/om/ledger/render-summary.ts +44 -0
  54. package/src/om/ledger/types.ts +206 -0
  55. package/src/om/model-budget.ts +9 -0
  56. package/src/om/pending.ts +225 -0
  57. package/src/om/reverse-recall.ts +130 -0
  58. package/src/om/runtime.ts +241 -0
  59. package/src/om/serialize.ts +224 -0
  60. package/src/om/tokens.ts +33 -0
  61. package/src/sections.ts +18 -0
  62. package/src/tools/recall.ts +212 -0
  63. package/src/types.ts +19 -0
  64. package/vitest.config.ts +41 -0
@@ -0,0 +1,191 @@
1
+ /**
2
+ * /memory command — shows memory pipeline status and content.
3
+ *
4
+ * Created by pi-vcc-om. Replaces OM's standalone /om-status and /om-view.
5
+ * Usage: /memory (status), /memory view, /memory full.
6
+ */
7
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
+ import { copyTextToClipboard } from "../om/clipboard.js";
9
+ import type { Runtime } from "../om/runtime.js";
10
+ import {
11
+ diffProjection,
12
+ foldLedger,
13
+ fullProjection,
14
+ observationToSummaryLine,
15
+ rawTokensSinceDropCoverage,
16
+ rawTokensSinceLastCompaction,
17
+ rawTokensSinceObservationCoverage,
18
+ rawTokensSinceReflectionCoverage,
19
+ reflectionToSummaryLine,
20
+ visibleProjection,
21
+ type Entry,
22
+ type Projection,
23
+ } from "../om/ledger/index.js";
24
+ import { readPendingState } from "../om/pending.js";
25
+
26
+ function firstArg(args: unknown): string | undefined {
27
+ if (Array.isArray(args)) return typeof args[0] === "string" ? args[0] : undefined;
28
+ if (typeof args === "string") return args.trim().split(/\s+/)[0];
29
+ if (args && typeof args === "object" && "mode" in args) {
30
+ const mode = (args as { mode?: unknown }).mode;
31
+ return typeof mode === "string" ? mode : undefined;
32
+ }
33
+ return undefined;
34
+ }
35
+
36
+ function pct(current: number, total: number): number {
37
+ return total > 0 ? Math.min(100, Math.round((current / total) * 100)) : 0;
38
+ }
39
+
40
+ function tokenSum(items: { tokenCount: number }[]): number {
41
+ return items.reduce((sum, item) => sum + item.tokenCount, 0);
42
+ }
43
+
44
+ function addedSuffix(count: number): string | undefined {
45
+ return count > 0 ? `+${count.toLocaleString()}` : undefined;
46
+ }
47
+
48
+ function removedSuffix(count: number): string | undefined {
49
+ return count > 0 ? `-${count.toLocaleString()}` : undefined;
50
+ }
51
+
52
+ function appendSuffixes(line: string, suffixes: (string | undefined)[]): string {
53
+ const rendered = suffixes.filter((s): s is string => s !== undefined);
54
+ return rendered.length > 0 ? `${line} ${rendered.join(" ")}` : line;
55
+ }
56
+
57
+ function renderList<T>(items: T[], render: (item: T) => string, empty: string): string {
58
+ return items.length > 0 ? items.map(render).join("\n") : empty;
59
+ }
60
+
61
+ function renderContentOnlyProjection(projection: Projection, emptyScope: "visible" | "recorded"): string {
62
+ return [
63
+ "── Reflections ──",
64
+ renderList(projection.reflections, reflectionToSummaryLine, `No ${emptyScope} reflections.`),
65
+ "",
66
+ "── Observations ──",
67
+ renderList(projection.observations, observationToSummaryLine, `No ${emptyScope} observations.`),
68
+ ].join("\n");
69
+ }
70
+
71
+ export function registerMemoryCommand(pi: ExtensionAPI, runtime: Runtime): void {
72
+ pi.registerCommand("blackhole-memory", {
73
+ description: "Show memory pipeline status & token counters. /blackhole-memory for overview, /blackhole-memory view for visible observations & reflections, /blackhole-memory full for complete recorded memory (copies to clipboard).",
74
+ handler: async (args, ctx) => {
75
+ runtime.ensureConfig(ctx.cwd);
76
+ const entries = ctx.sessionManager.getBranch() as Entry[];
77
+ const sessionId = ctx.sessionManager.getSessionId();
78
+ const mode = firstArg(args);
79
+
80
+ // /memory full — show full recorded memory + copy to clipboard
81
+ if (mode === "full") {
82
+ const projection = fullProjection(entries);
83
+ const output = renderContentOnlyProjection(projection, "recorded");
84
+ const copied = await copyTextToClipboard(output).catch(() => false);
85
+ ctx.ui.notify(
86
+ copied ? `${output}\n\nCopied to clipboard.` : `${output}\n\nFailed to copy to clipboard.`,
87
+ "info",
88
+ );
89
+ return;
90
+ }
91
+
92
+ // /memory view — show visible memory + copy to clipboard
93
+ if (mode === "view") {
94
+ const projection = visibleProjection(entries);
95
+ const output = renderContentOnlyProjection(projection, "visible");
96
+ const copied = await copyTextToClipboard(output).catch(() => false);
97
+ ctx.ui.notify(
98
+ copied ? `${output}\n\nCopied to clipboard.` : `${output}\n\nFailed to copy to clipboard.`,
99
+ "info",
100
+ );
101
+ return;
102
+ }
103
+
104
+ // /memory (no args) — show status
105
+ if (mode && mode !== "status") {
106
+ ctx.ui.notify("Usage: /blackhole-memory [status|view|full]", "info");
107
+ return;
108
+ }
109
+
110
+ const folded = foldLedger(entries);
111
+ const visible = visibleProjection(entries);
112
+ const full = fullProjection(entries);
113
+ const drift = diffProjection(visible, full);
114
+
115
+ const visibleObservationTokens = tokenSum(visible.observations);
116
+ const visibleReflectionTokens = tokenSum(visible.reflections);
117
+ const observationLine = appendSuffixes(
118
+ `Observations: ${folded.observations.length} recorded / ${folded.droppedObservationIds.size} dropped / ${visible.observations.length} visible`,
119
+ [
120
+ addedSuffix(drift.observationsOnlyInFull.length),
121
+ removedSuffix(drift.droppedOnlyInFull.length),
122
+ ],
123
+ );
124
+ const reflectionLine = appendSuffixes(
125
+ `Reflections: ${folded.reflections.length} recorded / ${visible.reflections.length} visible`,
126
+ [addedSuffix(drift.reflectionsOnlyInFull.length)],
127
+ );
128
+ const obsProgress = rawTokensSinceObservationCoverage(entries);
129
+ const reflectionProgress = rawTokensSinceReflectionCoverage(entries);
130
+ const dropProgress = rawTokensSinceDropCoverage(entries);
131
+ const compactionProgress = rawTokensSinceLastCompaction(entries);
132
+
133
+ const passiveLines = runtime.config.passive === true
134
+ ? [
135
+ "── Mode ──",
136
+ "Passive: automatic memory workers and auto-compaction disabled",
137
+ "",
138
+ ]
139
+ : [];
140
+
141
+ const lines = [
142
+ ...passiveLines,
143
+ "── Memory ──",
144
+ observationLine,
145
+ reflectionLine,
146
+ "",
147
+ "── Activity ──",
148
+ `Observer: ~${obsProgress.toLocaleString()} / ${runtime.config.observeAfterTokens.toLocaleString()} tokens (${pct(obsProgress, runtime.config.observeAfterTokens)}%)`,
149
+ `Reflector: ~${reflectionProgress.toLocaleString()} / ${runtime.config.reflectAfterTokens.toLocaleString()} tokens (${pct(reflectionProgress, runtime.config.reflectAfterTokens)}%)`,
150
+ `Dropper: ~${dropProgress.toLocaleString()} / ${runtime.config.reflectAfterTokens.toLocaleString()} tokens (${pct(dropProgress, runtime.config.reflectAfterTokens)}%)`,
151
+ `Compaction: ~${compactionProgress.toLocaleString()} / ${runtime.config.compactAfterTokens.toLocaleString()} tokens (${pct(compactionProgress, runtime.config.compactAfterTokens)}%)`,
152
+ `Obs pool: ~${visibleObservationTokens.toLocaleString()} / ${runtime.config.observationsPoolMaxTokens.toLocaleString()} tokens (${pct(visibleObservationTokens, runtime.config.observationsPoolMaxTokens)}%)`,
153
+ `Reflect pool: ~${visibleReflectionTokens.toLocaleString()} tokens`,
154
+ ];
155
+
156
+ // Show pending data when noAutoCompact is active
157
+ if (runtime.config.noAutoCompact) {
158
+ const pending = readPendingState(sessionId);
159
+ const hasObs = !!pending.observation;
160
+ const hasRef = !!pending.reflection;
161
+ const hasDrop = !!pending.dropped;
162
+ if (hasObs || hasRef || hasDrop) {
163
+ lines.push("", "── Pending (noAutoCompact) ──");
164
+ if (hasObs) lines.push("Observation: waiting in pending.json");
165
+ if (hasRef) lines.push("Reflection: waiting in pending.json");
166
+ if (hasDrop) lines.push("Dropper: waiting in pending.json");
167
+ lines.push("Run /blackhole to flush and compact.");
168
+ }
169
+ }
170
+
171
+ if (runtime.consolidationInFlight || runtime.compactInFlight || runtime.compactHookInFlight) {
172
+ lines.push("", "── In flight ──");
173
+ if (runtime.consolidationInFlight) {
174
+ const phase = runtime.consolidationPhase ? ` (${runtime.consolidationPhase})` : "";
175
+ lines.push(`Consolidation: running${phase}`);
176
+ }
177
+ if (runtime.compactInFlight) lines.push("Auto-compaction: running");
178
+ if (runtime.compactHookInFlight) lines.push("Compaction hook: running");
179
+ }
180
+
181
+ if (runtime.lastObserverError || runtime.lastReflectorError || runtime.lastDropperError) {
182
+ lines.push("", "── Last error ──");
183
+ if (runtime.lastObserverError) lines.push(`Observer: ${runtime.lastObserverError}`);
184
+ if (runtime.lastReflectorError) lines.push(`Reflector: ${runtime.lastReflectorError}`);
185
+ if (runtime.lastDropperError) lines.push(`Dropper: ${runtime.lastDropperError}`);
186
+ }
187
+
188
+ ctx.ui.notify(lines.join("\n"), "info");
189
+ },
190
+ });
191
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * /pi-vcc command — triggers pi-vcc compaction.
3
+ *
4
+ * Upstream: https://github.com/sting8k/pi-vcc (src/commands/pi-vcc.ts)
5
+ * Modified by pi-vcc-om:
6
+ * - Flushes pending OM state (observations/reflections/dropped) when noAutoCompact is active
7
+ * before triggering compaction, so the compaction summary includes all accumulated memory.
8
+ */
9
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
+ import type { Runtime } from "../om/runtime.js";
11
+ import { getLastCompactionStats, PI_VCC_COMPACT_INSTRUCTION } from "../hooks/before-compact";
12
+ import { saveUnifiedConfig } from "../core/unified-config.js";
13
+ import { readPendingState, clearPendingState, hasPendingData } from "../om/pending.js";
14
+ import {
15
+ OM_OBSERVATIONS_DROPPED,
16
+ OM_OBSERVATIONS_RECORDED,
17
+ OM_REFLECTIONS_RECORDED,
18
+ } from "../om/ledger/index.js";
19
+
20
+ const formatTokens = (n: number): string => {
21
+ if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
22
+ return String(n);
23
+ };
24
+
25
+ export const registerPiVccCommand = (pi: ExtensionAPI, runtime: Runtime) => {
26
+ pi.registerCommand("blackhole", {
27
+ description:
28
+ "Compact conversation — structured summary (with observational memory when enabled). " +
29
+ "Subcommands: /blackhole om-off (disable memory), /blackhole om-on (re-enable memory).",
30
+ handler: async (args, ctx) => {
31
+ const sessionId = ctx.sessionManager.getSessionId();
32
+
33
+ // Handle om-off / om-on subcommands
34
+ const trimmed = (typeof args === "string" ? args : "").trim();
35
+ if (trimmed === "om-off") {
36
+ const saved = saveUnifiedConfig({ memory: false });
37
+ runtime.config.memory = false;
38
+ ctx.ui.notify(
39
+ saved ? "Observational memory disabled. Use /blackhole om-on to re-enable." : "Failed to save config.",
40
+ "info",
41
+ );
42
+ return;
43
+ }
44
+ if (trimmed === "om-on") {
45
+ const saved = saveUnifiedConfig({ memory: true });
46
+ runtime.config.memory = true;
47
+ ctx.ui.notify(
48
+ saved ? "Observational memory enabled." : "Failed to save config.",
49
+ "info",
50
+ );
51
+ return;
52
+ }
53
+
54
+ // If noAutoCompact: flush pending OM entries into the branch
55
+ // before compacting so the summary includes accumulated memory.
56
+ if (runtime.config.noAutoCompact && hasPendingData(sessionId)) {
57
+ const pending = readPendingState(sessionId);
58
+ if (pending.observation) {
59
+ pi.appendEntry(OM_OBSERVATIONS_RECORDED, pending.observation.data);
60
+ }
61
+ if (pending.reflection) {
62
+ pi.appendEntry(OM_REFLECTIONS_RECORDED, pending.reflection.data);
63
+ }
64
+ if (pending.dropped) {
65
+ pi.appendEntry(OM_OBSERVATIONS_DROPPED, pending.dropped.data);
66
+ }
67
+ clearPendingState(sessionId);
68
+ ctx.ui.notify("Observational memory: pending entries flushed", "info");
69
+ }
70
+
71
+ ctx.compact({
72
+ customInstructions: PI_VCC_COMPACT_INSTRUCTION,
73
+ onComplete: () => {
74
+ const stats = getLastCompactionStats();
75
+ if (stats) {
76
+ ctx.ui.notify(
77
+ `blackhole: ${stats.summarized} source entries processed; tail kept ${stats.kept} (~${formatTokens(stats.keptTokensEst)} tok).`,
78
+ "info",
79
+ );
80
+ } else {
81
+ ctx.ui.notify("Compacted with blackhole", "info");
82
+ }
83
+ },
84
+ onError: (err) => {
85
+ if (err.message === "Compaction cancelled" || err.message === "Already compacted") {
86
+ ctx.ui.notify("Nothing to compact", "warning");
87
+ } else {
88
+ ctx.ui.notify(`Compaction failed: ${err.message}`, "error");
89
+ }
90
+ },
91
+ });
92
+ },
93
+ });
94
+ };
@@ -0,0 +1,112 @@
1
+ /**
2
+ * /blackhole-recall command — search session history.
3
+ *
4
+ * Upstream: https://github.com/sting8k/pi-vcc (src/commands/vcc-recall.ts)
5
+ * Ported and renamed to /blackhole-recall for blackhole.
6
+ */
7
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
+ import { loadAllMessages } from "../core/load-messages.js";
9
+ import { searchEntries } from "../core/search-entries.js";
10
+ import { formatRecallOutput } from "../core/format-recall.js";
11
+ import { getActiveLineageEntryIds } from "../core/lineage.js";
12
+ import { parseRecallScope } from "../core/recall-scope.js";
13
+ import {
14
+ findObservationsForEntryIds,
15
+ findReflectionsForEntryIds,
16
+ formatRelatedObservations,
17
+ } from "../om/reverse-recall.js";
18
+ import type { Entry } from "../om/ledger/recall.js";
19
+
20
+ const PAGE_SIZE = 5;
21
+ const DEFAULT_RECENT = 25;
22
+
23
+ async function augmentWithObservations(
24
+ output: string,
25
+ rendered: { id: string }[],
26
+ ctx: any,
27
+ ): 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;
39
+ }
40
+
41
+ export const registerVccRecallCommand = (pi: ExtensionAPI) => {
42
+ pi.registerCommand("blackhole-recall", {
43
+ description:
44
+ "Search session history. Defaults to active lineage; add scope:all for off-lineage branches. Usage: /blackhole-recall <query> [page:N] [scope:all]",
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
+ }
51
+
52
+ const raw = args.trim();
53
+ const parsed = parseRecallScope(raw);
54
+ const lineageEntryIds =
55
+ parsed.scope === "lineage"
56
+ ? getActiveLineageEntryIds(ctx.sessionManager)
57
+ : undefined;
58
+
59
+ if (!parsed.text) {
60
+ // No query: show recent entries
61
+ const { rendered } = loadAllMessages(sessionFile, false, lineageEntryIds);
62
+ const recent = rendered.slice(-DEFAULT_RECENT);
63
+ const base = (parsed.scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(recent);
64
+ const output = await augmentWithObservations(base, recent, ctx);
65
+ pi.sendMessage(
66
+ { customType: "blackhole-recall", content: output, display: true },
67
+ { triggerTurn: true },
68
+ );
69
+ return;
70
+ }
71
+
72
+ // Parse page:N from args
73
+ const pageMatch = parsed.text.match(/\bpage:(\d+)\b/i);
74
+ const page = pageMatch ? Math.max(1, parseInt(pageMatch[1], 10)) : 1;
75
+ const query = parsed.text.replace(/\bpage:\d+\b/i, "").trim();
76
+
77
+ if (!query) {
78
+ const { rendered } = loadAllMessages(sessionFile, false, lineageEntryIds);
79
+ const recent = rendered.slice(-DEFAULT_RECENT);
80
+ const base = (parsed.scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(recent);
81
+ const output = await augmentWithObservations(base, recent, ctx);
82
+ pi.sendMessage(
83
+ { customType: "blackhole-recall", content: output, display: true },
84
+ { triggerTurn: true },
85
+ );
86
+ return;
87
+ }
88
+
89
+ const { rendered, rawMessages } = loadAllMessages(sessionFile, false, lineageEntryIds);
90
+ const allResults = searchEntries(rendered, rawMessages, query);
91
+
92
+ const start = (page - 1) * PAGE_SIZE;
93
+ const pageResults = allResults.slice(start, start + PAGE_SIZE);
94
+ const totalPages = Math.ceil(allResults.length / PAGE_SIZE);
95
+ const scopeSuffix = parsed.scope === "all" ? " (scope: all)" : "";
96
+ const header =
97
+ totalPages > 1
98
+ ? `Page ${page}/${totalPages} (${allResults.length} total matches${scopeSuffix})`
99
+ : `${allResults.length} matches${scopeSuffix}`;
100
+ const footer =
101
+ page < totalPages
102
+ ? `\n--- /blackhole-recall ${query}${parsed.scope === "all" ? " scope:all" : ""} page:${page + 1} ---`
103
+ : "";
104
+ const base = formatRecallOutput(pageResults, query, header) + footer;
105
+ const output = await augmentWithObservations(base, pageResults, ctx);
106
+ pi.sendMessage(
107
+ { customType: "blackhole-recall", content: output, display: true },
108
+ { triggerTurn: true },
109
+ );
110
+ },
111
+ });
112
+ };