@aexol/spectral 0.3.6 → 0.3.8
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/dist/cli.js +12 -1
- package/dist/memory/branch.js +409 -0
- package/dist/memory/commands/status.js +91 -0
- package/dist/memory/commands/view.js +61 -0
- package/dist/memory/compaction.js +813 -0
- package/dist/memory/config.js +79 -0
- package/dist/memory/debug-log.js +45 -0
- package/dist/memory/hooks/compaction-hook.js +319 -0
- package/dist/memory/hooks/compaction-trigger.js +87 -0
- package/dist/memory/hooks/observer-trigger.js +108 -0
- package/dist/memory/ids.js +4 -0
- package/dist/memory/index.js +16 -0
- package/dist/memory/model-budget.js +6 -0
- package/dist/memory/observer.js +157 -0
- package/dist/memory/progress.js +134 -0
- package/dist/memory/prompts.js +287 -0
- package/dist/memory/relevance.js +14 -0
- package/dist/memory/runtime.js +62 -0
- package/dist/memory/serialize.js +199 -0
- package/dist/memory/tokens.js +26 -0
- package/dist/memory/tools/recall-observation.js +508 -0
- package/dist/memory/types.js +95 -0
- package/dist/relay/dispatcher.js +2 -0
- package/dist/server/session-stream.js +1 -0
- package/dist/server/storage.js +7 -1
- package/package.json +2 -1
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import { Type } from "@mariozechner/pi-ai";
|
|
2
|
+
import { defineTool } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
4
|
+
import { recallMemorySources, } from "../branch.js";
|
|
5
|
+
import { renderRecallSourceEntries, renderRecallSourceEntry } from "../serialize.js";
|
|
6
|
+
import { estimateEntryTokens } from "../tokens.js";
|
|
7
|
+
export const RECALL_OBSERVATION_TOOL_NAME = "recall";
|
|
8
|
+
const MEMORY_ID_PATTERN = /^[a-f0-9]{12}$/;
|
|
9
|
+
function pad(n) {
|
|
10
|
+
return n.toString().padStart(2, "0");
|
|
11
|
+
}
|
|
12
|
+
function fmtLocal(d) {
|
|
13
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
14
|
+
}
|
|
15
|
+
function formatDisplayTimestamp(...values) {
|
|
16
|
+
for (const v of values) {
|
|
17
|
+
if (v === undefined)
|
|
18
|
+
continue;
|
|
19
|
+
const d = new Date(v);
|
|
20
|
+
if (!Number.isNaN(d.getTime()))
|
|
21
|
+
return fmtLocal(d);
|
|
22
|
+
}
|
|
23
|
+
return "Unknown time";
|
|
24
|
+
}
|
|
25
|
+
function textContentBlocks(content) {
|
|
26
|
+
return Array.isArray(content) ? content.filter((block) => !!block && typeof block === "object") : [];
|
|
27
|
+
}
|
|
28
|
+
function uniqueStrings(items) {
|
|
29
|
+
return Array.from(new Set(items));
|
|
30
|
+
}
|
|
31
|
+
function sourceOriginAndQualifiers(entry) {
|
|
32
|
+
if (entry.type === "message" && entry.message && typeof entry.message === "object") {
|
|
33
|
+
const msg = entry.message;
|
|
34
|
+
const timestamp = formatDisplayTimestamp(msg.timestamp, entry.timestamp);
|
|
35
|
+
if (msg.role === "user")
|
|
36
|
+
return { origin: "User", timestamp, qualifiers: [] };
|
|
37
|
+
if (msg.role === "assistant") {
|
|
38
|
+
const toolCalls = uniqueStrings(textContentBlocks(msg.content)
|
|
39
|
+
.filter((block) => block.type === "toolCall" && typeof block.name === "string")
|
|
40
|
+
.map((block) => block.name));
|
|
41
|
+
return {
|
|
42
|
+
origin: "Assistant",
|
|
43
|
+
timestamp,
|
|
44
|
+
qualifiers: toolCalls.length > 0 ? [`tool calls: ${toolCalls.join(", ")}`] : [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const toolName = msg.toolName;
|
|
48
|
+
return { origin: `Tool result: ${typeof toolName === "string" && toolName ? toolName : "unknown"}`, timestamp, qualifiers: [] };
|
|
49
|
+
}
|
|
50
|
+
if (entry.type === "custom_message") {
|
|
51
|
+
return {
|
|
52
|
+
origin: "Custom message",
|
|
53
|
+
timestamp: formatDisplayTimestamp(entry.timestamp),
|
|
54
|
+
qualifiers: typeof entry.customType === "string" && entry.customType ? [`custom: ${entry.customType}`] : [],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (entry.type === "branch_summary") {
|
|
58
|
+
return { origin: "Branch summary", timestamp: formatDisplayTimestamp(entry.timestamp), qualifiers: [] };
|
|
59
|
+
}
|
|
60
|
+
return { origin: entry.type || "Entry", timestamp: formatDisplayTimestamp(entry.timestamp), qualifiers: [] };
|
|
61
|
+
}
|
|
62
|
+
function renderSourceEntryContentOnly(entry) {
|
|
63
|
+
const rendered = renderRecallSourceEntry(entry);
|
|
64
|
+
return rendered?.replace(/^\[[^\]]+\]:\s?/, "") || undefined;
|
|
65
|
+
}
|
|
66
|
+
function sourceEntryDetails(entry, includeContent) {
|
|
67
|
+
const { origin, timestamp, qualifiers } = sourceOriginAndQualifiers(entry);
|
|
68
|
+
const content = renderSourceEntryContentOnly(entry);
|
|
69
|
+
return {
|
|
70
|
+
id: entry.id,
|
|
71
|
+
origin,
|
|
72
|
+
timestamp,
|
|
73
|
+
tokens: estimateEntryTokens(entry),
|
|
74
|
+
qualifiers,
|
|
75
|
+
...(includeContent && content ? { content } : {}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function observationDetails(observation) {
|
|
79
|
+
return {
|
|
80
|
+
id: observation.id,
|
|
81
|
+
content: observation.content,
|
|
82
|
+
timestamp: observation.timestamp,
|
|
83
|
+
relevance: observation.relevance,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function reflectionDetails(reflection, reflectionIndex) {
|
|
87
|
+
return {
|
|
88
|
+
id: reflection.id,
|
|
89
|
+
content: reflection.content,
|
|
90
|
+
supportingObservationIds: reflection.supportingObservationIds,
|
|
91
|
+
...(reflection.legacy === true ? { legacy: true } : {}),
|
|
92
|
+
reflectionIndex,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function observationMatchDetails(match, includeSourceContent = true) {
|
|
96
|
+
if (match.status === "ok") {
|
|
97
|
+
return {
|
|
98
|
+
status: "ok",
|
|
99
|
+
observationEntryId: match.observationEntryId,
|
|
100
|
+
observationRecordIndex: match.observationRecordIndex,
|
|
101
|
+
observation: observationDetails(match.observation),
|
|
102
|
+
sourceEntryIds: match.sourceEntryIds,
|
|
103
|
+
sourceEntries: match.sourceEntries.map((entry) => sourceEntryDetails(entry, includeSourceContent)),
|
|
104
|
+
sourceCharacterCount: renderRecallSourceEntries(match.sourceEntries).length,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (match.status === "source_unavailable") {
|
|
108
|
+
return {
|
|
109
|
+
status: "source_unavailable",
|
|
110
|
+
observationEntryId: match.observationEntryId,
|
|
111
|
+
observationRecordIndex: match.observationRecordIndex,
|
|
112
|
+
observation: observationDetails(match.observation),
|
|
113
|
+
sourceEntryIds: match.sourceEntryIds,
|
|
114
|
+
...(includeSourceContent
|
|
115
|
+
? {
|
|
116
|
+
sourceEntries: match.sourceEntries.map((entry) => sourceEntryDetails(entry, true)),
|
|
117
|
+
sourceCharacterCount: renderRecallSourceEntries(match.sourceEntries).length,
|
|
118
|
+
}
|
|
119
|
+
: {}),
|
|
120
|
+
missingSourceEntryIds: match.missingSourceEntryIds,
|
|
121
|
+
nonSourceEntryIds: match.nonSourceEntryIds,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
status: "no_source",
|
|
126
|
+
observationEntryId: match.observationEntryId,
|
|
127
|
+
observationRecordIndex: match.observationRecordIndex,
|
|
128
|
+
observation: observationDetails(match.observation),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function textResult(text, details) {
|
|
132
|
+
return {
|
|
133
|
+
content: [{ type: "text", text }],
|
|
134
|
+
details,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function emptyDetails(status, memoryId, message) {
|
|
138
|
+
return {
|
|
139
|
+
status,
|
|
140
|
+
memoryId,
|
|
141
|
+
observationId: memoryId,
|
|
142
|
+
collision: false,
|
|
143
|
+
partial: false,
|
|
144
|
+
reflections: [],
|
|
145
|
+
directObservationMatches: [],
|
|
146
|
+
observations: [],
|
|
147
|
+
matches: [],
|
|
148
|
+
sourceEntries: [],
|
|
149
|
+
unavailableSupportingObservations: [],
|
|
150
|
+
unavailableReflectionProvenance: [],
|
|
151
|
+
missingSourceEntryIds: [],
|
|
152
|
+
nonSourceEntryIds: [],
|
|
153
|
+
message,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function aggregateStatus(details) {
|
|
157
|
+
const observationOnly = details.reflections.length === 0 && details.unavailableSupportingObservations.length === 0 && details.unavailableReflectionProvenance.length === 0;
|
|
158
|
+
if (observationOnly && details.observations.some((match) => match.status === "ok"))
|
|
159
|
+
return "ok";
|
|
160
|
+
if (observationOnly && details.observations.some((match) => match.status === "source_unavailable"))
|
|
161
|
+
return "source_unavailable";
|
|
162
|
+
if (observationOnly && details.observations.length > 0)
|
|
163
|
+
return "no_source";
|
|
164
|
+
if (details.unavailableReflectionProvenance.length > 0 && details.observations.length === 0 && details.sourceEntries.length === 0)
|
|
165
|
+
return "no_provenance";
|
|
166
|
+
if (details.partial)
|
|
167
|
+
return "partial";
|
|
168
|
+
if (details.sourceEntries.length > 0)
|
|
169
|
+
return "ok";
|
|
170
|
+
if (details.reflections.length > 0)
|
|
171
|
+
return "ok";
|
|
172
|
+
if (details.observations.length > 0)
|
|
173
|
+
return "no_source";
|
|
174
|
+
return "not_found";
|
|
175
|
+
}
|
|
176
|
+
function friendlyNoSourceMessage(memoryId) {
|
|
177
|
+
return `Observation ${memoryId} has no source entries associated with it. This can happen for legacy observations created before source recall was available.`;
|
|
178
|
+
}
|
|
179
|
+
function friendlySourceUnavailableMessage(match) {
|
|
180
|
+
const missing = match.missingSourceEntryIds && match.missingSourceEntryIds.length > 0 ? ` missing: ${match.missingSourceEntryIds.join(", ")}` : "";
|
|
181
|
+
const nonSource = match.nonSourceEntryIds && match.nonSourceEntryIds.length > 0 ? ` non-source: ${match.nonSourceEntryIds.join(", ")}` : "";
|
|
182
|
+
return `Observation ${match.observation.id} has source entries associated, but some are unavailable on the current branch or are not source-renderable.${missing}${nonSource}`;
|
|
183
|
+
}
|
|
184
|
+
function reflectionLineText(reflection) {
|
|
185
|
+
return `[${reflection.id}] ${reflection.content}`;
|
|
186
|
+
}
|
|
187
|
+
function observationLineText(observation) {
|
|
188
|
+
return `[${observation.id}] ${observation.timestamp} [${observation.relevance}] ${observation.content}`;
|
|
189
|
+
}
|
|
190
|
+
function renderObservationOnlyTextFromResult(result) {
|
|
191
|
+
const sections = [];
|
|
192
|
+
if (result.collision) {
|
|
193
|
+
sections.push(`Multiple observations share id ${result.memoryId}; returning all matching source results from the current branch.`);
|
|
194
|
+
}
|
|
195
|
+
for (const match of result.directObservationMatches) {
|
|
196
|
+
if (match.status === "ok") {
|
|
197
|
+
const sourceText = renderRecallSourceEntries(match.sourceEntries);
|
|
198
|
+
if (sourceText.trim())
|
|
199
|
+
sections.push(sourceText);
|
|
200
|
+
else
|
|
201
|
+
sections.push(`Observation ${match.observation.id} has source entries associated, but they rendered no text content.`);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (match.status === "source_unavailable") {
|
|
205
|
+
sections.push(friendlySourceUnavailableMessage(observationMatchDetails(match, false)));
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
sections.push(friendlyNoSourceMessage(match.observation.id));
|
|
209
|
+
}
|
|
210
|
+
return sections.join("\n\n");
|
|
211
|
+
}
|
|
212
|
+
function unavailableSupportingLineText(item) {
|
|
213
|
+
return `Supporting observation ${item.observationId} for reflection ${item.reflectionId} is unavailable on the current branch.`;
|
|
214
|
+
}
|
|
215
|
+
function unavailableReflectionProvenanceLineText(item) {
|
|
216
|
+
return `Reflection ${item.reflectionId} was migrated from legacy memory created before reflection provenance was recorded, so no supporting observations or raw sources are available.`;
|
|
217
|
+
}
|
|
218
|
+
function unavailableObservationSourceLineText(match) {
|
|
219
|
+
return `Observation ${match.observation.id} has no source entries associated. This can happen for legacy observations created before source recall was available.`;
|
|
220
|
+
}
|
|
221
|
+
function renderMemoryText(result) {
|
|
222
|
+
const sections = [];
|
|
223
|
+
if (result.collision) {
|
|
224
|
+
sections.push(`Memory id ${result.memoryId} matched multiple observations/reflections; returning all available evidence from the current branch.`);
|
|
225
|
+
}
|
|
226
|
+
if (result.reflectionMatches.length > 0) {
|
|
227
|
+
sections.push(`Reflections:\n${result.reflectionMatches.map((match) => reflectionLineText(reflectionDetails(match.reflection, match.reflectionIndex))).join("\n")}`);
|
|
228
|
+
}
|
|
229
|
+
if (result.observations.length > 0) {
|
|
230
|
+
sections.push(`Observations:\n${result.observations.map((match) => observationLineText(match.observation)).join("\n")}`);
|
|
231
|
+
}
|
|
232
|
+
if (result.unavailableSupportingObservations.length > 0) {
|
|
233
|
+
sections.push(`Unavailable supporting observations:\n${result.unavailableSupportingObservations
|
|
234
|
+
.map((item) => unavailableSupportingLineText({
|
|
235
|
+
reflectionId: item.reflection.id,
|
|
236
|
+
reflectionIndex: item.reflectionIndex,
|
|
237
|
+
observationId: item.observationId,
|
|
238
|
+
}))
|
|
239
|
+
.join("\n")}`);
|
|
240
|
+
}
|
|
241
|
+
if (result.unavailableReflectionProvenance.length > 0) {
|
|
242
|
+
sections.push(`Unavailable reflection provenance:\n${result.unavailableReflectionProvenance
|
|
243
|
+
.map((item) => unavailableReflectionProvenanceLineText({
|
|
244
|
+
reflectionId: item.reflection.id,
|
|
245
|
+
reflectionIndex: item.reflectionIndex,
|
|
246
|
+
reason: item.reason,
|
|
247
|
+
}))
|
|
248
|
+
.join("\n")}`);
|
|
249
|
+
}
|
|
250
|
+
const noSourceObservations = result.observations.filter((match) => match.status === "no_source");
|
|
251
|
+
if (noSourceObservations.length > 0) {
|
|
252
|
+
sections.push(`Unavailable observation sources:\n${noSourceObservations.map(unavailableObservationSourceLineText).join("\n")}`);
|
|
253
|
+
}
|
|
254
|
+
if (result.missingSourceEntryIds.length > 0 || result.nonSourceEntryIds.length > 0) {
|
|
255
|
+
const parts = [];
|
|
256
|
+
if (result.missingSourceEntryIds.length > 0)
|
|
257
|
+
parts.push(`missing: ${result.missingSourceEntryIds.join(", ")}`);
|
|
258
|
+
if (result.nonSourceEntryIds.length > 0)
|
|
259
|
+
parts.push(`non-source: ${result.nonSourceEntryIds.join(", ")}`);
|
|
260
|
+
sections.push(`Unavailable source entries: ${parts.join("; ")}`);
|
|
261
|
+
}
|
|
262
|
+
const sourceText = renderRecallSourceEntries(result.sourceEntries);
|
|
263
|
+
if (sourceText.trim())
|
|
264
|
+
sections.push(`Sources:\n${sourceText}`);
|
|
265
|
+
if (sections.length === 0)
|
|
266
|
+
sections.push(`Memory ${result.memoryId} was found, but no source evidence rendered.`);
|
|
267
|
+
return sections.join("\n\n");
|
|
268
|
+
}
|
|
269
|
+
function resultDetails(result, includeSourceContent = true) {
|
|
270
|
+
const reflections = result.reflectionMatches.map((match) => reflectionDetails(match.reflection, match.reflectionIndex));
|
|
271
|
+
const memoryLayerRecall = result.reflectionMatches.length > 0 || result.unavailableSupportingObservations.length > 0;
|
|
272
|
+
const includeObservationSources = (_match) => includeSourceContent;
|
|
273
|
+
const observations = result.observations.map((match) => observationMatchDetails(match, includeObservationSources(match)));
|
|
274
|
+
const directObservationMatches = result.directObservationMatches.map((match) => observationMatchDetails(match, includeObservationSources(match)));
|
|
275
|
+
const sourceEntries = memoryLayerRecall ? result.sourceEntries.map((entry) => sourceEntryDetails(entry, includeSourceContent)) : [];
|
|
276
|
+
const unavailableSupportingObservations = result.unavailableSupportingObservations.map((item) => ({
|
|
277
|
+
reflectionId: item.reflection.id,
|
|
278
|
+
reflectionIndex: item.reflectionIndex,
|
|
279
|
+
observationId: item.observationId,
|
|
280
|
+
}));
|
|
281
|
+
const unavailableReflectionProvenance = result.unavailableReflectionProvenance.map((item) => ({
|
|
282
|
+
reflectionId: item.reflection.id,
|
|
283
|
+
reflectionIndex: item.reflectionIndex,
|
|
284
|
+
reason: item.reason,
|
|
285
|
+
}));
|
|
286
|
+
const partial = result.partial;
|
|
287
|
+
const detailWithoutStatus = {
|
|
288
|
+
memoryId: result.memoryId,
|
|
289
|
+
observationId: result.memoryId,
|
|
290
|
+
collision: result.collision,
|
|
291
|
+
partial,
|
|
292
|
+
reflections,
|
|
293
|
+
directObservationMatches,
|
|
294
|
+
observations,
|
|
295
|
+
matches: directObservationMatches,
|
|
296
|
+
sourceEntries,
|
|
297
|
+
unavailableSupportingObservations,
|
|
298
|
+
unavailableReflectionProvenance,
|
|
299
|
+
missingSourceEntryIds: result.missingSourceEntryIds,
|
|
300
|
+
nonSourceEntryIds: result.nonSourceEntryIds,
|
|
301
|
+
sourceCharacterCount: renderRecallSourceEntries(result.sourceEntries).length,
|
|
302
|
+
};
|
|
303
|
+
return {
|
|
304
|
+
status: aggregateStatus(detailWithoutStatus),
|
|
305
|
+
...detailWithoutStatus,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function isObservationOnly(details) {
|
|
309
|
+
return details.reflections.length === 0 && details.unavailableSupportingObservations.length === 0 && details.unavailableReflectionProvenance.length === 0;
|
|
310
|
+
}
|
|
311
|
+
function renderFoundResult(result) {
|
|
312
|
+
const details = resultDetails(result);
|
|
313
|
+
const text = isObservationOnly(details) ? renderObservationOnlyTextFromResult(result) : renderMemoryText(result);
|
|
314
|
+
return textResult(text, details);
|
|
315
|
+
}
|
|
316
|
+
function plural(n, singular, pluralForm = `${singular}s`) {
|
|
317
|
+
return `${n.toLocaleString()} ${n === 1 ? singular : pluralForm}`;
|
|
318
|
+
}
|
|
319
|
+
function sourceEntriesFromDetails(details) {
|
|
320
|
+
if (!isObservationOnly(details))
|
|
321
|
+
return details.sourceEntries;
|
|
322
|
+
return details.matches.flatMap((match) => match.sourceEntries ?? []);
|
|
323
|
+
}
|
|
324
|
+
function tokenSummary(tokens) {
|
|
325
|
+
return `~${tokens.toLocaleString()} ${tokens === 1 ? "token" : "tokens"}`;
|
|
326
|
+
}
|
|
327
|
+
function isFailureStatus(status) {
|
|
328
|
+
return status === "invalid_id" || status === "not_found";
|
|
329
|
+
}
|
|
330
|
+
function observationCountForHeader(details) {
|
|
331
|
+
return isObservationOnly(details) ? details.matches.length : details.observations.length;
|
|
332
|
+
}
|
|
333
|
+
export function formatRecallHeaderForTui(details) {
|
|
334
|
+
if (isFailureStatus(details.status))
|
|
335
|
+
return "× failure";
|
|
336
|
+
const parts = ["✓ success"];
|
|
337
|
+
if (details.reflections.length > 0)
|
|
338
|
+
parts.push(plural(details.reflections.length, "reflection"));
|
|
339
|
+
const observations = observationCountForHeader(details);
|
|
340
|
+
if (observations > 0)
|
|
341
|
+
parts.push(plural(observations, "observation"));
|
|
342
|
+
const sources = sourceEntriesFromDetails(details);
|
|
343
|
+
if (sources.length > 0)
|
|
344
|
+
parts.push(plural(sources.length, "source"));
|
|
345
|
+
const tokens = sources.reduce((sum, source) => sum + source.tokens, 0);
|
|
346
|
+
if (tokens > 0)
|
|
347
|
+
parts.push(tokenSummary(tokens));
|
|
348
|
+
return parts.join(" · ");
|
|
349
|
+
}
|
|
350
|
+
const TUI_TYPE_WIDTH = 15;
|
|
351
|
+
const TUI_META_WIDTH = 31;
|
|
352
|
+
function alignedRow(type, meta, text) {
|
|
353
|
+
return `${type.padEnd(TUI_TYPE_WIDTH)} ${meta.padEnd(TUI_META_WIDTH)} ${text}`.trimEnd();
|
|
354
|
+
}
|
|
355
|
+
function sourceTag(source) {
|
|
356
|
+
const origin = source.origin.trim().toLowerCase();
|
|
357
|
+
if (origin === "user")
|
|
358
|
+
return "user";
|
|
359
|
+
if (origin === "assistant")
|
|
360
|
+
return "assistant";
|
|
361
|
+
if (origin.startsWith("tool result"))
|
|
362
|
+
return "tool";
|
|
363
|
+
if (origin.startsWith("custom message"))
|
|
364
|
+
return "custom";
|
|
365
|
+
if (origin.startsWith("branch summary"))
|
|
366
|
+
return "summary";
|
|
367
|
+
return origin.split(/[^a-z0-9]+/).find(Boolean) ?? "entry";
|
|
368
|
+
}
|
|
369
|
+
function sourceMetadataLine(source) {
|
|
370
|
+
return alignedRow("✓ source", `${source.timestamp} [${sourceTag(source)}]`, tokenSummary(source.tokens));
|
|
371
|
+
}
|
|
372
|
+
function observationLine(observation) {
|
|
373
|
+
return alignedRow("✓ observation", `${observation.timestamp} [${observation.relevance}]`, observation.content);
|
|
374
|
+
}
|
|
375
|
+
function reflectionLine(reflection) {
|
|
376
|
+
return alignedRow("✓ reflection", "", reflection.content);
|
|
377
|
+
}
|
|
378
|
+
function noteLine(kind, text) {
|
|
379
|
+
return alignedRow("• note", `[${kind}]`, text);
|
|
380
|
+
}
|
|
381
|
+
function indentContent(content) {
|
|
382
|
+
return content
|
|
383
|
+
.split("\n")
|
|
384
|
+
.map((line) => ` ${line}`)
|
|
385
|
+
.join("\n");
|
|
386
|
+
}
|
|
387
|
+
function unavailableEvidenceMessage(details) {
|
|
388
|
+
if (details.unavailableReflectionProvenance.length > 0 && details.observations.length === 0) {
|
|
389
|
+
return "migrated legacy reflection has no supporting observations";
|
|
390
|
+
}
|
|
391
|
+
return "no source entries are available for this memory id";
|
|
392
|
+
}
|
|
393
|
+
function pushSourceLines(lines, sources, expanded) {
|
|
394
|
+
for (const source of sources) {
|
|
395
|
+
lines.push(sourceMetadataLine(source));
|
|
396
|
+
if (expanded && source.content) {
|
|
397
|
+
lines.push(indentContent(source.content));
|
|
398
|
+
lines.push("");
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function memoryRows(details) {
|
|
403
|
+
if (isObservationOnly(details))
|
|
404
|
+
return details.matches.map((match) => observationLine(match.observation));
|
|
405
|
+
return [
|
|
406
|
+
...details.reflections.map((reflection) => reflectionLine(reflection)),
|
|
407
|
+
...details.observations.map((observation) => observationLine(observation.observation)),
|
|
408
|
+
];
|
|
409
|
+
}
|
|
410
|
+
function noteRows(details, sources) {
|
|
411
|
+
const notes = [];
|
|
412
|
+
if (details.status === "invalid_id") {
|
|
413
|
+
notes.push(noteLine("invalid id", `memory ids must be 12 lowercase hex characters; received ${details.memoryId}`));
|
|
414
|
+
return notes;
|
|
415
|
+
}
|
|
416
|
+
if (details.status === "not_found") {
|
|
417
|
+
notes.push(noteLine("not found", `no observation or reflection with id ${details.memoryId} was found on the current branch`));
|
|
418
|
+
return notes;
|
|
419
|
+
}
|
|
420
|
+
if (details.collision)
|
|
421
|
+
notes.push(noteLine("id collision", `multiple memory items share ${details.memoryId}`));
|
|
422
|
+
if (sources.length === 0 && (details.reflections.length > 0 || details.observations.length > 0 || details.matches.length > 0)) {
|
|
423
|
+
notes.push(noteLine("unavailable evidence", unavailableEvidenceMessage(details)));
|
|
424
|
+
}
|
|
425
|
+
return notes;
|
|
426
|
+
}
|
|
427
|
+
export function formatRecallResultForTui(result, expanded) {
|
|
428
|
+
const details = result.details;
|
|
429
|
+
if (!details) {
|
|
430
|
+
const text = result.content
|
|
431
|
+
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
432
|
+
.map((part) => part.text)
|
|
433
|
+
.join("\n");
|
|
434
|
+
return text || "recall";
|
|
435
|
+
}
|
|
436
|
+
const sources = sourceEntriesFromDetails(details);
|
|
437
|
+
const lines = [];
|
|
438
|
+
const rows = memoryRows(details);
|
|
439
|
+
const notes = noteRows(details, sources);
|
|
440
|
+
lines.push(...rows);
|
|
441
|
+
if (rows.length > 0 && notes.length > 0)
|
|
442
|
+
lines.push("");
|
|
443
|
+
lines.push(...notes);
|
|
444
|
+
if ((rows.length > 0 || notes.length > 0) && sources.length > 0)
|
|
445
|
+
lines.push("");
|
|
446
|
+
pushSourceLines(lines, sources, expanded);
|
|
447
|
+
if (!expanded && sources.some((source) => source.content)) {
|
|
448
|
+
lines.push("", "(Ctrl+O to expand)");
|
|
449
|
+
}
|
|
450
|
+
return lines.join("\n").trimEnd();
|
|
451
|
+
}
|
|
452
|
+
export function formatRecallCallForTui(id) {
|
|
453
|
+
return `recall ${id ?? "..."}`;
|
|
454
|
+
}
|
|
455
|
+
export function formatRecallRenderedResultForTui(result, expanded) {
|
|
456
|
+
const body = formatRecallResultForTui(result, expanded);
|
|
457
|
+
const header = result.details ? formatRecallHeaderForTui(result.details) : undefined;
|
|
458
|
+
if (header && body)
|
|
459
|
+
return `\n${header}\n\n${body}`;
|
|
460
|
+
if (header)
|
|
461
|
+
return `\n${header}`;
|
|
462
|
+
return body ? `\n${body}` : "";
|
|
463
|
+
}
|
|
464
|
+
export const recallObservationTool = defineTool({
|
|
465
|
+
name: RECALL_OBSERVATION_TOOL_NAME,
|
|
466
|
+
label: "Recall memory evidence",
|
|
467
|
+
description: "Recover exact evidence and source context behind a compacted observational-memory observation or reflection id on the current branch. " +
|
|
468
|
+
"Use when compressed memory is important and original source context is needed before acting.",
|
|
469
|
+
promptSnippet: "Use recall(<id>) to recover exact source context behind compacted memory observations/reflections when precision matters.",
|
|
470
|
+
promptGuidelines: [
|
|
471
|
+
"Use recall before making an important decision that depends on a compacted observation or reflection whose details are unclear.",
|
|
472
|
+
"Use recall when you need exact wording, rationale, file paths, commands, errors, commits, user constraints, or provenance behind a remembered claim.",
|
|
473
|
+
"Use recall when a broad reflection is relevant but you need its supporting observations or raw sources to continue safely.",
|
|
474
|
+
"Use recall when the user asks why you believe something, what supports a memory, or what was decided earlier.",
|
|
475
|
+
"Do not use recall as semantic search or transcript browsing; you must already have a specific 12-character memory id.",
|
|
476
|
+
"Do not recall every id preemptively. Recall only when exact source context will materially improve the next action.",
|
|
477
|
+
],
|
|
478
|
+
parameters: Type.Object({
|
|
479
|
+
id: Type.String({
|
|
480
|
+
pattern: "^[a-f0-9]{12}$",
|
|
481
|
+
description: "12-character lowercase hex observation or reflection id shown in compacted memory, /om-view, or a previous recall result. " +
|
|
482
|
+
"Must be a specific id; this tool does not search by topic.",
|
|
483
|
+
}),
|
|
484
|
+
}),
|
|
485
|
+
renderCall(args) {
|
|
486
|
+
return new Text(formatRecallCallForTui(args.id), 0, 0);
|
|
487
|
+
},
|
|
488
|
+
renderResult(result, options) {
|
|
489
|
+
return new Text(formatRecallRenderedResultForTui(result, options.expanded), 0, 0);
|
|
490
|
+
},
|
|
491
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
492
|
+
const memoryId = params.id;
|
|
493
|
+
if (!MEMORY_ID_PATTERN.test(memoryId)) {
|
|
494
|
+
const message = `Memory id must be 12 lowercase hex characters. Received: ${memoryId}`;
|
|
495
|
+
return textResult(message, emptyDetails("invalid_id", memoryId, message));
|
|
496
|
+
}
|
|
497
|
+
const branchEntries = ctx.sessionManager.getBranch();
|
|
498
|
+
const result = recallMemorySources(branchEntries, memoryId);
|
|
499
|
+
if (result.status === "not_found") {
|
|
500
|
+
const message = `No observation or reflection with id ${memoryId} was found on the current branch.`;
|
|
501
|
+
return textResult(message, emptyDetails("not_found", memoryId, message));
|
|
502
|
+
}
|
|
503
|
+
return renderFoundResult(result);
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
export function registerRecallTool(pi) {
|
|
507
|
+
pi.registerTool(recallObservationTool);
|
|
508
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export const OBSERVATION_CUSTOM_TYPE = "om.observation";
|
|
2
|
+
export const RELEVANCE_VALUES = ["low", "medium", "high", "critical"];
|
|
3
|
+
export const MEMORY_ID_PATTERN = /^[a-f0-9]{12}$/;
|
|
4
|
+
function isRelevance(v) {
|
|
5
|
+
return typeof v === "string" && RELEVANCE_VALUES.includes(v);
|
|
6
|
+
}
|
|
7
|
+
function isObservationRecord(v) {
|
|
8
|
+
if (!v || typeof v !== "object")
|
|
9
|
+
return false;
|
|
10
|
+
const o = v;
|
|
11
|
+
if (typeof o.id !== "string" ||
|
|
12
|
+
typeof o.content !== "string" ||
|
|
13
|
+
typeof o.timestamp !== "string" ||
|
|
14
|
+
!isRelevance(o.relevance)) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (o.sourceEntryIds === undefined)
|
|
18
|
+
return true;
|
|
19
|
+
return isNonEmptyStringArray(o.sourceEntryIds);
|
|
20
|
+
}
|
|
21
|
+
function isNonEmptyStringArray(v) {
|
|
22
|
+
return Array.isArray(v) && v.length > 0 && v.every((id) => typeof id === "string" && id.length > 0);
|
|
23
|
+
}
|
|
24
|
+
function isEmptyStringArray(v) {
|
|
25
|
+
return Array.isArray(v) && v.length === 0;
|
|
26
|
+
}
|
|
27
|
+
export function isReflectionRecord(v) {
|
|
28
|
+
if (!v || typeof v !== "object")
|
|
29
|
+
return false;
|
|
30
|
+
const o = v;
|
|
31
|
+
if (typeof o.id !== "string" ||
|
|
32
|
+
!MEMORY_ID_PATTERN.test(o.id) ||
|
|
33
|
+
typeof o.content !== "string" ||
|
|
34
|
+
o.content.trim().length === 0 ||
|
|
35
|
+
/[\r\n]/.test(o.content)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (o.legacy !== undefined && typeof o.legacy !== "boolean")
|
|
39
|
+
return false;
|
|
40
|
+
if (o.legacy === true)
|
|
41
|
+
return isEmptyStringArray(o.supportingObservationIds);
|
|
42
|
+
return isNonEmptyStringArray(o.supportingObservationIds);
|
|
43
|
+
}
|
|
44
|
+
export function isMemoryReflection(v) {
|
|
45
|
+
return typeof v === "string" || isReflectionRecord(v);
|
|
46
|
+
}
|
|
47
|
+
export function reflectionContent(reflection) {
|
|
48
|
+
return typeof reflection === "string" ? reflection : reflection.content;
|
|
49
|
+
}
|
|
50
|
+
export function reflectionId(reflection) {
|
|
51
|
+
return typeof reflection === "string" ? undefined : reflection.id;
|
|
52
|
+
}
|
|
53
|
+
export function reflectionToPromptLine(reflection) {
|
|
54
|
+
return typeof reflection === "string" ? reflection : `[${reflection.id}] ${reflection.content}`;
|
|
55
|
+
}
|
|
56
|
+
export function isMemoryDetailsV3(d) {
|
|
57
|
+
if (!d || typeof d !== "object")
|
|
58
|
+
return false;
|
|
59
|
+
const o = d;
|
|
60
|
+
if (o.type !== "observational-memory" || o.version !== 3)
|
|
61
|
+
return false;
|
|
62
|
+
if (!Array.isArray(o.observations) || !Array.isArray(o.reflections))
|
|
63
|
+
return false;
|
|
64
|
+
if (!o.observations.every(isObservationRecord))
|
|
65
|
+
return false;
|
|
66
|
+
return o.reflections.every((r) => typeof r === "string");
|
|
67
|
+
}
|
|
68
|
+
export function isMemoryDetailsV4(d) {
|
|
69
|
+
if (!d || typeof d !== "object")
|
|
70
|
+
return false;
|
|
71
|
+
const o = d;
|
|
72
|
+
if (o.type !== "observational-memory" || o.version !== 4)
|
|
73
|
+
return false;
|
|
74
|
+
if (!Array.isArray(o.observations) || !Array.isArray(o.reflections))
|
|
75
|
+
return false;
|
|
76
|
+
if (!o.observations.every(isObservationRecord))
|
|
77
|
+
return false;
|
|
78
|
+
return o.reflections.every(isMemoryReflection);
|
|
79
|
+
}
|
|
80
|
+
export function isSupportedMemoryDetails(d) {
|
|
81
|
+
return isMemoryDetailsV3(d) || isMemoryDetailsV4(d);
|
|
82
|
+
}
|
|
83
|
+
export function isMemoryDetails(d) {
|
|
84
|
+
return isMemoryDetailsV3(d);
|
|
85
|
+
}
|
|
86
|
+
export function isObservationEntryData(d) {
|
|
87
|
+
if (!d || typeof d !== "object")
|
|
88
|
+
return false;
|
|
89
|
+
const o = d;
|
|
90
|
+
return (Array.isArray(o.records) &&
|
|
91
|
+
o.records.every(isObservationRecord) &&
|
|
92
|
+
typeof o.coversFromId === "string" &&
|
|
93
|
+
typeof o.coversUpToId === "string" &&
|
|
94
|
+
typeof o.tokenCount === "number");
|
|
95
|
+
}
|
package/dist/relay/dispatcher.js
CHANGED
|
@@ -488,6 +488,7 @@ export function handleClientMessage(frame, deps) {
|
|
|
488
488
|
sessionId,
|
|
489
489
|
history: attachResult.history,
|
|
490
490
|
currentTurn: attachResult.currentTurn,
|
|
491
|
+
forkCompactPending: attachResult.forkCompactPending || undefined,
|
|
491
492
|
},
|
|
492
493
|
});
|
|
493
494
|
// Surface bridge-start failures as `error` events; otherwise the
|
|
@@ -559,6 +560,7 @@ export function handleSubscribe(frame, deps) {
|
|
|
559
560
|
sessionId,
|
|
560
561
|
history: attachResult.history,
|
|
561
562
|
currentTurn: attachResult.currentTurn,
|
|
563
|
+
forkCompactPending: attachResult.forkCompactPending || undefined,
|
|
562
564
|
},
|
|
563
565
|
});
|
|
564
566
|
if (isNewSubscriber) {
|
package/dist/server/storage.js
CHANGED
|
@@ -417,7 +417,7 @@ export class SessionStore {
|
|
|
417
417
|
if (!row)
|
|
418
418
|
return null;
|
|
419
419
|
const messages = this.getMessages(id);
|
|
420
|
-
|
|
420
|
+
const detail = {
|
|
421
421
|
id: row.id,
|
|
422
422
|
projectId: row.project_id,
|
|
423
423
|
title: row.title,
|
|
@@ -425,6 +425,12 @@ export class SessionStore {
|
|
|
425
425
|
updatedAt: row.updated_at,
|
|
426
426
|
messages,
|
|
427
427
|
};
|
|
428
|
+
// Expose the fork-compact flag so the UI can show a special send
|
|
429
|
+
// button for the first message of a forked session.
|
|
430
|
+
if (this.getForkCompactSource(id)) {
|
|
431
|
+
detail.forkCompactPending = true;
|
|
432
|
+
}
|
|
433
|
+
return detail;
|
|
428
434
|
}
|
|
429
435
|
/** Convenience for routes/managers that just need the projectId. */
|
|
430
436
|
getSessionProjectId(id) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aexol/spectral",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "Always-on coding agent for Aexol — branded pi wrapper with relay-based browser access.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"@mariozechner/jiti": "^2.6.5",
|
|
55
55
|
"@mariozechner/pi-coding-agent": "^0.70.2",
|
|
56
56
|
"better-sqlite3": "^12.9.0",
|
|
57
|
+
"@mariozechner/pi-agent-core": "^0.70.2",
|
|
57
58
|
"@mariozechner/pi-ai": "^0.70.2",
|
|
58
59
|
"@mariozechner/pi-tui": "^0.70.2",
|
|
59
60
|
"@modelcontextprotocol/sdk": "^1.25.1",
|