@orion-agents/orion-code 0.3.0 → 0.3.2

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 (172) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +45 -18
  3. package/README.zh-CN.md +39 -18
  4. package/dist/commands/model-command-handlers.d.ts.map +1 -1
  5. package/dist/commands/model-command-handlers.js +59 -14
  6. package/dist/commands/model-command-handlers.js.map +1 -1
  7. package/dist/commands/types.d.ts +6 -0
  8. package/dist/commands/types.d.ts.map +1 -1
  9. package/dist/commands/types.js.map +1 -1
  10. package/dist/commands/workflow-command-handlers.d.ts +1 -1
  11. package/dist/commands/workflow-command-handlers.d.ts.map +1 -1
  12. package/dist/commands/workflow-command-handlers.js +42 -3
  13. package/dist/commands/workflow-command-handlers.js.map +1 -1
  14. package/dist/commands/workflow-commands.d.ts.map +1 -1
  15. package/dist/commands/workflow-commands.js +2 -2
  16. package/dist/commands/workflow-commands.js.map +1 -1
  17. package/dist/core/tool-artifacts.d.ts +2 -0
  18. package/dist/core/tool-artifacts.d.ts.map +1 -1
  19. package/dist/core/tool-artifacts.js +27 -1
  20. package/dist/core/tool-artifacts.js.map +1 -1
  21. package/dist/runtime/agent-runtime-controller.d.ts +16 -2
  22. package/dist/runtime/agent-runtime-controller.d.ts.map +1 -1
  23. package/dist/runtime/agent-runtime-controller.js +116 -8
  24. package/dist/runtime/agent-runtime-controller.js.map +1 -1
  25. package/dist/runtime/agent-runtime-protocol.d.ts +7 -0
  26. package/dist/runtime/agent-runtime-protocol.d.ts.map +1 -1
  27. package/dist/runtime/agent-runtime-protocol.js.map +1 -1
  28. package/dist/runtime/agent-runtime-runner.d.ts +8 -0
  29. package/dist/runtime/agent-runtime-runner.d.ts.map +1 -1
  30. package/dist/runtime/durable-tool-receipt-reader.d.ts +32 -0
  31. package/dist/runtime/durable-tool-receipt-reader.d.ts.map +1 -0
  32. package/dist/runtime/durable-tool-receipt-reader.js +130 -0
  33. package/dist/runtime/durable-tool-receipt-reader.js.map +1 -0
  34. package/dist/runtime/legacy-thread-materializer.d.ts +14 -1
  35. package/dist/runtime/legacy-thread-materializer.d.ts.map +1 -1
  36. package/dist/runtime/legacy-thread-materializer.js +44 -5
  37. package/dist/runtime/legacy-thread-materializer.js.map +1 -1
  38. package/dist/runtime/orion-session-runner.d.ts +4 -0
  39. package/dist/runtime/orion-session-runner.d.ts.map +1 -1
  40. package/dist/runtime/orion-session-runner.js +15 -0
  41. package/dist/runtime/orion-session-runner.js.map +1 -1
  42. package/dist/runtime/plan-review.d.ts +38 -0
  43. package/dist/runtime/plan-review.d.ts.map +1 -0
  44. package/dist/runtime/plan-review.js +173 -0
  45. package/dist/runtime/plan-review.js.map +1 -0
  46. package/dist/runtime/product-bootstrap.d.ts.map +1 -1
  47. package/dist/runtime/product-bootstrap.js +28 -6
  48. package/dist/runtime/product-bootstrap.js.map +1 -1
  49. package/dist/runtime/product-orion-runtime.d.ts.map +1 -1
  50. package/dist/runtime/product-orion-runtime.js +14 -35
  51. package/dist/runtime/product-orion-runtime.js.map +1 -1
  52. package/dist/runtime/protocol/runtime-protocol-v1.d.ts +73 -0
  53. package/dist/runtime/protocol/runtime-protocol-v1.d.ts.map +1 -1
  54. package/dist/runtime/protocol/runtime-protocol-v1.js +37 -0
  55. package/dist/runtime/protocol/runtime-protocol-v1.js.map +1 -1
  56. package/dist/runtime/release-receipts.d.ts +8 -3
  57. package/dist/runtime/release-receipts.d.ts.map +1 -1
  58. package/dist/runtime/release-receipts.js +88 -4
  59. package/dist/runtime/release-receipts.js.map +1 -1
  60. package/dist/runtime/session-composer-control.d.ts +136 -0
  61. package/dist/runtime/session-composer-control.d.ts.map +1 -0
  62. package/dist/runtime/session-composer-control.js +532 -0
  63. package/dist/runtime/session-composer-control.js.map +1 -0
  64. package/dist/runtime/thread-event-store.d.ts +62 -9
  65. package/dist/runtime/thread-event-store.d.ts.map +1 -1
  66. package/dist/runtime/thread-event-store.js +363 -31
  67. package/dist/runtime/thread-event-store.js.map +1 -1
  68. package/dist/runtime/thread-projection.d.ts +17 -0
  69. package/dist/runtime/thread-projection.d.ts.map +1 -1
  70. package/dist/runtime/thread-projection.js +87 -0
  71. package/dist/runtime/thread-projection.js.map +1 -1
  72. package/dist/runtime/thread-session-index.d.ts +84 -0
  73. package/dist/runtime/thread-session-index.d.ts.map +1 -0
  74. package/dist/runtime/thread-session-index.js +503 -0
  75. package/dist/runtime/thread-session-index.js.map +1 -0
  76. package/dist/runtime/thread-session-view.d.ts +31 -13
  77. package/dist/runtime/thread-session-view.d.ts.map +1 -1
  78. package/dist/runtime/thread-session-view.js +116 -61
  79. package/dist/runtime/thread-session-view.js.map +1 -1
  80. package/dist/runtime/thread-ui-adapter.d.ts.map +1 -1
  81. package/dist/runtime/thread-ui-adapter.js +18 -58
  82. package/dist/runtime/thread-ui-adapter.js.map +1 -1
  83. package/dist/runtime/tool-detail-repository.d.ts.map +1 -1
  84. package/dist/runtime/tool-detail-repository.js +23 -19
  85. package/dist/runtime/tool-detail-repository.js.map +1 -1
  86. package/dist/runtime/tool-receipt-validator.d.ts +21 -0
  87. package/dist/runtime/tool-receipt-validator.d.ts.map +1 -0
  88. package/dist/runtime/tool-receipt-validator.js +157 -0
  89. package/dist/runtime/tool-receipt-validator.js.map +1 -0
  90. package/dist/runtime/ui-events.d.ts +6 -1
  91. package/dist/runtime/ui-events.d.ts.map +1 -1
  92. package/dist/runtime/ui-events.js.map +1 -1
  93. package/dist/services/redaction.d.ts +4 -0
  94. package/dist/services/redaction.d.ts.map +1 -1
  95. package/dist/services/redaction.js +63 -0
  96. package/dist/services/redaction.js.map +1 -1
  97. package/dist/services/session-index.d.ts +16 -0
  98. package/dist/services/session-index.d.ts.map +1 -1
  99. package/dist/services/session-index.js +45 -36
  100. package/dist/services/session-index.js.map +1 -1
  101. package/dist/services/session-storage.d.ts +36 -0
  102. package/dist/services/session-storage.d.ts.map +1 -1
  103. package/dist/services/session-storage.js +80 -7
  104. package/dist/services/session-storage.js.map +1 -1
  105. package/dist/services/workspace-registry.d.ts +42 -0
  106. package/dist/services/workspace-registry.d.ts.map +1 -0
  107. package/dist/services/workspace-registry.js +222 -0
  108. package/dist/services/workspace-registry.js.map +1 -0
  109. package/dist/web/errors.d.ts +6 -0
  110. package/dist/web/errors.d.ts.map +1 -0
  111. package/dist/web/errors.js +13 -0
  112. package/dist/web/errors.js.map +1 -0
  113. package/dist/web/event-hub.d.ts +4 -0
  114. package/dist/web/event-hub.d.ts.map +1 -1
  115. package/dist/web/event-hub.js +81 -11
  116. package/dist/web/event-hub.js.map +1 -1
  117. package/dist/web/file-read-service.d.ts +64 -0
  118. package/dist/web/file-read-service.d.ts.map +1 -0
  119. package/dist/web/file-read-service.js +409 -0
  120. package/dist/web/file-read-service.js.map +1 -0
  121. package/dist/web/git-read-model-service.d.ts +85 -0
  122. package/dist/web/git-read-model-service.d.ts.map +1 -0
  123. package/dist/web/git-read-model-service.js +657 -0
  124. package/dist/web/git-read-model-service.js.map +1 -0
  125. package/dist/web/protocol.d.ts +205 -1
  126. package/dist/web/protocol.d.ts.map +1 -1
  127. package/dist/web/protocol.js +229 -1
  128. package/dist/web/protocol.js.map +1 -1
  129. package/dist/web/review-service.d.ts +38 -0
  130. package/dist/web/review-service.d.ts.map +1 -0
  131. package/dist/web/review-service.js +74 -0
  132. package/dist/web/review-service.js.map +1 -0
  133. package/dist/web/server.d.ts.map +1 -1
  134. package/dist/web/server.js +312 -29
  135. package/dist/web/server.js.map +1 -1
  136. package/dist/web/terminal-manager.d.ts +156 -0
  137. package/dist/web/terminal-manager.d.ts.map +1 -0
  138. package/dist/web/terminal-manager.js +760 -0
  139. package/dist/web/terminal-manager.js.map +1 -0
  140. package/dist/web/terminal-server.d.ts +8 -0
  141. package/dist/web/terminal-server.d.ts.map +1 -0
  142. package/dist/web/terminal-server.js +275 -0
  143. package/dist/web/terminal-server.js.map +1 -0
  144. package/dist/web/workbench-controller.d.ts +71 -18
  145. package/dist/web/workbench-controller.d.ts.map +1 -1
  146. package/dist/web/workbench-controller.js +1187 -178
  147. package/dist/web/workbench-controller.js.map +1 -1
  148. package/dist/web-client/assets/DiffViewer-eaQNsnCh.js +5 -0
  149. package/dist/web-client/assets/FilesPanel-DYs1MhoM.js +2 -0
  150. package/dist/web-client/assets/GitPanel-CnU4XVr7.js +1 -0
  151. package/dist/web-client/assets/ReviewPanel-BDxG670T.js +1 -0
  152. package/dist/web-client/assets/TerminalPanel-DLuoa74B.css +1 -0
  153. package/dist/web-client/assets/TerminalPanel-Q5mSSsoK.js +23 -0
  154. package/dist/web-client/assets/index-BWV5sIvw.css +1 -0
  155. package/dist/web-client/assets/index-VVUKj6FE.js +17 -0
  156. package/dist/web-client/index.html +2 -2
  157. package/docs/architecture/agent-mode-permission-contract.md +87 -0
  158. package/docs/architecture/v0.3.0-web-api.yaml +44 -13
  159. package/docs/architecture/v0.3.1-web-api.yaml +2338 -0
  160. package/docs/architecture/v0.3.2-web-api.yaml +3029 -0
  161. package/docs/migration/v0.3.0-to-v0.3.1.md +88 -0
  162. package/docs/migration/v0.3.1-to-v0.3.2.md +94 -0
  163. package/docs/plan/v0.3.0-web-workbench-plan.md +58 -7
  164. package/docs/plan/v0.3.1-web-workbench-professional-shell-plan.md +848 -0
  165. package/docs/plan/v0.3.2-web-workbench-layout-and-composer-plan.md +869 -0
  166. package/docs/readme.md +20 -0
  167. package/docs/test/v0.3.1-web-workbench-e2e-plan.md +140 -0
  168. package/docs/test/v0.3.2-web-workbench-e2e-plan.md +100 -0
  169. package/npm-shrinkwrap.json +185 -354
  170. package/package.json +21 -4
  171. package/dist/web-client/assets/index-BAEO71Gp.css +0 -1
  172. package/dist/web-client/assets/index-DSkFy7gM.js +0 -13
@@ -18,17 +18,53 @@ export interface ThreadEventCommitV1 {
18
18
  readonly events: readonly RuntimeEventEnvelopeV1[];
19
19
  readonly projection: ThreadProjectionV1;
20
20
  }
21
+ export interface ThreadLogIdentityV1 {
22
+ readonly bytes: number;
23
+ readonly device: string;
24
+ readonly inode: string;
25
+ readonly mtimeNs: string;
26
+ readonly ctimeNs: string;
27
+ }
21
28
  export interface ThreadReadModelHeadV1 {
22
29
  readonly projection: ThreadProjectionV1;
23
30
  readonly lastEventTimestamp: number;
24
31
  readonly lastRecordHash: string | null;
25
- readonly log: {
26
- readonly bytes: number;
27
- readonly device: string;
28
- readonly inode: string;
29
- readonly mtimeNs: string;
30
- readonly ctimeNs: string;
31
- };
32
+ readonly log: ThreadLogIdentityV1;
33
+ }
34
+ export interface ThreadCheckpointHeadV1 {
35
+ readonly cursor: number;
36
+ readonly projectionDigest: string;
37
+ readonly lastEventTimestamp: number;
38
+ readonly lastRecordHash: string | null;
39
+ readonly log: ThreadLogIdentityV1;
40
+ readonly verifiedPrefixes: readonly ThreadVerifiedPrefixV1[];
41
+ }
42
+ export interface ThreadVerifiedPrefixV1 {
43
+ readonly cursor: number;
44
+ readonly eventDigest: string;
45
+ readonly projectionDigest: string;
46
+ }
47
+ /**
48
+ * Crash-safe derived checkpoint for opening a Thread in a fresh process.
49
+ *
50
+ * The JSONL remains authoritative. This receipt is accepted only while its
51
+ * exact file identity and independently digested projection still match; any
52
+ * mismatch falls back to the full hash-chain verification path.
53
+ */
54
+ export interface ThreadHeadV1 {
55
+ readonly version: 1;
56
+ readonly generation: number;
57
+ readonly threadId: string;
58
+ readonly cursor: number;
59
+ readonly projectionDigest: string;
60
+ readonly lastEventTimestamp: number;
61
+ readonly lastRecordHash: string | null;
62
+ readonly safeByteLength: number;
63
+ readonly discardedTailBytes: number;
64
+ readonly eventIds: readonly string[];
65
+ readonly verifiedPrefixes: readonly ThreadVerifiedPrefixV1[];
66
+ readonly log: ThreadLogIdentityV1;
67
+ readonly digest: string;
32
68
  }
33
69
  export type ThreadEventStoreBoundaryV1 = 'before_log_write' | 'after_log_write' | 'after_log_flush' | 'before_projection_write' | 'after_projection_write';
34
70
  export interface ThreadEventStoreOptionsV1 {
@@ -64,6 +100,7 @@ export declare class ThreadEventStore {
64
100
  readonly threadId: string;
65
101
  readonly logPath: string;
66
102
  readonly projectionPath: string;
103
+ readonly headPath: string;
67
104
  readonly compactStatePath: string;
68
105
  readonly compactCheckpointsDir: string;
69
106
  private readonly maxLogBytes;
@@ -84,7 +121,20 @@ export declare class ThreadEventStore {
84
121
  type: 'item.delta';
85
122
  }>>(input: AppendRuntimeEventV1<T>): RuntimeEventEnvelopeV1<T>;
86
123
  replay(cursor?: number, limit?: number): ThreadEventReplayV1;
124
+ /**
125
+ * Verify and seal an immutable imported prefix. The first call may scan the
126
+ * authoritative log; later processes reuse the prefix receipt while the
127
+ * exact log identity/projection checkpoint remains current. Internal append
128
+ * carries the proof forward because the hash-chained prefix cannot change.
129
+ */
130
+ verifyDurablePrefix(cursor: number, eventDigest: string, projectionDigest: string): boolean;
87
131
  getCursor(): number;
132
+ /**
133
+ * Read the compact persisted receipt without parsing the potentially large
134
+ * projection. Callers must fall back to the authoritative scan whenever the
135
+ * receipt is missing, stale, or lacks their required cutover proof.
136
+ */
137
+ capturePersistedCheckpointHead(): ThreadCheckpointHeadV1 | undefined;
88
138
  /** Capture the catalog read model and its exact verified log identity atomically. */
89
139
  captureReadModelHead(): ThreadReadModelHeadV1;
90
140
  loadProjection(): ThreadProjectionV1;
@@ -113,10 +163,13 @@ export declare class ThreadEventStore {
113
163
  */
114
164
  recoverIncomplete(reason?: string): ThreadProjectionV1;
115
165
  /**
116
- * Return the last fully verified log head, rescanning only after the file
117
- * identity changes. Callers already hold the Thread log lock.
166
+ * Return the last verified log head. A fresh process may adopt the persisted
167
+ * receipt only when the exact JSONL identity and independently validated
168
+ * projection still match. Callers that need historical events explicitly
169
+ * request a scan; cursor/projection/append paths stay O(head + projection).
118
170
  */
119
171
  private loadVerifiedHead;
172
+ private tryPersistVerifiedHead;
120
173
  private withLogLock;
121
174
  private assertCompactCheckpoint;
122
175
  private boundary;
@@ -1 +1 @@
1
- {"version":3,"file":"thread-event-store.d.ts","sourceRoot":"","sources":["../../src/runtime/thread-event-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"thread-event-store.d.ts","sourceRoot":"","sources":["../../src/runtime/thread-event-store.ts"],"names":[],"mappings":"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"}
@@ -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
- const { scan } = head;
60
- if (scan.discardedTailBytes > 0)
61
- (0, fs_1.truncateSync)(this.logPath, scan.safeByteLength);
62
- let seq = scan.events.length;
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(scan.events.map(event => event.eventId));
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 nextScan = {
107
- records: [...scan.records, ...records],
108
- events: [...scan.events, ...committedEvents],
109
- safeByteLength: (0, fs_1.statSync)(this.logPath).size,
110
- discardedTailBytes: 0,
111
- };
112
- this.cachedHead = {
113
- logFingerprint: fileFingerprint(this.logPath),
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 { scan } = this.loadVerifiedHead();
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.scan.events.at(-1)?.timestamp ?? 0,
189
- lastRecordHash: head.scan.records.at(-1)?.hash ?? null,
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
- this.cachedHead = { ...head, projectionFingerprint };
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
- this.cachedHead = {
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 { scan } = this.loadVerifiedHead();
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 { scan, projection } = this.loadVerifiedHead();
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 { scan, projection } = this.loadVerifiedHead();
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 { scan } = this.loadVerifiedHead();
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 fully verified log head, rescanning only after the file
404
- * identity changes. Callers already hold the Thread log lock.
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 logFingerprint = fileFingerprint(this.logPath);
409
- if (this.cachedHead?.logFingerprint === 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 head = { logFingerprint, scan, projection };
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 });