@unbrained/pm-cli 2026.9.1 → 2026.9.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/.claude-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +27 -0
- package/dist/cli/after-command-context-usage.js +16 -3
- package/dist/cli-bundle/bundle-manifest.json +153 -153
- package/dist/cli-bundle/chunks/{chunk-6XIIGE7U.js → chunk-34HASX3S.js} +2 -2
- package/dist/cli-bundle/chunks/chunk-AY324UJ5.js +9 -0
- package/dist/cli-bundle/chunks/{chunk-TAG4VC22.js → chunk-DKGP3QLE.js} +27 -27
- package/dist/cli-bundle/chunks/chunk-E5RBBCXO.js +230 -0
- package/dist/cli-bundle/chunks/{chunk-DTGWEL23.js → chunk-EDPIS7EB.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-X4RCGLTX.js → chunk-IEHHE6GA.js} +5 -5
- package/dist/cli-bundle/chunks/{chunk-U45JXJCC.js → chunk-L7B7343B.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-2MC2LCXZ.js → chunk-LENC656S.js} +41 -41
- package/dist/cli-bundle/chunks/chunk-LISESA7W.js +35 -0
- package/dist/cli-bundle/chunks/{chunk-3WICXLVT.js → chunk-UPPM2FOZ.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-Y7VOMSZT.js → chunk-Y4PJ6HPK.js} +2 -2
- package/dist/cli-bundle/chunks/{register-list-query-XYNX4ASQ.js → register-list-query-W2ZTOMKP.js} +2 -2
- package/dist/cli-bundle/chunks/{register-mutation-2MVLRS3J.js → register-mutation-RVND5NCG.js} +2 -2
- package/dist/cli-bundle/chunks/{register-operations-WCIDXBU3.js → register-operations-TTROIVC4.js} +2 -2
- package/dist/cli-bundle/chunks/{register-setup-KVNC5KPL.js → register-setup-D3ZFHY3L.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-3ZBI35HU.js +176 -0
- package/dist/cli-bundle/focused-chunks/{chunk-SVXJYD6G.js → chunk-4HJFY4MB.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-4RRHC3S4.js +6 -0
- package/dist/cli-bundle/focused-chunks/chunk-4WR46RMG.js +9 -0
- package/dist/cli-bundle/focused-chunks/chunk-5RFXUPN5.js +3 -0
- package/dist/cli-bundle/focused-chunks/chunk-BWKHEIVF.js +2 -0
- package/dist/cli-bundle/focused-chunks/{chunk-4O4CJAR6.js → chunk-D7KXFBXV.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-MKHG3T76.js → chunk-GHCZ3YFR.js} +3 -3
- package/dist/cli-bundle/focused-chunks/{chunk-KB5OFSFD.js → chunk-GT7MXDLW.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-KBLZRU62.js +2 -0
- package/dist/cli-bundle/focused-chunks/chunk-KZ2NB3ET.js +2 -0
- package/dist/cli-bundle/focused-chunks/chunk-M5M6SVHS.js +3 -0
- package/dist/cli-bundle/focused-chunks/{chunk-BEVHCEWG.js → chunk-OL5EH6I3.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-MEASX544.js → chunk-Q45NHPF2.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-Y6JJIX6I.js → chunk-QKHCSUGJ.js} +48 -48
- package/dist/cli-bundle/focused-chunks/chunk-S27XMBIN.js +8 -0
- package/dist/cli-bundle/focused-chunks/chunk-TXE4TSHM.js +12 -0
- package/dist/cli-bundle/focused-chunks/chunk-UCLO3XDU.js +2 -0
- package/dist/cli-bundle/main.js +15 -15
- package/dist/cli-bundle/sdk-authoring.js +1 -1
- package/dist/cli-bundle/sdk-contracts.js +1 -1
- package/dist/cli-bundle/sdk-core.js +28 -28
- package/dist/cli-bundle/sdk-governance.js +1 -1
- package/dist/cli-bundle/sdk-graph.js +1 -1
- package/dist/cli-bundle/sdk-merge.js +35 -33
- package/dist/cli-bundle/sdk-query.js +1 -1
- package/dist/cli-bundle/sdk-runtime.js +1 -1
- package/dist/cli-bundle/sdk-testing.js +1 -1
- package/dist/cli-bundle/sdk.js +9 -9
- package/dist/core/diagnostics/remediation.js +9 -4
- package/dist/core/history/event-classification.d.ts +21 -0
- package/dist/core/history/event-classification.js +109 -0
- package/dist/core/history/event-index.d.ts +14 -3
- package/dist/core/history/event-index.js +543 -88
- package/dist/core/history/history.js +47 -18
- package/dist/core/history/workspace-history.js +7 -6
- package/dist/core/lock/lock.d.ts +22 -2
- package/dist/core/lock/lock.js +121 -19
- package/dist/core/output/output.js +31 -7
- package/dist/core/shared/time.d.ts +2 -0
- package/dist/core/shared/time.js +8 -2
- package/dist/core/store/item-metadata-query-index.js +5 -5
- package/dist/sdk/context/recency.d.ts +9 -0
- package/dist/sdk/context/recency.js +34 -0
- package/dist/sdk/context/usage-egress.d.ts +9 -0
- package/dist/sdk/context/usage-egress.js +30 -0
- package/dist/sdk/context-intent-contracts.js +7 -3
- package/dist/sdk/context-relevance.d.ts +23 -0
- package/dist/sdk/context-relevance.js +94 -23
- package/dist/sdk/context-signal-store.d.ts +7 -3
- package/dist/sdk/context-signal-store.js +155 -42
- package/dist/sdk/context-usage.d.ts +54 -0
- package/dist/sdk/context-usage.js +245 -16
- package/dist/sdk/generated/generated-error-code-catalog-part-1.js +26 -14
- package/dist/sdk/generated/generated-error-code-catalog-part-2.js +14 -2
- package/dist/sdk/governance/health.js +77 -11
- package/dist/sdk/governance/validate.js +4 -2
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.js +4 -2
- package/dist/sdk/init.d.ts +2 -0
- package/dist/sdk/init.js +18 -9
- package/dist/sdk/merge/index.d.ts +1 -1
- package/dist/sdk/merge/index.js +2 -2
- package/dist/sdk/merge/receipts.d.ts +24 -0
- package/dist/sdk/merge/receipts.js +122 -30
- package/dist/sdk/query/context.d.ts +3 -1
- package/dist/sdk/query/context.js +21 -9
- package/dist/sdk/query/next.js +18 -9
- package/dist/sdk/runtime-input.d.ts +2 -0
- package/dist/sdk/runtime-input.js +25 -2
- package/dist/sdk/runtime.d.ts +1 -0
- package/dist/sdk/runtime.js +17 -26
- package/dist/types.d.ts +2 -0
- package/dist/types.js +2 -2
- package/docs/CONTEXT_RECOVERY_PRIMITIVES.md +11 -2
- package/docs/CONTEXT_RELEVANCE.md +44 -1
- package/docs/MERGE_SAFETY.md +16 -2
- package/docs/PERFORMANCE.md +6 -3
- package/docs/READ_OUTPUT_CONTRACTS.md +9 -1
- package/docs/SDK.md +11 -8
- package/docs/SDK_CONTEXT.md +7 -0
- package/docs/generated/REFUSAL_CLOSURE_CENSUS.md +5 -3
- package/marketplace.json +2 -2
- package/package.json +4 -4
- package/packages/pm-beads/package.json +1 -1
- package/packages/pm-calendar/package.json +1 -1
- package/packages/pm-command-kit/package.json +1 -1
- package/packages/pm-digital-twin/package.json +1 -1
- package/packages/pm-governance-audit/package.json +1 -1
- package/packages/pm-guide-shell/package.json +1 -1
- package/packages/pm-kanban/package.json +1 -1
- package/packages/pm-lifecycle-hooks/package.json +1 -1
- package/packages/pm-linked-test-adapters/package.json +1 -1
- package/packages/pm-search-advanced/package.json +1 -1
- package/packages/pm-templates/package.json +1 -1
- package/packages/pm-todos/package.json +1 -1
- package/packages/pm-vcs/package.json +1 -1
- package/plugins/pm-claude/.claude-plugin/plugin.json +1 -1
- package/plugins/pm-codex/.codex-plugin/plugin.json +1 -1
- package/sdk/public-surface.json +268 -28
- package/dist/cli-bundle/chunks/chunk-ALKKAUFN.js +0 -35
- package/dist/cli-bundle/chunks/chunk-GOACULJW.js +0 -13
- package/dist/cli-bundle/chunks/chunk-NOPL37X4.js +0 -205
- package/dist/cli-bundle/focused-chunks/chunk-2FEZ5SKM.js +0 -3
- package/dist/cli-bundle/focused-chunks/chunk-6GCRSLPG.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-7VATS5QY.js +0 -16
- package/dist/cli-bundle/focused-chunks/chunk-FWPLDEXF.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-JEFKLWMM.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-PF3E6Q36.js +0 -8
- package/dist/cli-bundle/focused-chunks/chunk-QDFJIRKN.js +0 -4
- package/dist/cli-bundle/focused-chunks/chunk-RMRCQPWU.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-S4U5CN5Q.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-T2LXXSOD.js +0 -158
- package/dist/cli-bundle/focused-chunks/chunk-Z2USIBR2.js +0 -5
|
@@ -5,20 +5,115 @@
|
|
|
5
5
|
* for cursor-resumable cross-item mutation event reads.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
8
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="72b285a7-970e-5965-bacf-a4e0cfc5f060")}catch(e){}}();
|
|
9
9
|
import fs from "node:fs/promises";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { randomUUID } from "node:crypto";
|
|
12
12
|
import { createRequire } from "node:module";
|
|
13
|
+
import { isFileMissingError } from "../fs/fs-utils.js";
|
|
14
|
+
import { acquireLock, captureProcessIdentity, isProcessIdentityAlive, } from "../lock/lock.js";
|
|
15
|
+
import { EXIT_CODE } from "../shared/constants.js";
|
|
16
|
+
import { PmCliError } from "../shared/errors.js";
|
|
17
|
+
import { isMillisecondPrecisionRfc3339DateTime } from "../shared/time.js";
|
|
13
18
|
import { readHistoryEntries } from "./read.js";
|
|
19
|
+
import { classifyHistoryEvent } from "./event-classification.js";
|
|
14
20
|
const EVENT_INDEX_FILENAME = "history-event-index.sqlite";
|
|
15
|
-
const EVENT_INDEX_VERSION = "
|
|
21
|
+
const EVENT_INDEX_VERSION = "5";
|
|
16
22
|
const AUTHORITATIVE_HISTORY_CACHE_LIMIT = 8;
|
|
23
|
+
const HISTORY_EVENT_INDEX_LOCK_ID = "history-event-index";
|
|
24
|
+
const HISTORY_EVENT_INDEX_LOCK_TTL_SECONDS = 300;
|
|
25
|
+
const HISTORY_EVENT_INDEX_LOCK_WAIT_MS = 30_000;
|
|
26
|
+
const HISTORY_EVENT_INDEX_INVALIDATION_LOCK_ID = "history-event-index-invalidation";
|
|
27
|
+
const HISTORY_EVENT_INDEX_INVALIDATION_DIRECTORY = "history-event-index-invalidations";
|
|
28
|
+
const HISTORY_EVENT_INDEX_INVALIDATION_MAX_UNVERIFIED_AGE_MS = HISTORY_EVENT_INDEX_LOCK_TTL_SECONDS * 2 * 1_000;
|
|
29
|
+
const HISTORY_INDEX_LOCK_CONTENDED = Symbol("history-index-lock-contended");
|
|
17
30
|
const authoritativeHistoryCaches = new Map();
|
|
18
31
|
function eventIndexPath(pmRoot) {
|
|
19
32
|
return path.join(pmRoot, "runtime", EVENT_INDEX_FILENAME);
|
|
20
33
|
}
|
|
21
|
-
|
|
34
|
+
function historyEventIndexInvalidationDirectory(pmRoot) {
|
|
35
|
+
return path.join(pmRoot, "runtime", HISTORY_EVENT_INDEX_INVALIDATION_DIRECTORY);
|
|
36
|
+
}
|
|
37
|
+
async function listHistoryEventIndexInvalidations(pmRoot) {
|
|
38
|
+
const entries = await fs
|
|
39
|
+
.readdir(historyEventIndexInvalidationDirectory(pmRoot))
|
|
40
|
+
.catch((error) => {
|
|
41
|
+
if (isFileMissingError(error))
|
|
42
|
+
return [];
|
|
43
|
+
throw error;
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
pending: entries.filter((name) => name.endsWith(".pending")).sort(),
|
|
47
|
+
committed: entries.filter((name) => name.endsWith(".committed")).sort(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Remove only pending markers proven abandoned by the caller's invalidation-lock ownership. */
|
|
51
|
+
async function recoverAbandonedHistoryEventIndexInvalidations(pmRoot) {
|
|
52
|
+
const invalidations = await listHistoryEventIndexInvalidations(pmRoot);
|
|
53
|
+
const pending = (await Promise.all(invalidations.pending.map(async (name) => {
|
|
54
|
+
const owner = /^(\d+)-(unknown|\d+)-(\d+)-.+\.pending$/u.exec(name);
|
|
55
|
+
if (owner !== null) {
|
|
56
|
+
const processStartIdentity = owner[2];
|
|
57
|
+
if (await isProcessIdentityAlive({
|
|
58
|
+
pid: Number(owner[1]),
|
|
59
|
+
...(processStartIdentity === "unknown"
|
|
60
|
+
? {}
|
|
61
|
+
: { process_start_identity: processStartIdentity }),
|
|
62
|
+
}, Math.max(0, Date.now() - Number(owner[3])), HISTORY_EVENT_INDEX_INVALIDATION_MAX_UNVERIFIED_AGE_MS)) {
|
|
63
|
+
return name;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
await fs.rm(path.join(historyEventIndexInvalidationDirectory(pmRoot), name), {
|
|
67
|
+
force: true,
|
|
68
|
+
});
|
|
69
|
+
return null;
|
|
70
|
+
}))).filter((name) => name !== null);
|
|
71
|
+
return { pending, committed: invalidations.committed };
|
|
72
|
+
}
|
|
73
|
+
async function withHistoryEventIndexInvalidationLock(pmRoot, operation) {
|
|
74
|
+
const release = await acquireLock(pmRoot, HISTORY_EVENT_INDEX_INVALIDATION_LOCK_ID, HISTORY_EVENT_INDEX_LOCK_TTL_SECONDS, `history-event-index-invalidation:${process.pid}`, false, false, HISTORY_EVENT_INDEX_LOCK_WAIT_MS);
|
|
75
|
+
try {
|
|
76
|
+
return await operation();
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
await release();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function beginHistoryEventIndexInvalidation(pmRoot) {
|
|
83
|
+
const directory = historyEventIndexInvalidationDirectory(pmRoot);
|
|
84
|
+
await fs.mkdir(directory, { recursive: true });
|
|
85
|
+
const token = randomUUID();
|
|
86
|
+
const processIdentity = await captureProcessIdentity();
|
|
87
|
+
const pendingPath = path.join(directory, `${processIdentity.pid}-${processIdentity.process_start_identity ?? "unknown"}-${Date.now()}-${token}.pending`);
|
|
88
|
+
await fs.writeFile(pendingPath, "pending\n", { flag: "wx" });
|
|
89
|
+
return {
|
|
90
|
+
pendingPath,
|
|
91
|
+
committedPath: path.join(directory, `${token}.committed`),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function sameInvalidationNames(left, right) {
|
|
95
|
+
return (left.length === right.length &&
|
|
96
|
+
left.every((name, index) => name === right[index]));
|
|
97
|
+
}
|
|
98
|
+
async function invalidateHistoryEventIndex(pmRoot) {
|
|
99
|
+
try {
|
|
100
|
+
await fs.rm(eventIndexPath(pmRoot), { force: true });
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// The authoritative append already committed. A retained projection remains
|
|
104
|
+
// unusable because its recorded stream byte size no longer matches authority.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function withHistoryEventIndexLock(pmRoot, operation) {
|
|
108
|
+
const release = await acquireLock(pmRoot, HISTORY_EVENT_INDEX_LOCK_ID, HISTORY_EVENT_INDEX_LOCK_TTL_SECONDS, `history-event-index:${process.pid}`, false, false, HISTORY_EVENT_INDEX_LOCK_WAIT_MS);
|
|
109
|
+
try {
|
|
110
|
+
return await operation();
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
await release();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function readAuthoritativeHistorySnapshot(pmRoot) {
|
|
22
117
|
const historyRoot = path.join(pmRoot, "history");
|
|
23
118
|
const entries = await fs
|
|
24
119
|
.readdir(historyRoot, { withFileTypes: true })
|
|
@@ -53,9 +148,10 @@ async function readAuthoritativeHistoryEvents(pmRoot) {
|
|
|
53
148
|
].join(":");
|
|
54
149
|
if (cached.streams.get(streamId)?.signature === signature)
|
|
55
150
|
continue;
|
|
56
|
-
const history = await
|
|
151
|
+
const history = await readIndexableHistoryEntries(historyPath, streamId);
|
|
57
152
|
cached.streams.set(streamId, {
|
|
58
153
|
signature,
|
|
154
|
+
byte_size: Number(stats.size),
|
|
59
155
|
events: history.map((entry, streamOffset) => ({
|
|
60
156
|
stream_id: streamId,
|
|
61
157
|
stream_offset: streamOffset,
|
|
@@ -81,7 +177,29 @@ async function readAuthoritativeHistoryEvents(pmRoot) {
|
|
|
81
177
|
const oldestRoot = authoritativeHistoryCaches.keys().next().value;
|
|
82
178
|
authoritativeHistoryCaches.delete(oldestRoot);
|
|
83
179
|
}
|
|
84
|
-
return
|
|
180
|
+
return {
|
|
181
|
+
events: cached.ordered_events,
|
|
182
|
+
stream_byte_sizes: new Map([...cached.streams].map(([streamId, stream]) => [
|
|
183
|
+
streamId,
|
|
184
|
+
stream.byte_size,
|
|
185
|
+
])),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
async function readIndexableHistoryEntries(historyPath, streamId) {
|
|
189
|
+
const entries = await readHistoryEntries(historyPath, streamId);
|
|
190
|
+
for (const [index, entry] of entries.entries()) {
|
|
191
|
+
if (typeof entry.ts !== "string" ||
|
|
192
|
+
!isMillisecondPrecisionRfc3339DateTime(entry.ts.trim())) {
|
|
193
|
+
throw new PmCliError(`History for ${streamId} contains an invalid or sub-millisecond timestamp at line ${index + 1}. Repair or restore the history stream and retry.`, EXIT_CODE.GENERIC_FAILURE, { code: "history_timestamp_invalid" });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return entries.map((entry) => ({
|
|
197
|
+
...entry,
|
|
198
|
+
ts: new Date(entry.ts.trim()).toISOString(),
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
async function readAuthoritativeHistoryEvents(pmRoot) {
|
|
202
|
+
return (await readAuthoritativeHistorySnapshot(pmRoot)).events;
|
|
85
203
|
}
|
|
86
204
|
function loadDatabaseSync(loadModule) {
|
|
87
205
|
try {
|
|
@@ -121,13 +239,20 @@ function createSchema(database) {
|
|
|
121
239
|
agent_harness TEXT,
|
|
122
240
|
agent_instance TEXT,
|
|
123
241
|
op TEXT NOT NULL,
|
|
242
|
+
event_class TEXT NOT NULL CHECK(event_class IN ('substantive', 'maintenance')),
|
|
124
243
|
entry_json TEXT NOT NULL,
|
|
125
244
|
PRIMARY KEY(stream_id, stream_offset)
|
|
126
245
|
) STRICT;
|
|
246
|
+
CREATE TABLE streams (
|
|
247
|
+
stream_id TEXT PRIMARY KEY,
|
|
248
|
+
byte_size INTEGER NOT NULL CHECK(byte_size >= 0)
|
|
249
|
+
) STRICT;
|
|
127
250
|
CREATE INDEX events_order
|
|
128
251
|
ON events(ts, stream_id, stream_offset);
|
|
129
252
|
CREATE INDEX events_op_order
|
|
130
253
|
ON events(op, ts, stream_id, stream_offset);
|
|
254
|
+
CREATE INDEX events_class_stream_order
|
|
255
|
+
ON events(event_class, stream_id, ts, stream_offset);
|
|
131
256
|
CREATE INDEX events_author_order
|
|
132
257
|
ON events(author, ts, stream_id, stream_offset);
|
|
133
258
|
CREATE INDEX events_harness_order
|
|
@@ -140,41 +265,92 @@ function createSchema(database) {
|
|
|
140
265
|
.run(EVENT_INDEX_VERSION);
|
|
141
266
|
}
|
|
142
267
|
function insertEvent(database, event) {
|
|
268
|
+
const entry = {
|
|
269
|
+
...event.entry,
|
|
270
|
+
ts: new Date(event.entry.ts.trim()).toISOString(),
|
|
271
|
+
};
|
|
143
272
|
database
|
|
144
273
|
.prepare(`INSERT INTO events(
|
|
145
274
|
stream_id, stream_offset, ts, author, agent_harness,
|
|
146
|
-
agent_instance, op, entry_json
|
|
147
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
148
|
-
.run(event.stream_id, event.stream_offset,
|
|
275
|
+
agent_instance, op, event_class, entry_json
|
|
276
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
277
|
+
.run(event.stream_id, event.stream_offset, entry.ts, entry.author, entry.agent_harness ?? null, entry.agent_instance ?? null, entry.op, classifyHistoryEvent(entry), JSON.stringify(entry));
|
|
278
|
+
}
|
|
279
|
+
function parseIndexedHistoryEvent(row) {
|
|
280
|
+
const entry = JSON.parse(String(row.entry_json));
|
|
281
|
+
if (typeof entry.ts !== "string" ||
|
|
282
|
+
!isMillisecondPrecisionRfc3339DateTime(entry.ts.trim())) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
stream_id: String(row.stream_id),
|
|
287
|
+
stream_offset: Number(row.stream_offset),
|
|
288
|
+
entry: { ...entry, ts: new Date(entry.ts.trim()).toISOString() },
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function upsertStreamByteSize(database, streamId, byteSize) {
|
|
292
|
+
database
|
|
293
|
+
.prepare(`INSERT INTO streams(stream_id, byte_size) VALUES (?, ?)
|
|
294
|
+
ON CONFLICT(stream_id) DO UPDATE SET byte_size = excluded.byte_size`)
|
|
295
|
+
.run(streamId, byteSize);
|
|
149
296
|
}
|
|
150
297
|
/** Rebuild the complete optional event projection from authoritative streams. */
|
|
151
298
|
export async function rebuildHistoryEventIndex(pmRoot) {
|
|
152
299
|
const Database = resolveDatabaseSync();
|
|
153
300
|
if (!Database)
|
|
154
301
|
return false;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
database = new Database(temporaryPath);
|
|
161
|
-
createSchema(database);
|
|
162
|
-
database.exec("BEGIN IMMEDIATE");
|
|
163
|
-
for (const event of await readAuthoritativeHistoryEvents(pmRoot)) {
|
|
164
|
-
insertEvent(database, event);
|
|
302
|
+
return withHistoryEventIndexLock(pmRoot, async () => {
|
|
303
|
+
let invalidations;
|
|
304
|
+
try {
|
|
305
|
+
invalidations = await withHistoryEventIndexInvalidationLock(pmRoot, () => recoverAbandonedHistoryEventIndexInvalidations(pmRoot));
|
|
165
306
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
await fs.
|
|
176
|
-
|
|
177
|
-
|
|
307
|
+
catch (error) {
|
|
308
|
+
if (error instanceof PmCliError && error.code === "lock_conflict") {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
if (invalidations.pending.length > 0)
|
|
314
|
+
return false;
|
|
315
|
+
const targetPath = eventIndexPath(pmRoot);
|
|
316
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
317
|
+
const temporaryPath = `${targetPath}.${randomUUID()}.tmp`;
|
|
318
|
+
let database;
|
|
319
|
+
try {
|
|
320
|
+
database = new Database(temporaryPath);
|
|
321
|
+
createSchema(database);
|
|
322
|
+
database.exec("BEGIN IMMEDIATE");
|
|
323
|
+
const authoritative = await readAuthoritativeHistorySnapshot(pmRoot);
|
|
324
|
+
for (const event of authoritative.events) {
|
|
325
|
+
insertEvent(database, event);
|
|
326
|
+
}
|
|
327
|
+
for (const [streamId, byteSize] of authoritative.stream_byte_sizes) {
|
|
328
|
+
upsertStreamByteSize(database, streamId, byteSize);
|
|
329
|
+
}
|
|
330
|
+
database.exec("COMMIT");
|
|
331
|
+
database.close();
|
|
332
|
+
database = undefined;
|
|
333
|
+
const published = await withHistoryEventIndexInvalidationLock(pmRoot, async () => {
|
|
334
|
+
const current = await recoverAbandonedHistoryEventIndexInvalidations(pmRoot);
|
|
335
|
+
if (current.pending.length > 0 ||
|
|
336
|
+
!sameInvalidationNames(invalidations.committed, current.committed)) {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
await fs.rm(targetPath, { force: true });
|
|
340
|
+
await fs.rename(temporaryPath, targetPath);
|
|
341
|
+
await Promise.all(invalidations.committed.map((name) => fs.rm(path.join(historyEventIndexInvalidationDirectory(pmRoot), name), { force: true })));
|
|
342
|
+
return true;
|
|
343
|
+
});
|
|
344
|
+
if (!published)
|
|
345
|
+
await fs.rm(temporaryPath, { force: true });
|
|
346
|
+
return published;
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
database?.close();
|
|
350
|
+
await fs.rm(temporaryPath, { force: true });
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
});
|
|
178
354
|
}
|
|
179
355
|
function matchesSet(value, accepted) {
|
|
180
356
|
return !accepted || accepted.length === 0 || accepted.includes(value);
|
|
@@ -184,6 +360,12 @@ function compareHistoryEventPosition(left, right) {
|
|
|
184
360
|
left.stream_id.localeCompare(right.stream_id) ||
|
|
185
361
|
left.stream_offset - right.stream_offset);
|
|
186
362
|
}
|
|
363
|
+
function canonicalHistoryQueryTimestamp(value) {
|
|
364
|
+
const milliseconds = Date.parse(value);
|
|
365
|
+
return Number.isFinite(milliseconds)
|
|
366
|
+
? new Date(milliseconds).toISOString()
|
|
367
|
+
: value;
|
|
368
|
+
}
|
|
187
369
|
function isInsideHistoryEventWindow(event, query) {
|
|
188
370
|
if (query.after_ts !== undefined &&
|
|
189
371
|
query.after_stream_id !== undefined &&
|
|
@@ -191,13 +373,16 @@ function isInsideHistoryEventWindow(event, query) {
|
|
|
191
373
|
if (compareHistoryEventPosition(event, {
|
|
192
374
|
stream_id: query.after_stream_id,
|
|
193
375
|
stream_offset: query.after_stream_offset,
|
|
194
|
-
entry: {
|
|
376
|
+
entry: {
|
|
377
|
+
...event.entry,
|
|
378
|
+
ts: canonicalHistoryQueryTimestamp(query.after_ts),
|
|
379
|
+
},
|
|
195
380
|
}) <= 0) {
|
|
196
381
|
return false;
|
|
197
382
|
}
|
|
198
383
|
}
|
|
199
384
|
else if (query.since_ts !== undefined &&
|
|
200
|
-
event.entry.ts < query.since_ts) {
|
|
385
|
+
event.entry.ts < canonicalHistoryQueryTimestamp(query.since_ts)) {
|
|
201
386
|
return false;
|
|
202
387
|
}
|
|
203
388
|
return true;
|
|
@@ -239,44 +424,116 @@ export async function queryHistoryEventStreams(pmRoot, query) {
|
|
|
239
424
|
};
|
|
240
425
|
}
|
|
241
426
|
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
427
|
+
* Serialize an authoritative append with its optional SQLite projection.
|
|
428
|
+
* The database write lock spans the file append and size capture, so compliant
|
|
429
|
+
* cross-process writers cannot certify a newer stream size with older rows.
|
|
244
430
|
*/
|
|
245
|
-
export async function
|
|
431
|
+
export async function appendHistoryEntryWithEventIndex(historyPath, entry, append) {
|
|
246
432
|
const pmRoot = path.dirname(path.dirname(historyPath));
|
|
247
|
-
const targetPath = eventIndexPath(pmRoot);
|
|
248
|
-
try {
|
|
249
|
-
await fs.access(targetPath);
|
|
250
|
-
}
|
|
251
|
-
catch {
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
433
|
const Database = resolveDatabaseSync();
|
|
255
|
-
if (!Database)
|
|
434
|
+
if (!Database) {
|
|
435
|
+
await append();
|
|
256
436
|
return;
|
|
257
|
-
|
|
437
|
+
}
|
|
438
|
+
let authoritativeAppendCommitted = false;
|
|
258
439
|
try {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
440
|
+
await withHistoryEventIndexLock(pmRoot, async () => {
|
|
441
|
+
const targetPath = eventIndexPath(pmRoot);
|
|
442
|
+
try {
|
|
443
|
+
await fs.access(targetPath);
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
await append();
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
let database;
|
|
450
|
+
try {
|
|
451
|
+
database = new Database(targetPath);
|
|
452
|
+
const version = database
|
|
453
|
+
.prepare("SELECT value FROM metadata WHERE key = 'version'")
|
|
454
|
+
.get();
|
|
455
|
+
if (version?.value !== EVENT_INDEX_VERSION) {
|
|
456
|
+
throw new TypeError("Unsupported history event index version");
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
database?.close();
|
|
461
|
+
await fs.rm(targetPath, { force: true });
|
|
462
|
+
await append();
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
database.exec("PRAGMA busy_timeout = 5000");
|
|
467
|
+
database.exec("BEGIN IMMEDIATE");
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
database.close();
|
|
471
|
+
throw error;
|
|
472
|
+
}
|
|
473
|
+
let appended = false;
|
|
474
|
+
try {
|
|
475
|
+
await append();
|
|
476
|
+
appended = true;
|
|
477
|
+
authoritativeAppendCommitted = true;
|
|
478
|
+
const streamId = path.basename(historyPath, ".jsonl");
|
|
479
|
+
const offset = database
|
|
480
|
+
.prepare("SELECT COALESCE(MAX(stream_offset), -1) + 1 AS value FROM events WHERE stream_id = ?")
|
|
481
|
+
.get(streamId);
|
|
482
|
+
insertEvent(database, {
|
|
483
|
+
stream_id: streamId,
|
|
484
|
+
stream_offset: Number(offset.value),
|
|
485
|
+
entry,
|
|
486
|
+
});
|
|
487
|
+
upsertStreamByteSize(database, streamId, (await fs.stat(historyPath)).size);
|
|
488
|
+
database.exec("COMMIT");
|
|
489
|
+
database.close();
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
database?.close();
|
|
493
|
+
if (!appended)
|
|
494
|
+
throw error;
|
|
495
|
+
await invalidateHistoryEventIndex(pmRoot);
|
|
496
|
+
}
|
|
274
497
|
});
|
|
275
|
-
database.close();
|
|
276
498
|
}
|
|
277
|
-
catch {
|
|
278
|
-
|
|
279
|
-
|
|
499
|
+
catch (error) {
|
|
500
|
+
if (authoritativeAppendCommitted) {
|
|
501
|
+
await invalidateHistoryEventIndex(pmRoot);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (!(error instanceof PmCliError) || error.code !== "lock_conflict") {
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
507
|
+
let pendingMarkerPath;
|
|
508
|
+
try {
|
|
509
|
+
await withHistoryEventIndexInvalidationLock(pmRoot, async () => {
|
|
510
|
+
const marker = await beginHistoryEventIndexInvalidation(pmRoot);
|
|
511
|
+
pendingMarkerPath = marker.pendingPath;
|
|
512
|
+
try {
|
|
513
|
+
await append();
|
|
514
|
+
authoritativeAppendCommitted = true;
|
|
515
|
+
}
|
|
516
|
+
catch (appendError) {
|
|
517
|
+
await fs.rm(marker.pendingPath, { force: true });
|
|
518
|
+
throw appendError;
|
|
519
|
+
}
|
|
520
|
+
await fs.rename(marker.pendingPath, marker.committedPath);
|
|
521
|
+
await invalidateHistoryEventIndex(pmRoot);
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
catch (fallbackError) {
|
|
525
|
+
if (authoritativeAppendCommitted) {
|
|
526
|
+
await invalidateHistoryEventIndex(pmRoot);
|
|
527
|
+
try {
|
|
528
|
+
await fs.rm(pendingMarkerPath, { force: true });
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
// The authoritative append is durable; marker cleanup is best effort.
|
|
532
|
+
}
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
throw fallbackError;
|
|
536
|
+
}
|
|
280
537
|
}
|
|
281
538
|
}
|
|
282
539
|
/** Invalidate the optional event projection after a non-append rewrite. */
|
|
@@ -315,11 +572,12 @@ export async function queryHistoryEventIndex(pmRoot, query) {
|
|
|
315
572
|
query.after_stream_id !== undefined &&
|
|
316
573
|
query.after_stream_offset !== undefined) {
|
|
317
574
|
clauses.push("(ts > ? OR (ts = ? AND stream_id > ?) OR (ts = ? AND stream_id = ? AND stream_offset > ?))");
|
|
318
|
-
|
|
575
|
+
const afterTimestamp = canonicalHistoryQueryTimestamp(query.after_ts);
|
|
576
|
+
parameters.push(afterTimestamp, afterTimestamp, query.after_stream_id, afterTimestamp, query.after_stream_id, query.after_stream_offset);
|
|
319
577
|
}
|
|
320
578
|
else if (query.since_ts !== undefined) {
|
|
321
579
|
clauses.push("ts >= ?");
|
|
322
|
-
parameters.push(query.since_ts);
|
|
580
|
+
parameters.push(canonicalHistoryQueryTimestamp(query.since_ts));
|
|
323
581
|
}
|
|
324
582
|
appendSetPredicate(clauses, parameters, "op", query.ops);
|
|
325
583
|
appendSetPredicate(clauses, parameters, "author", query.authors);
|
|
@@ -333,36 +591,233 @@ export async function queryHistoryEventIndex(pmRoot, query) {
|
|
|
333
591
|
const where = clauses.length === 0 ? "" : ` WHERE ${clauses.join(" AND ")}`;
|
|
334
592
|
let database;
|
|
335
593
|
try {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
594
|
+
return await withHistoryEventIndexLock(pmRoot, async () => {
|
|
595
|
+
return withHistoryEventIndexInvalidationLock(pmRoot, async () => {
|
|
596
|
+
const invalidations = await recoverAbandonedHistoryEventIndexInvalidations(pmRoot);
|
|
597
|
+
if (invalidations.pending.length > 0 ||
|
|
598
|
+
invalidations.committed.length > 0) {
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
database = new Database(eventIndexPath(pmRoot), { readOnly: true });
|
|
602
|
+
const version = database
|
|
603
|
+
.prepare("SELECT value FROM metadata WHERE key = 'version'")
|
|
604
|
+
.get();
|
|
605
|
+
const validationStreamIds = await historyIndexValidationStreamIds(database, pmRoot, query.stream_ids);
|
|
606
|
+
if (version?.value !== EVENT_INDEX_VERSION ||
|
|
607
|
+
!(await historyIndexMatchesStreamSizes(database, pmRoot, validationStreamIds))) {
|
|
608
|
+
database.close();
|
|
609
|
+
database = undefined;
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
const rows = database
|
|
613
|
+
.prepare(`SELECT stream_id, stream_offset, entry_json
|
|
614
|
+
FROM events${where}
|
|
615
|
+
ORDER BY ts, stream_id, stream_offset
|
|
616
|
+
LIMIT ?`)
|
|
617
|
+
.all(...parameters, Math.max(0, Math.floor(query.limit)) + 1);
|
|
618
|
+
const events = rows.slice(0, query.limit).map(parseIndexedHistoryEvent);
|
|
619
|
+
database.close();
|
|
620
|
+
database = undefined;
|
|
621
|
+
return events.includes(null)
|
|
622
|
+
? null
|
|
623
|
+
: {
|
|
624
|
+
events: events,
|
|
625
|
+
has_more: rows.length > query.limit,
|
|
626
|
+
};
|
|
627
|
+
});
|
|
628
|
+
});
|
|
358
629
|
}
|
|
359
630
|
catch {
|
|
360
631
|
database?.close();
|
|
361
632
|
return null;
|
|
362
633
|
}
|
|
363
634
|
}
|
|
635
|
+
function collectLatestSubstantiveEvents(events, requested) {
|
|
636
|
+
const latest = Object.create(null);
|
|
637
|
+
for (const event of events) {
|
|
638
|
+
if (!requested.has(event.stream_id) ||
|
|
639
|
+
classifyHistoryEvent(event.entry) !== "substantive") {
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
const current = latest[event.stream_id];
|
|
643
|
+
if (current === undefined ||
|
|
644
|
+
compareHistoryEventPosition(current, event) < 0) {
|
|
645
|
+
latest[event.stream_id] = event;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return latest;
|
|
649
|
+
}
|
|
650
|
+
async function readHistoryStreamByteSize(pmRoot, streamId) {
|
|
651
|
+
if (path.basename(streamId) !== streamId)
|
|
652
|
+
return null;
|
|
653
|
+
try {
|
|
654
|
+
return (await fs.stat(path.join(pmRoot, "history", `${streamId}.jsonl`)))
|
|
655
|
+
.size;
|
|
656
|
+
}
|
|
657
|
+
catch (error) {
|
|
658
|
+
if (isFileMissingError(error))
|
|
659
|
+
return 0;
|
|
660
|
+
throw error;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
async function historyIndexMatchesStreamSizes(database, pmRoot, streamIds) {
|
|
664
|
+
for (let start = 0; start < streamIds.length; start += 500) {
|
|
665
|
+
const chunk = streamIds.slice(start, start + 500);
|
|
666
|
+
const storedSizes = new Map(database
|
|
667
|
+
.prepare(`SELECT stream_id, byte_size FROM streams
|
|
668
|
+
WHERE stream_id IN (${chunk.map(() => "?").join(", ")})`)
|
|
669
|
+
.all(...chunk)
|
|
670
|
+
.map((row) => [String(row.stream_id), Number(row.byte_size)]));
|
|
671
|
+
const currentSizes = await Promise.all(chunk.map((streamId) => readHistoryStreamByteSize(pmRoot, streamId)));
|
|
672
|
+
for (const [index, currentSize] of currentSizes.entries()) {
|
|
673
|
+
if (currentSize === null)
|
|
674
|
+
return false;
|
|
675
|
+
const streamId = chunk[index];
|
|
676
|
+
const storedSize = storedSizes.get(streamId);
|
|
677
|
+
if (currentSize === 0 && storedSize === undefined)
|
|
678
|
+
continue;
|
|
679
|
+
if (currentSize !== storedSize)
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
async function historyIndexValidationStreamIds(database, pmRoot, requestedStreamIds) {
|
|
686
|
+
if (requestedStreamIds && requestedStreamIds.length > 0) {
|
|
687
|
+
return [
|
|
688
|
+
...new Set(requestedStreamIds.filter((streamId) => streamId.trim().length > 0)),
|
|
689
|
+
];
|
|
690
|
+
}
|
|
691
|
+
const indexed = database
|
|
692
|
+
.prepare("SELECT stream_id FROM streams")
|
|
693
|
+
.all()
|
|
694
|
+
.map((row) => String(row.stream_id));
|
|
695
|
+
const authoritative = await fs
|
|
696
|
+
.readdir(path.join(pmRoot, "history"), { withFileTypes: true })
|
|
697
|
+
.then((entries) => entries
|
|
698
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
|
|
699
|
+
.map((entry) => entry.name.slice(0, -".jsonl".length)))
|
|
700
|
+
.catch((error) => {
|
|
701
|
+
if (isFileMissingError(error))
|
|
702
|
+
return [];
|
|
703
|
+
throw error;
|
|
704
|
+
});
|
|
705
|
+
return [...new Set([...indexed, ...authoritative])];
|
|
706
|
+
}
|
|
707
|
+
async function readIndexedLatestSubstantiveEvents(Database, pmRoot, streamIds, requested) {
|
|
708
|
+
if (!Database)
|
|
709
|
+
return null;
|
|
710
|
+
let database;
|
|
711
|
+
try {
|
|
712
|
+
return await withHistoryEventIndexLock(pmRoot, async () => {
|
|
713
|
+
return withHistoryEventIndexInvalidationLock(pmRoot, async () => {
|
|
714
|
+
const invalidations = await recoverAbandonedHistoryEventIndexInvalidations(pmRoot);
|
|
715
|
+
if (invalidations.pending.length > 0 ||
|
|
716
|
+
invalidations.committed.length > 0) {
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
database = new Database(eventIndexPath(pmRoot), { readOnly: true });
|
|
720
|
+
const version = database
|
|
721
|
+
.prepare("SELECT value FROM metadata WHERE key = 'version'")
|
|
722
|
+
.get();
|
|
723
|
+
if (version?.value !== EVENT_INDEX_VERSION ||
|
|
724
|
+
!(await historyIndexMatchesStreamSizes(database, pmRoot, streamIds))) {
|
|
725
|
+
database.close();
|
|
726
|
+
database = undefined;
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
const indexed = [];
|
|
730
|
+
for (let start = 0; start < streamIds.length; start += 500) {
|
|
731
|
+
const chunk = streamIds.slice(start, start + 500);
|
|
732
|
+
const rows = database
|
|
733
|
+
.prepare(`SELECT stream_id, stream_offset, entry_json
|
|
734
|
+
FROM (
|
|
735
|
+
SELECT stream_id, stream_offset, entry_json,
|
|
736
|
+
ROW_NUMBER() OVER (
|
|
737
|
+
PARTITION BY stream_id
|
|
738
|
+
ORDER BY ts DESC, stream_offset DESC
|
|
739
|
+
) AS recency_rank
|
|
740
|
+
FROM events
|
|
741
|
+
WHERE event_class = 'substantive'
|
|
742
|
+
AND stream_id IN (${chunk.map(() => "?").join(", ")})
|
|
743
|
+
)
|
|
744
|
+
WHERE recency_rank = 1`)
|
|
745
|
+
.all(...chunk);
|
|
746
|
+
const parsed = rows.map(parseIndexedHistoryEvent);
|
|
747
|
+
if (parsed.includes(null)) {
|
|
748
|
+
database.close();
|
|
749
|
+
database = undefined;
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
indexed.push(...parsed);
|
|
753
|
+
}
|
|
754
|
+
database.close();
|
|
755
|
+
database = undefined;
|
|
756
|
+
return collectLatestSubstantiveEvents(indexed, requested);
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
catch (error) {
|
|
761
|
+
database?.close();
|
|
762
|
+
if (error instanceof PmCliError && error.code === "lock_conflict") {
|
|
763
|
+
return HISTORY_INDEX_LOCK_CONTENDED;
|
|
764
|
+
}
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Read one latest substantive coordinate per requested stream. The optional
|
|
770
|
+
* SQLite projection is preferred; individual authoritative streams are the
|
|
771
|
+
* fail-closed fallback and remain the source of truth.
|
|
772
|
+
*/
|
|
773
|
+
export async function readLatestSubstantiveHistoryEvents(pmRoot, streamIds) {
|
|
774
|
+
const uniqueIds = [
|
|
775
|
+
...new Set(streamIds.filter((id) => id.trim().length > 0 && path.basename(id) === id)),
|
|
776
|
+
];
|
|
777
|
+
if (uniqueIds.length === 0)
|
|
778
|
+
return {};
|
|
779
|
+
const requested = new Set(uniqueIds);
|
|
780
|
+
const Database = resolveDatabaseSync();
|
|
781
|
+
const indexed = await readIndexedLatestSubstantiveEvents(Database, pmRoot, uniqueIds, requested);
|
|
782
|
+
if (indexed !== null && indexed !== HISTORY_INDEX_LOCK_CONTENDED) {
|
|
783
|
+
return indexed;
|
|
784
|
+
}
|
|
785
|
+
if (Database && indexed !== HISTORY_INDEX_LOCK_CONTENDED) {
|
|
786
|
+
try {
|
|
787
|
+
if (await rebuildHistoryEventIndex(pmRoot)) {
|
|
788
|
+
const rebuilt = await readIndexedLatestSubstantiveEvents(Database, pmRoot, uniqueIds, requested);
|
|
789
|
+
if (rebuilt !== null && rebuilt !== HISTORY_INDEX_LOCK_CONTENDED) {
|
|
790
|
+
return rebuilt;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
catch {
|
|
795
|
+
// A corrupt unrelated stream must not prevent requested authoritative reads.
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
const authoritative = [];
|
|
799
|
+
await Promise.all(uniqueIds.map(async (streamId) => {
|
|
800
|
+
const historyPath = path.join(pmRoot, "history", `${streamId}.jsonl`);
|
|
801
|
+
const entries = await readIndexableHistoryEntries(historyPath, streamId).catch((error) => {
|
|
802
|
+
if (error instanceof PmCliError &&
|
|
803
|
+
error.code === "history_timestamp_invalid") {
|
|
804
|
+
return [];
|
|
805
|
+
}
|
|
806
|
+
throw error;
|
|
807
|
+
});
|
|
808
|
+
entries.forEach((entry, streamOffset) => {
|
|
809
|
+
authoritative.push({
|
|
810
|
+
stream_id: streamId,
|
|
811
|
+
stream_offset: streamOffset,
|
|
812
|
+
entry,
|
|
813
|
+
});
|
|
814
|
+
});
|
|
815
|
+
}));
|
|
816
|
+
return collectLatestSubstantiveEvents(authoritative, requested);
|
|
817
|
+
}
|
|
364
818
|
/** Test-only dependency seam for runtimes without `node:sqlite`. */
|
|
365
819
|
export const _testOnly = {
|
|
820
|
+
collectLatestSubstantiveEvents,
|
|
366
821
|
loadDatabaseSync,
|
|
367
822
|
loadStableDatabaseSync,
|
|
368
823
|
setDatabaseSync(databaseSync) {
|
|
@@ -374,4 +829,4 @@ export const _testOnly = {
|
|
|
374
829
|
},
|
|
375
830
|
};
|
|
376
831
|
//# sourceMappingURL=event-index.js.map
|
|
377
|
-
//# debugId=
|
|
832
|
+
//# debugId=72b285a7-970e-5965-bacf-a4e0cfc5f060
|