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.
@@ -0,0 +1,129 @@
1
+ import { estimateEntryTokens } from "../tokens.js";
2
+ import {
3
+ OM_OBSERVATIONS_DROPPED,
4
+ OM_OBSERVATIONS_RECORDED,
5
+ OM_REFLECTIONS_RECORDED,
6
+ type Entry,
7
+ type V3MemoryCustomType,
8
+ } from "./types.js";
9
+
10
+ const SOURCE_ENTRY_TYPES = new Set(["message", "custom_message", "branch_summary"]);
11
+
12
+ export function isSourceEntry(entry: Entry): boolean {
13
+ return SOURCE_ENTRY_TYPES.has(entry.type);
14
+ }
15
+
16
+ export function entryIndexById(entries: Entry[]): Map<string, number> {
17
+ const idToIndex = new Map<string, number>();
18
+ for (let i = 0; i < entries.length; i++) idToIndex.set(entries[i].id, i);
19
+ return idToIndex;
20
+ }
21
+
22
+ export function entryIndexForId(entries: Entry[], entryId: string | undefined): number {
23
+ if (!entryId) return -1;
24
+ const idx = entryIndexById(entries).get(entryId);
25
+ return idx ?? -1;
26
+ }
27
+
28
+ function isObject(value: unknown): value is Record<string, unknown> {
29
+ return typeof value === "object" && value !== null;
30
+ }
31
+
32
+ function isNonEmptyArray(value: unknown): value is unknown[] {
33
+ return Array.isArray(value) && value.length > 0;
34
+ }
35
+
36
+ function isValidCoverageEntry(entry: Entry, customType: V3MemoryCustomType): entry is Entry & { data: { coversUpToId: string } } {
37
+ if (entry.type !== "custom" || entry.customType !== customType) return false;
38
+ if (!isObject(entry.data) || typeof entry.data.coversUpToId !== "string") return false;
39
+
40
+ if (customType === OM_OBSERVATIONS_RECORDED) return isNonEmptyArray(entry.data.observations);
41
+ if (customType === OM_REFLECTIONS_RECORDED) return isNonEmptyArray(entry.data.reflections);
42
+ return isNonEmptyArray(entry.data.observationIds);
43
+ }
44
+
45
+ export function latestCoverageIndex(entries: Entry[], customType: V3MemoryCustomType): number {
46
+ const idToIndex = entryIndexById(entries);
47
+ let latest = -1;
48
+
49
+ for (const entry of entries) {
50
+ if (!isValidCoverageEntry(entry, customType)) continue;
51
+ const coveredIndex = idToIndex.get(entry.data.coversUpToId);
52
+ if (coveredIndex === undefined) continue;
53
+ if (coveredIndex > latest) latest = coveredIndex;
54
+ }
55
+
56
+ return latest;
57
+ }
58
+
59
+ export function latestCoverageMarkerId(entries: Entry[], customType: V3MemoryCustomType): string | undefined {
60
+ const idToIndex = entryIndexById(entries);
61
+ let latestIndex = -1;
62
+ let latestMarkerId: string | undefined;
63
+
64
+ for (const entry of entries) {
65
+ if (!isValidCoverageEntry(entry, customType)) continue;
66
+ const coveredIndex = idToIndex.get(entry.data.coversUpToId);
67
+ if (coveredIndex === undefined) continue;
68
+ if (coveredIndex > latestIndex) {
69
+ latestIndex = coveredIndex;
70
+ latestMarkerId = entry.data.coversUpToId;
71
+ }
72
+ }
73
+
74
+ return latestMarkerId;
75
+ }
76
+
77
+ export function earlierCoverageMarkerId(entries: Entry[], firstId: string | undefined, secondId: string | undefined): string | undefined {
78
+ if (!firstId) return secondId;
79
+ if (!secondId) return firstId;
80
+
81
+ const idToIndex = entryIndexById(entries);
82
+ const firstIndex = idToIndex.get(firstId);
83
+ const secondIndex = idToIndex.get(secondId);
84
+ if (firstIndex === undefined) return secondIndex === undefined ? undefined : secondId;
85
+ if (secondIndex === undefined) return firstId;
86
+ return firstIndex <= secondIndex ? firstId : secondId;
87
+ }
88
+
89
+ export function rawTokensAfterIndex(entries: Entry[], index: number): number {
90
+ let total = 0;
91
+ for (let i = Math.max(0, index + 1); i < entries.length; i++) {
92
+ if (isSourceEntry(entries[i])) total += estimateEntryTokens(entries[i]);
93
+ }
94
+ return total;
95
+ }
96
+
97
+ export function rawTokensSinceCoverage(entries: Entry[], customType: V3MemoryCustomType): number {
98
+ return rawTokensAfterIndex(entries, latestCoverageIndex(entries, customType));
99
+ }
100
+
101
+ export function rawTokensSinceObservationCoverage(entries: Entry[]): number {
102
+ return rawTokensSinceCoverage(entries, OM_OBSERVATIONS_RECORDED);
103
+ }
104
+
105
+ export function rawTokensSinceReflectionCoverage(entries: Entry[]): number {
106
+ return rawTokensSinceCoverage(entries, OM_REFLECTIONS_RECORDED);
107
+ }
108
+
109
+ export function rawTokensSinceDropCoverage(entries: Entry[]): number {
110
+ return rawTokensSinceCoverage(entries, OM_OBSERVATIONS_DROPPED);
111
+ }
112
+
113
+ export function findLastCompactionIndex(entries: Entry[]): number {
114
+ for (let i = entries.length - 1; i >= 0; i--) {
115
+ if (entries[i].type === "compaction") return i;
116
+ }
117
+ return -1;
118
+ }
119
+
120
+ export function rawTokensSinceLastCompaction(entries: Entry[]): number {
121
+ const compactionIndex = findLastCompactionIndex(entries);
122
+ if (compactionIndex === -1) return rawTokensAfterIndex(entries, -1);
123
+
124
+ const firstKeptEntryId = entries[compactionIndex].firstKeptEntryId;
125
+ const firstKeptIndex = entryIndexForId(entries, firstKeptEntryId);
126
+
127
+ if (firstKeptIndex === -1) return rawTokensAfterIndex(entries, compactionIndex);
128
+ return rawTokensAfterIndex(entries, firstKeptIndex - 1);
129
+ }
@@ -0,0 +1,220 @@
1
+ import {
2
+ OM_FOLDED,
3
+ isMemoryDetails,
4
+ isObservationsDroppedEntry,
5
+ isObservationsRecordedEntry,
6
+ isReflectionsRecordedEntry,
7
+ type Entry,
8
+ type MemoryDetails,
9
+ type Observation,
10
+ type Reflection,
11
+ } from "./types.js";
12
+
13
+ export type Projection = {
14
+ observations: Observation[];
15
+ reflections: Reflection[];
16
+ };
17
+
18
+ export type ProjectionDiff = {
19
+ observationsOnlyInFull: Observation[];
20
+ reflectionsOnlyInFull: Reflection[];
21
+ droppedOnlyInFull: Observation[];
22
+ };
23
+
24
+ export type CompactionProjectionConfig = {
25
+ observationsPoolMaxTokens: number;
26
+ };
27
+
28
+ export type CompactionProjection = Projection & {
29
+ fullFold: boolean;
30
+ details: MemoryDetails;
31
+ };
32
+
33
+ type ProjectionBoundary =
34
+ | { kind: "entry"; entryId: string }
35
+ | { kind: "tip" }
36
+ | { kind: "none" };
37
+
38
+ type ProjectionFoldOptions = {
39
+ observationsBoundary: ProjectionBoundary;
40
+ reflectionsBoundary: ProjectionBoundary;
41
+ dropsBoundary: ProjectionBoundary;
42
+ };
43
+
44
+ function entryIndexById(entries: Entry[]): Map<string, number> {
45
+ const indexes = new Map<string, number>();
46
+ for (let i = 0; i < entries.length; i++) indexes.set(entries[i].id, i);
47
+ return indexes;
48
+ }
49
+
50
+ function entryBoundary(entryId: string): ProjectionBoundary {
51
+ return { kind: "entry", entryId };
52
+ }
53
+
54
+ function tipBoundary(): ProjectionBoundary {
55
+ return { kind: "tip" };
56
+ }
57
+
58
+ function noneBoundary(): ProjectionBoundary {
59
+ return { kind: "none" };
60
+ }
61
+
62
+ function boundaryIndex(entries: Entry[], indexes: Map<string, number>, boundary: ProjectionBoundary): number {
63
+ if (boundary.kind === "tip") return entries.length - 1;
64
+ if (boundary.kind === "none") return -1;
65
+ return indexes.get(boundary.entryId) ?? -1;
66
+ }
67
+
68
+ function coverageIndex(entry: Entry & { data: { coversUpToId: string } }, indexes: Map<string, number>): number {
69
+ return indexes.get(entry.data.coversUpToId) ?? -1;
70
+ }
71
+
72
+ function isAtOrBefore(index: number, boundaryIndex: number): boolean {
73
+ return index >= 0 && boundaryIndex >= 0 && index <= boundaryIndex;
74
+ }
75
+
76
+ function isCoveredAtOrBefore(
77
+ entry: Entry & { data: { coversUpToId: string } },
78
+ indexes: Map<string, number>,
79
+ boundaryIndex: number,
80
+ ): boolean {
81
+ return isAtOrBefore(coverageIndex(entry, indexes), boundaryIndex);
82
+ }
83
+
84
+ function foldProjection(entries: Entry[], options: ProjectionFoldOptions): Projection {
85
+ const indexes = entryIndexById(entries);
86
+ const observationsBoundary = boundaryIndex(entries, indexes, options.observationsBoundary);
87
+ const reflectionsBoundary = boundaryIndex(entries, indexes, options.reflectionsBoundary);
88
+ const dropsBoundary = boundaryIndex(entries, indexes, options.dropsBoundary);
89
+ const observations: Observation[] = [];
90
+ const reflections: Reflection[] = [];
91
+ const observationsById = new Set<string>();
92
+ const reflectionsById = new Set<string>();
93
+ const droppedObservationIds = new Set<string>();
94
+
95
+ for (const entry of entries) {
96
+ if (isObservationsRecordedEntry(entry) && isCoveredAtOrBefore(entry, indexes, observationsBoundary)) {
97
+ for (const observation of entry.data.observations) {
98
+ if (observationsById.has(observation.id)) continue;
99
+ observationsById.add(observation.id);
100
+ observations.push(observation);
101
+ }
102
+ continue;
103
+ }
104
+
105
+ if (isReflectionsRecordedEntry(entry) && isCoveredAtOrBefore(entry, indexes, reflectionsBoundary)) {
106
+ for (const reflection of entry.data.reflections) {
107
+ if (reflectionsById.has(reflection.id)) continue;
108
+ reflectionsById.add(reflection.id);
109
+ reflections.push(reflection);
110
+ }
111
+ continue;
112
+ }
113
+
114
+ if (isObservationsDroppedEntry(entry) && isCoveredAtOrBefore(entry, indexes, dropsBoundary)) {
115
+ for (const observationId of entry.data.observationIds) droppedObservationIds.add(observationId);
116
+ }
117
+ }
118
+
119
+ return {
120
+ observations: observations.filter((observation) => !droppedObservationIds.has(observation.id)),
121
+ reflections,
122
+ };
123
+ }
124
+
125
+ function projectionFromMemoryDetails(details: MemoryDetails): Projection {
126
+ return {
127
+ observations: [...details.observations],
128
+ reflections: [...details.reflections],
129
+ };
130
+ }
131
+
132
+ function latestV3CompactionDetails(entries: Entry[]): MemoryDetails | undefined {
133
+ for (let i = entries.length - 1; i >= 0; i--) {
134
+ const entry = entries[i];
135
+ if (entry.type !== "compaction") continue;
136
+ if (isMemoryDetails(entry.details)) return entry.details;
137
+ }
138
+ return undefined;
139
+ }
140
+
141
+ export function fullProjection(entries: Entry[], upToEntryId?: string): Projection {
142
+ const boundary = upToEntryId ? entryBoundary(upToEntryId) : tipBoundary();
143
+ return foldProjection(entries, {
144
+ observationsBoundary: boundary,
145
+ reflectionsBoundary: boundary,
146
+ dropsBoundary: boundary,
147
+ });
148
+ }
149
+
150
+ export function visibleProjection(entries: Entry[], upToEntryId?: string): Projection {
151
+ if (!upToEntryId) {
152
+ const details = latestV3CompactionDetails(entries);
153
+ return details ? projectionFromMemoryDetails(details) : { observations: [], reflections: [] };
154
+ }
155
+
156
+ return buildCompactionProjection(entries, upToEntryId, { observationsPoolMaxTokens: Number.POSITIVE_INFINITY });
157
+ }
158
+
159
+ export function latestFullFoldBoundaryId(entries: Entry[]): string | undefined {
160
+ const indexes = entryIndexById(entries);
161
+ for (let i = entries.length - 1; i >= 0; i--) {
162
+ const entry = entries[i];
163
+ if (entry.type !== "compaction") continue;
164
+ if (!isMemoryDetails(entry.details)) continue;
165
+ if (!entry.details.fullFold) continue;
166
+ if (!entry.firstKeptEntryId) continue;
167
+ if (!indexes.has(entry.firstKeptEntryId)) continue;
168
+ return entry.firstKeptEntryId;
169
+ }
170
+ return undefined;
171
+ }
172
+
173
+ export function buildCompactionProjection(
174
+ entries: Entry[],
175
+ firstKeptEntryId: string,
176
+ config: CompactionProjectionConfig,
177
+ ): CompactionProjection {
178
+ const fullFoldBoundaryId = latestFullFoldBoundaryId(entries);
179
+ const maintenanceBoundary = fullFoldBoundaryId ? entryBoundary(fullFoldBoundaryId) : noneBoundary();
180
+ const normalProjection = foldProjection(entries, {
181
+ observationsBoundary: entryBoundary(firstKeptEntryId),
182
+ reflectionsBoundary: maintenanceBoundary,
183
+ dropsBoundary: maintenanceBoundary,
184
+ });
185
+ const observationTokens = normalProjection.observations.reduce(
186
+ (total, observation) => total + observation.tokenCount,
187
+ 0,
188
+ );
189
+ const fullFold = observationTokens >= config.observationsPoolMaxTokens;
190
+ const projection = fullFold
191
+ ? fullProjection(entries, firstKeptEntryId)
192
+ : normalProjection;
193
+
194
+ const details: MemoryDetails = {
195
+ type: OM_FOLDED,
196
+ version: 1,
197
+ fullFold,
198
+ observations: projection.observations,
199
+ reflections: projection.reflections,
200
+ };
201
+
202
+ return {
203
+ fullFold,
204
+ observations: projection.observations,
205
+ reflections: projection.reflections,
206
+ details,
207
+ };
208
+ }
209
+
210
+ export function diffProjection(visible: Projection, full: Projection): ProjectionDiff {
211
+ const visibleObservationIds = new Set(visible.observations.map((observation) => observation.id));
212
+ const fullObservationIds = new Set(full.observations.map((observation) => observation.id));
213
+ const visibleReflectionIds = new Set(visible.reflections.map((reflection) => reflection.id));
214
+
215
+ return {
216
+ observationsOnlyInFull: full.observations.filter((observation) => !visibleObservationIds.has(observation.id)),
217
+ reflectionsOnlyInFull: full.reflections.filter((reflection) => !visibleReflectionIds.has(reflection.id)),
218
+ droppedOnlyInFull: visible.observations.filter((observation) => !fullObservationIds.has(observation.id)),
219
+ };
220
+ }
@@ -0,0 +1,237 @@
1
+ import {
2
+ isObservationsDroppedEntry,
3
+ isObservationsRecordedEntry,
4
+ isReflectionsRecordedEntry,
5
+ type Entry,
6
+ type Observation,
7
+ type Reflection,
8
+ } from "./types.js";
9
+
10
+ const SOURCE_TYPES = new Set(["message", "custom_message", "branch_summary"]);
11
+
12
+ export type { Entry, Observation, Reflection };
13
+
14
+ type ObservationLedgerLocation = {
15
+ entryId: string;
16
+ entryIndex: number;
17
+ recordIndex: number;
18
+ };
19
+
20
+ type ReflectionLedgerLocation = {
21
+ entryId: string;
22
+ entryIndex: number;
23
+ recordIndex: number;
24
+ };
25
+
26
+ export type RecalledObservation = {
27
+ observation: Observation;
28
+ observationEntryId: string;
29
+ observationRecordIndex: number;
30
+ status: "active" | "dropped";
31
+ sourceEntryIds: string[];
32
+ sourceEntries: Entry[];
33
+ missingSourceEntryIds: string[];
34
+ nonSourceEntryIds: string[];
35
+ };
36
+
37
+ export type RecalledReflection = {
38
+ reflection: Reflection;
39
+ reflectionEntryId: string;
40
+ reflectionRecordIndex: number;
41
+ };
42
+
43
+ export type RecallResult =
44
+ | {
45
+ status: "not_found";
46
+ memoryId: string;
47
+ kind: undefined;
48
+ reflections: [];
49
+ observations: [];
50
+ sourceEntries: [];
51
+ missingSourceEntryIds: [];
52
+ nonSourceEntryIds: [];
53
+ missingSupportingObservationIds: [];
54
+ collision: false;
55
+ partial: false;
56
+ }
57
+ | {
58
+ status: "found";
59
+ memoryId: string;
60
+ kind: "observation" | "reflection" | "mixed";
61
+ reflections: RecalledReflection[];
62
+ observations: RecalledObservation[];
63
+ sourceEntries: Entry[];
64
+ missingSourceEntryIds: string[];
65
+ nonSourceEntryIds: string[];
66
+ missingSupportingObservationIds: string[];
67
+ collision: boolean;
68
+ partial: boolean;
69
+ };
70
+
71
+ type IndexedObservation = ObservationLedgerLocation & { observation: Observation };
72
+ type IndexedReflection = ReflectionLedgerLocation & { reflection: Reflection };
73
+
74
+ function isSourceEntry(entry: Entry): boolean {
75
+ return SOURCE_TYPES.has(entry.type);
76
+ }
77
+
78
+ function uniqueById(entries: Entry[]): Entry[] {
79
+ const seen = new Set<string>();
80
+ const result: Entry[] = [];
81
+ for (const entry of entries) {
82
+ if (seen.has(entry.id)) continue;
83
+ seen.add(entry.id);
84
+ result.push(entry);
85
+ }
86
+ return result;
87
+ }
88
+
89
+ function uniqueStrings(values: string[]): string[] {
90
+ return Array.from(new Set(values));
91
+ }
92
+
93
+ function indexLedger(entries: Entry[]): {
94
+ observations: IndexedObservation[];
95
+ reflections: IndexedReflection[];
96
+ droppedIds: Set<string>;
97
+ } {
98
+ const observations: IndexedObservation[] = [];
99
+ const reflections: IndexedReflection[] = [];
100
+ const droppedIds = new Set<string>();
101
+
102
+ for (let entryIndex = 0; entryIndex < entries.length; entryIndex++) {
103
+ const entry = entries[entryIndex];
104
+ if (isObservationsRecordedEntry(entry)) {
105
+ entry.data.observations.forEach((observation, recordIndex) => {
106
+ observations.push({ observation, entryId: entry.id, entryIndex, recordIndex });
107
+ });
108
+ continue;
109
+ }
110
+ if (isReflectionsRecordedEntry(entry)) {
111
+ entry.data.reflections.forEach((reflection, recordIndex) => {
112
+ reflections.push({ reflection, entryId: entry.id, entryIndex, recordIndex });
113
+ });
114
+ continue;
115
+ }
116
+ if (isObservationsDroppedEntry(entry)) {
117
+ entry.data.observationIds.forEach((id) => droppedIds.add(id));
118
+ }
119
+ }
120
+
121
+ return { observations, reflections, droppedIds };
122
+ }
123
+
124
+ function resolveObservationSources(entries: Entry[], observation: Observation, location: ObservationLedgerLocation): RecalledObservation {
125
+ const sourceEntryIds = uniqueStrings(observation.sourceEntryIds);
126
+ const byId = new Map(entries.map((entry) => [entry.id, entry]));
127
+ const sourceEntries: Entry[] = [];
128
+ const missingSourceEntryIds: string[] = [];
129
+ const nonSourceEntryIds: string[] = [];
130
+
131
+ for (const sourceEntryId of sourceEntryIds) {
132
+ const sourceEntry = byId.get(sourceEntryId);
133
+ if (!sourceEntry) {
134
+ missingSourceEntryIds.push(sourceEntryId);
135
+ continue;
136
+ }
137
+ if (!isSourceEntry(sourceEntry)) {
138
+ nonSourceEntryIds.push(sourceEntryId);
139
+ continue;
140
+ }
141
+ sourceEntries.push(sourceEntry);
142
+ }
143
+
144
+ return {
145
+ observation,
146
+ observationEntryId: location.entryId,
147
+ observationRecordIndex: location.recordIndex,
148
+ status: "active",
149
+ sourceEntryIds,
150
+ sourceEntries,
151
+ missingSourceEntryIds,
152
+ nonSourceEntryIds,
153
+ };
154
+ }
155
+
156
+ function notFound(memoryId: string): RecallResult {
157
+ return {
158
+ status: "not_found",
159
+ memoryId,
160
+ kind: undefined,
161
+ reflections: [],
162
+ observations: [],
163
+ sourceEntries: [],
164
+ missingSourceEntryIds: [],
165
+ nonSourceEntryIds: [],
166
+ missingSupportingObservationIds: [],
167
+ collision: false,
168
+ partial: false,
169
+ };
170
+ }
171
+
172
+ export function recallMemorySources(entries: Entry[], memoryId: string): RecallResult {
173
+ const { observations: indexedObservations, reflections: indexedReflections, droppedIds } = indexLedger(entries);
174
+ const directObservationMatches = indexedObservations.filter(({ observation }) => observation.id === memoryId);
175
+ const reflectionMatches = indexedReflections.filter(({ reflection }) => reflection.id === memoryId);
176
+
177
+ if (directObservationMatches.length === 0 && reflectionMatches.length === 0) return notFound(memoryId);
178
+
179
+ const observationsById = new Map<string, IndexedObservation>();
180
+ for (const indexed of indexedObservations) {
181
+ if (!observationsById.has(indexed.observation.id)) observationsById.set(indexed.observation.id, indexed);
182
+ }
183
+
184
+ const recalledByKey = new Map<string, RecalledObservation>();
185
+ const missingSupportingObservationIds: string[] = [];
186
+
187
+ function addObservation(indexed: IndexedObservation): void {
188
+ const key = `${indexed.entryId}:${indexed.recordIndex}`;
189
+ if (recalledByKey.has(key)) return;
190
+ const recalled = resolveObservationSources(entries, indexed.observation, indexed);
191
+ recalled.status = droppedIds.has(indexed.observation.id) ? "dropped" : "active";
192
+ recalledByKey.set(key, recalled);
193
+ }
194
+
195
+ for (const match of directObservationMatches) addObservation(match);
196
+
197
+ for (const { reflection } of reflectionMatches) {
198
+ for (const observationId of uniqueStrings(reflection.supportingObservationIds)) {
199
+ const indexed = observationsById.get(observationId);
200
+ if (!indexed) {
201
+ missingSupportingObservationIds.push(observationId);
202
+ continue;
203
+ }
204
+ addObservation(indexed);
205
+ }
206
+ }
207
+
208
+ const recalledObservations = Array.from(recalledByKey.values());
209
+ const recalledReflections: RecalledReflection[] = reflectionMatches.map(({ reflection, entryId, recordIndex }) => ({
210
+ reflection,
211
+ reflectionEntryId: entryId,
212
+ reflectionRecordIndex: recordIndex,
213
+ }));
214
+ const sourceEntries = uniqueById(recalledObservations.flatMap((match) => match.sourceEntries));
215
+ const missingSourceEntryIds = uniqueStrings(recalledObservations.flatMap((match) => match.missingSourceEntryIds));
216
+ const nonSourceEntryIds = uniqueStrings(recalledObservations.flatMap((match) => match.nonSourceEntryIds));
217
+ const uniqueMissingSupportingObservationIds = uniqueStrings(missingSupportingObservationIds);
218
+ const matchCount = directObservationMatches.length + reflectionMatches.length;
219
+
220
+ return {
221
+ status: "found",
222
+ memoryId,
223
+ kind: directObservationMatches.length > 0 && reflectionMatches.length > 0
224
+ ? "mixed"
225
+ : reflectionMatches.length > 0
226
+ ? "reflection"
227
+ : "observation",
228
+ reflections: recalledReflections,
229
+ observations: recalledObservations,
230
+ sourceEntries,
231
+ missingSourceEntryIds,
232
+ nonSourceEntryIds,
233
+ missingSupportingObservationIds: uniqueMissingSupportingObservationIds,
234
+ collision: matchCount > 1,
235
+ partial: missingSourceEntryIds.length > 0 || nonSourceEntryIds.length > 0 || uniqueMissingSupportingObservationIds.length > 0,
236
+ };
237
+ }
@@ -0,0 +1,31 @@
1
+ import type { Observation, Reflection } from "./types.js";
2
+
3
+ const CONTEXT_USAGE_INSTRUCTIONS = `These are condensed memories from earlier in this session.
4
+
5
+ - Reflections: stable, long-lived facts about the user, project, decisions, and constraints. New reflection lines may include ids in brackets.
6
+ - Observations: timestamped events from the conversation history, in chronological order. Observation lines include ids in brackets.
7
+
8
+ Treat these as past records. When entries conflict, the most recent observation reflects the latest known state. Work that prior observations describe as completed should not be redone unless the user explicitly asks to revisit it.
9
+
10
+ When exact source context is needed for precision or traceability, use the recall tool with the relevant observation or reflection id. This is especially useful when a reflection materially affects a decision or is too compressed to continue confidently. Do not use recall as broad search or inject raw source unless it is needed.`;
11
+
12
+ export function observationToSummaryLine(observation: Observation): string {
13
+ return `[${observation.id}] ${observation.timestamp} [${observation.relevance}] ${observation.content}`;
14
+ }
15
+
16
+ export function reflectionToSummaryLine(reflection: Reflection): string {
17
+ return `[${reflection.id}] ${reflection.content}`;
18
+ }
19
+
20
+ export function renderSummary(reflections: Reflection[], observations: Observation[]): string {
21
+ if (reflections.length === 0 && observations.length === 0) return "";
22
+
23
+ const parts: string[] = [CONTEXT_USAGE_INSTRUCTIONS];
24
+ if (reflections.length > 0) {
25
+ parts.push(`## Reflections\n${reflections.map(reflectionToSummaryLine).join("\n")}`);
26
+ }
27
+ if (observations.length > 0) {
28
+ parts.push(`## Observations\n${observations.map(observationToSummaryLine).join("\n")}`);
29
+ }
30
+ return parts.join("\n\n");
31
+ }