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
@@ -4,221 +4,245 @@
4
4
  * Upstream: https://github.com/elpapi42/pi-observational-memory (src/serialize.ts)
5
5
  * Unmodified.
6
6
  */
7
- import type { Message, TextContent, ToolResultMessage } from "@earendil-works/pi-ai";
7
+ import type {
8
+ Message,
9
+ TextContent,
10
+ ToolResultMessage,
11
+ } from "@earendil-works/pi-ai";
8
12
 
9
13
  function pad(n: number): string {
10
- return n.toString().padStart(2, "0");
14
+ return n.toString().padStart(2, "0");
11
15
  }
12
16
 
13
17
  function fmtLocal(d: Date): string {
14
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
18
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
15
19
  }
16
20
 
17
21
  function formatTimestamp(v: number | string | undefined): string {
18
- if (v === undefined) return "????-??-?? ??:??";
19
- const d = new Date(v);
20
- return Number.isNaN(d.getTime()) ? "????-??-?? ??:??" : fmtLocal(d);
22
+ if (v === undefined) return "????-??-?? ??:??";
23
+ const d = new Date(v);
24
+ return Number.isNaN(d.getTime()) ? "????-??-?? ??:??" : fmtLocal(d);
21
25
  }
22
26
 
23
- function formatRecallTimestamp(...values: Array<number | string | undefined>): string {
24
- for (const v of values) {
25
- if (v === undefined) continue;
26
- const d = new Date(v);
27
- if (!Number.isNaN(d.getTime())) return fmtLocal(d);
28
- }
29
- return "Unknown time";
27
+ function formatRecallTimestamp(
28
+ ...values: Array<number | string | undefined>
29
+ ): string {
30
+ for (const v of values) {
31
+ if (v === undefined) continue;
32
+ const d = new Date(v);
33
+ if (!Number.isNaN(d.getTime())) return fmtLocal(d);
34
+ }
35
+ return "Unknown time";
30
36
  }
31
37
 
32
38
  function textAndPlaceholders(
33
- content: unknown,
34
- options: { omitRedactedThinking?: boolean; includeThinking?: boolean } = {},
39
+ content: unknown,
40
+ options: { omitRedactedThinking?: boolean; includeThinking?: boolean } = {},
35
41
  ): string {
36
- if (typeof content === "string") return content;
37
- if (!Array.isArray(content)) return "[non-text content omitted]";
38
-
39
- const parts: string[] = [];
40
- for (const block of content as Array<Record<string, unknown>>) {
41
- if (!block || typeof block !== "object") {
42
- parts.push("[non-text content omitted]");
43
- continue;
44
- }
45
- if (block.type === "text" && typeof block.text === "string") {
46
- parts.push(block.text);
47
- continue;
48
- }
49
- if (block.type === "thinking") {
50
- if (options.omitRedactedThinking && block.redacted === true) continue;
51
- if (options.includeThinking && typeof block.thinking === "string") {
52
- parts.push(`[thinking: ${block.thinking}]`);
53
- continue;
54
- }
55
- parts.push("[non-text content omitted]");
56
- continue;
57
- }
58
- if (block.type === "toolCall" && typeof block.name === "string") {
59
- parts.push(`[${block.name}(${JSON.stringify(block.arguments ?? {})})]`);
60
- continue;
61
- }
62
- parts.push("[non-text content omitted]");
63
- }
64
- return parts.join("\n");
42
+ if (typeof content === "string") return content;
43
+ if (!Array.isArray(content)) return "[non-text content omitted]";
44
+
45
+ const parts: string[] = [];
46
+ for (const block of content as Array<Record<string, unknown>>) {
47
+ if (!block || typeof block !== "object") {
48
+ parts.push("[non-text content omitted]");
49
+ continue;
50
+ }
51
+ if (block.type === "text" && typeof block.text === "string") {
52
+ parts.push(block.text);
53
+ continue;
54
+ }
55
+ if (block.type === "thinking") {
56
+ if (options.omitRedactedThinking && block.redacted === true) continue;
57
+ if (options.includeThinking && typeof block.thinking === "string") {
58
+ parts.push(`[thinking: ${block.thinking}]`);
59
+ continue;
60
+ }
61
+ parts.push("[non-text content omitted]");
62
+ continue;
63
+ }
64
+ if (block.type === "toolCall" && typeof block.name === "string") {
65
+ parts.push(`[${block.name}(${JSON.stringify(block.arguments ?? {})})]`);
66
+ continue;
67
+ }
68
+ parts.push("[non-text content omitted]");
69
+ }
70
+ return parts.join("\n");
65
71
  }
66
72
 
67
73
  function textOnly(content: unknown): string {
68
- if (content == null) return "";
69
- if (typeof content === "string") return content;
70
- if (!Array.isArray(content)) return "";
71
- return content
72
- .filter((b): b is TextContent => b?.type === "text" && typeof b.text === "string")
73
- .map((b) => b.text)
74
- .join("\n");
74
+ if (content == null) return "";
75
+ if (typeof content === "string") return content;
76
+ if (!Array.isArray(content)) return "";
77
+ return content
78
+ .filter(
79
+ (b): b is TextContent => b?.type === "text" && typeof b.text === "string",
80
+ )
81
+ .map((b) => b.text)
82
+ .join("\n");
75
83
  }
76
84
 
77
85
  export function serializeConversation(messages: Message[]): string {
78
- return messages
79
- .map((msg): string | null => {
80
- const time = formatTimestamp(msg.timestamp);
81
- if (msg.role === "user") {
82
- const text = textOnly(msg.content);
83
- return `[User @ ${time}]: ${text}`;
84
- }
85
- if (msg.role === "assistant") {
86
- const body = textAndPlaceholders(msg.content, {
87
- includeThinking: true,
88
- omitRedactedThinking: true,
89
- })
90
- .split("\n")
91
- .filter(Boolean)
92
- .join("\n");
93
- if (!body) return null;
94
- return `[Assistant @ ${time}]: ${body}`;
95
- }
96
- const text = textOnly(msg.content);
97
- return `[Tool result for ${(msg as ToolResultMessage).toolName} @ ${time}]: ${text}`;
98
- })
99
- .filter((line): line is string => line !== null)
100
- .join("\n\n");
86
+ return messages
87
+ .map((msg): string | null => {
88
+ const time = formatTimestamp(msg.timestamp);
89
+ if (msg.role === "user") {
90
+ const text = textOnly(msg.content);
91
+ return `[User @ ${time}]: ${text}`;
92
+ }
93
+ if (msg.role === "assistant") {
94
+ const body = textAndPlaceholders(msg.content, {
95
+ includeThinking: true,
96
+ omitRedactedThinking: true,
97
+ })
98
+ .split("\n")
99
+ .filter(Boolean)
100
+ .join("\n");
101
+ if (!body) return null;
102
+ return `[Assistant @ ${time}]: ${body}`;
103
+ }
104
+ const text = textOnly(msg.content);
105
+ return `[Tool result for ${(msg as ToolResultMessage).toolName} @ ${time}]: ${text}`;
106
+ })
107
+ .filter((line): line is string => line !== null)
108
+ .join("\n\n");
101
109
  }
102
110
 
103
111
  export function nowTimestamp(): string {
104
- return fmtLocal(new Date());
112
+ return fmtLocal(new Date());
105
113
  }
106
114
 
107
115
  export const MAX_RECORD_CONTENT_CHARS = 10_000;
108
116
 
109
117
  export function truncateRecordContent(content: string): string {
110
- if (content.length <= MAX_RECORD_CONTENT_CHARS) return content;
111
- const head = content.slice(0, MAX_RECORD_CONTENT_CHARS);
112
- const dropped = content.length - MAX_RECORD_CONTENT_CHARS;
113
- return `${head} … [truncated ${dropped} chars]`;
118
+ if (content.length <= MAX_RECORD_CONTENT_CHARS) return content;
119
+ const head = content.slice(0, MAX_RECORD_CONTENT_CHARS);
120
+ const dropped = content.length - MAX_RECORD_CONTENT_CHARS;
121
+ return `${head} … [truncated ${dropped} chars]`;
114
122
  }
115
123
 
116
124
  export type RenderableEntry = {
117
- type: string;
118
- id?: string;
119
- timestamp?: string;
120
- message?: unknown;
121
- customType?: string;
122
- content?: unknown;
123
- summary?: unknown;
125
+ type: string;
126
+ id?: string;
127
+ timestamp?: string;
128
+ message?: unknown;
129
+ customType?: string;
130
+ content?: unknown;
131
+ summary?: unknown;
124
132
  };
125
133
 
126
- function renderCustomMessage(entry: RenderableEntry, options: { recallFormat: boolean }): string {
127
- const time = options.recallFormat ? formatRecallTimestamp(entry.timestamp) : formatTimestamp(entry.timestamp);
128
- const text = options.recallFormat
129
- ? textAndPlaceholders(entry.content)
130
- : typeof entry.content === "string"
131
- ? entry.content
132
- : Array.isArray(entry.content)
133
- ? (entry.content as Array<{ type?: string; text?: string }>)
134
- .filter((b) => b?.type === "text" && typeof b.text === "string")
135
- .map((b) => b.text as string)
136
- .join("\n")
137
- : "";
138
- if (options.recallFormat) {
139
- const origin = entry.customType ? `Custom message (${entry.customType})` : "Custom message";
140
- return `[${origin} @ ${time}]: ${text}`;
141
- }
142
- const tag = entry.customType ? `Custom (${entry.customType})` : "Custom";
143
- return `[${tag} @ ${time}]: ${text}`;
134
+ function renderCustomMessage(
135
+ entry: RenderableEntry,
136
+ options: { recallFormat: boolean },
137
+ ): string {
138
+ const time = options.recallFormat
139
+ ? formatRecallTimestamp(entry.timestamp)
140
+ : formatTimestamp(entry.timestamp);
141
+ const text = options.recallFormat
142
+ ? textAndPlaceholders(entry.content)
143
+ : typeof entry.content === "string"
144
+ ? entry.content
145
+ : Array.isArray(entry.content)
146
+ ? (entry.content as Array<{ type?: string; text?: string }>)
147
+ .filter((b) => b?.type === "text" && typeof b.text === "string")
148
+ .map((b) => b.text as string)
149
+ .join("\n")
150
+ : "";
151
+ if (options.recallFormat) {
152
+ const origin = entry.customType
153
+ ? `Custom message (${entry.customType})`
154
+ : "Custom message";
155
+ return `[${origin} @ ${time}]: ${text}`;
156
+ }
157
+ const tag = entry.customType ? `Custom (${entry.customType})` : "Custom";
158
+ return `[${tag} @ ${time}]: ${text}`;
144
159
  }
145
160
 
146
161
  export function serializeBranchEntries(entries: RenderableEntry[]): string {
147
- const blocks: string[] = [];
148
- for (const entry of entries) {
149
- if (entry.type === "message" && entry.message) {
150
- const part = serializeConversation([entry.message as Message]);
151
- if (part) blocks.push(part);
152
- continue;
153
- }
154
- if (entry.type === "custom_message") {
155
- blocks.push(renderCustomMessage(entry, { recallFormat: false }));
156
- continue;
157
- }
158
- if (entry.type === "branch_summary" && typeof entry.summary === "string") {
159
- const time = formatTimestamp(entry.timestamp);
160
- blocks.push(`[Branch summary @ ${time}]: ${entry.summary}`);
161
- }
162
- }
163
- return blocks.join("\n\n");
162
+ const blocks: string[] = [];
163
+ for (const entry of entries) {
164
+ if (entry.type === "message" && entry.message) {
165
+ const part = serializeConversation([entry.message as Message]);
166
+ if (part) blocks.push(part);
167
+ continue;
168
+ }
169
+ if (entry.type === "custom_message") {
170
+ blocks.push(renderCustomMessage(entry, { recallFormat: false }));
171
+ continue;
172
+ }
173
+ if (entry.type === "branch_summary" && typeof entry.summary === "string") {
174
+ const time = formatTimestamp(entry.timestamp);
175
+ blocks.push(`[Branch summary @ ${time}]: ${entry.summary}`);
176
+ }
177
+ }
178
+ return blocks.join("\n\n");
164
179
  }
165
180
 
166
181
  export type SourceAddressedSerialization = {
167
- text: string;
168
- sourceEntryIds: string[];
182
+ text: string;
183
+ sourceEntryIds: string[];
169
184
  };
170
185
 
171
186
  function isSourceRenderableEntry(entry: RenderableEntry): boolean {
172
- return entry.type === "message" || entry.type === "custom_message" || entry.type === "branch_summary";
173
- }
174
-
175
- export function serializeSourceAddressedBranchEntries(entries: RenderableEntry[]): SourceAddressedSerialization {
176
- const blocks: string[] = [];
177
- const sourceEntryIds: string[] = [];
178
- for (const entry of entries) {
179
- if (!entry.id || !isSourceRenderableEntry(entry)) continue;
180
- const rendered = serializeBranchEntries([entry]);
181
- if (!rendered.trim()) continue;
182
- sourceEntryIds.push(entry.id);
183
- blocks.push(`[Source entry id: ${entry.id}]\n${rendered}`);
184
- }
185
- return { text: blocks.join("\n\n"), sourceEntryIds };
187
+ return (
188
+ entry.type === "message" ||
189
+ entry.type === "custom_message" ||
190
+ entry.type === "branch_summary"
191
+ );
192
+ }
193
+
194
+ export function serializeSourceAddressedBranchEntries(
195
+ entries: RenderableEntry[],
196
+ ): SourceAddressedSerialization {
197
+ const blocks: string[] = [];
198
+ const sourceEntryIds: string[] = [];
199
+ for (const entry of entries) {
200
+ if (!entry.id || !isSourceRenderableEntry(entry)) continue;
201
+ const rendered = serializeBranchEntries([entry]);
202
+ if (!rendered.trim()) continue;
203
+ sourceEntryIds.push(entry.id);
204
+ blocks.push(`[Source entry id: ${entry.id}]\n${rendered}`);
205
+ }
206
+ return { text: blocks.join("\n\n"), sourceEntryIds };
186
207
  }
187
208
 
188
209
  function renderRecallMessage(entry: RenderableEntry): string | null {
189
- if (!entry.message || typeof entry.message !== "object") return null;
190
- const msg = entry.message as Message;
191
- const time = formatRecallTimestamp(msg.timestamp, entry.timestamp);
192
- if (msg.role === "user") {
193
- return `[User @ ${time}]: ${textAndPlaceholders(msg.content)}`;
194
- }
195
- if (msg.role === "assistant") {
196
- const body = textAndPlaceholders(msg.content, {
197
- includeThinking: true,
198
- omitRedactedThinking: true,
199
- })
200
- .split("\n")
201
- .filter(Boolean)
202
- .join("\n");
203
- if (!body) return null;
204
- return `[Assistant @ ${time}]: ${body}`;
205
- }
206
- return `[Tool result: ${(msg as ToolResultMessage).toolName} @ ${time}]: ${textAndPlaceholders(msg.content)}`;
210
+ if (!entry.message || typeof entry.message !== "object") return null;
211
+ const msg = entry.message as Message;
212
+ const time = formatRecallTimestamp(msg.timestamp, entry.timestamp);
213
+ if (msg.role === "user") {
214
+ return `[User @ ${time}]: ${textAndPlaceholders(msg.content)}`;
215
+ }
216
+ if (msg.role === "assistant") {
217
+ const body = textAndPlaceholders(msg.content, {
218
+ includeThinking: true,
219
+ omitRedactedThinking: true,
220
+ })
221
+ .split("\n")
222
+ .filter(Boolean)
223
+ .join("\n");
224
+ if (!body) return null;
225
+ return `[Assistant @ ${time}]: ${body}`;
226
+ }
227
+ return `[Tool result: ${(msg as ToolResultMessage).toolName} @ ${time}]: ${textAndPlaceholders(msg.content)}`;
207
228
  }
208
229
 
209
230
  export function renderRecallSourceEntry(entry: RenderableEntry): string | null {
210
- if (entry.type === "message") return renderRecallMessage(entry);
211
- if (entry.type === "custom_message") return renderCustomMessage(entry, { recallFormat: true });
212
- if (entry.type === "branch_summary" && typeof entry.summary === "string") {
213
- const time = formatRecallTimestamp(entry.timestamp);
214
- return `[Branch summary @ ${time}]: ${entry.summary}`;
215
- }
216
- return null;
231
+ if (entry.type === "message") return renderRecallMessage(entry);
232
+ if (entry.type === "custom_message")
233
+ return renderCustomMessage(entry, { recallFormat: true });
234
+ if (entry.type === "branch_summary" && typeof entry.summary === "string") {
235
+ const time = formatRecallTimestamp(entry.timestamp);
236
+ return `[Branch summary @ ${time}]: ${entry.summary}`;
237
+ }
238
+ return null;
217
239
  }
218
240
 
219
241
  export function renderRecallSourceEntries(entries: RenderableEntry[]): string {
220
- return entries
221
- .map(renderRecallSourceEntry)
222
- .filter((block): block is string => block !== null && block.trim().length > 0)
223
- .join("\n\n");
242
+ return entries
243
+ .map(renderRecallSourceEntry)
244
+ .filter(
245
+ (block): block is string => block !== null && block.trim().length > 0,
246
+ )
247
+ .join("\n\n");
224
248
  }