@matthewfl/pi-contemplator 0.0.9 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +17 -11
  2. package/package.json +8 -6
  3. package/src/agents/contemplator/agent.ts +325 -91
  4. package/src/agents/contemplator/prompts.ts +6 -6
  5. package/src/agents/observer/agent.ts +14 -6
  6. package/src/agents/observer/prompts.ts +16 -7
  7. package/src/agents/reviewer/agent.ts +24 -4
  8. package/src/agents/reviewer/prompts.ts +1 -1
  9. package/src/agents/reviewer/tools.ts +24 -9
  10. package/src/agents/stream-errors.ts +1 -1
  11. package/src/agents/summarizer/agent.ts +597 -0
  12. package/src/agents/summarizer/prompts.ts +46 -0
  13. package/src/agents/summarizer/sampling.ts +80 -0
  14. package/src/commands/contemplator-view.ts +22 -1
  15. package/src/commands/settings.ts +73 -69
  16. package/src/commands/status.ts +60 -36
  17. package/src/commands/summarizer-view.ts +58 -0
  18. package/src/commands/view.ts +22 -10
  19. package/src/config.ts +25 -32
  20. package/src/hooks/compaction-hook.ts +36 -19
  21. package/src/hooks/compaction-resume.ts +4 -4
  22. package/src/hooks/compaction-trigger.ts +96 -56
  23. package/src/hooks/consolidation-trigger.ts +213 -196
  24. package/src/memory-citations.ts +37 -0
  25. package/src/required-tool-choice.ts +28 -0
  26. package/src/runtime.ts +116 -33
  27. package/src/session-ledger/fold.ts +82 -53
  28. package/src/session-ledger/index.ts +1 -0
  29. package/src/session-ledger/pools.ts +77 -0
  30. package/src/session-ledger/progress.ts +7 -18
  31. package/src/session-ledger/projection.ts +45 -177
  32. package/src/session-ledger/recall.ts +129 -127
  33. package/src/session-ledger/render-summary.ts +20 -19
  34. package/src/session-ledger/search.ts +99 -115
  35. package/src/session-ledger/types.ts +102 -75
  36. package/src/tools/compact-context.ts +1 -1
  37. package/src/tools/recall-observation.ts +99 -459
  38. package/src/tools/search-memories.ts +31 -72
  39. package/src/agents/dropper/agent.ts +0 -291
  40. package/src/agents/dropper/coverage.ts +0 -128
  41. package/src/agents/dropper/pool.ts +0 -67
  42. package/src/agents/dropper/prompts.ts +0 -48
  43. package/src/agents/reflector/agent.ts +0 -213
  44. package/src/agents/reflector/prompts.ts +0 -81
@@ -1,32 +1,26 @@
1
1
  import {
2
2
  OM_FOLDED,
3
3
  isMemoryDetails,
4
- isObservationsDroppedEntry,
5
- isObservationsRecordedEntry,
6
- isReflectionsRecordedEntry,
7
- isReviewResultEntry,
8
4
  type Entry,
9
5
  type MemoryDetails,
10
6
  type Observation,
11
- type Reflection,
12
7
  type ReviewResult,
8
+ type Summary,
13
9
  } from "./types.js";
10
+ import { foldLedger } from "./fold.js";
14
11
 
15
12
  export type Projection = {
16
13
  observations: Observation[];
17
- reflections: Reflection[];
18
- /** Optional for compatibility with older callers; projections produced here always populate it. */
14
+ summaries: Summary[];
15
+ /** Reviews are retained for search/recall and are never rendered into automatic memory. */
19
16
  reviews?: ReviewResult[];
20
17
  };
21
18
 
22
19
  export type ProjectionDiff = {
23
20
  observationsOnlyInFull: Observation[];
24
- reflectionsOnlyInFull: Reflection[];
25
- droppedOnlyInFull: Observation[];
26
- };
27
-
28
- export type CompactionProjectionConfig = {
29
- observationsPoolMaxTokens: number;
21
+ summariesOnlyInFull: Summary[];
22
+ observationsOnlyInVisible: Observation[];
23
+ summariesOnlyInVisible: Summary[];
30
24
  };
31
25
 
32
26
  export type CompactionProjection = Projection & {
@@ -34,108 +28,12 @@ export type CompactionProjection = Projection & {
34
28
  details: MemoryDetails;
35
29
  };
36
30
 
37
- type ProjectionBoundary =
38
- | { kind: "entry"; entryId: string }
39
- | { kind: "tip" }
40
- | { kind: "none" };
41
-
42
- type ProjectionFoldOptions = {
43
- observationsBoundary: ProjectionBoundary;
44
- reflectionsBoundary: ProjectionBoundary;
45
- dropsBoundary: ProjectionBoundary;
46
- reviewsBoundary: ProjectionBoundary;
47
- };
48
-
49
- function entryIndexById(entries: Entry[]): Map<string, number> {
50
- const indexes = new Map<string, number>();
51
- for (let i = 0; i < entries.length; i++) indexes.set(entries[i].id, i);
52
- return indexes;
53
- }
54
-
55
- function entryBoundary(entryId: string): ProjectionBoundary {
56
- return { kind: "entry", entryId };
57
- }
58
-
59
- function tipBoundary(): ProjectionBoundary {
60
- return { kind: "tip" };
61
- }
62
-
63
- function noneBoundary(): ProjectionBoundary {
64
- return { kind: "none" };
65
- }
66
-
67
- function boundaryIndex(entries: Entry[], indexes: Map<string, number>, boundary: ProjectionBoundary): number {
68
- if (boundary.kind === "tip") return entries.length - 1;
69
- if (boundary.kind === "none") return -1;
70
- return indexes.get(boundary.entryId) ?? -1;
71
- }
72
-
73
- function coverageIndex(entry: Entry & { data: { coversUpToId: string } }, indexes: Map<string, number>): number {
74
- return indexes.get(entry.data.coversUpToId) ?? -1;
75
- }
76
-
77
- function isAtOrBefore(index: number, boundaryIndex: number): boolean {
78
- return index >= 0 && boundaryIndex >= 0 && index <= boundaryIndex;
79
- }
80
-
81
- function isCoveredAtOrBefore(
82
- entry: Entry & { data: { coversUpToId: string } },
83
- indexes: Map<string, number>,
84
- boundaryIndex: number,
85
- ): boolean {
86
- return isAtOrBefore(coverageIndex(entry, indexes), boundaryIndex);
87
- }
88
-
89
- function foldProjection(entries: Entry[], options: ProjectionFoldOptions): Projection {
90
- const indexes = entryIndexById(entries);
91
- const observationsBoundary = boundaryIndex(entries, indexes, options.observationsBoundary);
92
- const reflectionsBoundary = boundaryIndex(entries, indexes, options.reflectionsBoundary);
93
- const dropsBoundary = boundaryIndex(entries, indexes, options.dropsBoundary);
94
- const reviewsBoundary = boundaryIndex(entries, indexes, options.reviewsBoundary);
95
- const observations: Observation[] = [];
96
- const reflections: Reflection[] = [];
97
- const reviews: ReviewResult[] = [];
98
- const observationsById = new Set<string>();
99
- const reflectionsById = new Set<string>();
100
- const reviewIds = new Set<string>();
101
- const droppedObservationIds = new Set<string>();
102
-
103
- for (const entry of entries) {
104
- if (isObservationsRecordedEntry(entry) && isCoveredAtOrBefore(entry, indexes, observationsBoundary)) {
105
- for (const observation of entry.data.observations) {
106
- if (observationsById.has(observation.id)) continue;
107
- observationsById.add(observation.id);
108
- observations.push(observation);
109
- }
110
- continue;
111
- }
112
-
113
- if (isReflectionsRecordedEntry(entry) && isCoveredAtOrBefore(entry, indexes, reflectionsBoundary)) {
114
- for (const reflection of entry.data.reflections) {
115
- if (reflectionsById.has(reflection.id)) continue;
116
- reflectionsById.add(reflection.id);
117
- reflections.push(reflection);
118
- }
119
- continue;
120
- }
121
-
122
- if (isObservationsDroppedEntry(entry) && isCoveredAtOrBefore(entry, indexes, dropsBoundary)) {
123
- for (const observationId of entry.data.observationIds) droppedObservationIds.add(observationId);
124
- continue;
125
- }
126
-
127
- if (isReviewResultEntry(entry) && isAtOrBefore(indexes.get(entry.id) ?? -1, reviewsBoundary)) {
128
- if (!reviewIds.has(entry.data.result.id)) {
129
- reviewIds.add(entry.data.result.id);
130
- reviews.push(entry.data.result);
131
- }
132
- }
133
- }
134
-
31
+ function projectionFromFold(entries: Entry[], upToEntryId?: string): Projection {
32
+ const folded = foldLedger(entries, { upToEntryId });
135
33
  return {
136
- observations: observations.filter((observation) => !droppedObservationIds.has(observation.id)),
137
- reflections,
138
- ...(reviews.length > 0 ? { reviews } : {}),
34
+ observations: folded.activeObservations,
35
+ summaries: folded.activeSummaries,
36
+ ...(folded.reviews.length > 0 ? { reviews: folded.reviews } : {}),
139
37
  };
140
38
  }
141
39
 
@@ -143,101 +41,71 @@ function projectionFromMemoryDetails(details: MemoryDetails): Projection {
143
41
  const reviews = details.reviews ?? [];
144
42
  return {
145
43
  observations: [...details.observations],
146
- reflections: [...details.reflections],
44
+ summaries: [...details.summaries],
147
45
  ...(reviews.length > 0 ? { reviews: [...reviews] } : {}),
148
46
  };
149
47
  }
150
48
 
151
- function latestV3CompactionDetails(entries: Entry[]): MemoryDetails | undefined {
49
+ function latestCompactionDetails(entries: Entry[]): MemoryDetails | undefined {
152
50
  for (let i = entries.length - 1; i >= 0; i--) {
153
51
  const entry = entries[i];
154
- if (entry.type !== "compaction") continue;
155
- if (isMemoryDetails(entry.details)) return entry.details;
52
+ if (entry.type === "compaction" && isMemoryDetails(entry.details)) return entry.details;
156
53
  }
157
54
  return undefined;
158
55
  }
159
56
 
160
57
  export function fullProjection(entries: Entry[], upToEntryId?: string): Projection {
161
- const boundary = upToEntryId ? entryBoundary(upToEntryId) : tipBoundary();
162
- return foldProjection(entries, {
163
- observationsBoundary: boundary,
164
- reflectionsBoundary: boundary,
165
- dropsBoundary: boundary,
166
- reviewsBoundary: boundary,
167
- });
58
+ return projectionFromFold(entries, upToEntryId);
168
59
  }
169
60
 
61
+ /** Memories already present in the latest compacted context. */
170
62
  export function visibleProjection(entries: Entry[], upToEntryId?: string): Projection {
171
- if (!upToEntryId) {
172
- const details = latestV3CompactionDetails(entries);
173
- return details ? projectionFromMemoryDetails(details) : { observations: [], reflections: [] };
174
- }
175
-
176
- return buildCompactionProjection(entries, upToEntryId, { observationsPoolMaxTokens: Number.POSITIVE_INFINITY });
177
- }
178
-
179
- export function latestFullFoldBoundaryId(entries: Entry[]): string | undefined {
180
- const indexes = entryIndexById(entries);
181
- for (let i = entries.length - 1; i >= 0; i--) {
182
- const entry = entries[i];
183
- if (entry.type !== "compaction") continue;
184
- if (!isMemoryDetails(entry.details)) continue;
185
- if (!entry.details.fullFold) continue;
186
- if (!entry.firstKeptEntryId) continue;
187
- if (!indexes.has(entry.firstKeptEntryId)) continue;
188
- return entry.firstKeptEntryId;
189
- }
190
- return undefined;
63
+ if (upToEntryId) return projectionFromFold(entries, upToEntryId);
64
+ const details = latestCompactionDetails(entries);
65
+ return details ? projectionFromMemoryDetails(details) : { observations: [], summaries: [] };
191
66
  }
192
67
 
193
68
  export function buildCompactionProjection(
194
69
  entries: Entry[],
195
70
  firstKeptEntryId: string,
196
- config: CompactionProjectionConfig,
197
71
  ): CompactionProjection {
198
- const fullFoldBoundaryId = latestFullFoldBoundaryId(entries);
199
- const maintenanceBoundary = fullFoldBoundaryId ? entryBoundary(fullFoldBoundaryId) : noneBoundary();
200
- const normalProjection = foldProjection(entries, {
201
- observationsBoundary: entryBoundary(firstKeptEntryId),
202
- reflectionsBoundary: maintenanceBoundary,
203
- dropsBoundary: maintenanceBoundary,
204
- reviewsBoundary: entryBoundary(firstKeptEntryId),
205
- });
206
- const observationTokens = normalProjection.observations.reduce(
207
- (total, observation) => total + observation.tokenCount,
208
- 0,
209
- );
210
- const fullFold = observationTokens >= config.observationsPoolMaxTokens;
211
- const projection = fullFold
212
- ? fullProjection(entries, firstKeptEntryId)
213
- : normalProjection;
214
-
72
+ const durable = foldLedger(entries, { upToEntryId: firstKeptEntryId });
73
+ const observations = durable.activeObservations;
74
+ const summaries = durable.activeSummaries;
75
+ // Retained in the v2 detail shape for schema stability. The old observation-
76
+ // pool pressure mode no longer exists; summarizer consumption controls visibility.
77
+ const fullFold = false;
215
78
  const details: MemoryDetails = {
216
79
  type: OM_FOLDED,
217
- version: 1,
80
+ version: 2,
218
81
  fullFold,
219
- observations: projection.observations,
220
- reflections: projection.reflections,
221
- ...(projection.reviews?.length ? { reviews: projection.reviews } : {}),
82
+ observations,
83
+ summaries,
84
+ archive: {
85
+ observations: durable.observations,
86
+ summaries: durable.summaries,
87
+ },
88
+ ...(durable.reviews.length > 0 ? { reviews: durable.reviews } : {}),
222
89
  };
223
90
 
224
91
  return {
225
92
  fullFold,
226
- observations: projection.observations,
227
- reflections: projection.reflections,
228
- ...(projection.reviews?.length ? { reviews: projection.reviews } : {}),
93
+ observations,
94
+ summaries,
95
+ ...(durable.reviews.length > 0 ? { reviews: durable.reviews } : {}),
229
96
  details,
230
97
  };
231
98
  }
232
99
 
233
100
  export function diffProjection(visible: Projection, full: Projection): ProjectionDiff {
234
- const visibleObservationIds = new Set(visible.observations.map((observation) => observation.id));
235
- const fullObservationIds = new Set(full.observations.map((observation) => observation.id));
236
- const visibleReflectionIds = new Set(visible.reflections.map((reflection) => reflection.id));
237
-
101
+ const visibleObservationIds = new Set(visible.observations.map((memory) => memory.id));
102
+ const fullObservationIds = new Set(full.observations.map((memory) => memory.id));
103
+ const visibleSummaryIds = new Set(visible.summaries.map((memory) => memory.id));
104
+ const fullSummaryIds = new Set(full.summaries.map((memory) => memory.id));
238
105
  return {
239
- observationsOnlyInFull: full.observations.filter((observation) => !visibleObservationIds.has(observation.id)),
240
- reflectionsOnlyInFull: full.reflections.filter((reflection) => !visibleReflectionIds.has(reflection.id)),
241
- droppedOnlyInFull: visible.observations.filter((observation) => !fullObservationIds.has(observation.id)),
106
+ observationsOnlyInFull: full.observations.filter((memory) => !visibleObservationIds.has(memory.id)),
107
+ summariesOnlyInFull: full.summaries.filter((memory) => !visibleSummaryIds.has(memory.id)),
108
+ observationsOnlyInVisible: visible.observations.filter((memory) => !fullObservationIds.has(memory.id)),
109
+ summariesOnlyInVisible: visible.summaries.filter((memory) => !fullSummaryIds.has(memory.id)),
242
110
  };
243
111
  }
@@ -1,25 +1,21 @@
1
1
  import {
2
- isObservationsDroppedEntry,
2
+ isMemoryDetails,
3
3
  isObservationsRecordedEntry,
4
- isReflectionsRecordedEntry,
5
4
  isReviewResultEntry,
5
+ isSummarizerCommitEntry,
6
6
  type Entry,
7
+ type MemoryVisibility,
7
8
  type Observation,
8
- type Reflection,
9
9
  type ReviewResult,
10
+ type Summary,
10
11
  } from "./types.js";
12
+ import { foldLedger } from "./fold.js";
11
13
 
12
14
  const SOURCE_TYPES = new Set(["message", "custom_message", "branch_summary"]);
13
15
 
14
- export type { Entry, Observation, Reflection };
16
+ export type { Entry, Observation, Summary };
15
17
 
16
- type ObservationLedgerLocation = {
17
- entryId: string;
18
- entryIndex: number;
19
- recordIndex: number;
20
- };
21
-
22
- type ReflectionLedgerLocation = {
18
+ type LedgerLocation = {
23
19
  entryId: string;
24
20
  entryIndex: number;
25
21
  recordIndex: number;
@@ -29,22 +25,31 @@ export type RecalledObservation = {
29
25
  observation: Observation;
30
26
  observationEntryId: string;
31
27
  observationRecordIndex: number;
32
- status: "active" | "dropped";
28
+ visibility: MemoryVisibility;
29
+ consumedBySummaryId?: string;
30
+ citedBySummaryIds: string[];
33
31
  sourceEntryIds: string[];
34
32
  sourceEntries: Entry[];
35
33
  missingSourceEntryIds: string[];
36
34
  nonSourceEntryIds: string[];
37
35
  };
38
36
 
39
- export type RecalledReflection = {
40
- reflection: Reflection;
41
- reflectionEntryId: string;
42
- reflectionRecordIndex: number;
37
+ export type RecalledSummary = {
38
+ summary: Summary;
39
+ summaryEntryId: string;
40
+ summaryRecordIndex: number;
41
+ visibility: MemoryVisibility;
42
+ consumedBySummaryId?: string;
43
+ citedBySummaryIds: string[];
44
+ sourceMemoryIds: string[];
45
+ consumedMemoryIds: string[];
46
+ missingSourceMemoryIds: string[];
43
47
  };
44
48
 
45
49
  export type RecalledReviewResult = {
46
50
  review: ReviewResult;
47
51
  reviewEntryId: string;
52
+ citedBySummaryIds: string[];
48
53
  };
49
54
 
50
55
  export type RecallResult =
@@ -52,34 +57,34 @@ export type RecallResult =
52
57
  status: "not_found";
53
58
  memoryId: string;
54
59
  kind: undefined;
55
- reflections: [];
60
+ summaries: [];
56
61
  reviews: [];
57
62
  observations: [];
58
63
  sourceEntries: [];
59
64
  missingSourceEntryIds: [];
60
65
  nonSourceEntryIds: [];
61
- missingSupportingObservationIds: [];
66
+ missingSourceMemoryIds: [];
62
67
  collision: false;
63
68
  partial: false;
64
69
  }
65
70
  | {
66
71
  status: "found";
67
72
  memoryId: string;
68
- kind: "observation" | "reflection" | "review" | "mixed";
69
- reflections: RecalledReflection[];
73
+ kind: "observation" | "summary" | "review" | "mixed";
74
+ summaries: RecalledSummary[];
70
75
  reviews: RecalledReviewResult[];
71
76
  observations: RecalledObservation[];
72
77
  sourceEntries: Entry[];
73
78
  missingSourceEntryIds: string[];
74
79
  nonSourceEntryIds: string[];
75
- missingSupportingObservationIds: string[];
80
+ missingSourceMemoryIds: string[];
76
81
  collision: boolean;
77
82
  partial: boolean;
78
83
  };
79
84
 
80
- type IndexedObservation = ObservationLedgerLocation & { observation: Observation };
81
- type IndexedReflection = ReflectionLedgerLocation & { reflection: Reflection };
82
- type IndexedReviewResult = { review: ReviewResult; entryId: string };
85
+ type IndexedObservation = LedgerLocation & { observation: Observation };
86
+ type IndexedSummary = LedgerLocation & { summary: Summary };
87
+ type IndexedReviewResult = { review: ReviewResult; entryId: string; entryIndex: number };
83
88
 
84
89
  function isSourceEntry(entry: Entry): boolean {
85
90
  return SOURCE_TYPES.has(entry.type);
@@ -87,79 +92,85 @@ function isSourceEntry(entry: Entry): boolean {
87
92
 
88
93
  function uniqueById(entries: Entry[]): Entry[] {
89
94
  const seen = new Set<string>();
90
- const result: Entry[] = [];
91
- for (const entry of entries) {
92
- if (seen.has(entry.id)) continue;
95
+ return entries.filter((entry) => {
96
+ if (seen.has(entry.id)) return false;
93
97
  seen.add(entry.id);
94
- result.push(entry);
95
- }
96
- return result;
98
+ return true;
99
+ });
97
100
  }
98
101
 
99
- function uniqueStrings(values: string[]): string[] {
102
+ function uniqueStrings(values: readonly string[]): string[] {
100
103
  return Array.from(new Set(values));
101
104
  }
102
105
 
103
106
  function indexLedger(entries: Entry[]): {
104
107
  observations: IndexedObservation[];
105
- reflections: IndexedReflection[];
108
+ summaries: IndexedSummary[];
106
109
  reviews: IndexedReviewResult[];
107
- droppedIds: Set<string>;
108
110
  } {
109
111
  const observations: IndexedObservation[] = [];
110
- const reflections: IndexedReflection[] = [];
112
+ const summaries: IndexedSummary[] = [];
111
113
  const reviews: IndexedReviewResult[] = [];
112
- const droppedIds = new Set<string>();
114
+ const observationKeys = new Set<string>();
115
+ const summaryKeys = new Set<string>();
116
+ const reviewKeys = new Set<string>();
117
+ const addObservation = (observation: Observation, location: LedgerLocation): void => {
118
+ const key = `${observation.id}:${uniqueStrings(observation.sourceEntryIds).sort().join(",")}`;
119
+ if (observationKeys.has(key)) return;
120
+ observationKeys.add(key);
121
+ observations.push({ observation, ...location });
122
+ };
123
+ const addSummary = (summary: Summary, location: LedgerLocation): void => {
124
+ const key = `${summary.id}:${summary.sourceMemoryIds.join(",")}:${summary.consumedMemoryIds.join(",")}`;
125
+ if (summaryKeys.has(key)) return;
126
+ summaryKeys.add(key);
127
+ summaries.push({ summary, ...location });
128
+ };
129
+ const addReview = (review: ReviewResult, entryId: string, entryIndex: number): void => {
130
+ if (reviewKeys.has(review.id)) return;
131
+ reviewKeys.add(review.id);
132
+ reviews.push({ review, entryId, entryIndex });
133
+ };
113
134
 
114
135
  for (let entryIndex = 0; entryIndex < entries.length; entryIndex++) {
115
136
  const entry = entries[entryIndex];
116
- if (isObservationsRecordedEntry(entry)) {
117
- entry.data.observations.forEach((observation, recordIndex) => {
118
- observations.push({ observation, entryId: entry.id, entryIndex, recordIndex });
119
- });
137
+ if (entry.type === "compaction" && isMemoryDetails(entry.details)) {
138
+ const archivedObservations = entry.details.archive?.observations ?? entry.details.observations;
139
+ const archivedSummaries = entry.details.archive?.summaries ?? entry.details.summaries;
140
+ archivedObservations.forEach((observation, recordIndex) => addObservation(observation, { entryId: entry.id, entryIndex, recordIndex }));
141
+ archivedSummaries.forEach((summary, recordIndex) => addSummary(summary, { entryId: entry.id, entryIndex, recordIndex }));
142
+ for (const review of entry.details.reviews ?? []) addReview(review, entry.id, entryIndex);
120
143
  continue;
121
144
  }
122
- if (isReflectionsRecordedEntry(entry)) {
123
- entry.data.reflections.forEach((reflection, recordIndex) => {
124
- reflections.push({ reflection, entryId: entry.id, entryIndex, recordIndex });
125
- });
145
+ if (isObservationsRecordedEntry(entry)) {
146
+ entry.data.observations.forEach((observation, recordIndex) => addObservation(observation, { entryId: entry.id, entryIndex, recordIndex }));
126
147
  continue;
127
148
  }
128
- if (isObservationsDroppedEntry(entry)) {
129
- entry.data.observationIds.forEach((id) => droppedIds.add(id));
149
+ if (isSummarizerCommitEntry(entry)) {
150
+ entry.data.summaries.forEach((summary, recordIndex) => addSummary(summary, { entryId: entry.id, entryIndex, recordIndex }));
130
151
  continue;
131
152
  }
132
- if (isReviewResultEntry(entry)) reviews.push({ review: entry.data.result, entryId: entry.id });
153
+ if (isReviewResultEntry(entry)) addReview(entry.data.result, entry.id, entryIndex);
133
154
  }
134
-
135
- return { observations, reflections, reviews, droppedIds };
155
+ return { observations, summaries, reviews };
136
156
  }
137
157
 
138
- function resolveObservationSources(entries: Entry[], observation: Observation, location: ObservationLedgerLocation): RecalledObservation {
139
- const sourceEntryIds = uniqueStrings(observation.sourceEntryIds);
158
+ function resolveObservationSources(entries: Entry[], indexed: IndexedObservation): Omit<RecalledObservation, "visibility" | "consumedBySummaryId" | "citedBySummaryIds"> {
159
+ const sourceEntryIds = uniqueStrings(indexed.observation.sourceEntryIds);
140
160
  const byId = new Map(entries.map((entry) => [entry.id, entry]));
141
161
  const sourceEntries: Entry[] = [];
142
162
  const missingSourceEntryIds: string[] = [];
143
163
  const nonSourceEntryIds: string[] = [];
144
-
145
164
  for (const sourceEntryId of sourceEntryIds) {
146
165
  const sourceEntry = byId.get(sourceEntryId);
147
- if (!sourceEntry) {
148
- missingSourceEntryIds.push(sourceEntryId);
149
- continue;
150
- }
151
- if (!isSourceEntry(sourceEntry)) {
152
- nonSourceEntryIds.push(sourceEntryId);
153
- continue;
154
- }
155
- sourceEntries.push(sourceEntry);
166
+ if (!sourceEntry) missingSourceEntryIds.push(sourceEntryId);
167
+ else if (!isSourceEntry(sourceEntry)) nonSourceEntryIds.push(sourceEntryId);
168
+ else sourceEntries.push(sourceEntry);
156
169
  }
157
-
158
170
  return {
159
- observation,
160
- observationEntryId: location.entryId,
161
- observationRecordIndex: location.recordIndex,
162
- status: "active",
171
+ observation: indexed.observation,
172
+ observationEntryId: indexed.entryId,
173
+ observationRecordIndex: indexed.recordIndex,
163
174
  sourceEntryIds,
164
175
  sourceEntries,
165
176
  missingSourceEntryIds,
@@ -172,87 +183,78 @@ function notFound(memoryId: string): RecallResult {
172
183
  status: "not_found",
173
184
  memoryId,
174
185
  kind: undefined,
175
- reflections: [],
186
+ summaries: [],
176
187
  reviews: [],
177
188
  observations: [],
178
189
  sourceEntries: [],
179
190
  missingSourceEntryIds: [],
180
191
  nonSourceEntryIds: [],
181
- missingSupportingObservationIds: [],
192
+ missingSourceMemoryIds: [],
182
193
  collision: false,
183
194
  partial: false,
184
195
  };
185
196
  }
186
197
 
198
+ /** Recall exactly one graph node plus immediate backward/forward pointers. */
187
199
  export function recallMemorySources(entries: Entry[], memoryId: string): RecallResult {
188
- const { observations: indexedObservations, reflections: indexedReflections, reviews: indexedReviews, droppedIds } = indexLedger(entries);
189
- const directObservationMatches = indexedObservations.filter(({ observation }) => observation.id === memoryId);
190
- const reflectionMatches = indexedReflections.filter(({ reflection }) => reflection.id === memoryId);
191
- const reviewMatches = indexedReviews.filter(({ review }) => review.id === memoryId);
192
-
193
- if (directObservationMatches.length === 0 && reflectionMatches.length === 0 && reviewMatches.length === 0) return notFound(memoryId);
194
-
195
- const observationsById = new Map<string, IndexedObservation>();
196
- for (const indexed of indexedObservations) {
197
- if (!observationsById.has(indexed.observation.id)) observationsById.set(indexed.observation.id, indexed);
198
- }
199
-
200
- const recalledByKey = new Map<string, RecalledObservation>();
201
- const missingSupportingObservationIds: string[] = [];
200
+ const indexed = indexLedger(entries);
201
+ const folded = foldLedger(entries);
202
+ const observationMatches = indexed.observations.filter(({ observation }) => observation.id === memoryId);
203
+ const summaryMatches = indexed.summaries.filter(({ summary }) => summary.id === memoryId);
204
+ const reviewMatches = indexed.reviews.filter(({ review }) => review.id === memoryId);
205
+ if (observationMatches.length === 0 && summaryMatches.length === 0 && reviewMatches.length === 0) return notFound(memoryId);
202
206
 
203
- function addObservation(indexed: IndexedObservation): void {
204
- const key = `${indexed.entryId}:${indexed.recordIndex}`;
205
- if (recalledByKey.has(key)) return;
206
- const recalled = resolveObservationSources(entries, indexed.observation, indexed);
207
- recalled.status = droppedIds.has(indexed.observation.id) ? "dropped" : "active";
208
- recalledByKey.set(key, recalled);
209
- }
210
-
211
- for (const match of directObservationMatches) addObservation(match);
212
-
213
- for (const { reflection } of reflectionMatches) {
214
- for (const observationId of uniqueStrings(reflection.supportingObservationIds)) {
215
- const indexed = observationsById.get(observationId);
216
- if (!indexed) {
217
- missingSupportingObservationIds.push(observationId);
218
- continue;
219
- }
220
- addObservation(indexed);
221
- }
222
- }
223
-
224
- const recalledObservations = Array.from(recalledByKey.values());
225
- const recalledReflections: RecalledReflection[] = reflectionMatches.map(({ reflection, entryId, recordIndex }) => ({
226
- reflection,
227
- reflectionEntryId: entryId,
228
- reflectionRecordIndex: recordIndex,
207
+ const knownMemoryIds = new Set([
208
+ ...folded.observationsById.keys(),
209
+ ...folded.summariesById.keys(),
210
+ ...folded.reviewsById.keys(),
211
+ ]);
212
+ const observations: RecalledObservation[] = observationMatches.map((match) => {
213
+ const consumedBySummaryId = folded.consumedBySummaryId.get(match.observation.id);
214
+ return {
215
+ ...resolveObservationSources(entries, match),
216
+ visibility: consumedBySummaryId ? "summarized" : "visible",
217
+ ...(consumedBySummaryId ? { consumedBySummaryId } : {}),
218
+ citedBySummaryIds: folded.citedBySummaryIds.get(match.observation.id) ?? [],
219
+ };
220
+ });
221
+ const summaries: RecalledSummary[] = summaryMatches.map((match) => {
222
+ const consumedBySummaryId = folded.consumedBySummaryId.get(match.summary.id);
223
+ return {
224
+ summary: match.summary,
225
+ summaryEntryId: match.entryId,
226
+ summaryRecordIndex: match.recordIndex,
227
+ visibility: consumedBySummaryId ? "summarized" : "visible",
228
+ ...(consumedBySummaryId ? { consumedBySummaryId } : {}),
229
+ citedBySummaryIds: folded.citedBySummaryIds.get(match.summary.id) ?? [],
230
+ sourceMemoryIds: match.summary.sourceMemoryIds,
231
+ consumedMemoryIds: match.summary.consumedMemoryIds,
232
+ missingSourceMemoryIds: match.summary.sourceMemoryIds.filter((id) => !knownMemoryIds.has(id)),
233
+ };
234
+ });
235
+ const reviews: RecalledReviewResult[] = reviewMatches.map(({ review, entryId }) => ({
236
+ review,
237
+ reviewEntryId: entryId,
238
+ citedBySummaryIds: folded.citedBySummaryIds.get(review.id) ?? [],
229
239
  }));
230
- const recalledReviews: RecalledReviewResult[] = reviewMatches.map(({ review, entryId }) => ({ review, reviewEntryId: entryId }));
231
- const sourceEntries = uniqueById(recalledObservations.flatMap((match) => match.sourceEntries));
232
- const missingSourceEntryIds = uniqueStrings(recalledObservations.flatMap((match) => match.missingSourceEntryIds));
233
- const nonSourceEntryIds = uniqueStrings(recalledObservations.flatMap((match) => match.nonSourceEntryIds));
234
- const uniqueMissingSupportingObservationIds = uniqueStrings(missingSupportingObservationIds);
235
- const matchCount = directObservationMatches.length + reflectionMatches.length + reviewMatches.length;
236
- const kinds = [directObservationMatches.length > 0, reflectionMatches.length > 0, reviewMatches.length > 0].filter(Boolean).length;
237
-
240
+ const sourceEntries = uniqueById(observations.flatMap((match) => match.sourceEntries));
241
+ const missingSourceEntryIds = uniqueStrings(observations.flatMap((match) => match.missingSourceEntryIds));
242
+ const nonSourceEntryIds = uniqueStrings(observations.flatMap((match) => match.nonSourceEntryIds));
243
+ const missingSourceMemoryIds = uniqueStrings(summaries.flatMap((match) => match.missingSourceMemoryIds));
244
+ const kinds = [observationMatches.length > 0, summaryMatches.length > 0, reviewMatches.length > 0].filter(Boolean).length;
245
+ const matchCount = observationMatches.length + summaryMatches.length + reviewMatches.length;
238
246
  return {
239
247
  status: "found",
240
248
  memoryId,
241
- kind: kinds > 1
242
- ? "mixed"
243
- : reviewMatches.length > 0
244
- ? "review"
245
- : reflectionMatches.length > 0
246
- ? "reflection"
247
- : "observation",
248
- reflections: recalledReflections,
249
- reviews: recalledReviews,
250
- observations: recalledObservations,
249
+ kind: kinds > 1 ? "mixed" : reviewMatches.length > 0 ? "review" : summaryMatches.length > 0 ? "summary" : "observation",
250
+ summaries,
251
+ reviews,
252
+ observations,
251
253
  sourceEntries,
252
254
  missingSourceEntryIds,
253
255
  nonSourceEntryIds,
254
- missingSupportingObservationIds: uniqueMissingSupportingObservationIds,
256
+ missingSourceMemoryIds,
255
257
  collision: matchCount > 1,
256
- partial: missingSourceEntryIds.length > 0 || nonSourceEntryIds.length > 0 || uniqueMissingSupportingObservationIds.length > 0,
258
+ partial: missingSourceEntryIds.length > 0 || nonSourceEntryIds.length > 0 || missingSourceMemoryIds.length > 0,
257
259
  };
258
260
  }