@orion-agents/orion-code 0.3.0 → 0.3.1
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/CHANGELOG.md +42 -0
- package/README.md +24 -8
- package/README.zh-CN.md +23 -9
- package/dist/core/tool-artifacts.d.ts +2 -0
- package/dist/core/tool-artifacts.d.ts.map +1 -1
- package/dist/core/tool-artifacts.js +27 -1
- package/dist/core/tool-artifacts.js.map +1 -1
- package/dist/runtime/agent-runtime-controller.js +1 -2
- package/dist/runtime/agent-runtime-controller.js.map +1 -1
- package/dist/runtime/agent-runtime-protocol.d.ts +4 -0
- package/dist/runtime/agent-runtime-protocol.d.ts.map +1 -1
- package/dist/runtime/agent-runtime-protocol.js.map +1 -1
- package/dist/runtime/durable-tool-receipt-reader.d.ts +32 -0
- package/dist/runtime/durable-tool-receipt-reader.d.ts.map +1 -0
- package/dist/runtime/durable-tool-receipt-reader.js +130 -0
- package/dist/runtime/durable-tool-receipt-reader.js.map +1 -0
- package/dist/runtime/legacy-thread-materializer.d.ts +14 -1
- package/dist/runtime/legacy-thread-materializer.d.ts.map +1 -1
- package/dist/runtime/legacy-thread-materializer.js +44 -5
- package/dist/runtime/legacy-thread-materializer.js.map +1 -1
- package/dist/runtime/release-receipts.d.ts +6 -3
- package/dist/runtime/release-receipts.d.ts.map +1 -1
- package/dist/runtime/release-receipts.js +47 -4
- package/dist/runtime/release-receipts.js.map +1 -1
- package/dist/runtime/thread-event-store.d.ts +62 -9
- package/dist/runtime/thread-event-store.d.ts.map +1 -1
- package/dist/runtime/thread-event-store.js +363 -31
- package/dist/runtime/thread-event-store.js.map +1 -1
- package/dist/runtime/thread-session-index.d.ts +84 -0
- package/dist/runtime/thread-session-index.d.ts.map +1 -0
- package/dist/runtime/thread-session-index.js +503 -0
- package/dist/runtime/thread-session-index.js.map +1 -0
- package/dist/runtime/thread-session-view.d.ts +31 -13
- package/dist/runtime/thread-session-view.d.ts.map +1 -1
- package/dist/runtime/thread-session-view.js +116 -61
- package/dist/runtime/thread-session-view.js.map +1 -1
- package/dist/runtime/thread-ui-adapter.d.ts.map +1 -1
- package/dist/runtime/thread-ui-adapter.js +18 -58
- package/dist/runtime/thread-ui-adapter.js.map +1 -1
- package/dist/runtime/tool-detail-repository.d.ts.map +1 -1
- package/dist/runtime/tool-detail-repository.js +23 -19
- package/dist/runtime/tool-detail-repository.js.map +1 -1
- package/dist/runtime/tool-receipt-validator.d.ts +21 -0
- package/dist/runtime/tool-receipt-validator.d.ts.map +1 -0
- package/dist/runtime/tool-receipt-validator.js +157 -0
- package/dist/runtime/tool-receipt-validator.js.map +1 -0
- package/dist/services/redaction.d.ts +4 -0
- package/dist/services/redaction.d.ts.map +1 -1
- package/dist/services/redaction.js +63 -0
- package/dist/services/redaction.js.map +1 -1
- package/dist/services/session-index.d.ts +16 -0
- package/dist/services/session-index.d.ts.map +1 -1
- package/dist/services/session-index.js +45 -36
- package/dist/services/session-index.js.map +1 -1
- package/dist/services/session-storage.d.ts.map +1 -1
- package/dist/services/session-storage.js +3 -6
- package/dist/services/session-storage.js.map +1 -1
- package/dist/services/workspace-registry.d.ts +42 -0
- package/dist/services/workspace-registry.d.ts.map +1 -0
- package/dist/services/workspace-registry.js +222 -0
- package/dist/services/workspace-registry.js.map +1 -0
- package/dist/web/errors.d.ts +6 -0
- package/dist/web/errors.d.ts.map +1 -0
- package/dist/web/errors.js +13 -0
- package/dist/web/errors.js.map +1 -0
- package/dist/web/event-hub.d.ts +4 -0
- package/dist/web/event-hub.d.ts.map +1 -1
- package/dist/web/event-hub.js +71 -11
- package/dist/web/event-hub.js.map +1 -1
- package/dist/web/file-read-service.d.ts +55 -0
- package/dist/web/file-read-service.d.ts.map +1 -0
- package/dist/web/file-read-service.js +399 -0
- package/dist/web/file-read-service.js.map +1 -0
- package/dist/web/git-read-model-service.d.ts +77 -0
- package/dist/web/git-read-model-service.d.ts.map +1 -0
- package/dist/web/git-read-model-service.js +631 -0
- package/dist/web/git-read-model-service.js.map +1 -0
- package/dist/web/protocol.d.ts +63 -1
- package/dist/web/protocol.d.ts.map +1 -1
- package/dist/web/protocol.js +22 -1
- package/dist/web/protocol.js.map +1 -1
- package/dist/web/review-service.d.ts +38 -0
- package/dist/web/review-service.d.ts.map +1 -0
- package/dist/web/review-service.js +74 -0
- package/dist/web/review-service.js.map +1 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +273 -27
- package/dist/web/server.js.map +1 -1
- package/dist/web/terminal-manager.d.ts +155 -0
- package/dist/web/terminal-manager.d.ts.map +1 -0
- package/dist/web/terminal-manager.js +728 -0
- package/dist/web/terminal-manager.js.map +1 -0
- package/dist/web/terminal-server.d.ts +8 -0
- package/dist/web/terminal-server.d.ts.map +1 -0
- package/dist/web/terminal-server.js +275 -0
- package/dist/web/terminal-server.js.map +1 -0
- package/dist/web/workbench-controller.d.ts +48 -12
- package/dist/web/workbench-controller.d.ts.map +1 -1
- package/dist/web/workbench-controller.js +651 -158
- package/dist/web/workbench-controller.js.map +1 -1
- package/dist/web-client/assets/DiffViewer-DpKoD07C.js +5 -0
- package/dist/web-client/assets/FilesPanel-BkoLmgjR.js +2 -0
- package/dist/web-client/assets/GitPanel-D20StcdG.js +1 -0
- package/dist/web-client/assets/ReviewPanel-3MVWbMQV.js +1 -0
- package/dist/web-client/assets/TerminalPanel-BLQ592Fb.js +21 -0
- package/dist/web-client/assets/TerminalPanel-DLuoa74B.css +1 -0
- package/dist/web-client/assets/index-BkIDl_xk.js +16 -0
- package/dist/web-client/assets/index-DXMpnWE8.css +1 -0
- package/dist/web-client/index.html +2 -2
- package/docs/architecture/v0.3.0-web-api.yaml +44 -13
- package/docs/architecture/v0.3.1-web-api.yaml +2290 -0
- package/docs/migration/v0.3.0-to-v0.3.1.md +88 -0
- package/docs/plan/v0.3.0-web-workbench-plan.md +58 -7
- package/docs/plan/v0.3.1-web-workbench-professional-shell-plan.md +848 -0
- package/docs/readme.md +10 -0
- package/docs/test/v0.3.1-web-workbench-e2e-plan.md +140 -0
- package/npm-shrinkwrap.json +185 -354
- package/package.json +16 -4
- package/dist/web-client/assets/index-BAEO71Gp.css +0 -1
- package/dist/web-client/assets/index-DSkFy7gM.js +0 -13
|
@@ -8,6 +8,7 @@ const file_lock_1 = require("../services/file-lock");
|
|
|
8
8
|
const canonical_1 = require("./protocol/canonical");
|
|
9
9
|
const runtime_protocol_v1_1 = require("./protocol/runtime-protocol-v1");
|
|
10
10
|
const thread_projection_1 = require("./thread-projection");
|
|
11
|
+
const thread_session_index_1 = require("./thread-session-index");
|
|
11
12
|
class ThreadEventStoreError extends Error {
|
|
12
13
|
constructor(code, message, offset) {
|
|
13
14
|
super(message);
|
|
@@ -36,6 +37,7 @@ class ThreadEventStore {
|
|
|
36
37
|
}
|
|
37
38
|
this.logPath = (0, path_1.join)(rootDir, `${threadId}.events.v1.jsonl`);
|
|
38
39
|
this.projectionPath = (0, path_1.join)(rootDir, `${threadId}.projection.v1.json`);
|
|
40
|
+
this.headPath = (0, path_1.join)(rootDir, `${threadId}.head.v1.json`);
|
|
39
41
|
this.compactStatePath = (0, path_1.join)(rootDir, `${threadId}.compact-state.v1.json`);
|
|
40
42
|
this.compactCheckpointsDir = (0, path_1.join)(rootDir, `${threadId}.compact-checkpoints.v1`);
|
|
41
43
|
this.maxLogBytes = positiveInteger(options.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES, 'maxLogBytes');
|
|
@@ -56,13 +58,12 @@ class ThreadEventStore {
|
|
|
56
58
|
const commit = this.withLogLock(() => {
|
|
57
59
|
ensurePrivateDirectory(this.rootDir);
|
|
58
60
|
const head = this.loadVerifiedHead();
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
let
|
|
63
|
-
let previousHash = scan.records.at(-1)?.hash ?? null;
|
|
61
|
+
if (head.discardedTailBytes > 0)
|
|
62
|
+
(0, fs_1.truncateSync)(this.logPath, head.safeByteLength);
|
|
63
|
+
let seq = head.projection.cursor;
|
|
64
|
+
let previousHash = head.lastRecordHash;
|
|
64
65
|
const records = [];
|
|
65
|
-
const eventIds = new Set(
|
|
66
|
+
const eventIds = new Set(head.eventIdSet);
|
|
66
67
|
for (const input of inputs) {
|
|
67
68
|
seq += 1;
|
|
68
69
|
const eventId = this.idFactory();
|
|
@@ -103,18 +104,47 @@ class ThreadEventStore {
|
|
|
103
104
|
writeProjection(this.projectionPath, projection);
|
|
104
105
|
fsyncDirectory(this.rootDir);
|
|
105
106
|
this.boundary('after_projection_write', seq);
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
107
|
+
const nextLogIdentity = readFileIdentity(this.logPath);
|
|
108
|
+
const safeByteLength = nextLogIdentity.bytes;
|
|
109
|
+
const nextScan = head.scan
|
|
110
|
+
? {
|
|
111
|
+
records: [...head.scan.records, ...records],
|
|
112
|
+
events: [...head.scan.events, ...committedEvents],
|
|
113
|
+
safeByteLength,
|
|
114
|
+
discardedTailBytes: 0,
|
|
115
|
+
}
|
|
116
|
+
: undefined;
|
|
117
|
+
const nextHead = {
|
|
118
|
+
generation: head.generation + 1,
|
|
119
|
+
logFingerprint: nextLogIdentity.fingerprint,
|
|
120
|
+
log: storedLogIdentity(nextLogIdentity),
|
|
114
121
|
projectionFingerprint: fileFingerprint(this.projectionPath),
|
|
115
|
-
scan: nextScan,
|
|
122
|
+
...(nextScan ? { scan: nextScan } : {}),
|
|
116
123
|
projection,
|
|
124
|
+
lastEventTimestamp: committedEvents.at(-1)?.timestamp ?? head.lastEventTimestamp,
|
|
125
|
+
lastRecordHash: previousHash,
|
|
126
|
+
safeByteLength,
|
|
127
|
+
discardedTailBytes: 0,
|
|
128
|
+
eventIds: [...eventIds],
|
|
129
|
+
eventIdSet: eventIds,
|
|
130
|
+
verifiedPrefixes: head.verifiedPrefixes,
|
|
117
131
|
};
|
|
132
|
+
this.cachedHead = nextHead;
|
|
133
|
+
this.tryPersistVerifiedHead(nextHead);
|
|
134
|
+
try {
|
|
135
|
+
(0, thread_session_index_1.advanceThreadSessionIndexV1)({
|
|
136
|
+
rootDir: this.rootDir,
|
|
137
|
+
threadId: this.threadId,
|
|
138
|
+
previousHead: sessionIndexHead(head),
|
|
139
|
+
nextHead: sessionIndexHead(nextHead),
|
|
140
|
+
projection,
|
|
141
|
+
committedEvents,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// The immutable transcript index is derived. A stale manifest forces
|
|
146
|
+
// a verified rebuild on the next snapshot and cannot veto the fact.
|
|
147
|
+
}
|
|
118
148
|
return deepFreeze({ events: committedEvents, projection });
|
|
119
149
|
});
|
|
120
150
|
this.notifyCommitted(commit.events);
|
|
@@ -151,7 +181,7 @@ class ThreadEventStore {
|
|
|
151
181
|
throw new ThreadEventStoreError('ORION_THREAD_EVENT_LIMIT', `Replay limit ${safeLimit} exceeds ${this.maxReplayEvents}`);
|
|
152
182
|
}
|
|
153
183
|
return this.withLogLock(() => {
|
|
154
|
-
const
|
|
184
|
+
const scan = requireVerifiedScan(this.loadVerifiedHead(true));
|
|
155
185
|
const lastSeq = scan.events.length;
|
|
156
186
|
if (cursor > lastSeq) {
|
|
157
187
|
throw new ThreadEventStoreError('ORION_THREAD_EVENT_CURSOR', `Replay cursor ${cursor} is ahead of durable cursor ${lastSeq}`);
|
|
@@ -166,6 +196,44 @@ class ThreadEventStore {
|
|
|
166
196
|
});
|
|
167
197
|
});
|
|
168
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Verify and seal an immutable imported prefix. The first call may scan the
|
|
201
|
+
* authoritative log; later processes reuse the prefix receipt while the
|
|
202
|
+
* exact log identity/projection checkpoint remains current. Internal append
|
|
203
|
+
* carries the proof forward because the hash-chained prefix cannot change.
|
|
204
|
+
*/
|
|
205
|
+
verifyDurablePrefix(cursor, eventDigest, projectionDigest) {
|
|
206
|
+
if (!Number.isSafeInteger(cursor) ||
|
|
207
|
+
cursor <= 0 ||
|
|
208
|
+
!isSha256(eventDigest) ||
|
|
209
|
+
!isSha256(projectionDigest)) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
return this.withLogLock(() => {
|
|
213
|
+
let head = this.loadVerifiedHead();
|
|
214
|
+
if (cursor > head.projection.cursor)
|
|
215
|
+
return false;
|
|
216
|
+
const expected = { cursor, eventDigest, projectionDigest };
|
|
217
|
+
if (head.verifiedPrefixes.some(prefix => sameVerifiedPrefix(prefix, expected)))
|
|
218
|
+
return true;
|
|
219
|
+
head = this.loadVerifiedHead(true);
|
|
220
|
+
const scan = requireVerifiedScan(head);
|
|
221
|
+
const events = scan.events.slice(0, cursor);
|
|
222
|
+
if (events.length !== cursor ||
|
|
223
|
+
(0, canonical_1.digestRuntimeValue)(events) !== eventDigest ||
|
|
224
|
+
(0, thread_projection_1.projectThreadEvents)(this.threadId, events).digest !== projectionDigest) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
const next = {
|
|
228
|
+
...head,
|
|
229
|
+
generation: head.generation + 1,
|
|
230
|
+
verifiedPrefixes: [...head.verifiedPrefixes, expected].sort((left, right) => left.cursor - right.cursor),
|
|
231
|
+
};
|
|
232
|
+
this.cachedHead = next;
|
|
233
|
+
this.tryPersistVerifiedHead(next);
|
|
234
|
+
return true;
|
|
235
|
+
});
|
|
236
|
+
}
|
|
169
237
|
getCursor() {
|
|
170
238
|
const cached = this.cachedHead;
|
|
171
239
|
if (cached && cached.logFingerprint === fileFingerprint(this.logPath)) {
|
|
@@ -173,6 +241,34 @@ class ThreadEventStore {
|
|
|
173
241
|
}
|
|
174
242
|
return this.withLogLock(() => this.loadVerifiedHead().projection.cursor);
|
|
175
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Read the compact persisted receipt without parsing the potentially large
|
|
246
|
+
* projection. Callers must fall back to the authoritative scan whenever the
|
|
247
|
+
* receipt is missing, stale, or lacks their required cutover proof.
|
|
248
|
+
*/
|
|
249
|
+
capturePersistedCheckpointHead() {
|
|
250
|
+
return this.withLogLock(() => {
|
|
251
|
+
const identity = readFileIdentity(this.logPath);
|
|
252
|
+
const cached = this.cachedHead;
|
|
253
|
+
if (cached?.logFingerprint === identity.fingerprint) {
|
|
254
|
+
return checkpointHead(cached);
|
|
255
|
+
}
|
|
256
|
+
const stored = readStoredThreadHead(this.headPath, this.threadId);
|
|
257
|
+
if (!stored ||
|
|
258
|
+
!sameStoredLogIdentity(stored.log, identity) ||
|
|
259
|
+
stored.safeByteLength + stored.discardedTailBytes !== identity.bytes) {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
return deepFreeze({
|
|
263
|
+
cursor: stored.cursor,
|
|
264
|
+
projectionDigest: stored.projectionDigest,
|
|
265
|
+
lastEventTimestamp: stored.lastEventTimestamp,
|
|
266
|
+
lastRecordHash: stored.lastRecordHash,
|
|
267
|
+
log: { ...stored.log },
|
|
268
|
+
verifiedPrefixes: stored.verifiedPrefixes.map(prefix => ({ ...prefix })),
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
}
|
|
176
272
|
/** Capture the catalog read model and its exact verified log identity atomically. */
|
|
177
273
|
captureReadModelHead() {
|
|
178
274
|
return this.withLogLock(() => {
|
|
@@ -185,8 +281,8 @@ class ThreadEventStore {
|
|
|
185
281
|
}
|
|
186
282
|
return deepFreeze({
|
|
187
283
|
projection: head.projection,
|
|
188
|
-
lastEventTimestamp: head.
|
|
189
|
-
lastRecordHash: head.
|
|
284
|
+
lastEventTimestamp: head.lastEventTimestamp,
|
|
285
|
+
lastRecordHash: head.lastRecordHash,
|
|
190
286
|
log: {
|
|
191
287
|
bytes: identity.bytes,
|
|
192
288
|
device: identity.device,
|
|
@@ -205,23 +301,29 @@ class ThreadEventStore {
|
|
|
205
301
|
const head = this.loadVerifiedHead();
|
|
206
302
|
const replayed = head.projection;
|
|
207
303
|
const projectionFingerprint = fileFingerprint(this.projectionPath);
|
|
208
|
-
if (head.projectionFingerprint === projectionFingerprint)
|
|
304
|
+
if (head.projectionFingerprint === projectionFingerprint) {
|
|
305
|
+
this.tryPersistVerifiedHead(head);
|
|
209
306
|
return replayed;
|
|
307
|
+
}
|
|
210
308
|
const cached = readProjection(this.projectionPath);
|
|
211
309
|
if (cached &&
|
|
212
310
|
cached.threadId === this.threadId &&
|
|
213
311
|
cached.cursor === replayed.cursor &&
|
|
214
312
|
cached.digest === replayed.digest &&
|
|
215
313
|
(0, thread_projection_1.verifyThreadProjectionDigest)(cached)) {
|
|
216
|
-
|
|
314
|
+
const current = { ...head, projectionFingerprint };
|
|
315
|
+
this.cachedHead = current;
|
|
316
|
+
this.tryPersistVerifiedHead(current);
|
|
217
317
|
return replayed;
|
|
218
318
|
}
|
|
219
319
|
writeProjection(this.projectionPath, replayed);
|
|
220
320
|
fsyncDirectory(this.rootDir);
|
|
221
|
-
|
|
321
|
+
const current = {
|
|
222
322
|
...head,
|
|
223
323
|
projectionFingerprint: fileFingerprint(this.projectionPath),
|
|
224
324
|
};
|
|
325
|
+
this.cachedHead = current;
|
|
326
|
+
this.tryPersistVerifiedHead(current);
|
|
225
327
|
return replayed;
|
|
226
328
|
});
|
|
227
329
|
}
|
|
@@ -231,7 +333,7 @@ class ThreadEventStore {
|
|
|
231
333
|
*/
|
|
232
334
|
loadAuthoritativeModelHistory() {
|
|
233
335
|
return this.withLogLock(() => {
|
|
234
|
-
const
|
|
336
|
+
const scan = requireVerifiedScan(this.loadVerifiedHead(true));
|
|
235
337
|
const state = readCompactState(this.compactStatePath, this.threadId);
|
|
236
338
|
if (state)
|
|
237
339
|
this.assertCompactCheckpoint(state);
|
|
@@ -248,7 +350,9 @@ class ThreadEventStore {
|
|
|
248
350
|
captureCompactSource(turnId) {
|
|
249
351
|
return this.withLogLock(() => {
|
|
250
352
|
ensurePrivateDirectory(this.rootDir);
|
|
251
|
-
const
|
|
353
|
+
const head = this.loadVerifiedHead(true);
|
|
354
|
+
const scan = requireVerifiedScan(head);
|
|
355
|
+
const { projection } = head;
|
|
252
356
|
assertActiveMaintenanceTurn(projection, turnId);
|
|
253
357
|
const state = readCompactState(this.compactStatePath, this.threadId);
|
|
254
358
|
if (state)
|
|
@@ -283,7 +387,9 @@ class ThreadEventStore {
|
|
|
283
387
|
ensurePrivateDirectory(this.compactCheckpointsDir);
|
|
284
388
|
validateCompactCommitInput(input, this.threadId);
|
|
285
389
|
input.onBoundary('before_cas_recheck');
|
|
286
|
-
const
|
|
390
|
+
const head = this.loadVerifiedHead(true);
|
|
391
|
+
const scan = requireVerifiedScan(head);
|
|
392
|
+
const { projection } = head;
|
|
287
393
|
const eventsAfterAnchor = scan.events.slice(input.expectedEventAnchor.cursor);
|
|
288
394
|
const onlyQueuedFollowUps = eventsAfterAnchor.length > 0 &&
|
|
289
395
|
eventsAfterAnchor.every(event => event.payload.type === 'turn.queued');
|
|
@@ -348,7 +454,7 @@ class ThreadEventStore {
|
|
|
348
454
|
}
|
|
349
455
|
listCompactEvents() {
|
|
350
456
|
return this.withLogLock(() => {
|
|
351
|
-
const
|
|
457
|
+
const scan = requireVerifiedScan(this.loadVerifiedHead(true));
|
|
352
458
|
return deepFreeze(scan.events.filter((event) => event.payload.type === 'compact.started' ||
|
|
353
459
|
event.payload.type === 'compact.completed' ||
|
|
354
460
|
event.payload.type === 'compact.failed'));
|
|
@@ -400,14 +506,27 @@ class ThreadEventStore {
|
|
|
400
506
|
return inputs.length > 0 ? this.appendDurableBatch(inputs).projection : projection;
|
|
401
507
|
}
|
|
402
508
|
/**
|
|
403
|
-
* Return the last
|
|
404
|
-
*
|
|
509
|
+
* Return the last verified log head. A fresh process may adopt the persisted
|
|
510
|
+
* receipt only when the exact JSONL identity and independently validated
|
|
511
|
+
* projection still match. Callers that need historical events explicitly
|
|
512
|
+
* request a scan; cursor/projection/append paths stay O(head + projection).
|
|
405
513
|
*/
|
|
406
|
-
loadVerifiedHead() {
|
|
514
|
+
loadVerifiedHead(requireScan = false) {
|
|
407
515
|
ensurePrivateDirectory(this.rootDir);
|
|
408
|
-
const
|
|
409
|
-
if (this.cachedHead?.logFingerprint ===
|
|
516
|
+
const logIdentity = readFileIdentity(this.logPath);
|
|
517
|
+
if (this.cachedHead?.logFingerprint === logIdentity.fingerprint &&
|
|
518
|
+
(!requireScan || this.cachedHead.scan)) {
|
|
410
519
|
return this.cachedHead;
|
|
520
|
+
}
|
|
521
|
+
const stored = readStoredThreadHead(this.headPath, this.threadId);
|
|
522
|
+
if (!requireScan && stored) {
|
|
523
|
+
const projection = readProjection(this.projectionPath);
|
|
524
|
+
const persisted = hydratePersistedThreadHead(stored, logIdentity, projection, fileFingerprint(this.projectionPath));
|
|
525
|
+
if (persisted) {
|
|
526
|
+
this.cachedHead = persisted;
|
|
527
|
+
return persisted;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
411
530
|
const scan = scanThreadEventLog(this.logPath, this.threadId, this.maxLogBytes);
|
|
412
531
|
this.onLogScan?.({
|
|
413
532
|
threadId: this.threadId,
|
|
@@ -415,10 +534,50 @@ class ThreadEventStore {
|
|
|
415
534
|
events: scan.events.length,
|
|
416
535
|
});
|
|
417
536
|
const projection = (0, thread_projection_1.projectThreadEvents)(this.threadId, scan.events);
|
|
418
|
-
const
|
|
537
|
+
const eventIds = scan.events.map(event => event.eventId);
|
|
538
|
+
const projectionFingerprint = projectionFileMatches(this.projectionPath, projection)
|
|
539
|
+
? fileFingerprint(this.projectionPath)
|
|
540
|
+
: undefined;
|
|
541
|
+
const head = {
|
|
542
|
+
generation: (stored?.generation ?? 0) + 1,
|
|
543
|
+
logFingerprint: logIdentity.fingerprint,
|
|
544
|
+
log: storedLogIdentity(logIdentity),
|
|
545
|
+
...(projectionFingerprint ? { projectionFingerprint } : {}),
|
|
546
|
+
scan,
|
|
547
|
+
projection,
|
|
548
|
+
lastEventTimestamp: scan.events.at(-1)?.timestamp ?? 0,
|
|
549
|
+
lastRecordHash: scan.records.at(-1)?.hash ?? null,
|
|
550
|
+
safeByteLength: scan.safeByteLength,
|
|
551
|
+
discardedTailBytes: scan.discardedTailBytes,
|
|
552
|
+
eventIds,
|
|
553
|
+
eventIdSet: new Set(eventIds),
|
|
554
|
+
verifiedPrefixes: retainVerifiedPrefixes(stored?.verifiedPrefixes ?? [], scan, this.threadId),
|
|
555
|
+
};
|
|
419
556
|
this.cachedHead = head;
|
|
557
|
+
if (projectionFingerprint)
|
|
558
|
+
this.tryPersistVerifiedHead(head);
|
|
420
559
|
return head;
|
|
421
560
|
}
|
|
561
|
+
tryPersistVerifiedHead(head) {
|
|
562
|
+
try {
|
|
563
|
+
const logIdentity = readFileIdentity(this.logPath);
|
|
564
|
+
if (logIdentity.fingerprint !== head.logFingerprint)
|
|
565
|
+
return;
|
|
566
|
+
if (!projectionFileMatches(this.projectionPath, head.projection))
|
|
567
|
+
return;
|
|
568
|
+
const stored = createStoredThreadHead(head, logIdentity);
|
|
569
|
+
(0, atomic_write_1.atomicWriteFileSync)(this.headPath, `${(0, canonical_1.canonicalRuntimeJson)(stored)}\n`, {
|
|
570
|
+
mode: 0o600,
|
|
571
|
+
fsync: true,
|
|
572
|
+
});
|
|
573
|
+
fsyncDirectory(this.rootDir);
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
// The JSONL fact and projection are already durable. A missing/stale
|
|
577
|
+
// derived head merely makes the next process take the full verification
|
|
578
|
+
// path; it must never turn a committed fact into a reported failure.
|
|
579
|
+
}
|
|
580
|
+
}
|
|
422
581
|
withLogLock(operation) {
|
|
423
582
|
ensurePrivateDirectory((0, path_1.dirname)(this.logPath));
|
|
424
583
|
return (0, file_lock_1.withFileLockSync)(this.logPath, operation, { waitMs: this.lockWaitMs });
|
|
@@ -569,6 +728,179 @@ function readProjection(path) {
|
|
|
569
728
|
return null;
|
|
570
729
|
}
|
|
571
730
|
}
|
|
731
|
+
function projectionFileMatches(path, expected) {
|
|
732
|
+
const projection = readProjection(path);
|
|
733
|
+
return Boolean(projection &&
|
|
734
|
+
projection.threadId === expected.threadId &&
|
|
735
|
+
projection.cursor === expected.cursor &&
|
|
736
|
+
projection.digest === expected.digest &&
|
|
737
|
+
(0, thread_projection_1.verifyThreadProjectionDigest)(projection));
|
|
738
|
+
}
|
|
739
|
+
function readStoredThreadHead(path, threadId) {
|
|
740
|
+
if (!(0, fs_1.existsSync)(path))
|
|
741
|
+
return null;
|
|
742
|
+
try {
|
|
743
|
+
const value = JSON.parse((0, fs_1.readFileSync)(path, 'utf8'));
|
|
744
|
+
if (!isRecord(value))
|
|
745
|
+
return null;
|
|
746
|
+
const { digest: _digest, ...content } = value;
|
|
747
|
+
void _digest;
|
|
748
|
+
if (value.version !== 1 ||
|
|
749
|
+
value.threadId !== threadId ||
|
|
750
|
+
!Number.isSafeInteger(value.generation) ||
|
|
751
|
+
value.generation < 1 ||
|
|
752
|
+
!Number.isSafeInteger(value.cursor) ||
|
|
753
|
+
value.cursor < 0 ||
|
|
754
|
+
typeof value.projectionDigest !== 'string' ||
|
|
755
|
+
!Number.isSafeInteger(value.lastEventTimestamp) ||
|
|
756
|
+
value.lastEventTimestamp < 0 ||
|
|
757
|
+
(value.lastRecordHash !== null && !isSha256(value.lastRecordHash)) ||
|
|
758
|
+
!Number.isSafeInteger(value.safeByteLength) ||
|
|
759
|
+
value.safeByteLength < 0 ||
|
|
760
|
+
!Number.isSafeInteger(value.discardedTailBytes) ||
|
|
761
|
+
value.discardedTailBytes < 0 ||
|
|
762
|
+
!Array.isArray(value.eventIds) ||
|
|
763
|
+
!Array.isArray(value.verifiedPrefixes) ||
|
|
764
|
+
value.verifiedPrefixes.length > 32 ||
|
|
765
|
+
!isStoredLogIdentity(value.log) ||
|
|
766
|
+
typeof value.digest !== 'string' ||
|
|
767
|
+
(0, canonical_1.digestRuntimeValue)(content) !== value.digest) {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
const eventIds = value.eventIds;
|
|
771
|
+
if (eventIds.length !== value.cursor ||
|
|
772
|
+
eventIds.some(eventId => typeof eventId !== 'string' || !(0, runtime_protocol_v1_1.isRuntimeId)(eventId)) ||
|
|
773
|
+
new Set(eventIds).size !== eventIds.length ||
|
|
774
|
+
(value.cursor === 0) !== (value.lastRecordHash === null) ||
|
|
775
|
+
(value.cursor === 0 && value.lastEventTimestamp !== 0)) {
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
const prefixes = value.verifiedPrefixes;
|
|
779
|
+
if (prefixes.some(prefix => !isStoredVerifiedPrefix(prefix, value.cursor)) ||
|
|
780
|
+
new Set(prefixes.map(prefix => prefix.cursor)).size !==
|
|
781
|
+
prefixes.length) {
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
return deepFreeze(value);
|
|
785
|
+
}
|
|
786
|
+
catch {
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
function hydratePersistedThreadHead(stored, logIdentity, projection, projectionFingerprint) {
|
|
791
|
+
if (!sameStoredLogIdentity(stored.log, logIdentity) ||
|
|
792
|
+
stored.safeByteLength + stored.discardedTailBytes !== logIdentity.bytes ||
|
|
793
|
+
!projection ||
|
|
794
|
+
projection.threadId !== stored.threadId ||
|
|
795
|
+
projection.cursor !== stored.cursor ||
|
|
796
|
+
projection.digest !== stored.projectionDigest ||
|
|
797
|
+
!(0, thread_projection_1.verifyThreadProjectionDigest)(projection)) {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
const eventIds = [...stored.eventIds];
|
|
801
|
+
return {
|
|
802
|
+
generation: stored.generation,
|
|
803
|
+
logFingerprint: logIdentity.fingerprint,
|
|
804
|
+
log: storedLogIdentity(logIdentity),
|
|
805
|
+
projectionFingerprint,
|
|
806
|
+
projection,
|
|
807
|
+
lastEventTimestamp: stored.lastEventTimestamp,
|
|
808
|
+
lastRecordHash: stored.lastRecordHash,
|
|
809
|
+
safeByteLength: stored.safeByteLength,
|
|
810
|
+
discardedTailBytes: stored.discardedTailBytes,
|
|
811
|
+
eventIds,
|
|
812
|
+
eventIdSet: new Set(eventIds),
|
|
813
|
+
verifiedPrefixes: stored.verifiedPrefixes.map(prefix => ({ ...prefix })),
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function createStoredThreadHead(head, logIdentity) {
|
|
817
|
+
const content = {
|
|
818
|
+
version: 1,
|
|
819
|
+
generation: head.generation,
|
|
820
|
+
threadId: head.projection.threadId,
|
|
821
|
+
cursor: head.projection.cursor,
|
|
822
|
+
projectionDigest: head.projection.digest,
|
|
823
|
+
lastEventTimestamp: head.lastEventTimestamp,
|
|
824
|
+
lastRecordHash: head.lastRecordHash,
|
|
825
|
+
safeByteLength: head.safeByteLength,
|
|
826
|
+
discardedTailBytes: head.discardedTailBytes,
|
|
827
|
+
eventIds: [...head.eventIds],
|
|
828
|
+
verifiedPrefixes: head.verifiedPrefixes.map(prefix => ({ ...prefix })),
|
|
829
|
+
log: storedLogIdentity(logIdentity),
|
|
830
|
+
};
|
|
831
|
+
return deepFreeze({ ...content, digest: (0, canonical_1.digestRuntimeValue)(content) });
|
|
832
|
+
}
|
|
833
|
+
function retainVerifiedPrefixes(prefixes, scan, threadId) {
|
|
834
|
+
return prefixes.filter(prefix => {
|
|
835
|
+
if (prefix.cursor > scan.events.length)
|
|
836
|
+
return false;
|
|
837
|
+
const events = scan.events.slice(0, prefix.cursor);
|
|
838
|
+
return ((0, canonical_1.digestRuntimeValue)(events) === prefix.eventDigest &&
|
|
839
|
+
(0, thread_projection_1.projectThreadEvents)(threadId, events).digest === prefix.projectionDigest);
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
function requireVerifiedScan(head) {
|
|
843
|
+
if (!head.scan)
|
|
844
|
+
throw new Error('Thread event history was not loaded');
|
|
845
|
+
return head.scan;
|
|
846
|
+
}
|
|
847
|
+
function sessionIndexHead(head) {
|
|
848
|
+
return {
|
|
849
|
+
cursor: head.projection.cursor,
|
|
850
|
+
projectionDigest: head.projection.digest,
|
|
851
|
+
lastEventTimestamp: head.lastEventTimestamp,
|
|
852
|
+
lastRecordHash: head.lastRecordHash,
|
|
853
|
+
log: head.log,
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
function checkpointHead(head) {
|
|
857
|
+
return deepFreeze({
|
|
858
|
+
cursor: head.projection.cursor,
|
|
859
|
+
projectionDigest: head.projection.digest,
|
|
860
|
+
lastEventTimestamp: head.lastEventTimestamp,
|
|
861
|
+
lastRecordHash: head.lastRecordHash,
|
|
862
|
+
log: { ...head.log },
|
|
863
|
+
verifiedPrefixes: head.verifiedPrefixes.map(prefix => ({ ...prefix })),
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
function isStoredVerifiedPrefix(value, headCursor) {
|
|
867
|
+
return (isRecord(value) &&
|
|
868
|
+
Number.isSafeInteger(value.cursor) &&
|
|
869
|
+
value.cursor > 0 &&
|
|
870
|
+
value.cursor <= headCursor &&
|
|
871
|
+
isSha256(value.eventDigest) &&
|
|
872
|
+
isSha256(value.projectionDigest));
|
|
873
|
+
}
|
|
874
|
+
function sameVerifiedPrefix(left, right) {
|
|
875
|
+
return (left.cursor === right.cursor &&
|
|
876
|
+
left.eventDigest === right.eventDigest &&
|
|
877
|
+
left.projectionDigest === right.projectionDigest);
|
|
878
|
+
}
|
|
879
|
+
function isStoredLogIdentity(value) {
|
|
880
|
+
return (isRecord(value) &&
|
|
881
|
+
Number.isSafeInteger(value.bytes) &&
|
|
882
|
+
value.bytes >= 0 &&
|
|
883
|
+
['device', 'inode', 'mtimeNs', 'ctimeNs'].every(key => typeof value[key] === 'string' && /^\d+$/.test(value[key])));
|
|
884
|
+
}
|
|
885
|
+
function sameStoredLogIdentity(stored, current) {
|
|
886
|
+
return (stored.bytes === current.bytes &&
|
|
887
|
+
stored.device === current.device &&
|
|
888
|
+
stored.inode === current.inode &&
|
|
889
|
+
stored.mtimeNs === current.mtimeNs &&
|
|
890
|
+
stored.ctimeNs === current.ctimeNs);
|
|
891
|
+
}
|
|
892
|
+
function storedLogIdentity(identity) {
|
|
893
|
+
return {
|
|
894
|
+
bytes: identity.bytes,
|
|
895
|
+
device: identity.device,
|
|
896
|
+
inode: identity.inode,
|
|
897
|
+
mtimeNs: identity.mtimeNs,
|
|
898
|
+
ctimeNs: identity.ctimeNs,
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
function isSha256(value) {
|
|
902
|
+
return typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
903
|
+
}
|
|
572
904
|
function readFileIdentity(path) {
|
|
573
905
|
try {
|
|
574
906
|
const stats = (0, fs_1.statSync)(path, { bigint: true });
|