pi-blackhole 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +15 -0
  2. package/dist/index.js +11660 -0
  3. package/dist/index.js.map +1 -0
  4. package/example-config.json +1 -1
  5. package/index.ts +37 -63
  6. package/package.json +21 -9
  7. package/src/commands/cleanup.ts +279 -240
  8. package/src/commands/memory.ts +236 -184
  9. package/src/commands/pi-vcc.ts +202 -152
  10. package/src/commands/vcc-recall.ts +126 -95
  11. package/src/core/brief.ts +167 -33
  12. package/src/core/build-sections.ts +8 -2
  13. package/src/core/config-env.ts +117 -0
  14. package/src/core/content.ts +31 -7
  15. package/src/core/drill-down.ts +41 -11
  16. package/src/core/filter-noise.ts +9 -3
  17. package/src/core/format-recall.ts +15 -6
  18. package/src/core/format.ts +14 -4
  19. package/src/core/lineage.ts +9 -3
  20. package/src/core/load-messages.ts +24 -5
  21. package/src/core/normalize.ts +38 -14
  22. package/src/core/recall-scope.ts +11 -3
  23. package/src/core/render-entries.ts +22 -6
  24. package/src/core/sanitize.ts +5 -1
  25. package/src/core/search-entries.ts +111 -19
  26. package/src/core/settings.ts +1 -3
  27. package/src/core/summarize.ts +42 -21
  28. package/src/core/unified-config.ts +549 -411
  29. package/src/extract/commits.ts +4 -2
  30. package/src/extract/files.ts +10 -5
  31. package/src/extract/goals.ts +7 -2
  32. package/src/hooks/before-compact.ts +210 -88
  33. package/src/om/agents/dropper/agent.ts +380 -265
  34. package/src/om/agents/dropper/coverage.ts +102 -82
  35. package/src/om/agents/observer/agent.ts +242 -206
  36. package/src/om/agents/reflector/agent.ts +212 -153
  37. package/src/om/cleanup.ts +239 -218
  38. package/src/om/clipboard.ts +59 -51
  39. package/src/om/compaction-trigger.ts +448 -333
  40. package/src/om/config.ts +13 -6
  41. package/src/om/configure-overlay.ts +518 -355
  42. package/src/om/consolidation.ts +1460 -953
  43. package/src/om/cooldown.ts +75 -65
  44. package/src/om/debug-log.ts +86 -68
  45. package/src/om/ids.ts +1 -1
  46. package/src/om/ledger/fold.ts +89 -78
  47. package/src/om/ledger/progress.ts +181 -153
  48. package/src/om/ledger/projection.ts +248 -185
  49. package/src/om/ledger/recall.ts +247 -196
  50. package/src/om/ledger/render-summary.ts +79 -50
  51. package/src/om/ledger/types.ts +146 -117
  52. package/src/om/model-budget.ts +23 -13
  53. package/src/om/pending.ts +243 -179
  54. package/src/om/provider-stream.ts +52 -7
  55. package/src/om/retryable-error.ts +12 -16
  56. package/src/om/reverse-recall.ts +97 -91
  57. package/src/om/runtime.ts +474 -375
  58. package/src/om/serialize.ts +190 -166
  59. package/src/om/status-overlay.ts +246 -195
  60. package/src/om/tokens.ts +28 -21
  61. package/src/pi-base/blackhole-settings.ts +437 -0
  62. package/src/pi-base/config-manager.ts +440 -0
  63. package/src/pi-base/config.ts +469 -0
  64. package/src/pi-base/env.ts +43 -0
  65. package/src/pi-base/paths.ts +47 -0
  66. package/src/pi-base/settings/body.ts +1648 -0
  67. package/src/pi-base/settings/fields/action.ts +43 -0
  68. package/src/pi-base/settings/fields/boolean.ts +47 -0
  69. package/src/pi-base/settings/fields/custom.ts +72 -0
  70. package/src/pi-base/settings/fields/enum.ts +310 -0
  71. package/src/pi-base/settings/fields/index.ts +46 -0
  72. package/src/pi-base/settings/fields/model.ts +452 -0
  73. package/src/pi-base/settings/fields/string.ts +527 -0
  74. package/src/pi-base/settings/fields/text.ts +115 -0
  75. package/src/pi-base/settings/frame.ts +197 -0
  76. package/src/pi-base/settings/index.ts +77 -0
  77. package/src/pi-base/settings/inline-edit.ts +313 -0
  78. package/src/pi-base/settings/modal.ts +152 -0
  79. package/src/pi-base/settings/types.ts +500 -0
  80. package/src/pi-base/settings/validate-field.ts +113 -0
  81. package/src/pi-base/shell.ts +117 -0
  82. package/src/pi-base/types.ts +6 -0
  83. package/src/pi-base/ui.ts +32 -0
  84. package/src/tools/recall.ts +347 -225
  85. package/src/types.ts +20 -3
  86. package/tsup.config.ts +23 -0
  87. package/vitest.config.ts +15 -15
@@ -5,12 +5,12 @@
5
5
  * Unmodified.
6
6
  */
7
7
  import {
8
- isObservationsDroppedEntry,
9
- isObservationsRecordedEntry,
10
- isReflectionsRecordedEntry,
11
- type Entry,
12
- type Observation,
13
- type Reflection,
8
+ isObservationsDroppedEntry,
9
+ isObservationsRecordedEntry,
10
+ isReflectionsRecordedEntry,
11
+ type Entry,
12
+ type Observation,
13
+ type Reflection,
14
14
  } from "./types.js";
15
15
 
16
16
  const SOURCE_TYPES = new Set(["message", "custom_message", "branch_summary"]);
@@ -18,226 +18,277 @@ const SOURCE_TYPES = new Set(["message", "custom_message", "branch_summary"]);
18
18
  export type { Entry, Observation, Reflection };
19
19
 
20
20
  type ObservationLedgerLocation = {
21
- entryId: string;
22
- entryIndex: number;
23
- recordIndex: number;
21
+ entryId: string;
22
+ entryIndex: number;
23
+ recordIndex: number;
24
24
  };
25
25
 
26
26
  type ReflectionLedgerLocation = {
27
- entryId: string;
28
- entryIndex: number;
29
- recordIndex: number;
27
+ entryId: string;
28
+ entryIndex: number;
29
+ recordIndex: number;
30
30
  };
31
31
 
32
32
  export type RecalledObservation = {
33
- observation: Observation;
34
- observationEntryId: string;
35
- observationRecordIndex: number;
36
- status: "active" | "dropped";
37
- sourceEntryIds: string[];
38
- sourceEntries: Entry[];
39
- missingSourceEntryIds: string[];
40
- nonSourceEntryIds: string[];
33
+ observation: Observation;
34
+ observationEntryId: string;
35
+ observationRecordIndex: number;
36
+ status: "active" | "dropped";
37
+ sourceEntryIds: string[];
38
+ sourceEntries: Entry[];
39
+ missingSourceEntryIds: string[];
40
+ nonSourceEntryIds: string[];
41
41
  };
42
42
 
43
43
  export type RecalledReflection = {
44
- reflection: Reflection;
45
- reflectionEntryId: string;
46
- reflectionRecordIndex: number;
44
+ reflection: Reflection;
45
+ reflectionEntryId: string;
46
+ reflectionRecordIndex: number;
47
47
  };
48
48
 
49
49
  export type RecallResult =
50
- | {
51
- status: "not_found";
52
- memoryId: string;
53
- kind: undefined;
54
- reflections: [];
55
- observations: [];
56
- sourceEntries: [];
57
- missingSourceEntryIds: [];
58
- nonSourceEntryIds: [];
59
- missingSupportingObservationIds: [];
60
- collision: false;
61
- partial: false;
62
- }
63
- | {
64
- status: "found";
65
- memoryId: string;
66
- kind: "observation" | "reflection" | "mixed";
67
- reflections: RecalledReflection[];
68
- observations: RecalledObservation[];
69
- sourceEntries: Entry[];
70
- missingSourceEntryIds: string[];
71
- nonSourceEntryIds: string[];
72
- missingSupportingObservationIds: string[];
73
- collision: boolean;
74
- partial: boolean;
75
- };
76
-
77
- type IndexedObservation = ObservationLedgerLocation & { observation: Observation };
50
+ | {
51
+ status: "not_found";
52
+ memoryId: string;
53
+ kind: undefined;
54
+ reflections: [];
55
+ observations: [];
56
+ sourceEntries: [];
57
+ missingSourceEntryIds: [];
58
+ nonSourceEntryIds: [];
59
+ missingSupportingObservationIds: [];
60
+ collision: false;
61
+ partial: false;
62
+ }
63
+ | {
64
+ status: "found";
65
+ memoryId: string;
66
+ kind: "observation" | "reflection" | "mixed";
67
+ reflections: RecalledReflection[];
68
+ observations: RecalledObservation[];
69
+ sourceEntries: Entry[];
70
+ missingSourceEntryIds: string[];
71
+ nonSourceEntryIds: string[];
72
+ missingSupportingObservationIds: string[];
73
+ collision: boolean;
74
+ partial: boolean;
75
+ };
76
+
77
+ type IndexedObservation = ObservationLedgerLocation & {
78
+ observation: Observation;
79
+ };
78
80
  type IndexedReflection = ReflectionLedgerLocation & { reflection: Reflection };
79
81
 
80
82
  function isSourceEntry(entry: Entry): boolean {
81
- return SOURCE_TYPES.has(entry.type);
83
+ return SOURCE_TYPES.has(entry.type);
82
84
  }
83
85
 
84
86
  function uniqueById(entries: Entry[]): Entry[] {
85
- const seen = new Set<string>();
86
- const result: Entry[] = [];
87
- for (const entry of entries) {
88
- if (seen.has(entry.id)) continue;
89
- seen.add(entry.id);
90
- result.push(entry);
91
- }
92
- return result;
87
+ const seen = new Set<string>();
88
+ const result: Entry[] = [];
89
+ for (const entry of entries) {
90
+ if (seen.has(entry.id)) continue;
91
+ seen.add(entry.id);
92
+ result.push(entry);
93
+ }
94
+ return result;
93
95
  }
94
96
 
95
97
  function uniqueStrings(values: string[]): string[] {
96
- return Array.from(new Set(values));
98
+ return Array.from(new Set(values));
97
99
  }
98
100
 
99
101
  export function indexLedger(entries: Entry[]): {
100
- observations: IndexedObservation[];
101
- reflections: IndexedReflection[];
102
- droppedIds: Set<string>;
102
+ observations: IndexedObservation[];
103
+ reflections: IndexedReflection[];
104
+ droppedIds: Set<string>;
103
105
  } {
104
- const observations: IndexedObservation[] = [];
105
- const reflections: IndexedReflection[] = [];
106
- const droppedIds = new Set<string>();
107
-
108
- for (let entryIndex = 0; entryIndex < entries.length; entryIndex++) {
109
- const entry = entries[entryIndex];
110
- if (isObservationsRecordedEntry(entry)) {
111
- entry.data.observations.forEach((observation, recordIndex) => {
112
- observations.push({ observation, entryId: entry.id, entryIndex, recordIndex });
113
- });
114
- continue;
115
- }
116
- if (isReflectionsRecordedEntry(entry)) {
117
- entry.data.reflections.forEach((reflection, recordIndex) => {
118
- reflections.push({ reflection, entryId: entry.id, entryIndex, recordIndex });
119
- });
120
- continue;
121
- }
122
- if (isObservationsDroppedEntry(entry)) {
123
- entry.data.observationIds.forEach((id) => droppedIds.add(id));
124
- }
125
- }
126
-
127
- return { observations, reflections, droppedIds };
106
+ const observations: IndexedObservation[] = [];
107
+ const reflections: IndexedReflection[] = [];
108
+ const droppedIds = new Set<string>();
109
+
110
+ for (let entryIndex = 0; entryIndex < entries.length; entryIndex++) {
111
+ const entry = entries[entryIndex];
112
+ if (isObservationsRecordedEntry(entry)) {
113
+ entry.data.observations.forEach((observation, recordIndex) => {
114
+ observations.push({
115
+ observation,
116
+ entryId: entry.id,
117
+ entryIndex,
118
+ recordIndex,
119
+ });
120
+ });
121
+ continue;
122
+ }
123
+ if (isReflectionsRecordedEntry(entry)) {
124
+ entry.data.reflections.forEach((reflection, recordIndex) => {
125
+ reflections.push({
126
+ reflection,
127
+ entryId: entry.id,
128
+ entryIndex,
129
+ recordIndex,
130
+ });
131
+ });
132
+ continue;
133
+ }
134
+ if (isObservationsDroppedEntry(entry)) {
135
+ entry.data.observationIds.forEach((id) => droppedIds.add(id));
136
+ }
137
+ }
138
+
139
+ return { observations, reflections, droppedIds };
128
140
  }
129
141
 
130
- function resolveObservationSources(entries: Entry[], observation: Observation, location: ObservationLedgerLocation): RecalledObservation {
131
- const sourceEntryIds = uniqueStrings(observation.sourceEntryIds);
132
- const byId = new Map(entries.map((entry) => [entry.id, entry]));
133
- const sourceEntries: Entry[] = [];
134
- const missingSourceEntryIds: string[] = [];
135
- const nonSourceEntryIds: string[] = [];
136
-
137
- for (const sourceEntryId of sourceEntryIds) {
138
- const sourceEntry = byId.get(sourceEntryId);
139
- if (!sourceEntry) {
140
- missingSourceEntryIds.push(sourceEntryId);
141
- continue;
142
- }
143
- if (!isSourceEntry(sourceEntry)) {
144
- nonSourceEntryIds.push(sourceEntryId);
145
- continue;
146
- }
147
- sourceEntries.push(sourceEntry);
148
- }
149
-
150
- return {
151
- observation,
152
- observationEntryId: location.entryId,
153
- observationRecordIndex: location.recordIndex,
154
- status: "active",
155
- sourceEntryIds,
156
- sourceEntries,
157
- missingSourceEntryIds,
158
- nonSourceEntryIds,
159
- };
142
+ function resolveObservationSources(
143
+ entries: Entry[],
144
+ observation: Observation,
145
+ location: ObservationLedgerLocation,
146
+ ): RecalledObservation {
147
+ const sourceEntryIds = uniqueStrings(observation.sourceEntryIds);
148
+ const byId = new Map(entries.map((entry) => [entry.id, entry]));
149
+ const sourceEntries: Entry[] = [];
150
+ const missingSourceEntryIds: string[] = [];
151
+ const nonSourceEntryIds: string[] = [];
152
+
153
+ for (const sourceEntryId of sourceEntryIds) {
154
+ const sourceEntry = byId.get(sourceEntryId);
155
+ if (!sourceEntry) {
156
+ missingSourceEntryIds.push(sourceEntryId);
157
+ continue;
158
+ }
159
+ if (!isSourceEntry(sourceEntry)) {
160
+ nonSourceEntryIds.push(sourceEntryId);
161
+ continue;
162
+ }
163
+ sourceEntries.push(sourceEntry);
164
+ }
165
+
166
+ return {
167
+ observation,
168
+ observationEntryId: location.entryId,
169
+ observationRecordIndex: location.recordIndex,
170
+ status: "active",
171
+ sourceEntryIds,
172
+ sourceEntries,
173
+ missingSourceEntryIds,
174
+ nonSourceEntryIds,
175
+ };
160
176
  }
161
177
 
162
178
  function notFound(memoryId: string): RecallResult {
163
- return {
164
- status: "not_found",
165
- memoryId,
166
- kind: undefined,
167
- reflections: [],
168
- observations: [],
169
- sourceEntries: [],
170
- missingSourceEntryIds: [],
171
- nonSourceEntryIds: [],
172
- missingSupportingObservationIds: [],
173
- collision: false,
174
- partial: false,
175
- };
179
+ return {
180
+ status: "not_found",
181
+ memoryId,
182
+ kind: undefined,
183
+ reflections: [],
184
+ observations: [],
185
+ sourceEntries: [],
186
+ missingSourceEntryIds: [],
187
+ nonSourceEntryIds: [],
188
+ missingSupportingObservationIds: [],
189
+ collision: false,
190
+ partial: false,
191
+ };
176
192
  }
177
193
 
178
- export function recallMemorySources(entries: Entry[], memoryId: string): RecallResult {
179
- const { observations: indexedObservations, reflections: indexedReflections, droppedIds } = indexLedger(entries);
180
- const directObservationMatches = indexedObservations.filter(({ observation }) => observation.id === memoryId);
181
- const reflectionMatches = indexedReflections.filter(({ reflection }) => reflection.id === memoryId);
182
-
183
- if (directObservationMatches.length === 0 && reflectionMatches.length === 0) return notFound(memoryId);
184
-
185
- const observationsById = new Map<string, IndexedObservation>();
186
- for (const indexed of indexedObservations) {
187
- if (!observationsById.has(indexed.observation.id)) observationsById.set(indexed.observation.id, indexed);
188
- }
189
-
190
- const recalledByKey = new Map<string, RecalledObservation>();
191
- const rawMissingSupportingObservationIds: string[] = [];
192
-
193
- function addObservation(indexed: IndexedObservation): void {
194
- const key = `${indexed.entryId}:${indexed.recordIndex}`;
195
- if (recalledByKey.has(key)) return;
196
- const recalled = resolveObservationSources(entries, indexed.observation, indexed);
197
- recalled.status = droppedIds.has(indexed.observation.id) ? "dropped" : "active";
198
- recalledByKey.set(key, recalled);
199
- }
200
-
201
- for (const match of directObservationMatches) addObservation(match);
202
-
203
- for (const { reflection } of reflectionMatches) {
204
- for (const observationId of uniqueStrings(reflection.supportingObservationIds)) {
205
- const indexed = observationsById.get(observationId);
206
- if (!indexed) {
207
- rawMissingSupportingObservationIds.push(observationId);
208
- continue;
209
- }
210
- addObservation(indexed);
211
- }
212
- }
213
-
214
- const recalledObservations = Array.from(recalledByKey.values());
215
- const recalledReflections: RecalledReflection[] = reflectionMatches.map(({ reflection, entryId, recordIndex }) => ({
216
- reflection,
217
- reflectionEntryId: entryId,
218
- reflectionRecordIndex: recordIndex,
219
- }));
220
- const sourceEntries = uniqueById(recalledObservations.flatMap((match) => match.sourceEntries));
221
- const missingSourceEntryIds = uniqueStrings(recalledObservations.flatMap((match) => match.missingSourceEntryIds));
222
- const nonSourceEntryIds = uniqueStrings(recalledObservations.flatMap((match) => match.nonSourceEntryIds));
223
- const uniqueMissingSupportingObservationIds = uniqueStrings(rawMissingSupportingObservationIds);
224
- const matchCount = directObservationMatches.length + reflectionMatches.length;
225
-
226
- return {
227
- status: "found",
228
- memoryId,
229
- kind: directObservationMatches.length > 0 && reflectionMatches.length > 0
230
- ? "mixed"
231
- : reflectionMatches.length > 0
232
- ? "reflection"
233
- : "observation",
234
- reflections: recalledReflections,
235
- observations: recalledObservations,
236
- sourceEntries,
237
- missingSourceEntryIds,
238
- nonSourceEntryIds,
239
- missingSupportingObservationIds: uniqueMissingSupportingObservationIds,
240
- collision: matchCount > 1,
241
- partial: missingSourceEntryIds.length > 0 || nonSourceEntryIds.length > 0 || uniqueMissingSupportingObservationIds.length > 0,
242
- };
194
+ export function recallMemorySources(
195
+ entries: Entry[],
196
+ memoryId: string,
197
+ ): RecallResult {
198
+ const {
199
+ observations: indexedObservations,
200
+ reflections: indexedReflections,
201
+ droppedIds,
202
+ } = indexLedger(entries);
203
+ const directObservationMatches = indexedObservations.filter(
204
+ ({ observation }) => observation.id === memoryId,
205
+ );
206
+ const reflectionMatches = indexedReflections.filter(
207
+ ({ reflection }) => reflection.id === memoryId,
208
+ );
209
+
210
+ if (directObservationMatches.length === 0 && reflectionMatches.length === 0)
211
+ return notFound(memoryId);
212
+
213
+ const observationsById = new Map<string, IndexedObservation>();
214
+ for (const indexed of indexedObservations) {
215
+ if (!observationsById.has(indexed.observation.id))
216
+ observationsById.set(indexed.observation.id, indexed);
217
+ }
218
+
219
+ const recalledByKey = new Map<string, RecalledObservation>();
220
+ const rawMissingSupportingObservationIds: string[] = [];
221
+
222
+ function addObservation(indexed: IndexedObservation): void {
223
+ const key = `${indexed.entryId}:${indexed.recordIndex}`;
224
+ if (recalledByKey.has(key)) return;
225
+ const recalled = resolveObservationSources(
226
+ entries,
227
+ indexed.observation,
228
+ indexed,
229
+ );
230
+ recalled.status = droppedIds.has(indexed.observation.id)
231
+ ? "dropped"
232
+ : "active";
233
+ recalledByKey.set(key, recalled);
234
+ }
235
+
236
+ for (const match of directObservationMatches) addObservation(match);
237
+
238
+ for (const { reflection } of reflectionMatches) {
239
+ for (const observationId of uniqueStrings(
240
+ reflection.supportingObservationIds,
241
+ )) {
242
+ const indexed = observationsById.get(observationId);
243
+ if (!indexed) {
244
+ rawMissingSupportingObservationIds.push(observationId);
245
+ continue;
246
+ }
247
+ addObservation(indexed);
248
+ }
249
+ }
250
+
251
+ const recalledObservations = Array.from(recalledByKey.values());
252
+ const recalledReflections: RecalledReflection[] = reflectionMatches.map(
253
+ ({ reflection, entryId, recordIndex }) => ({
254
+ reflection,
255
+ reflectionEntryId: entryId,
256
+ reflectionRecordIndex: recordIndex,
257
+ }),
258
+ );
259
+ const sourceEntries = uniqueById(
260
+ recalledObservations.flatMap((match) => match.sourceEntries),
261
+ );
262
+ const missingSourceEntryIds = uniqueStrings(
263
+ recalledObservations.flatMap((match) => match.missingSourceEntryIds),
264
+ );
265
+ const nonSourceEntryIds = uniqueStrings(
266
+ recalledObservations.flatMap((match) => match.nonSourceEntryIds),
267
+ );
268
+ const uniqueMissingSupportingObservationIds = uniqueStrings(
269
+ rawMissingSupportingObservationIds,
270
+ );
271
+ const matchCount = directObservationMatches.length + reflectionMatches.length;
272
+
273
+ return {
274
+ status: "found",
275
+ memoryId,
276
+ kind:
277
+ directObservationMatches.length > 0 && reflectionMatches.length > 0
278
+ ? "mixed"
279
+ : reflectionMatches.length > 0
280
+ ? "reflection"
281
+ : "observation",
282
+ reflections: recalledReflections,
283
+ observations: recalledObservations,
284
+ sourceEntries,
285
+ missingSourceEntryIds,
286
+ nonSourceEntryIds,
287
+ missingSupportingObservationIds: uniqueMissingSupportingObservationIds,
288
+ collision: matchCount > 1,
289
+ partial:
290
+ missingSourceEntryIds.length > 0 ||
291
+ nonSourceEntryIds.length > 0 ||
292
+ uniqueMissingSupportingObservationIds.length > 0,
293
+ };
243
294
  }
@@ -18,18 +18,26 @@ export const OM_FOOTER_FULL = `----\n${OM_INSTRUCTIONS_FULL}\n----`;
18
18
  export const OM_FOOTER_BASIC = `----\n${OM_INSTRUCTIONS_BASIC}\n----`;
19
19
 
20
20
  export function observationToSummaryLine(observation: Observation): string {
21
- return `[${observation.id}] ${observation.timestamp} [${observation.relevance}] ${observation.content}`;
21
+ return `[${observation.id}] ${observation.timestamp} [${observation.relevance}] ${observation.content}`;
22
22
  }
23
23
 
24
24
  /** Score an observation for cap/trim selection.
25
25
  * Relevance tier dominates: medium (5+) always outranks low (max 2).
26
26
  * Recency is based on position in the flat-mapped array (0 = oldest, N-1 = newest),
27
27
  * avoiding wall-clock dependency that punishes sessions spanning days or weeks. */
28
- export function scoreObservation(obs: Observation, index: number, total: number): number {
29
- const base = obs.relevance === "high" || obs.relevance === "critical" ? 10
30
- : obs.relevance === "medium" ? 5 : 1;
31
- const recency = total > 1 ? index / (total - 1) : 1;
32
- return base + recency;
28
+ export function scoreObservation(
29
+ obs: Observation,
30
+ index: number,
31
+ total: number,
32
+ ): number {
33
+ const base =
34
+ obs.relevance === "high" || obs.relevance === "critical"
35
+ ? 10
36
+ : obs.relevance === "medium"
37
+ ? 5
38
+ : 1;
39
+ const recency = total > 1 ? index / (total - 1) : 1;
40
+ return base + recency;
33
41
  }
34
42
 
35
43
  /** Select observations up to a token budget, keeping all high-relevance items
@@ -39,58 +47,79 @@ export function scoreObservation(obs: Observation, index: number, total: number)
39
47
  * Reflections are never trimmed — they are inherently rare and always stay.
40
48
  * Observations stay in the branch either way; this only caps what is rendered
41
49
  * in the compaction summary output. */
42
- export function selectPriorObservations(observations: Observation[], maxTokens: number): Observation[] {
43
- // Track original indices so we can restore chronological order after scoring
44
- const indexed = observations.map((obs, i) => ({ obs, originalIndex: i }));
45
- const high = indexed.filter(item => item.obs.relevance === "high" || item.obs.relevance === "critical");
46
- const rest = indexed.filter(item => item.obs.relevance !== "high" && item.obs.relevance !== "critical");
50
+ export function selectPriorObservations(
51
+ observations: Observation[],
52
+ maxTokens: number,
53
+ ): Observation[] {
54
+ // Track original indices so we can restore chronological order after scoring
55
+ const indexed = observations.map((obs, i) => ({ obs, originalIndex: i }));
56
+ const high = indexed.filter(
57
+ (item) =>
58
+ item.obs.relevance === "high" || item.obs.relevance === "critical",
59
+ );
60
+ const rest = indexed.filter(
61
+ (item) =>
62
+ item.obs.relevance !== "high" && item.obs.relevance !== "critical",
63
+ );
47
64
 
48
- // High always kept — consume budget first
49
- let budget = maxTokens;
50
- const selected = new Set<{ obs: Observation; originalIndex: number }>();
51
- for (const item of high) {
52
- const lineTokens = Math.ceil(observationToSummaryLine(item.obs).length / 4);
53
- selected.add(item);
54
- budget -= lineTokens;
55
- }
65
+ // High always kept — consume budget first
66
+ let budget = maxTokens;
67
+ const selected = new Set<{ obs: Observation; originalIndex: number }>();
68
+ for (const item of high) {
69
+ const lineTokens = Math.ceil(observationToSummaryLine(item.obs).length / 4);
70
+ selected.add(item);
71
+ budget -= lineTokens;
72
+ }
56
73
 
57
- // Score medium + low and select best within remaining budget
58
- if (rest.length > 0 && budget > 0) {
59
- const scored = rest.map((item, i) => ({ item, score: scoreObservation(item.obs, i, rest.length) }));
60
- scored.sort((a, b) => b.score - a.score); // highest score first
61
- for (const { item } of scored) {
62
- const lineTokens = Math.ceil(observationToSummaryLine(item.obs).length / 4);
63
- if (budget - lineTokens < 0) break;
64
- selected.add(item);
65
- budget -= lineTokens;
66
- }
67
- }
74
+ // Score medium + low and select best within remaining budget
75
+ if (rest.length > 0 && budget > 0) {
76
+ const scored = rest.map((item, i) => ({
77
+ item,
78
+ score: scoreObservation(item.obs, i, rest.length),
79
+ }));
80
+ scored.sort((a, b) => b.score - a.score); // highest score first
81
+ for (const { item } of scored) {
82
+ const lineTokens = Math.ceil(
83
+ observationToSummaryLine(item.obs).length / 4,
84
+ );
85
+ if (budget - lineTokens < 0) break;
86
+ selected.add(item);
87
+ budget -= lineTokens;
88
+ }
89
+ }
68
90
 
69
- // Restore original chronological order before returning
70
- return Array.from(selected)
71
- .sort((a, b) => a.originalIndex - b.originalIndex)
72
- .map(item => item.obs);
91
+ // Restore original chronological order before returning
92
+ return Array.from(selected)
93
+ .sort((a, b) => a.originalIndex - b.originalIndex)
94
+ .map((item) => item.obs);
73
95
  }
74
96
 
75
97
  export function reflectionToSummaryLine(reflection: Reflection): string {
76
- return `[${reflection.id}] ${reflection.content}`;
98
+ return `[${reflection.id}] ${reflection.content}`;
77
99
  }
78
100
 
79
- export function renderSummary(reflections: Reflection[], observations: Observation[]): string {
80
- const hasContent = reflections.length > 0 || observations.length > 0;
101
+ export function renderSummary(
102
+ reflections: Reflection[],
103
+ observations: Observation[],
104
+ ): string {
105
+ const hasContent = reflections.length > 0 || observations.length > 0;
81
106
 
82
- const parts: string[] = [];
83
- if (reflections.length > 0) {
84
- parts.push(`## Reflections\n${reflections.map(reflectionToSummaryLine).join("\n")}`);
85
- }
86
- if (observations.length > 0) {
87
- parts.push(`## Observations\n${observations.map(observationToSummaryLine).join("\n")}`);
88
- }
107
+ const parts: string[] = [];
108
+ if (reflections.length > 0) {
109
+ parts.push(
110
+ `## Reflections\n${reflections.map(reflectionToSummaryLine).join("\n")}`,
111
+ );
112
+ }
113
+ if (observations.length > 0) {
114
+ parts.push(
115
+ `## Observations\n${observations.map(observationToSummaryLine).join("\n")}`,
116
+ );
117
+ }
89
118
 
90
- const footer = hasContent ? OM_FOOTER_FULL : OM_FOOTER_BASIC;
91
- if (parts.length > 0) {
92
- parts.push(footer);
93
- return parts.join("\n\n");
94
- }
95
- return footer;
119
+ const footer = hasContent ? OM_FOOTER_FULL : OM_FOOTER_BASIC;
120
+ if (parts.length > 0) {
121
+ parts.push(footer);
122
+ return parts.join("\n\n");
123
+ }
124
+ return footer;
96
125
  }