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.
- package/README.md +15 -0
- package/dist/index.js +11660 -0
- package/dist/index.js.map +1 -0
- package/example-config.json +1 -1
- package/index.ts +37 -63
- package/package.json +21 -9
- package/src/commands/cleanup.ts +279 -240
- package/src/commands/memory.ts +236 -184
- package/src/commands/pi-vcc.ts +202 -152
- package/src/commands/vcc-recall.ts +126 -95
- package/src/core/brief.ts +167 -33
- package/src/core/build-sections.ts +8 -2
- package/src/core/config-env.ts +117 -0
- package/src/core/content.ts +31 -7
- package/src/core/drill-down.ts +41 -11
- package/src/core/filter-noise.ts +9 -3
- package/src/core/format-recall.ts +15 -6
- package/src/core/format.ts +14 -4
- package/src/core/lineage.ts +9 -3
- package/src/core/load-messages.ts +24 -5
- package/src/core/normalize.ts +38 -14
- package/src/core/recall-scope.ts +11 -3
- package/src/core/render-entries.ts +22 -6
- package/src/core/sanitize.ts +5 -1
- package/src/core/search-entries.ts +111 -19
- package/src/core/settings.ts +1 -3
- package/src/core/summarize.ts +42 -21
- package/src/core/unified-config.ts +549 -411
- package/src/extract/commits.ts +4 -2
- package/src/extract/files.ts +10 -5
- package/src/extract/goals.ts +7 -2
- package/src/hooks/before-compact.ts +210 -88
- package/src/om/agents/dropper/agent.ts +380 -265
- package/src/om/agents/dropper/coverage.ts +102 -82
- package/src/om/agents/observer/agent.ts +242 -206
- package/src/om/agents/reflector/agent.ts +212 -153
- package/src/om/cleanup.ts +239 -218
- package/src/om/clipboard.ts +59 -51
- package/src/om/compaction-trigger.ts +448 -333
- package/src/om/config.ts +13 -6
- package/src/om/configure-overlay.ts +518 -355
- package/src/om/consolidation.ts +1460 -953
- package/src/om/cooldown.ts +75 -65
- package/src/om/debug-log.ts +86 -68
- package/src/om/ids.ts +1 -1
- package/src/om/ledger/fold.ts +89 -78
- package/src/om/ledger/progress.ts +181 -153
- package/src/om/ledger/projection.ts +248 -185
- package/src/om/ledger/recall.ts +247 -196
- package/src/om/ledger/render-summary.ts +79 -50
- package/src/om/ledger/types.ts +146 -117
- package/src/om/model-budget.ts +23 -13
- package/src/om/pending.ts +243 -179
- package/src/om/provider-stream.ts +52 -7
- package/src/om/retryable-error.ts +12 -16
- package/src/om/reverse-recall.ts +97 -91
- package/src/om/runtime.ts +474 -375
- package/src/om/serialize.ts +190 -166
- package/src/om/status-overlay.ts +246 -195
- package/src/om/tokens.ts +28 -21
- package/src/pi-base/blackhole-settings.ts +437 -0
- package/src/pi-base/config-manager.ts +440 -0
- package/src/pi-base/config.ts +469 -0
- package/src/pi-base/env.ts +43 -0
- package/src/pi-base/paths.ts +47 -0
- package/src/pi-base/settings/body.ts +1648 -0
- package/src/pi-base/settings/fields/action.ts +43 -0
- package/src/pi-base/settings/fields/boolean.ts +47 -0
- package/src/pi-base/settings/fields/custom.ts +72 -0
- package/src/pi-base/settings/fields/enum.ts +310 -0
- package/src/pi-base/settings/fields/index.ts +46 -0
- package/src/pi-base/settings/fields/model.ts +452 -0
- package/src/pi-base/settings/fields/string.ts +527 -0
- package/src/pi-base/settings/fields/text.ts +115 -0
- package/src/pi-base/settings/frame.ts +197 -0
- package/src/pi-base/settings/index.ts +77 -0
- package/src/pi-base/settings/inline-edit.ts +313 -0
- package/src/pi-base/settings/modal.ts +152 -0
- package/src/pi-base/settings/types.ts +500 -0
- package/src/pi-base/settings/validate-field.ts +113 -0
- package/src/pi-base/shell.ts +117 -0
- package/src/pi-base/types.ts +6 -0
- package/src/pi-base/ui.ts +32 -0
- package/src/tools/recall.ts +347 -225
- package/src/types.ts +20 -3
- package/tsup.config.ts +23 -0
- package/vitest.config.ts +15 -15
package/src/om/cooldown.ts
CHANGED
|
@@ -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
|
-
|
|
31
|
+
return join(getAgentDir(), CONFIG_DIR, COOLDOWN_FILE);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export interface CooldownEntry {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
44
|
+
return `${model.provider}/${model.id}`;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// ── Load / save ─────────────────────────────────────────────────────────────
|
|
48
48
|
|
|
49
49
|
function readCooldownMap(): CooldownMap {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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(
|
|
82
|
-
|
|
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(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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";
|
package/src/om/debug-log.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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>(
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
93
|
+
flushDebugLog();
|
|
80
94
|
});
|
|
81
95
|
|
|
82
|
-
export function debugLog(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
package/src/om/ledger/fold.ts
CHANGED
|
@@ -5,46 +5,49 @@
|
|
|
5
5
|
* Unmodified.
|
|
6
6
|
*/
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
for (let i = 0; i <= endIdx; i++) {
|
|
70
|
+
const entry = entries[i];
|
|
71
|
+
if (!entry) continue;
|
|
66
72
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
return {
|
|
118
|
+
observations,
|
|
119
|
+
activeObservations,
|
|
120
|
+
droppedObservationIds,
|
|
121
|
+
reflections,
|
|
122
|
+
observationsById,
|
|
123
|
+
reflectionsById,
|
|
124
|
+
};
|
|
114
125
|
}
|