pi-blackhole 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/index.js +11660 -0
- package/dist/index.js.map +1 -0
- package/example-config.json +1 -1
- package/index.ts +37 -63
- package/package.json +21 -9
- package/src/commands/cleanup.ts +279 -240
- package/src/commands/memory.ts +236 -184
- package/src/commands/pi-vcc.ts +202 -152
- package/src/commands/vcc-recall.ts +126 -95
- package/src/core/brief.ts +167 -33
- package/src/core/build-sections.ts +8 -2
- package/src/core/config-env.ts +117 -0
- package/src/core/content.ts +31 -7
- package/src/core/drill-down.ts +41 -11
- package/src/core/filter-noise.ts +9 -3
- package/src/core/format-recall.ts +15 -6
- package/src/core/format.ts +14 -4
- package/src/core/lineage.ts +9 -3
- package/src/core/load-messages.ts +24 -5
- package/src/core/normalize.ts +38 -14
- package/src/core/recall-scope.ts +11 -3
- package/src/core/render-entries.ts +22 -6
- package/src/core/sanitize.ts +5 -1
- package/src/core/search-entries.ts +111 -19
- package/src/core/settings.ts +1 -3
- package/src/core/summarize.ts +42 -21
- package/src/core/unified-config.ts +549 -411
- package/src/extract/commits.ts +4 -2
- package/src/extract/files.ts +10 -5
- package/src/extract/goals.ts +7 -2
- package/src/hooks/before-compact.ts +210 -88
- package/src/om/agents/dropper/agent.ts +380 -265
- package/src/om/agents/dropper/coverage.ts +102 -82
- package/src/om/agents/observer/agent.ts +242 -206
- package/src/om/agents/reflector/agent.ts +212 -153
- package/src/om/cleanup.ts +239 -218
- package/src/om/clipboard.ts +59 -51
- package/src/om/compaction-trigger.ts +448 -333
- package/src/om/config.ts +13 -6
- package/src/om/configure-overlay.ts +518 -355
- package/src/om/consolidation.ts +1460 -953
- package/src/om/cooldown.ts +75 -65
- package/src/om/debug-log.ts +86 -68
- package/src/om/ids.ts +1 -1
- package/src/om/ledger/fold.ts +89 -78
- package/src/om/ledger/progress.ts +181 -153
- package/src/om/ledger/projection.ts +248 -185
- package/src/om/ledger/recall.ts +247 -196
- package/src/om/ledger/render-summary.ts +79 -50
- package/src/om/ledger/types.ts +146 -117
- package/src/om/model-budget.ts +23 -13
- package/src/om/pending.ts +243 -179
- package/src/om/provider-stream.ts +52 -7
- package/src/om/retryable-error.ts +12 -16
- package/src/om/reverse-recall.ts +97 -91
- package/src/om/runtime.ts +474 -375
- package/src/om/serialize.ts +190 -166
- package/src/om/status-overlay.ts +246 -195
- package/src/om/tokens.ts +28 -21
- package/src/pi-base/blackhole-settings.ts +437 -0
- package/src/pi-base/config-manager.ts +440 -0
- package/src/pi-base/config.ts +469 -0
- package/src/pi-base/env.ts +43 -0
- package/src/pi-base/paths.ts +47 -0
- package/src/pi-base/settings/body.ts +1648 -0
- package/src/pi-base/settings/fields/action.ts +43 -0
- package/src/pi-base/settings/fields/boolean.ts +47 -0
- package/src/pi-base/settings/fields/custom.ts +72 -0
- package/src/pi-base/settings/fields/enum.ts +310 -0
- package/src/pi-base/settings/fields/index.ts +46 -0
- package/src/pi-base/settings/fields/model.ts +452 -0
- package/src/pi-base/settings/fields/string.ts +527 -0
- package/src/pi-base/settings/fields/text.ts +115 -0
- package/src/pi-base/settings/frame.ts +197 -0
- package/src/pi-base/settings/index.ts +77 -0
- package/src/pi-base/settings/inline-edit.ts +313 -0
- package/src/pi-base/settings/modal.ts +152 -0
- package/src/pi-base/settings/types.ts +500 -0
- package/src/pi-base/settings/validate-field.ts +113 -0
- package/src/pi-base/shell.ts +117 -0
- package/src/pi-base/types.ts +6 -0
- package/src/pi-base/ui.ts +32 -0
- package/src/tools/recall.ts +347 -225
- package/src/types.ts +20 -3
- package/tsup.config.ts +23 -0
- package/vitest.config.ts +15 -15
package/src/om/serialize.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
14
|
+
return n.toString().padStart(2, "0");
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
function fmtLocal(d: Date): string {
|
|
14
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
34
|
-
|
|
39
|
+
content: unknown,
|
|
40
|
+
options: { omitRedactedThinking?: boolean; includeThinking?: boolean } = {},
|
|
35
41
|
): string {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
168
|
-
|
|
182
|
+
text: string;
|
|
183
|
+
sourceEntryIds: string[];
|
|
169
184
|
};
|
|
170
185
|
|
|
171
186
|
function isSourceRenderableEntry(entry: RenderableEntry): boolean {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
}
|