pi-observational-memory 2.4.3 → 3.0.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.
@@ -1,17 +1,17 @@
1
- import { Type } from "@mariozechner/pi-ai";
2
- import type { Message, ToolResultMessage } from "@mariozechner/pi-ai";
3
- import { defineTool, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
4
- import type { AgentToolResult } from "@mariozechner/pi-agent-core";
5
- import { Text } from "@mariozechner/pi-tui";
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import { Type } from "@earendil-works/pi-ai";
3
+ import type { Message, ToolResultMessage } from "@earendil-works/pi-ai";
4
+ import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
+ import { Text } from "@earendil-works/pi-tui";
6
6
  import {
7
7
  recallMemorySources,
8
8
  type Entry,
9
- type RecallMemoryObservation,
10
- type RecallMemorySourcesResult,
11
- } from "../branch.js";
9
+ type RecallResult,
10
+ type RecalledObservation,
11
+ } from "../session-ledger/recall.js";
12
+ import type { Observation, Reflection } from "../session-ledger/index.js";
12
13
  import { renderRecallSourceEntries, renderRecallSourceEntry } from "../serialize.js";
13
14
  import { estimateEntryTokens } from "../tokens.js";
14
- import type { ObservationRecord, ReflectionRecord } from "../types.js";
15
15
 
16
16
  export const RECALL_OBSERVATION_TOOL_NAME = "recall";
17
17
 
@@ -23,11 +23,10 @@ type RecallObservationToolStatus =
23
23
  | "invalid_id"
24
24
  | "not_found"
25
25
  | "no_source"
26
- | "source_unavailable"
27
- | "no_provenance";
26
+ | "source_unavailable";
28
27
 
29
- type ObservationDetails = Pick<ObservationRecord, "id" | "content" | "timestamp" | "relevance">;
30
- type ReflectionDetails = Pick<ReflectionRecord, "id" | "content" | "supportingObservationIds" | "legacy"> & { reflectionIndex: number };
28
+ type ObservationDetails = Pick<Observation, "id" | "content" | "timestamp" | "relevance"> & { status?: "active" | "dropped" };
29
+ type ReflectionDetails = Pick<Reflection, "id" | "content" | "supportingObservationIds"> & { reflectionIndex: number };
31
30
 
32
31
  export type RecallSourceEntryDetails = {
33
32
  id: string;
@@ -39,7 +38,7 @@ export type RecallSourceEntryDetails = {
39
38
  };
40
39
 
41
40
  type RecallObservationMatchDetails = {
42
- status: RecallMemoryObservation["status"];
41
+ status: "active" | "dropped" | "source_unavailable" | "no_source";
43
42
  observationEntryId: string;
44
43
  observationRecordIndex: number;
45
44
  observation: ObservationDetails;
@@ -51,17 +50,9 @@ type RecallObservationMatchDetails = {
51
50
  };
52
51
 
53
52
  type RecallUnavailableSupportingObservationDetails = {
54
- reflectionId: string;
55
- reflectionIndex: number;
56
53
  observationId: string;
57
54
  };
58
55
 
59
- type RecallUnavailableReflectionProvenanceDetails = {
60
- reflectionId: string;
61
- reflectionIndex: number;
62
- reason: "legacy";
63
- };
64
-
65
56
  export type RecallObservationToolDetails = {
66
57
  status: RecallObservationToolStatus;
67
58
  memoryId: string;
@@ -74,7 +65,6 @@ export type RecallObservationToolDetails = {
74
65
  matches: RecallObservationMatchDetails[];
75
66
  sourceEntries: RecallSourceEntryDetails[];
76
67
  unavailableSupportingObservations: RecallUnavailableSupportingObservationDetails[];
77
- unavailableReflectionProvenance: RecallUnavailableReflectionProvenanceDetails[];
78
68
  missingSourceEntryIds: string[];
79
69
  nonSourceEntryIds: string[];
80
70
  sourceCharacterCount?: number;
@@ -117,16 +107,11 @@ function sourceOriginAndQualifiers(entry: Entry): { origin: string; timestamp: s
117
107
  .filter((block) => block.type === "toolCall" && typeof block.name === "string")
118
108
  .map((block) => block.name as string),
119
109
  );
120
- return {
121
- origin: "Assistant",
122
- timestamp,
123
- qualifiers: toolCalls.length > 0 ? [`tool calls: ${toolCalls.join(", ")}`] : [],
124
- };
110
+ return { origin: "Assistant", timestamp, qualifiers: toolCalls.length > 0 ? [`tool calls: ${toolCalls.join(", ")}`] : [] };
125
111
  }
126
112
  const toolName = (msg as ToolResultMessage).toolName;
127
113
  return { origin: `Tool result: ${typeof toolName === "string" && toolName ? toolName : "unknown"}`, timestamp, qualifiers: [] };
128
114
  }
129
-
130
115
  if (entry.type === "custom_message") {
131
116
  return {
132
117
  origin: "Custom message",
@@ -134,11 +119,7 @@ function sourceOriginAndQualifiers(entry: Entry): { origin: string; timestamp: s
134
119
  qualifiers: typeof entry.customType === "string" && entry.customType ? [`custom: ${entry.customType}`] : [],
135
120
  };
136
121
  }
137
-
138
- if (entry.type === "branch_summary") {
139
- return { origin: "Branch summary", timestamp: formatDisplayTimestamp(entry.timestamp), qualifiers: [] };
140
- }
141
-
122
+ if (entry.type === "branch_summary") return { origin: "Branch summary", timestamp: formatDisplayTimestamp(entry.timestamp), qualifiers: [] };
142
123
  return { origin: entry.type || "Entry", timestamp: formatDisplayTimestamp(entry.timestamp), qualifiers: [] };
143
124
  }
144
125
 
@@ -160,67 +141,32 @@ function sourceEntryDetails(entry: Entry, includeContent: boolean): RecallSource
160
141
  };
161
142
  }
162
143
 
163
- function observationDetails(observation: ObservationRecord): ObservationDetails {
164
- return {
165
- id: observation.id,
166
- content: observation.content,
167
- timestamp: observation.timestamp,
168
- relevance: observation.relevance,
169
- };
144
+ function observationDetails(observation: Observation, status?: "active" | "dropped"): ObservationDetails {
145
+ return { id: observation.id, content: observation.content, timestamp: observation.timestamp, relevance: observation.relevance, ...(status ? { status } : {}) };
170
146
  }
171
147
 
172
- function reflectionDetails(reflection: ReflectionRecord, reflectionIndex: number): ReflectionDetails {
173
- return {
174
- id: reflection.id,
175
- content: reflection.content,
176
- supportingObservationIds: reflection.supportingObservationIds,
177
- ...(reflection.legacy === true ? { legacy: true } : {}),
178
- reflectionIndex,
179
- };
148
+ function reflectionDetails(reflection: Reflection, reflectionIndex: number): ReflectionDetails {
149
+ return { id: reflection.id, content: reflection.content, supportingObservationIds: reflection.supportingObservationIds, reflectionIndex };
180
150
  }
181
151
 
182
- function observationMatchDetails(match: RecallMemoryObservation, includeSourceContent = true): RecallObservationMatchDetails {
183
- if (match.status === "ok") {
184
- return {
185
- status: "ok",
186
- observationEntryId: match.observationEntryId,
187
- observationRecordIndex: match.observationRecordIndex,
188
- observation: observationDetails(match.observation),
189
- sourceEntryIds: match.sourceEntryIds,
190
- sourceEntries: match.sourceEntries.map((entry) => sourceEntryDetails(entry, includeSourceContent)),
191
- sourceCharacterCount: renderRecallSourceEntries(match.sourceEntries).length,
192
- };
193
- }
194
- if (match.status === "source_unavailable") {
195
- return {
196
- status: "source_unavailable",
197
- observationEntryId: match.observationEntryId,
198
- observationRecordIndex: match.observationRecordIndex,
199
- observation: observationDetails(match.observation),
200
- sourceEntryIds: match.sourceEntryIds,
201
- ...(includeSourceContent
202
- ? {
203
- sourceEntries: match.sourceEntries.map((entry) => sourceEntryDetails(entry, true)),
204
- sourceCharacterCount: renderRecallSourceEntries(match.sourceEntries).length,
205
- }
206
- : {}),
207
- missingSourceEntryIds: match.missingSourceEntryIds,
208
- nonSourceEntryIds: match.nonSourceEntryIds,
209
- };
210
- }
152
+ function observationMatchDetails(match: RecalledObservation, includeSourceContent = true): RecallObservationMatchDetails {
153
+ const unavailable = match.missingSourceEntryIds.length > 0 || match.nonSourceEntryIds.length > 0;
154
+ const status = unavailable ? "source_unavailable" : match.sourceEntries.length === 0 ? "no_source" : match.status;
211
155
  return {
212
- status: "no_source",
156
+ status,
213
157
  observationEntryId: match.observationEntryId,
214
158
  observationRecordIndex: match.observationRecordIndex,
215
- observation: observationDetails(match.observation),
159
+ observation: observationDetails(match.observation, match.status),
160
+ sourceEntryIds: match.sourceEntryIds,
161
+ sourceEntries: match.sourceEntries.map((entry) => sourceEntryDetails(entry, includeSourceContent)),
162
+ missingSourceEntryIds: match.missingSourceEntryIds,
163
+ nonSourceEntryIds: match.nonSourceEntryIds,
164
+ sourceCharacterCount: renderRecallSourceEntries(match.sourceEntries).length,
216
165
  };
217
166
  }
218
167
 
219
168
  function textResult(text: string, details: RecallObservationToolDetails) {
220
- return {
221
- content: [{ type: "text" as const, text }],
222
- details,
223
- };
169
+ return { content: [{ type: "text" as const, text }], details };
224
170
  }
225
171
 
226
172
  function emptyDetails(status: RecallObservationToolStatus, memoryId: string, message: string): RecallObservationToolDetails {
@@ -236,7 +182,6 @@ function emptyDetails(status: RecallObservationToolStatus, memoryId: string, mes
236
182
  matches: [],
237
183
  sourceEntries: [],
238
184
  unavailableSupportingObservations: [],
239
- unavailableReflectionProvenance: [],
240
185
  missingSourceEntryIds: [],
241
186
  nonSourceEntryIds: [],
242
187
  message,
@@ -244,20 +189,15 @@ function emptyDetails(status: RecallObservationToolStatus, memoryId: string, mes
244
189
  }
245
190
 
246
191
  function aggregateStatus(details: Omit<RecallObservationToolDetails, "status">): RecallObservationToolStatus {
247
- const observationOnly = details.reflections.length === 0 && details.unavailableSupportingObservations.length === 0 && details.unavailableReflectionProvenance.length === 0;
248
- if (observationOnly && details.observations.some((match) => match.status === "ok")) return "ok";
249
- if (observationOnly && details.observations.some((match) => match.status === "source_unavailable")) return "source_unavailable";
250
- if (observationOnly && details.observations.length > 0) return "no_source";
251
- if (details.unavailableReflectionProvenance.length > 0 && details.observations.length === 0 && details.sourceEntries.length === 0) return "no_provenance";
192
+ const observationOnly = details.reflections.length === 0 && details.unavailableSupportingObservations.length === 0;
252
193
  if (details.partial) return "partial";
253
- if (details.sourceEntries.length > 0) return "ok";
254
- if (details.reflections.length > 0) return "ok";
255
- if (details.observations.length > 0) return "no_source";
256
- return "not_found";
194
+ if (observationOnly && details.observations.some((match) => match.status === "source_unavailable")) return "source_unavailable";
195
+ if (observationOnly && details.observations.length > 0 && details.sourceEntries.length === 0 && details.matches.every((match) => (match.sourceEntries ?? []).length === 0)) return "no_source";
196
+ return "ok";
257
197
  }
258
198
 
259
199
  function friendlyNoSourceMessage(memoryId: string): string {
260
- return `Observation ${memoryId} has no source entries associated with it. This can happen for legacy observations created before source recall was available.`;
200
+ return `Observation ${memoryId} has no source entries associated with it.`;
261
201
  }
262
202
 
263
203
  function friendlySourceUnavailableMessage(match: RecallObservationMatchDetails): string {
@@ -271,79 +211,43 @@ function reflectionLineText(reflection: ReflectionDetails): string {
271
211
  }
272
212
 
273
213
  function observationLineText(observation: ObservationDetails): string {
274
- return `[${observation.id}] ${observation.timestamp} [${observation.relevance}] ${observation.content}`;
214
+ const status = observation.status === "dropped" ? " [dropped]" : "";
215
+ return `[${observation.id}]${status} ${observation.timestamp} [${observation.relevance}] ${observation.content}`;
216
+ }
217
+
218
+ function directObservationMatches(result: Extract<RecallResult, { status: "found" }>): RecalledObservation[] {
219
+ return result.observations.filter((match) => match.observation.id === result.memoryId);
275
220
  }
276
221
 
277
- function renderObservationOnlyTextFromResult(result: Extract<RecallMemorySourcesResult, { status: "found" }>): string {
222
+ function renderObservationOnlyTextFromResult(result: Extract<RecallResult, { status: "found" }>): string {
278
223
  const sections: string[] = [];
279
- if (result.collision) {
280
- sections.push(`Multiple observations share id ${result.memoryId}; returning all matching source results from the current branch.`);
281
- }
282
- for (const match of result.directObservationMatches) {
283
- if (match.status === "ok") {
284
- const sourceText = renderRecallSourceEntries(match.sourceEntries);
285
- if (sourceText.trim()) sections.push(sourceText);
286
- else sections.push(`Observation ${match.observation.id} has source entries associated, but they rendered no text content.`);
224
+ if (result.collision) sections.push(`Memory id ${result.memoryId} matched multiple observations; returning all matching source results from the current branch.`);
225
+ for (const match of directObservationMatches(result)) {
226
+ if (match.status === "dropped") sections.push(`Observation ${match.observation.id} is dropped from active memory but remains recallable.`);
227
+ if (match.missingSourceEntryIds.length > 0 || match.nonSourceEntryIds.length > 0) {
228
+ sections.push(friendlySourceUnavailableMessage(observationMatchDetails(match, false)));
287
229
  continue;
288
230
  }
289
- if (match.status === "source_unavailable") {
290
- sections.push(friendlySourceUnavailableMessage(observationMatchDetails(match, false)));
231
+ if (match.sourceEntries.length === 0) {
232
+ sections.push(friendlyNoSourceMessage(match.observation.id));
291
233
  continue;
292
234
  }
293
- sections.push(friendlyNoSourceMessage(match.observation.id));
235
+ const sourceText = renderRecallSourceEntries(match.sourceEntries);
236
+ sections.push(sourceText.trim() ? sourceText : `Observation ${match.observation.id} has source entries associated, but they rendered no text content.`);
294
237
  }
295
238
  return sections.join("\n\n");
296
239
  }
297
240
 
298
241
  function unavailableSupportingLineText(item: RecallUnavailableSupportingObservationDetails): string {
299
- return `Supporting observation ${item.observationId} for reflection ${item.reflectionId} is unavailable on the current branch.`;
300
- }
301
-
302
- function unavailableReflectionProvenanceLineText(item: RecallUnavailableReflectionProvenanceDetails): string {
303
- return `Reflection ${item.reflectionId} was migrated from legacy memory created before reflection provenance was recorded, so no supporting observations or raw sources are available.`;
304
- }
305
-
306
- function unavailableObservationSourceLineText(match: RecallMemoryObservation): string {
307
- return `Observation ${match.observation.id} has no source entries associated. This can happen for legacy observations created before source recall was available.`;
242
+ return `Supporting observation ${item.observationId} is unavailable on the current branch.`;
308
243
  }
309
244
 
310
- function renderMemoryText(result: Extract<RecallMemorySourcesResult, { status: "found" }>): string {
245
+ function renderMemoryText(result: Extract<RecallResult, { status: "found" }>): string {
311
246
  const sections: string[] = [];
312
- if (result.collision) {
313
- sections.push(`Memory id ${result.memoryId} matched multiple observations/reflections; returning all available evidence from the current branch.`);
314
- }
315
- if (result.reflectionMatches.length > 0) {
316
- sections.push(`Reflections:\n${result.reflectionMatches.map((match) => reflectionLineText(reflectionDetails(match.reflection, match.reflectionIndex))).join("\n")}`);
317
- }
318
- if (result.observations.length > 0) {
319
- sections.push(`Observations:\n${result.observations.map((match) => observationLineText(match.observation)).join("\n")}`);
320
- }
321
- if (result.unavailableSupportingObservations.length > 0) {
322
- sections.push(
323
- `Unavailable supporting observations:\n${result.unavailableSupportingObservations
324
- .map((item) => unavailableSupportingLineText({
325
- reflectionId: item.reflection.id,
326
- reflectionIndex: item.reflectionIndex,
327
- observationId: item.observationId,
328
- }))
329
- .join("\n")}`,
330
- );
331
- }
332
- if (result.unavailableReflectionProvenance.length > 0) {
333
- sections.push(
334
- `Unavailable reflection provenance:\n${result.unavailableReflectionProvenance
335
- .map((item) => unavailableReflectionProvenanceLineText({
336
- reflectionId: item.reflection.id,
337
- reflectionIndex: item.reflectionIndex,
338
- reason: item.reason,
339
- }))
340
- .join("\n")}`,
341
- );
342
- }
343
- const noSourceObservations = result.observations.filter((match) => match.status === "no_source");
344
- if (noSourceObservations.length > 0) {
345
- sections.push(`Unavailable observation sources:\n${noSourceObservations.map(unavailableObservationSourceLineText).join("\n")}`);
346
- }
247
+ if (result.collision) sections.push(`Memory id ${result.memoryId} matched multiple observations/reflections; returning all available evidence from the current branch.`);
248
+ if (result.reflections.length > 0) sections.push(`Reflections:\n${result.reflections.map((match) => reflectionLineText(reflectionDetails(match.reflection, match.reflectionRecordIndex))).join("\n")}`);
249
+ if (result.observations.length > 0) sections.push(`Observations:\n${result.observations.map((match) => observationLineText(observationDetails(match.observation, match.status))).join("\n")}`);
250
+ if (result.missingSupportingObservationIds.length > 0) sections.push(`Unavailable supporting observations:\n${result.missingSupportingObservationIds.map((id) => unavailableSupportingLineText({ observationId: id })).join("\n")}`);
347
251
  if (result.missingSourceEntryIds.length > 0 || result.nonSourceEntryIds.length > 0) {
348
252
  const parts: string[] = [];
349
253
  if (result.missingSourceEntryIds.length > 0) parts.push(`missing: ${result.missingSourceEntryIds.join(", ")}`);
@@ -356,53 +260,36 @@ function renderMemoryText(result: Extract<RecallMemorySourcesResult, { status: "
356
260
  return sections.join("\n\n");
357
261
  }
358
262
 
359
- function resultDetails(result: Extract<RecallMemorySourcesResult, { status: "found" }>, includeSourceContent = true): RecallObservationToolDetails {
360
- const reflections = result.reflectionMatches.map((match) => reflectionDetails(match.reflection, match.reflectionIndex));
361
- const memoryLayerRecall = result.reflectionMatches.length > 0 || result.unavailableSupportingObservations.length > 0;
362
- const includeObservationSources = (_match: RecallMemoryObservation) => includeSourceContent;
363
- const observations = result.observations.map((match) => observationMatchDetails(match, includeObservationSources(match)));
364
- const directObservationMatches = result.directObservationMatches.map((match) => observationMatchDetails(match, includeObservationSources(match)));
365
- const sourceEntries = memoryLayerRecall ? result.sourceEntries.map((entry) => sourceEntryDetails(entry, includeSourceContent)) : [];
366
- const unavailableSupportingObservations = result.unavailableSupportingObservations.map((item) => ({
367
- reflectionId: item.reflection.id,
368
- reflectionIndex: item.reflectionIndex,
369
- observationId: item.observationId,
370
- }));
371
- const unavailableReflectionProvenance = result.unavailableReflectionProvenance.map((item) => ({
372
- reflectionId: item.reflection.id,
373
- reflectionIndex: item.reflectionIndex,
374
- reason: item.reason,
375
- }));
376
- const partial = result.partial;
263
+ function resultDetails(result: Extract<RecallResult, { status: "found" }>, includeSourceContent = true): RecallObservationToolDetails {
264
+ const reflections = result.reflections.map((match) => reflectionDetails(match.reflection, match.reflectionRecordIndex));
265
+ const observations = result.observations.map((match) => observationMatchDetails(match, includeSourceContent));
266
+ const directMatches = directObservationMatches(result).map((match) => observationMatchDetails(match, includeSourceContent));
267
+ const sourceEntries = result.sourceEntries.map((entry) => sourceEntryDetails(entry, includeSourceContent));
377
268
  const detailWithoutStatus = {
378
269
  memoryId: result.memoryId,
379
270
  observationId: result.memoryId,
380
271
  collision: result.collision,
381
- partial,
272
+ partial: result.partial,
382
273
  reflections,
383
- directObservationMatches,
274
+ directObservationMatches: directMatches,
384
275
  observations,
385
- matches: directObservationMatches,
276
+ matches: directMatches,
386
277
  sourceEntries,
387
- unavailableSupportingObservations,
388
- unavailableReflectionProvenance,
278
+ unavailableSupportingObservations: result.missingSupportingObservationIds.map((observationId) => ({ observationId })),
389
279
  missingSourceEntryIds: result.missingSourceEntryIds,
390
280
  nonSourceEntryIds: result.nonSourceEntryIds,
391
281
  sourceCharacterCount: renderRecallSourceEntries(result.sourceEntries).length,
392
282
  };
393
- return {
394
- status: aggregateStatus(detailWithoutStatus),
395
- ...detailWithoutStatus,
396
- };
283
+ return { status: aggregateStatus(detailWithoutStatus), ...detailWithoutStatus };
397
284
  }
398
285
 
399
286
  function isObservationOnly(details: RecallObservationToolDetails): boolean {
400
- return details.reflections.length === 0 && details.unavailableSupportingObservations.length === 0 && details.unavailableReflectionProvenance.length === 0;
287
+ return details.reflections.length === 0 && details.unavailableSupportingObservations.length === 0;
401
288
  }
402
289
 
403
- function renderFoundResult(result: Extract<RecallMemorySourcesResult, { status: "found" }>): ReturnType<typeof textResult> {
290
+ function renderFoundResult(result: Extract<RecallResult, { status: "found" }>): ReturnType<typeof textResult> {
404
291
  const details = resultDetails(result);
405
- const text = isObservationOnly(details) ? renderObservationOnlyTextFromResult(result) : renderMemoryText(result);
292
+ const text = result.kind === "observation" ? renderObservationOnlyTextFromResult(result) : renderMemoryText(result);
406
293
  return textResult(text, details);
407
294
  }
408
295
 
@@ -429,7 +316,6 @@ function observationCountForHeader(details: RecallObservationToolDetails): numbe
429
316
 
430
317
  export function formatRecallHeaderForTui(details: RecallObservationToolDetails): string {
431
318
  if (isFailureStatus(details.status)) return "× failure";
432
-
433
319
  const parts = ["✓ success"];
434
320
  if (details.reflections.length > 0) parts.push(plural(details.reflections.length, "reflection"));
435
321
  const observations = observationCountForHeader(details);
@@ -438,6 +324,7 @@ export function formatRecallHeaderForTui(details: RecallObservationToolDetails):
438
324
  if (sources.length > 0) parts.push(plural(sources.length, "source"));
439
325
  const tokens = sources.reduce((sum, source) => sum + source.tokens, 0);
440
326
  if (tokens > 0) parts.push(tokenSummary(tokens));
327
+ if (details.partial && details.status !== "ok") parts.push(details.status.replace(/_/g, " "));
441
328
  return parts.join(" · ");
442
329
  }
443
330
 
@@ -463,7 +350,8 @@ function sourceMetadataLine(source: RecallSourceEntryDetails): string {
463
350
  }
464
351
 
465
352
  function observationLine(observation: ObservationDetails): string {
466
- return alignedRow("✓ observation", `${observation.timestamp} [${observation.relevance}]`, observation.content);
353
+ const status = observation.status === "dropped" ? " dropped" : "";
354
+ return alignedRow("✓ observation", `${observation.timestamp} [${observation.relevance}]${status}`, observation.content);
467
355
  }
468
356
 
469
357
  function reflectionLine(reflection: ReflectionDetails): string {
@@ -475,16 +363,10 @@ function noteLine(kind: string, text: string): string {
475
363
  }
476
364
 
477
365
  function indentContent(content: string): string {
478
- return content
479
- .split("\n")
480
- .map((line) => ` ${line}`)
481
- .join("\n");
366
+ return content.split("\n").map((line) => ` ${line}`).join("\n");
482
367
  }
483
368
 
484
- function unavailableEvidenceMessage(details: RecallObservationToolDetails): string {
485
- if (details.unavailableReflectionProvenance.length > 0 && details.observations.length === 0) {
486
- return "migrated legacy reflection has no supporting observations";
487
- }
369
+ function unavailableEvidenceMessage(_details: RecallObservationToolDetails): string {
488
370
  return "no source entries are available for this memory id";
489
371
  }
490
372
 
@@ -500,10 +382,7 @@ function pushSourceLines(lines: string[], sources: RecallSourceEntryDetails[], e
500
382
 
501
383
  function memoryRows(details: RecallObservationToolDetails): string[] {
502
384
  if (isObservationOnly(details)) return details.matches.map((match) => observationLine(match.observation));
503
- return [
504
- ...details.reflections.map((reflection) => reflectionLine(reflection)),
505
- ...details.observations.map((observation) => observationLine(observation.observation)),
506
- ];
385
+ return [...details.reflections.map((reflection) => reflectionLine(reflection)), ...details.observations.map((observation) => observationLine(observation.observation))];
507
386
  }
508
387
 
509
388
  function noteRows(details: RecallObservationToolDetails, sources: RecallSourceEntryDetails[]): string[] {
@@ -517,22 +396,20 @@ function noteRows(details: RecallObservationToolDetails, sources: RecallSourceEn
517
396
  return notes;
518
397
  }
519
398
  if (details.collision) notes.push(noteLine("id collision", `multiple memory items share ${details.memoryId}`));
520
- if (sources.length === 0 && (details.reflections.length > 0 || details.observations.length > 0 || details.matches.length > 0)) {
521
- notes.push(noteLine("unavailable evidence", unavailableEvidenceMessage(details)));
522
- }
399
+ if (details.observations.some((match) => match.observation.status === "dropped")) notes.push(noteLine("dropped", "one or more observations are dropped from active memory but remain recallable"));
400
+ if (details.unavailableSupportingObservations.length > 0) notes.push(noteLine("missing support", details.unavailableSupportingObservations.map((item) => item.observationId).join(", ")));
401
+ if (details.missingSourceEntryIds.length > 0) notes.push(noteLine("missing source", details.missingSourceEntryIds.join(", ")));
402
+ if (details.nonSourceEntryIds.length > 0) notes.push(noteLine("non-source", details.nonSourceEntryIds.join(", ")));
403
+ if (sources.length === 0 && (details.reflections.length > 0 || details.observations.length > 0 || details.matches.length > 0)) notes.push(noteLine("unavailable evidence", unavailableEvidenceMessage(details)));
523
404
  return notes;
524
405
  }
525
406
 
526
407
  export function formatRecallResultForTui(result: AgentToolResult<RecallObservationToolDetails>, expanded: boolean): string {
527
408
  const details = result.details;
528
409
  if (!details) {
529
- const text = result.content
530
- .filter((part): part is { type: "text"; text: string } => part.type === "text" && typeof part.text === "string")
531
- .map((part) => part.text)
532
- .join("\n");
410
+ const text = result.content.filter((part): part is { type: "text"; text: string } => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n");
533
411
  return text || "recall";
534
412
  }
535
-
536
413
  const sources = sourceEntriesFromDetails(details);
537
414
  const lines: string[] = [];
538
415
  const rows = memoryRows(details);
@@ -542,10 +419,7 @@ export function formatRecallResultForTui(result: AgentToolResult<RecallObservati
542
419
  lines.push(...notes);
543
420
  if ((rows.length > 0 || notes.length > 0) && sources.length > 0) lines.push("");
544
421
  pushSourceLines(lines, sources, expanded);
545
-
546
- if (!expanded && sources.some((source) => source.content)) {
547
- lines.push("", "(Ctrl+O to expand)");
548
- }
422
+ if (!expanded && sources.some((source) => source.content)) lines.push("", "(Ctrl+O to expand)");
549
423
  return lines.join("\n").trimEnd();
550
424
  }
551
425
 
@@ -579,9 +453,7 @@ export const recallObservationTool = defineTool({
579
453
  parameters: Type.Object({
580
454
  id: Type.String({
581
455
  pattern: "^[a-f0-9]{12}$",
582
- description:
583
- "12-character lowercase hex observation or reflection id shown in compacted memory, /om-view, or a previous recall result. " +
584
- "Must be a specific id; this tool does not search by topic.",
456
+ description: "12-character lowercase hex observation or reflection id shown in compacted memory, /om-view, or a previous recall result. Must be a specific id; this tool does not search by topic.",
585
457
  }),
586
458
  }),
587
459
  renderCall(args) {
@@ -596,14 +468,12 @@ export const recallObservationTool = defineTool({
596
468
  const message = `Memory id must be 12 lowercase hex characters. Received: ${memoryId}`;
597
469
  return textResult(message, emptyDetails("invalid_id", memoryId, message));
598
470
  }
599
-
600
471
  const branchEntries = ctx.sessionManager.getBranch() as Entry[];
601
472
  const result = recallMemorySources(branchEntries, memoryId);
602
473
  if (result.status === "not_found") {
603
474
  const message = `No observation or reflection with id ${memoryId} was found on the current branch.`;
604
475
  return textResult(message, emptyDetails("not_found", memoryId, message));
605
476
  }
606
-
607
477
  return renderFoundResult(result);
608
478
  },
609
479
  });