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
@@ -8,7 +8,7 @@
8
8
 
9
9
  /**
10
10
  * Cooldown persistence for retryable API errors.
11
- *
11
+ *
12
12
  * Created by pi-vcc-om. Records per-model cooldowns to disk so rate-limited
13
13
  * or down models are skipped until their cooldown window expires.
14
14
  *
@@ -28,46 +28,46 @@ const CONFIG_DIR = "pi-blackhole";
28
28
  const COOLDOWN_FILE = "pi-blackhole-cooldown.json";
29
29
 
30
30
  function cooldownPath(): string {
31
- return join(getAgentDir(), CONFIG_DIR, COOLDOWN_FILE);
31
+ return join(getAgentDir(), CONFIG_DIR, COOLDOWN_FILE);
32
32
  }
33
33
 
34
34
  export interface CooldownEntry {
35
- until: string; // ISO 8601 timestamp
36
- reason: string;
37
- stage: string; // "observer" | "reflector" | "dropper"
35
+ until: string; // ISO 8601 timestamp
36
+ reason: string;
37
+ stage: string; // "observer" | "reflector" | "dropper"
38
38
  }
39
39
 
40
40
  type CooldownMap = Record<string, CooldownEntry>;
41
41
 
42
42
  /** Provider/id key for cooldown lookup. */
43
43
  export function modelKey(model: OmModelConfig): string {
44
- return `${model.provider}/${model.id}`;
44
+ return `${model.provider}/${model.id}`;
45
45
  }
46
46
 
47
47
  // ── Load / save ─────────────────────────────────────────────────────────────
48
48
 
49
49
  function readCooldownMap(): CooldownMap {
50
- const path = cooldownPath();
51
- if (!existsSync(path)) return {};
52
- try {
53
- return JSON.parse(readFileSync(path, "utf-8"));
54
- } catch {
55
- return {};
56
- }
50
+ const path = cooldownPath();
51
+ if (!existsSync(path)) return {};
52
+ try {
53
+ return JSON.parse(readFileSync(path, "utf-8"));
54
+ } catch {
55
+ return {};
56
+ }
57
57
  }
58
58
 
59
59
  function writeCooldownMap(map: CooldownMap): void {
60
- try {
61
- const path = cooldownPath();
62
- const dir = dirname(path);
63
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
64
- writeFileSync(path, `${JSON.stringify(map, null, 2)}\n`);
65
- } catch {
66
- // Best-effort: cooldowns are advisory. Losing them means a rate-limited
67
- // model might be retried before its cooldown window expires — slightly
68
- // more API traffic, no data loss. This also prevents a process crash
69
- // on read-only filesystems.
70
- }
60
+ try {
61
+ const path = cooldownPath();
62
+ const dir = dirname(path);
63
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
64
+ writeFileSync(path, `${JSON.stringify(map, null, 2)}\n`);
65
+ } catch {
66
+ // Best-effort: cooldowns are advisory. Losing them means a rate-limited
67
+ // model might be retried before its cooldown window expires — slightly
68
+ // more API traffic, no data loss. This also prevents a process crash
69
+ // on read-only filesystems.
70
+ }
71
71
  }
72
72
 
73
73
  // ── API ─────────────────────────────────────────────────────────────────────
@@ -78,33 +78,39 @@ function writeCooldownMap(map: CooldownMap): void {
78
78
  *
79
79
  * When cooldownHours is explicitly 0, cooldown is disabled — always returns false.
80
80
  */
81
- export function isCooldownActive(model: OmModelConfig, now: Date = new Date()): boolean {
82
- return getCooldownEntry(model, now) !== undefined;
81
+ export function isCooldownActive(
82
+ model: OmModelConfig,
83
+ now: Date = new Date(),
84
+ ): boolean {
85
+ return getCooldownEntry(model, now) !== undefined;
83
86
  }
84
87
 
85
88
  /**
86
89
  * Returns the active cooldown entry for a model, or undefined if not cooled down.
87
90
  * Expired entries are cleaned up lazily.
88
91
  */
89
- export function getCooldownEntry(model: OmModelConfig, now: Date = new Date()): CooldownEntry | undefined {
90
- // cooldownHours === 0 means cooldown disabled
91
- if (model.cooldownHours === 0) return undefined;
92
-
93
- const map = readCooldownMap();
94
- const key = modelKey(model);
95
- const entry = map[key];
96
- if (!entry) return undefined;
97
-
98
- const until = new Date(entry.until);
99
- if (isNaN(until.getTime())) return undefined;
100
-
101
- if (now >= until) {
102
- // Expired clean up
103
- delete map[key];
104
- writeCooldownMap(map);
105
- return undefined;
106
- }
107
- return entry;
92
+ export function getCooldownEntry(
93
+ model: OmModelConfig,
94
+ now: Date = new Date(),
95
+ ): CooldownEntry | undefined {
96
+ // cooldownHours === 0 means cooldown disabled
97
+ if (model.cooldownHours === 0) return undefined;
98
+
99
+ const map = readCooldownMap();
100
+ const key = modelKey(model);
101
+ const entry = map[key];
102
+ if (!entry) return undefined;
103
+
104
+ const until = new Date(entry.until);
105
+ if (isNaN(until.getTime())) return undefined;
106
+
107
+ if (now >= until) {
108
+ // Expired — clean up
109
+ delete map[key];
110
+ writeCooldownMap(map);
111
+ return undefined;
112
+ }
113
+ return entry;
108
114
  }
109
115
 
110
116
  /**
@@ -116,15 +122,19 @@ export function getCooldownEntry(model: OmModelConfig, now: Date = new Date()):
116
122
  * @param reason Human-readable error reason (e.g. "429 Too Many Requests").
117
123
  * @param stage Which pipeline stage failed ("observer" | "reflector" | "dropper").
118
124
  */
119
- export function recordCooldown(model: OmModelConfig, reason: string, stage: string): void {
120
- // cooldownHours === 0 means cooldown disabled
121
- if (model.cooldownHours === 0) return;
122
-
123
- const hours = model.cooldownHours ?? 1;
124
- const until = new Date(Date.now() + hours * 3_600_000).toISOString();
125
- const map = readCooldownMap();
126
- map[modelKey(model)] = { until, reason, stage };
127
- writeCooldownMap(map);
125
+ export function recordCooldown(
126
+ model: OmModelConfig,
127
+ reason: string,
128
+ stage: string,
129
+ ): void {
130
+ // cooldownHours === 0 means cooldown disabled
131
+ if (model.cooldownHours === 0) return;
132
+
133
+ const hours = model.cooldownHours ?? 1;
134
+ const until = new Date(Date.now() + hours * 3_600_000).toISOString();
135
+ const map = readCooldownMap();
136
+ map[modelKey(model)] = { until, reason, stage };
137
+ writeCooldownMap(map);
128
138
  }
129
139
 
130
140
  /**
@@ -132,17 +142,17 @@ export function recordCooldown(model: OmModelConfig, reason: string, stage: stri
132
142
  * Call on session_start or config reload to clean up.
133
143
  */
134
144
  export function expireCooldowns(): void {
135
- const map = readCooldownMap();
136
- const now = new Date();
137
- let changed = false;
138
- for (const [key, entry] of Object.entries(map)) {
139
- const until = new Date(entry.until);
140
- if (isNaN(until.getTime()) || now >= until) {
141
- delete map[key];
142
- changed = true;
143
- }
144
- }
145
- if (changed) writeCooldownMap(map);
145
+ const map = readCooldownMap();
146
+ const now = new Date();
147
+ let changed = false;
148
+ for (const [key, entry] of Object.entries(map)) {
149
+ const until = new Date(entry.until);
150
+ if (isNaN(until.getTime()) || now >= until) {
151
+ delete map[key];
152
+ changed = true;
153
+ }
154
+ }
155
+ if (changed) writeCooldownMap(map);
146
156
  }
147
157
 
148
158
  import { isRetryableError } from "./retryable-error.js";
@@ -8,7 +8,14 @@
8
8
  * Modified: path changed from observational-memory/ to pi-blackhole/; async buffered.
9
9
  */
10
10
  import { AsyncLocalStorage } from "node:async_hooks";
11
- import { existsSync, mkdirSync, renameSync, statSync, unlinkSync, appendFileSync } from "node:fs";
11
+ import {
12
+ existsSync,
13
+ mkdirSync,
14
+ renameSync,
15
+ statSync,
16
+ unlinkSync,
17
+ appendFileSync,
18
+ } from "node:fs";
12
19
  import { appendFile } from "node:fs/promises";
13
20
  import { dirname, join } from "node:path";
14
21
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
@@ -17,16 +24,19 @@ export const DEBUG_LOG_MAX_BYTES = 10 * 1024 * 1024;
17
24
  export const DEBUG_LOG_RELATIVE_PATH = join("pi-blackhole", "debug.ndjson");
18
25
 
19
26
  interface DebugLogContext {
20
- enabled: boolean;
21
- cwd?: string;
22
- runId?: string;
27
+ enabled: boolean;
28
+ cwd?: string;
29
+ runId?: string;
23
30
  }
24
31
 
25
32
  const storage = new AsyncLocalStorage<DebugLogContext>();
26
33
 
27
- export function withDebugLogContext<T>(context: DebugLogContext, fn: () => T): T {
28
- const parent = storage.getStore();
29
- return storage.run({ ...parent, ...context }, fn);
34
+ export function withDebugLogContext<T>(
35
+ context: DebugLogContext,
36
+ fn: () => T,
37
+ ): T {
38
+ const parent = storage.getStore();
39
+ return storage.run({ ...parent, ...context }, fn);
30
40
  }
31
41
 
32
42
  // ── Async buffer ────────────────────────────────────────────────────────────
@@ -39,61 +49,69 @@ let flushing = false;
39
49
  let lastWriteMs = 0;
40
50
 
41
51
  function ensureFlushTimer(): void {
42
- if (flushTimer) return;
43
- flushTimer = setInterval(() => {
44
- // Stop the timer if buffer has been empty for a while
45
- if (buffer.length === 0 && lastWriteMs > 0 && Date.now() - lastWriteMs > FLUSH_IDLE_MS) {
46
- clearInterval(flushTimer!);
47
- flushTimer = null;
48
- return;
49
- }
50
- flushBuffer().catch(() => {});
51
- }, BUFFER_FLUSH_MS);
52
- // Don't prevent process exit
53
- if (flushTimer && typeof flushTimer === "object" && "unref" in flushTimer) {
54
- flushTimer.unref();
55
- }
52
+ if (flushTimer) return;
53
+ flushTimer = setInterval(() => {
54
+ // Stop the timer if buffer has been empty for a while
55
+ if (
56
+ buffer.length === 0 &&
57
+ lastWriteMs > 0 &&
58
+ Date.now() - lastWriteMs > FLUSH_IDLE_MS
59
+ ) {
60
+ clearInterval(flushTimer!);
61
+ flushTimer = null;
62
+ return;
63
+ }
64
+ flushBuffer().catch(() => {});
65
+ }, BUFFER_FLUSH_MS);
66
+ // Don't prevent process exit
67
+ if (flushTimer && typeof flushTimer === "object" && "unref" in flushTimer) {
68
+ flushTimer.unref();
69
+ }
56
70
  }
57
71
 
58
72
  async function flushBuffer(): Promise<void> {
59
- if (flushing) return;
60
- if (buffer.length === 0) return;
61
- flushing = true;
62
- // Drain the buffer atomically so flushDebugLog doesn't split entries
63
- const batch = buffer;
64
- buffer = [];
65
- try {
66
- const path = join(getAgentDir(), DEBUG_LOG_RELATIVE_PATH);
67
- mkdirSync(dirname(path), { recursive: true });
68
- rotateIfNeeded(path);
69
- await appendFile(path, batch.join(""), "utf-8");
70
- } catch (error) {
71
- console.error("blackhole: debug log write failed", error);
72
- } finally {
73
- flushing = false;
74
- }
73
+ if (flushing) return;
74
+ if (buffer.length === 0) return;
75
+ flushing = true;
76
+ // Drain the buffer atomically so flushDebugLog doesn't split entries
77
+ const batch = buffer;
78
+ buffer = [];
79
+ try {
80
+ const path = join(getAgentDir(), DEBUG_LOG_RELATIVE_PATH);
81
+ mkdirSync(dirname(path), { recursive: true });
82
+ rotateIfNeeded(path);
83
+ await appendFile(path, batch.join(""), "utf-8");
84
+ } catch (error) {
85
+ console.error("blackhole: debug log write failed", error);
86
+ } finally {
87
+ flushing = false;
88
+ }
75
89
  }
76
90
 
77
91
  // Flush remaining buffer on exit — synchronous to work with process.exit() too
78
92
  process.on("exit", () => {
79
- flushDebugLog();
93
+ flushDebugLog();
80
94
  });
81
95
 
82
- export function debugLog(event: string, data: Record<string, unknown> = {}, forceEnabled?: boolean): void {
83
- const context = storage.getStore();
84
- const enabled = forceEnabled ?? context?.enabled ?? false;
85
- if (enabled !== true) return;
96
+ export function debugLog(
97
+ event: string,
98
+ data: Record<string, unknown> = {},
99
+ forceEnabled?: boolean,
100
+ ): void {
101
+ const context = storage.getStore();
102
+ const enabled = forceEnabled ?? context?.enabled ?? false;
103
+ if (enabled !== true) return;
86
104
 
87
- const payload = {
88
- ts: new Date().toISOString(),
89
- event,
90
- cwd: context?.cwd,
91
- runId: context?.runId,
92
- data,
93
- };
94
- buffer.push(JSON.stringify(payload) + "\n");
95
- lastWriteMs = Date.now();
96
- ensureFlushTimer();
105
+ const payload = {
106
+ ts: new Date().toISOString(),
107
+ event,
108
+ cwd: context?.cwd,
109
+ runId: context?.runId,
110
+ data,
111
+ };
112
+ buffer.push(JSON.stringify(payload) + "\n");
113
+ lastWriteMs = Date.now();
114
+ ensureFlushTimer();
97
115
  }
98
116
 
99
117
  /**
@@ -102,23 +120,23 @@ export function debugLog(event: string, data: Record<string, unknown> = {}, forc
102
120
  * In production, the background timer handles flushing automatically.
103
121
  */
104
122
  export function flushDebugLog(): void {
105
- if (flushing || buffer.length === 0) return;
106
- const batch = buffer;
107
- buffer = [];
108
- try {
109
- const path = join(getAgentDir(), DEBUG_LOG_RELATIVE_PATH);
110
- mkdirSync(dirname(path), { recursive: true });
111
- rotateIfNeeded(path);
112
- appendFileSync(path, batch.join(""), "utf-8");
113
- } catch (error) {
114
- console.error("blackhole: debug log flush failed", error);
115
- }
123
+ if (flushing || buffer.length === 0) return;
124
+ const batch = buffer;
125
+ buffer = [];
126
+ try {
127
+ const path = join(getAgentDir(), DEBUG_LOG_RELATIVE_PATH);
128
+ mkdirSync(dirname(path), { recursive: true });
129
+ rotateIfNeeded(path);
130
+ appendFileSync(path, batch.join(""), "utf-8");
131
+ } catch (error) {
132
+ console.error("blackhole: debug log flush failed", error);
133
+ }
116
134
  }
117
135
 
118
136
  function rotateIfNeeded(path: string): void {
119
- if (!existsSync(path)) return;
120
- if (statSync(path).size < DEBUG_LOG_MAX_BYTES) return;
121
- const backupPath = `${path}.1`;
122
- if (existsSync(backupPath)) unlinkSync(backupPath);
123
- renameSync(path, backupPath);
137
+ if (!existsSync(path)) return;
138
+ if (statSync(path).size < DEBUG_LOG_MAX_BYTES) return;
139
+ const backupPath = `${path}.1`;
140
+ if (existsSync(backupPath)) unlinkSync(backupPath);
141
+ renameSync(path, backupPath);
124
142
  }
package/src/om/ids.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
2
 
3
3
  export function hashId(content: string): string {
4
- return createHash("sha256").update(content).digest("hex").slice(0, 12);
4
+ return createHash("sha256").update(content).digest("hex").slice(0, 12);
5
5
  }
@@ -5,46 +5,49 @@
5
5
  * Unmodified.
6
6
  */
7
7
  import {
8
- isObservationsDroppedData,
9
- isObservationsRecordedData,
10
- isReflectionsRecordedData,
11
- OM_OBSERVATIONS_DROPPED,
12
- OM_OBSERVATIONS_RECORDED,
13
- OM_REFLECTIONS_RECORDED,
14
- type Entry,
15
- type Observation,
16
- type Reflection,
8
+ isObservationsDroppedData,
9
+ isObservationsRecordedData,
10
+ isReflectionsRecordedData,
11
+ OM_OBSERVATIONS_DROPPED,
12
+ OM_OBSERVATIONS_RECORDED,
13
+ OM_REFLECTIONS_RECORDED,
14
+ type Entry,
15
+ type Observation,
16
+ type Reflection,
17
17
  } from "./types.js";
18
18
  import { debugLog } from "../debug-log.js";
19
19
 
20
20
  export type FoldLedgerOptions = {
21
- /** Fold entries from branch root through this entry id, inclusive. Omit to fold through branch tip. */
22
- upToEntryId?: string;
21
+ /** Fold entries from branch root through this entry id, inclusive. Omit to fold through branch tip. */
22
+ upToEntryId?: string;
23
23
  };
24
24
 
25
25
  export type FoldedLedger = {
26
- /** All first-valid observation records encountered through the fold boundary, including dropped observations. */
27
- observations: Observation[];
28
- /** Observation records not tombstoned by a folded drop entry. */
29
- activeObservations: Observation[];
30
- /** Tombstoned observation ids, including ids that may not have a corresponding folded observation. */
31
- droppedObservationIds: Set<string>;
32
- /** All first-valid reflection records encountered through the fold boundary. */
33
- reflections: Reflection[];
34
- /** All first-valid observation records by id, including dropped observations. */
35
- observationsById: Map<string, Observation>;
36
- /** All first-valid reflection records by id. */
37
- reflectionsById: Map<string, Reflection>;
26
+ /** All first-valid observation records encountered through the fold boundary, including dropped observations. */
27
+ observations: Observation[];
28
+ /** Observation records not tombstoned by a folded drop entry. */
29
+ activeObservations: Observation[];
30
+ /** Tombstoned observation ids, including ids that may not have a corresponding folded observation. */
31
+ droppedObservationIds: Set<string>;
32
+ /** All first-valid reflection records encountered through the fold boundary. */
33
+ reflections: Reflection[];
34
+ /** All first-valid observation records by id, including dropped observations. */
35
+ observationsById: Map<string, Observation>;
36
+ /** All first-valid reflection records by id. */
37
+ reflectionsById: Map<string, Reflection>;
38
38
  };
39
39
 
40
- function foldEndIndex(entries: Entry[], upToEntryId: string | undefined): number {
41
- if (!upToEntryId) return entries.length - 1;
42
- const idx = entries.findIndex((entry) => entry.id === upToEntryId);
43
- return idx === -1 ? entries.length - 1 : idx;
40
+ function foldEndIndex(
41
+ entries: Entry[],
42
+ upToEntryId: string | undefined,
43
+ ): number {
44
+ if (!upToEntryId) return entries.length - 1;
45
+ const idx = entries.findIndex((entry) => entry.id === upToEntryId);
46
+ return idx === -1 ? entries.length - 1 : idx;
44
47
  }
45
48
 
46
49
  function isCustomEntry(entry: Entry, customType: string): boolean {
47
- return entry.type === "custom" && entry.customType === customType;
50
+ return entry.type === "custom" && entry.customType === customType;
48
51
  }
49
52
 
50
53
  /**
@@ -54,61 +57,69 @@ function isCustomEntry(entry: Entry, customType: string): boolean {
54
57
  * Observations and reflections use first-valid-record-wins semantics. Drops are tombstones and are
55
58
  * retained even when the dropped id is unknown at the time of folding.
56
59
  */
57
- export function foldLedger(entries: Entry[], options: FoldLedgerOptions = {}): FoldedLedger {
58
- const observationsById = new Map<string, Observation>();
59
- const reflectionsById = new Map<string, Reflection>();
60
- const droppedObservationIds = new Set<string>();
61
- const endIdx = foldEndIndex(entries, options.upToEntryId);
60
+ export function foldLedger(
61
+ entries: Entry[],
62
+ options: FoldLedgerOptions = {},
63
+ ): FoldedLedger {
64
+ const observationsById = new Map<string, Observation>();
65
+ const reflectionsById = new Map<string, Reflection>();
66
+ const droppedObservationIds = new Set<string>();
67
+ const endIdx = foldEndIndex(entries, options.upToEntryId);
62
68
 
63
- for (let i = 0; i <= endIdx; i++) {
64
- const entry = entries[i];
65
- if (!entry) continue;
69
+ for (let i = 0; i <= endIdx; i++) {
70
+ const entry = entries[i];
71
+ if (!entry) continue;
66
72
 
67
- if (isCustomEntry(entry, OM_OBSERVATIONS_RECORDED)) {
68
- if (!isObservationsRecordedData(entry.data)) continue;
69
- for (const observation of entry.data.observations) {
70
- if (!observationsById.has(observation.id)) {
71
- observationsById.set(observation.id, observation);
72
- }
73
- }
74
- continue;
75
- }
73
+ if (isCustomEntry(entry, OM_OBSERVATIONS_RECORDED)) {
74
+ if (!isObservationsRecordedData(entry.data)) continue;
75
+ for (const observation of entry.data.observations) {
76
+ if (!observationsById.has(observation.id)) {
77
+ observationsById.set(observation.id, observation);
78
+ }
79
+ }
80
+ continue;
81
+ }
76
82
 
77
- if (isCustomEntry(entry, OM_REFLECTIONS_RECORDED)) {
78
- if (!isReflectionsRecordedData(entry.data)) continue;
79
- for (const reflection of entry.data.reflections) {
80
- if (!reflectionsById.has(reflection.id)) {
81
- reflectionsById.set(reflection.id, reflection);
82
- }
83
- }
84
- continue;
85
- }
83
+ if (isCustomEntry(entry, OM_REFLECTIONS_RECORDED)) {
84
+ if (!isReflectionsRecordedData(entry.data)) continue;
85
+ for (const reflection of entry.data.reflections) {
86
+ if (!reflectionsById.has(reflection.id)) {
87
+ reflectionsById.set(reflection.id, reflection);
88
+ }
89
+ }
90
+ continue;
91
+ }
86
92
 
87
- if (isCustomEntry(entry, OM_OBSERVATIONS_DROPPED)) {
88
- if (!isObservationsDroppedData(entry.data)) continue;
89
- for (const observationId of entry.data.observationIds) {
90
- droppedObservationIds.add(observationId);
91
- }
92
- continue;
93
- }
93
+ if (isCustomEntry(entry, OM_OBSERVATIONS_DROPPED)) {
94
+ if (!isObservationsDroppedData(entry.data)) continue;
95
+ for (const observationId of entry.data.observationIds) {
96
+ droppedObservationIds.add(observationId);
97
+ }
98
+ continue;
99
+ }
94
100
 
95
- // Log unknown custom entry types — they are silently skipped, but
96
- // should be visible when debugging extension compatibility issues.
97
- if (entry.type === "custom" && entry.customType) {
98
- debugLog("fold.unknown_custom_type", { customType: entry.customType, entryId: entry.id });
99
- }
100
- }
101
+ // Log unknown custom entry types — they are silently skipped, but
102
+ // should be visible when debugging extension compatibility issues.
103
+ if (entry.type === "custom" && entry.customType) {
104
+ debugLog("fold.unknown_custom_type", {
105
+ customType: entry.customType,
106
+ entryId: entry.id,
107
+ });
108
+ }
109
+ }
101
110
 
102
- const observations = Array.from(observationsById.values());
103
- const activeObservations = observations.filter((observation) => !droppedObservationIds.has(observation.id));
104
- const reflections = Array.from(reflectionsById.values());
111
+ const observations = Array.from(observationsById.values());
112
+ const activeObservations = observations.filter(
113
+ (observation) => !droppedObservationIds.has(observation.id),
114
+ );
115
+ const reflections = Array.from(reflectionsById.values());
105
116
 
106
- return {
107
- observations,
108
- activeObservations,
109
- droppedObservationIds,
110
- reflections,
111
- observationsById,
112
- reflectionsById,
113
- };
117
+ return {
118
+ observations,
119
+ activeObservations,
120
+ droppedObservationIds,
121
+ reflections,
122
+ observationsById,
123
+ reflectionsById,
124
+ };
114
125
  }