@orion-agents/orion-code 0.2.2 → 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.
Files changed (217) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +89 -14
  3. package/README.zh-CN.md +77 -14
  4. package/bin/orion +15 -4
  5. package/dist/cli.js +51 -172
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/model-command-handlers.d.ts +2 -2
  8. package/dist/commands/model-command-handlers.d.ts.map +1 -1
  9. package/dist/commands/model-command-handlers.js +113 -32
  10. package/dist/commands/model-command-handlers.js.map +1 -1
  11. package/dist/commands/session-command-handlers.d.ts.map +1 -1
  12. package/dist/commands/session-command-handlers.js +106 -15
  13. package/dist/commands/session-command-handlers.js.map +1 -1
  14. package/dist/commands/types.d.ts +13 -3
  15. package/dist/commands/types.d.ts.map +1 -1
  16. package/dist/commands/types.js +1 -1
  17. package/dist/commands/types.js.map +1 -1
  18. package/dist/core/tool-artifacts.d.ts +2 -0
  19. package/dist/core/tool-artifacts.d.ts.map +1 -1
  20. package/dist/core/tool-artifacts.js +27 -1
  21. package/dist/core/tool-artifacts.js.map +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/runtime/agent-loop.d.ts.map +1 -1
  25. package/dist/runtime/agent-loop.js +3 -3
  26. package/dist/runtime/agent-loop.js.map +1 -1
  27. package/dist/runtime/agent-runtime-controller.d.ts +32 -1
  28. package/dist/runtime/agent-runtime-controller.d.ts.map +1 -1
  29. package/dist/runtime/agent-runtime-controller.js +241 -27
  30. package/dist/runtime/agent-runtime-controller.js.map +1 -1
  31. package/dist/runtime/agent-runtime-protocol.d.ts +4 -0
  32. package/dist/runtime/agent-runtime-protocol.d.ts.map +1 -1
  33. package/dist/runtime/agent-runtime-protocol.js.map +1 -1
  34. package/dist/runtime/agent-runtime-runner.d.ts +2 -1
  35. package/dist/runtime/agent-runtime-runner.d.ts.map +1 -1
  36. package/dist/runtime/durable-tool-receipt-reader.d.ts +32 -0
  37. package/dist/runtime/durable-tool-receipt-reader.d.ts.map +1 -0
  38. package/dist/runtime/durable-tool-receipt-reader.js +130 -0
  39. package/dist/runtime/durable-tool-receipt-reader.js.map +1 -0
  40. package/dist/runtime/legacy-thread-materializer.d.ts +31 -0
  41. package/dist/runtime/legacy-thread-materializer.d.ts.map +1 -1
  42. package/dist/runtime/legacy-thread-materializer.js +71 -17
  43. package/dist/runtime/legacy-thread-materializer.js.map +1 -1
  44. package/dist/runtime/orion-runtime-v1.d.ts +2 -0
  45. package/dist/runtime/orion-runtime-v1.d.ts.map +1 -1
  46. package/dist/runtime/orion-runtime-v1.js +7 -1
  47. package/dist/runtime/orion-runtime-v1.js.map +1 -1
  48. package/dist/runtime/orion-session-runner.d.ts +7 -2
  49. package/dist/runtime/orion-session-runner.d.ts.map +1 -1
  50. package/dist/runtime/orion-session-runner.js +16 -6
  51. package/dist/runtime/orion-session-runner.js.map +1 -1
  52. package/dist/runtime/product-bootstrap.d.ts +22 -0
  53. package/dist/runtime/product-bootstrap.d.ts.map +1 -0
  54. package/dist/runtime/product-bootstrap.js +374 -0
  55. package/dist/runtime/product-bootstrap.js.map +1 -0
  56. package/dist/runtime/product-orion-runtime.d.ts +2 -1
  57. package/dist/runtime/product-orion-runtime.d.ts.map +1 -1
  58. package/dist/runtime/product-orion-runtime.js +83 -13
  59. package/dist/runtime/product-orion-runtime.js.map +1 -1
  60. package/dist/runtime/protocol/runtime-protocol-v1.d.ts +1 -1
  61. package/dist/runtime/protocol/runtime-protocol-v1.js +1 -1
  62. package/dist/runtime/protocol/runtime-protocol-v1.js.map +1 -1
  63. package/dist/runtime/release-receipts.d.ts +66 -3
  64. package/dist/runtime/release-receipts.d.ts.map +1 -1
  65. package/dist/runtime/release-receipts.js +340 -7
  66. package/dist/runtime/release-receipts.js.map +1 -1
  67. package/dist/runtime/step-snapshot.d.ts.map +1 -1
  68. package/dist/runtime/step-snapshot.js +22 -2
  69. package/dist/runtime/step-snapshot.js.map +1 -1
  70. package/dist/runtime/subagents/runtime-integration.d.ts +1 -1
  71. package/dist/runtime/subagents/runtime-integration.d.ts.map +1 -1
  72. package/dist/runtime/subagents/runtime-integration.js +5 -2
  73. package/dist/runtime/subagents/runtime-integration.js.map +1 -1
  74. package/dist/runtime/thread-event-store.d.ts +80 -0
  75. package/dist/runtime/thread-event-store.d.ts.map +1 -1
  76. package/dist/runtime/thread-event-store.js +456 -22
  77. package/dist/runtime/thread-event-store.js.map +1 -1
  78. package/dist/runtime/thread-projection.d.ts +10 -0
  79. package/dist/runtime/thread-projection.d.ts.map +1 -1
  80. package/dist/runtime/thread-projection.js +40 -0
  81. package/dist/runtime/thread-projection.js.map +1 -1
  82. package/dist/runtime/thread-runtime.d.ts +3 -1
  83. package/dist/runtime/thread-runtime.d.ts.map +1 -1
  84. package/dist/runtime/thread-runtime.js +6 -0
  85. package/dist/runtime/thread-runtime.js.map +1 -1
  86. package/dist/runtime/thread-session-index.d.ts +84 -0
  87. package/dist/runtime/thread-session-index.d.ts.map +1 -0
  88. package/dist/runtime/thread-session-index.js +503 -0
  89. package/dist/runtime/thread-session-index.js.map +1 -0
  90. package/dist/runtime/thread-session-view.d.ts +70 -9
  91. package/dist/runtime/thread-session-view.d.ts.map +1 -1
  92. package/dist/runtime/thread-session-view.js +200 -82
  93. package/dist/runtime/thread-session-view.js.map +1 -1
  94. package/dist/runtime/thread-ui-adapter.d.ts +2 -0
  95. package/dist/runtime/thread-ui-adapter.d.ts.map +1 -1
  96. package/dist/runtime/thread-ui-adapter.js +86 -4
  97. package/dist/runtime/thread-ui-adapter.js.map +1 -1
  98. package/dist/runtime/tool-detail-repository.d.ts.map +1 -1
  99. package/dist/runtime/tool-detail-repository.js +23 -19
  100. package/dist/runtime/tool-detail-repository.js.map +1 -1
  101. package/dist/runtime/tool-receipt-validator.d.ts +21 -0
  102. package/dist/runtime/tool-receipt-validator.d.ts.map +1 -0
  103. package/dist/runtime/tool-receipt-validator.js +157 -0
  104. package/dist/runtime/tool-receipt-validator.js.map +1 -0
  105. package/dist/runtime/ui-events.d.ts +35 -0
  106. package/dist/runtime/ui-events.d.ts.map +1 -1
  107. package/dist/runtime/ui-events.js +1 -0
  108. package/dist/runtime/ui-events.js.map +1 -1
  109. package/dist/runtime/ui-view-model.d.ts +2 -0
  110. package/dist/runtime/ui-view-model.d.ts.map +1 -1
  111. package/dist/runtime/ui-view-model.js +7 -3
  112. package/dist/runtime/ui-view-model.js.map +1 -1
  113. package/dist/services/global-config.d.ts +25 -0
  114. package/dist/services/global-config.d.ts.map +1 -1
  115. package/dist/services/global-config.js +373 -5
  116. package/dist/services/global-config.js.map +1 -1
  117. package/dist/services/redaction.d.ts +4 -0
  118. package/dist/services/redaction.d.ts.map +1 -1
  119. package/dist/services/redaction.js +63 -0
  120. package/dist/services/redaction.js.map +1 -1
  121. package/dist/services/session-index.d.ts +16 -0
  122. package/dist/services/session-index.d.ts.map +1 -1
  123. package/dist/services/session-index.js +45 -36
  124. package/dist/services/session-index.js.map +1 -1
  125. package/dist/services/session-storage.d.ts +59 -0
  126. package/dist/services/session-storage.d.ts.map +1 -1
  127. package/dist/services/session-storage.js +306 -107
  128. package/dist/services/session-storage.js.map +1 -1
  129. package/dist/services/settings-coordinator.d.ts +174 -0
  130. package/dist/services/settings-coordinator.d.ts.map +1 -0
  131. package/dist/services/settings-coordinator.js +605 -0
  132. package/dist/services/settings-coordinator.js.map +1 -0
  133. package/dist/services/settings-document-repository.d.ts +138 -0
  134. package/dist/services/settings-document-repository.d.ts.map +1 -0
  135. package/dist/services/settings-document-repository.js +551 -0
  136. package/dist/services/settings-document-repository.js.map +1 -0
  137. package/dist/services/workspace-registry.d.ts +42 -0
  138. package/dist/services/workspace-registry.d.ts.map +1 -0
  139. package/dist/services/workspace-registry.js +222 -0
  140. package/dist/services/workspace-registry.js.map +1 -0
  141. package/dist/terminal-ui/launch.d.ts.map +1 -1
  142. package/dist/terminal-ui/launch.js +5 -0
  143. package/dist/terminal-ui/launch.js.map +1 -1
  144. package/dist/tui-ui/state.d.ts.map +1 -1
  145. package/dist/tui-ui/state.js +5 -0
  146. package/dist/tui-ui/state.js.map +1 -1
  147. package/dist/web/errors.d.ts +6 -0
  148. package/dist/web/errors.d.ts.map +1 -0
  149. package/dist/web/errors.js +13 -0
  150. package/dist/web/errors.js.map +1 -0
  151. package/dist/web/event-hub.d.ts +43 -0
  152. package/dist/web/event-hub.d.ts.map +1 -0
  153. package/dist/web/event-hub.js +323 -0
  154. package/dist/web/event-hub.js.map +1 -0
  155. package/dist/web/file-read-service.d.ts +55 -0
  156. package/dist/web/file-read-service.d.ts.map +1 -0
  157. package/dist/web/file-read-service.js +399 -0
  158. package/dist/web/file-read-service.js.map +1 -0
  159. package/dist/web/git-read-model-service.d.ts +77 -0
  160. package/dist/web/git-read-model-service.d.ts.map +1 -0
  161. package/dist/web/git-read-model-service.js +631 -0
  162. package/dist/web/git-read-model-service.js.map +1 -0
  163. package/dist/web/index.d.ts +6 -0
  164. package/dist/web/index.d.ts.map +1 -0
  165. package/dist/web/index.js +28 -0
  166. package/dist/web/index.js.map +1 -0
  167. package/dist/web/launch.d.ts +9 -0
  168. package/dist/web/launch.d.ts.map +1 -0
  169. package/dist/web/launch.js +64 -0
  170. package/dist/web/launch.js.map +1 -0
  171. package/dist/web/protocol.d.ts +408 -0
  172. package/dist/web/protocol.d.ts.map +1 -0
  173. package/dist/web/protocol.js +369 -0
  174. package/dist/web/protocol.js.map +1 -0
  175. package/dist/web/review-service.d.ts +38 -0
  176. package/dist/web/review-service.d.ts.map +1 -0
  177. package/dist/web/review-service.js +74 -0
  178. package/dist/web/review-service.js.map +1 -0
  179. package/dist/web/server.d.ts +20 -0
  180. package/dist/web/server.d.ts.map +1 -0
  181. package/dist/web/server.js +744 -0
  182. package/dist/web/server.js.map +1 -0
  183. package/dist/web/terminal-manager.d.ts +155 -0
  184. package/dist/web/terminal-manager.d.ts.map +1 -0
  185. package/dist/web/terminal-manager.js +728 -0
  186. package/dist/web/terminal-manager.js.map +1 -0
  187. package/dist/web/terminal-server.d.ts +8 -0
  188. package/dist/web/terminal-server.d.ts.map +1 -0
  189. package/dist/web/terminal-server.js +275 -0
  190. package/dist/web/terminal-server.js.map +1 -0
  191. package/dist/web/workbench-controller.d.ts +99 -0
  192. package/dist/web/workbench-controller.d.ts.map +1 -0
  193. package/dist/web/workbench-controller.js +1267 -0
  194. package/dist/web/workbench-controller.js.map +1 -0
  195. package/dist/web-client/assets/DiffViewer-DpKoD07C.js +5 -0
  196. package/dist/web-client/assets/FilesPanel-BkoLmgjR.js +2 -0
  197. package/dist/web-client/assets/GitPanel-D20StcdG.js +1 -0
  198. package/dist/web-client/assets/ReviewPanel-3MVWbMQV.js +1 -0
  199. package/dist/web-client/assets/TerminalPanel-BLQ592Fb.js +21 -0
  200. package/dist/web-client/assets/TerminalPanel-DLuoa74B.css +1 -0
  201. package/dist/web-client/assets/index-BkIDl_xk.js +16 -0
  202. package/dist/web-client/assets/index-DXMpnWE8.css +1 -0
  203. package/dist/web-client/index.html +16 -0
  204. package/docs/architecture/v0.3.0-web-api.yaml +1340 -0
  205. package/docs/architecture/v0.3.1-web-api.yaml +2290 -0
  206. package/docs/migration/v0.2.2-to-v0.3.0-settings.md +114 -0
  207. package/docs/migration/v0.2.2-to-v0.3.0.md +55 -0
  208. package/docs/migration/v0.3.0-to-v0.3.1.md +88 -0
  209. package/docs/orion.example.json +9 -2
  210. package/docs/plan/v0.3.0-node-runtime-compatibility-plan.md +57 -0
  211. package/docs/plan/v0.3.0-settings-integration-plan.md +740 -0
  212. package/docs/plan/v0.3.0-web-workbench-plan.md +376 -0
  213. package/docs/plan/v0.3.1-web-workbench-professional-shell-plan.md +848 -0
  214. package/docs/readme.md +22 -1
  215. package/docs/test/v0.3.1-web-workbench-e2e-plan.md +140 -0
  216. package/npm-shrinkwrap.json +1229 -45
  217. package/package.json +47 -11
@@ -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');
@@ -44,6 +46,7 @@ class ThreadEventStore {
44
46
  this.clock = options.clock ?? Date.now;
45
47
  this.idFactory = options.idFactory ?? runtime_protocol_v1_1.createRuntimeId;
46
48
  this.onBoundary = options.onBoundary;
49
+ this.onLogScan = options.onLogScan;
47
50
  }
48
51
  appendDurable(input) {
49
52
  return this.appendDurableBatch([input]).events[0];
@@ -54,13 +57,13 @@ class ThreadEventStore {
54
57
  }
55
58
  const commit = this.withLogLock(() => {
56
59
  ensurePrivateDirectory(this.rootDir);
57
- const scan = scanThreadEventLog(this.logPath, this.threadId, this.maxLogBytes);
58
- if (scan.discardedTailBytes > 0)
59
- (0, fs_1.truncateSync)(this.logPath, scan.safeByteLength);
60
- let seq = scan.events.length;
61
- let previousHash = scan.records.at(-1)?.hash ?? null;
60
+ const head = this.loadVerifiedHead();
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;
62
65
  const records = [];
63
- const eventIds = new Set(scan.events.map(event => event.eventId));
66
+ const eventIds = new Set(head.eventIdSet);
64
67
  for (const input of inputs) {
65
68
  seq += 1;
66
69
  const eventId = this.idFactory();
@@ -85,10 +88,12 @@ class ThreadEventStore {
85
88
  records.push({ version: 1, previousHash, hash, event });
86
89
  previousHash = hash;
87
90
  }
88
- // Validate the complete lifecycle before a byte becomes durable. A bad
89
- // duplicate terminal event must never poison the append-only fact log.
90
- const allEvents = [...scan.events, ...records.map(record => record.event)];
91
- const projection = (0, thread_projection_1.projectThreadEvents)(this.threadId, allEvents);
91
+ // Validate the new tail against the already verified projection before a
92
+ // byte becomes durable. Historical lifecycle facts were validated when
93
+ // cachedHead was established and are invalidated by any log fingerprint
94
+ // change, so append cost no longer grows with Session age.
95
+ const committedEvents = records.map(record => record.event);
96
+ const projection = (0, thread_projection_1.advanceThreadProjection)(head.projection, committedEvents);
92
97
  this.boundary('before_log_write', seq);
93
98
  appendRecordsAndFlush(this.logPath, records, () => this.boundary('after_log_write', seq));
94
99
  // appendRecordsAndFlush fsyncs before returning; this boundary is the
@@ -99,7 +104,48 @@ class ThreadEventStore {
99
104
  writeProjection(this.projectionPath, projection);
100
105
  fsyncDirectory(this.rootDir);
101
106
  this.boundary('after_projection_write', seq);
102
- return deepFreeze({ events: records.map(record => record.event), projection });
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),
121
+ projectionFingerprint: fileFingerprint(this.projectionPath),
122
+ ...(nextScan ? { scan: nextScan } : {}),
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,
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
+ }
148
+ return deepFreeze({ events: committedEvents, projection });
103
149
  });
104
150
  this.notifyCommitted(commit.events);
105
151
  return commit;
@@ -135,7 +181,7 @@ class ThreadEventStore {
135
181
  throw new ThreadEventStoreError('ORION_THREAD_EVENT_LIMIT', `Replay limit ${safeLimit} exceeds ${this.maxReplayEvents}`);
136
182
  }
137
183
  return this.withLogLock(() => {
138
- const scan = scanThreadEventLog(this.logPath, this.threadId, this.maxLogBytes);
184
+ const scan = requireVerifiedScan(this.loadVerifiedHead(true));
139
185
  const lastSeq = scan.events.length;
140
186
  if (cursor > lastSeq) {
141
187
  throw new ThreadEventStoreError('ORION_THREAD_EVENT_CURSOR', `Replay cursor ${cursor} is ahead of durable cursor ${lastSeq}`);
@@ -150,24 +196,134 @@ class ThreadEventStore {
150
196
  });
151
197
  });
152
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
+ }
153
237
  getCursor() {
154
- return this.withLogLock(() => scanThreadEventLog(this.logPath, this.threadId, this.maxLogBytes).events.length);
238
+ const cached = this.cachedHead;
239
+ if (cached && cached.logFingerprint === fileFingerprint(this.logPath)) {
240
+ return cached.projection.cursor;
241
+ }
242
+ return this.withLogLock(() => this.loadVerifiedHead().projection.cursor);
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
+ }
272
+ /** Capture the catalog read model and its exact verified log identity atomically. */
273
+ captureReadModelHead() {
274
+ return this.withLogLock(() => {
275
+ for (let attempt = 0; attempt < 3; attempt += 1) {
276
+ const head = this.loadVerifiedHead();
277
+ const identity = readFileIdentity(this.logPath);
278
+ if (identity.fingerprint !== head.logFingerprint) {
279
+ this.cachedHead = undefined;
280
+ continue;
281
+ }
282
+ return deepFreeze({
283
+ projection: head.projection,
284
+ lastEventTimestamp: head.lastEventTimestamp,
285
+ lastRecordHash: head.lastRecordHash,
286
+ log: {
287
+ bytes: identity.bytes,
288
+ device: identity.device,
289
+ inode: identity.inode,
290
+ mtimeNs: identity.mtimeNs,
291
+ ctimeNs: identity.ctimeNs,
292
+ },
293
+ });
294
+ }
295
+ throw corrupt('Thread event log changed while capturing its read model', 0);
296
+ });
155
297
  }
156
298
  loadProjection() {
157
299
  return this.withLogLock(() => {
158
300
  ensurePrivateDirectory(this.rootDir);
159
- const scan = scanThreadEventLog(this.logPath, this.threadId, this.maxLogBytes);
160
- const replayed = (0, thread_projection_1.projectThreadEvents)(this.threadId, scan.events);
301
+ const head = this.loadVerifiedHead();
302
+ const replayed = head.projection;
303
+ const projectionFingerprint = fileFingerprint(this.projectionPath);
304
+ if (head.projectionFingerprint === projectionFingerprint) {
305
+ this.tryPersistVerifiedHead(head);
306
+ return replayed;
307
+ }
161
308
  const cached = readProjection(this.projectionPath);
162
309
  if (cached &&
163
310
  cached.threadId === this.threadId &&
164
311
  cached.cursor === replayed.cursor &&
165
312
  cached.digest === replayed.digest &&
166
313
  (0, thread_projection_1.verifyThreadProjectionDigest)(cached)) {
167
- return deepFreeze(cached);
314
+ const current = { ...head, projectionFingerprint };
315
+ this.cachedHead = current;
316
+ this.tryPersistVerifiedHead(current);
317
+ return replayed;
168
318
  }
169
319
  writeProjection(this.projectionPath, replayed);
170
320
  fsyncDirectory(this.rootDir);
321
+ const current = {
322
+ ...head,
323
+ projectionFingerprint: fileFingerprint(this.projectionPath),
324
+ };
325
+ this.cachedHead = current;
326
+ this.tryPersistVerifiedHead(current);
171
327
  return replayed;
172
328
  });
173
329
  }
@@ -177,7 +333,7 @@ class ThreadEventStore {
177
333
  */
178
334
  loadAuthoritativeModelHistory() {
179
335
  return this.withLogLock(() => {
180
- const scan = scanThreadEventLog(this.logPath, this.threadId, this.maxLogBytes);
336
+ const scan = requireVerifiedScan(this.loadVerifiedHead(true));
181
337
  const state = readCompactState(this.compactStatePath, this.threadId);
182
338
  if (state)
183
339
  this.assertCompactCheckpoint(state);
@@ -194,8 +350,9 @@ class ThreadEventStore {
194
350
  captureCompactSource(turnId) {
195
351
  return this.withLogLock(() => {
196
352
  ensurePrivateDirectory(this.rootDir);
197
- const scan = scanThreadEventLog(this.logPath, this.threadId, this.maxLogBytes);
198
- const projection = (0, thread_projection_1.projectThreadEvents)(this.threadId, scan.events);
353
+ const head = this.loadVerifiedHead(true);
354
+ const scan = requireVerifiedScan(head);
355
+ const { projection } = head;
199
356
  assertActiveMaintenanceTurn(projection, turnId);
200
357
  const state = readCompactState(this.compactStatePath, this.threadId);
201
358
  if (state)
@@ -230,8 +387,9 @@ class ThreadEventStore {
230
387
  ensurePrivateDirectory(this.compactCheckpointsDir);
231
388
  validateCompactCommitInput(input, this.threadId);
232
389
  input.onBoundary('before_cas_recheck');
233
- const scan = scanThreadEventLog(this.logPath, this.threadId, this.maxLogBytes);
234
- const projection = (0, thread_projection_1.projectThreadEvents)(this.threadId, scan.events);
390
+ const head = this.loadVerifiedHead(true);
391
+ const scan = requireVerifiedScan(head);
392
+ const { projection } = head;
235
393
  const eventsAfterAnchor = scan.events.slice(input.expectedEventAnchor.cursor);
236
394
  const onlyQueuedFollowUps = eventsAfterAnchor.length > 0 &&
237
395
  eventsAfterAnchor.every(event => event.payload.type === 'turn.queued');
@@ -296,7 +454,7 @@ class ThreadEventStore {
296
454
  }
297
455
  listCompactEvents() {
298
456
  return this.withLogLock(() => {
299
- const scan = scanThreadEventLog(this.logPath, this.threadId, this.maxLogBytes);
457
+ const scan = requireVerifiedScan(this.loadVerifiedHead(true));
300
458
  return deepFreeze(scan.events.filter((event) => event.payload.type === 'compact.started' ||
301
459
  event.payload.type === 'compact.completed' ||
302
460
  event.payload.type === 'compact.failed'));
@@ -347,6 +505,79 @@ class ThreadEventStore {
347
505
  }
348
506
  return inputs.length > 0 ? this.appendDurableBatch(inputs).projection : projection;
349
507
  }
508
+ /**
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).
513
+ */
514
+ loadVerifiedHead(requireScan = false) {
515
+ ensurePrivateDirectory(this.rootDir);
516
+ const logIdentity = readFileIdentity(this.logPath);
517
+ if (this.cachedHead?.logFingerprint === logIdentity.fingerprint &&
518
+ (!requireScan || this.cachedHead.scan)) {
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
+ }
530
+ const scan = scanThreadEventLog(this.logPath, this.threadId, this.maxLogBytes);
531
+ this.onLogScan?.({
532
+ threadId: this.threadId,
533
+ bytes: scan.safeByteLength + scan.discardedTailBytes,
534
+ events: scan.events.length,
535
+ });
536
+ const projection = (0, thread_projection_1.projectThreadEvents)(this.threadId, scan.events);
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
+ };
556
+ this.cachedHead = head;
557
+ if (projectionFingerprint)
558
+ this.tryPersistVerifiedHead(head);
559
+ return head;
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
+ }
350
581
  withLogLock(operation) {
351
582
  ensurePrivateDirectory((0, path_1.dirname)(this.logPath));
352
583
  return (0, file_lock_1.withFileLockSync)(this.logPath, operation, { waitMs: this.lockWaitMs });
@@ -497,6 +728,209 @@ function readProjection(path) {
497
728
  return null;
498
729
  }
499
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
+ }
904
+ function readFileIdentity(path) {
905
+ try {
906
+ const stats = (0, fs_1.statSync)(path, { bigint: true });
907
+ const device = stats.dev.toString();
908
+ const inode = stats.ino.toString();
909
+ const mtimeNs = stats.mtimeNs.toString();
910
+ const ctimeNs = stats.ctimeNs.toString();
911
+ return {
912
+ fingerprint: [device, inode, stats.size, mtimeNs, ctimeNs].join(':'),
913
+ bytes: Number(stats.size),
914
+ device,
915
+ inode,
916
+ mtimeNs,
917
+ ctimeNs,
918
+ };
919
+ }
920
+ catch {
921
+ return {
922
+ fingerprint: 'missing',
923
+ bytes: 0,
924
+ device: '0',
925
+ inode: '0',
926
+ mtimeNs: '0',
927
+ ctimeNs: '0',
928
+ };
929
+ }
930
+ }
931
+ function fileFingerprint(path) {
932
+ return readFileIdentity(path).fingerprint;
933
+ }
500
934
  function ensurePrivateDirectory(path) {
501
935
  (0, fs_1.mkdirSync)(path, { recursive: true, mode: 0o700 });
502
936
  }