sintbot 1.0.16 → 1.3.0

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 (137) hide show
  1. package/CHANGELOG.en.json +99 -0
  2. package/CHANGELOG.json +99 -0
  3. package/README.md +20 -158
  4. package/README.zh-CN.md +20 -166
  5. package/THIRD-PARTY-NOTICES.md +394 -0
  6. package/bin/sintbot-v2.js +363 -0
  7. package/docs/AI-ASSISTED-INSTALL.md +11 -35
  8. package/docs/install.md +61 -167
  9. package/docs/zh-CN/AI-ASSISTED-INSTALL.md +11 -35
  10. package/docs/zh-CN/install.md +61 -165
  11. package/install.sh +84 -0
  12. package/package.json +73 -37
  13. package/src/agent/claudecode.js +149 -108
  14. package/src/agent/codexapp.js +458 -0
  15. package/src/agent/codexcode.js +210 -107
  16. package/src/agent/errtag.js +34 -0
  17. package/src/agent/privatecore.js +790 -0
  18. package/src/agent/privatecore.wasm +0 -0
  19. package/src/agent/runner.js +10 -8
  20. package/src/agent/winexec.js +35 -5
  21. package/src/c2install.js +117 -0
  22. package/src/config.js +2 -1
  23. package/src/core/c2cardeffectbridge.js +51 -0
  24. package/src/core/c2cardterminalpath.js +216 -0
  25. package/src/core/c2durableingress.js +148 -0
  26. package/src/core/c2durablejournal.js +1560 -0
  27. package/src/core/c2durableruntime.js +2361 -0
  28. package/src/core/c2eventidentity.js +90 -0
  29. package/src/core/c2globalfault.js +401 -0
  30. package/src/core/c2localeffectexecutor.js +193 -0
  31. package/src/core/c2payloadhandles.js +333 -0
  32. package/src/core/c2persistrelation.js +179 -0
  33. package/src/core/c2persiststate.js +342 -0
  34. package/src/core/c2platformrecoverysender.js +76 -0
  35. package/src/core/c2recoverygate.js +385 -0
  36. package/src/core/c2runnerinput.js +63 -0
  37. package/src/core/c2servicestartgate.js +103 -0
  38. package/src/core/c2sessionbridge.js +72 -0
  39. package/src/core/c2sourcequarantine.js +30 -0
  40. package/src/core/c2startuprecovery.js +105 -0
  41. package/src/core/c2successorbridge.js +151 -0
  42. package/src/core/c2terminaleffectbridge.js +154 -0
  43. package/src/core/c2terminalprune.js +108 -0
  44. package/src/core/c2turnprojection.js +177 -0
  45. package/src/core/enginecheckpoint.js +211 -0
  46. package/src/core/engineeffectcursor.js +394 -0
  47. package/src/core/enginestepcoordinator.js +183 -0
  48. package/src/core/store.js +52 -8
  49. package/src/core/v2engine.js +1469 -0
  50. package/src/platform/feishu.js +324 -67
  51. package/src/plugins.js +1 -9
  52. package/assets/design-styles/acid-pop.md +0 -155
  53. package/assets/design-styles/archive-index.md +0 -161
  54. package/assets/design-styles/art-deco.md +0 -164
  55. package/assets/design-styles/cinematic-noir.md +0 -178
  56. package/assets/design-styles/editorial.md +0 -121
  57. package/assets/design-styles/extra/README.md +0 -23
  58. package/assets/design-styles/extra/constructivism.md +0 -146
  59. package/assets/design-styles/extra/neo-brutalism.md +0 -175
  60. package/assets/design-styles/extra/new-wave.md +0 -166
  61. package/assets/design-styles/extra/nordic-material.md +0 -162
  62. package/assets/design-styles/extra/paris-couture.md +0 -162
  63. package/assets/design-styles/extra/tropical-modern.md +0 -168
  64. package/assets/design-styles/extra/ulm-functional.md +0 -182
  65. package/assets/design-styles/extra/wa-shikimen.md +0 -160
  66. package/assets/design-styles/extra/y2k-chrome.md +0 -172
  67. package/assets/design-styles/geometric-pop.md +0 -122
  68. package/assets/design-styles/hand-drawn.md +0 -117
  69. package/assets/design-styles/islamic-geometry.md +0 -155
  70. package/assets/design-styles/liquid-glass.md +0 -183
  71. package/assets/design-styles/neo-chinese.md +0 -136
  72. package/assets/design-styles/precision-mono.md +0 -156
  73. package/assets/design-styles/quiet-luxury.md +0 -163
  74. package/assets/design-styles/riso-print.md +0 -116
  75. package/assets/design-styles/soft-friendly.md +0 -174
  76. package/assets/design-styles/swiss-grid.md +0 -156
  77. package/assets/design-styles/tech-dark.md +0 -182
  78. package/assets/favicon.svg +0 -6
  79. package/assets/logo.svg +0 -24
  80. package/bin/sbot.js +0 -4716
  81. package/docs/slack-app-manifest.yaml +0 -104
  82. package/sbot.config.example.json +0 -235
  83. package/src/agent/askonce.js +0 -76
  84. package/src/agent/claudemodels.js +0 -60
  85. package/src/agent/claudestatus.js +0 -74
  86. package/src/agent/claudeusage.js +0 -200
  87. package/src/agent/codexstatus.js +0 -181
  88. package/src/agent/imagen.js +0 -458
  89. package/src/agent/quotaerr.js +0 -44
  90. package/src/agent/transienterr.js +0 -32
  91. package/src/cdp.js +0 -254
  92. package/src/cli-secret.js +0 -36
  93. package/src/clilang.js +0 -31
  94. package/src/core/cron.js +0 -182
  95. package/src/core/cronfire.js +0 -120
  96. package/src/core/cronrun.js +0 -116
  97. package/src/core/engine.js +0 -2758
  98. package/src/core/interfaces.js +0 -114
  99. package/src/core/issues.js +0 -185
  100. package/src/core/ledger.js +0 -30
  101. package/src/core/opsalert.js +0 -214
  102. package/src/core/pending.js +0 -123
  103. package/src/core/proxy.js +0 -424
  104. package/src/core/quotaalert.js +0 -79
  105. package/src/core/quotaerr.js +0 -55
  106. package/src/core/scheduler.js +0 -59
  107. package/src/design.js +0 -5161
  108. package/src/gitrepo.js +0 -150
  109. package/src/health.js +0 -353
  110. package/src/installdeps.js +0 -72
  111. package/src/leftover.js +0 -174
  112. package/src/platform/boardnotice.js +0 -77
  113. package/src/platform/boardops.js +0 -36
  114. package/src/platform/boardwatch.js +0 -447
  115. package/src/platform/cli.js +0 -45
  116. package/src/platform/slack.js +0 -1289
  117. package/src/platform/takeovercard.js +0 -37
  118. package/src/platform/wecom.js +0 -938
  119. package/src/platform/wecomcli.js +0 -128
  120. package/src/platform/wecomgate.js +0 -108
  121. package/src/platform/wecomname.js +0 -43
  122. package/src/platform/wecompoll.js +0 -241
  123. package/src/preview.js +0 -581
  124. package/src/projectmemory.js +0 -175
  125. package/src/service.js +0 -317
  126. package/src/ui/auth.js +0 -31
  127. package/src/ui/browser.html +0 -392
  128. package/src/ui/browserroutes.js +0 -234
  129. package/src/ui/console.html +0 -5526
  130. package/src/ui/dashboard.js +0 -168
  131. package/src/ui/feishulogin.js +0 -136
  132. package/src/ui/server.js +0 -2314
  133. package/src/ui/sessions.js +0 -78
  134. package/src/ui/takeover.js +0 -108
  135. package/src/updater.js +0 -243
  136. package/src/upgrade.js +0 -35
  137. package/src/usage.js +0 -144
@@ -0,0 +1,1560 @@
1
+ 'use strict'
2
+
3
+ // Development-only C2 authority. The daemon lock must be held by the caller.
4
+ // No Engine path uses this journal until state and receipt projections are wired.
5
+ const fs = require('node:fs')
6
+ const path = require('node:path')
7
+ const { createHash, randomUUID } = require('node:crypto')
8
+ const { isDeepStrictEqual } = require('node:util')
9
+ const { callbackRegistrationId } = require('./c2eventidentity')
10
+ const { validatePersistentReceipts } = require('./c2persistrelation')
11
+
12
+ const MAGIC = Buffer.from('SBOT-C2-JOURNAL-v1\n')
13
+ const FRAME_HEADER = 36 // uint32be payload length, then SHA-256 of payload
14
+ const MAX_RECORD_BYTES = 8 * 1024 * 1024
15
+ const MAX_CHECKPOINT_BYTES = 64 * 1024 * 1024
16
+ const MAX_CHECKPOINT_ITEMS = 1_000_000
17
+ const REGISTRATION_ID = /^sbot-c2-registration-v1:[0-9a-f]{64}$/
18
+ const RECEIPT_ID = /^[0-9a-f]{32}$/
19
+ const GENERATION = /^[0-9a-f]{32}$/
20
+ const RUNNER_CLOSED_ID = /^sbot-c2-runner-closed-v1:[0-9a-f]{32}:[0-9a-f]{32}$/
21
+ const RUNNER_NOT_STARTED_ID = /^sbot-c2-runner-not-started-v1:[0-9a-f]{32}:[0-9a-f]{32}$/
22
+ const DESCRIPTOR_TYPES = Object.fromEntries(Object.entries({
23
+ queue: 'dispatch dropQueued interruptMissingPayload recalledQueued queueNotice',
24
+ lifecycle: 'turnStarted noActiveTask clearQueue abortController forceRelease continueBatch recallAccepted recordDone armHardTimer clearHardTimer releaseLock dequeue clearSession',
25
+ recovery: 'enqueueFront freshSession sayFreshRetry sayEmpty markExhausted alertDown notifyNoTarget probeHome applyFailover notifySwitched',
26
+ failover: 'clearExhausted markExhausted alertUp alertDown setPending clearPending applyFailover clearExhaustedHome notifyBack notifySwitched probeHome setProbing touchCheckedAt callProbe setPendingBack touchUntil logProbeError',
27
+ intake: 'notifyOrphan sendUnboundGuide clearQueue handleCommand persistReceipt dispatch',
28
+ slots: 'grant reject',
29
+ session: 'writeSession markTainted resetSessionAndStats',
30
+ steer: 'followupKind eligible acceptControl drainQueued callSteer enqueue pullForSteer relocateCard replyQueued propagateSteerError replySteered applyLanded markLandedRecalled forgetUnconfirmed enqueueFront clearControl',
31
+ terminal: 'markTainted clearTainted clearExhausted verifyTurn finalReply quietCompletion',
32
+ outbound: 'card.create message.reply message.create cardElement.content card.update card.settings message.delete',
33
+ turn: 'turnToken effectAllowed',
34
+ handoff: 'attemptQueueRelease retryQueueRelease sealGroup notifyAccepted holdHandoff scheduleDispatch clearHandoff dispatch',
35
+ atomicHandoff: 'dispatch attemptQueueRelease clearHandoff dropQueued holdHandoff scheduleDispatch',
36
+ control: 'stopAck',
37
+ }).map(([domain, types]) => [domain, new Set(types.split(' '))]))
38
+ const DESCRIPTOR_FIELDS = ['effectId', 'domain', 'scope', 'type', 'turnToken',
39
+ 'receiptId', 'targetId']
40
+ const REGISTRATION_FIELDS = new Set(['id', 'receiptId', 'generation', 'domain', 'kind',
41
+ 'ordinal', 'registeredAt'])
42
+
43
+ function id(value, name, max = 256) {
44
+ if (typeof value !== 'string' || !value || Buffer.byteLength(value) > max) {
45
+ throw new TypeError(`invalid C2 ${name}`)
46
+ }
47
+ return value
48
+ }
49
+
50
+ function effectDescriptor(value) {
51
+ if (!value || typeof value !== 'object' || Array.isArray(value) ||
52
+ Object.getPrototypeOf(value) !== Object.prototype ||
53
+ Reflect.ownKeys(value).length !== DESCRIPTOR_FIELDS.length ||
54
+ Reflect.ownKeys(value).some(key => typeof key !== 'string' ||
55
+ !DESCRIPTOR_FIELDS.includes(key) ||
56
+ !Object.getOwnPropertyDescriptor(value, key)?.enumerable ||
57
+ !Object.hasOwn(Object.getOwnPropertyDescriptor(value, key), 'value'))) {
58
+ throw new TypeError('invalid C2 effect descriptor fields')
59
+ }
60
+ const copy = exactJson(value)
61
+ id(copy.effectId, 'effect ID', 512)
62
+ if (!Object.hasOwn(DESCRIPTOR_TYPES, copy.domain) ||
63
+ typeof copy.scope !== 'string' || !/^[A-Za-z0-9:_-]{1,256}$/.test(copy.scope) ||
64
+ !DESCRIPTOR_TYPES[copy.domain].has(copy.type) ||
65
+ !(copy.turnToken === null || Number.isSafeInteger(copy.turnToken) && copy.turnToken > 0) ||
66
+ !(copy.receiptId === null || typeof copy.receiptId === 'string' && RECEIPT_ID.test(copy.receiptId)) ||
67
+ !(copy.targetId === null || typeof copy.targetId === 'string' &&
68
+ (['queue', 'handoff', 'atomicHandoff'].includes(copy.domain) &&
69
+ copy.type === 'dispatch' && copy.receiptId !== null &&
70
+ (copy.targetId === copy.receiptId || copy.targetId === `receipt:job:${copy.receiptId}`) ||
71
+ copy.domain === 'control' && copy.type === 'stopAck' &&
72
+ copy.receiptId !== null && copy.targetId === `receipt:control:${copy.receiptId}`)) ||
73
+ (copy.domain === 'control' &&
74
+ (copy.type !== 'stopAck' || copy.turnToken !== null ||
75
+ copy.receiptId === null || copy.targetId !== `receipt:control:${copy.receiptId}`)) ||
76
+ (['terminal', 'outbound'].includes(copy.domain) &&
77
+ (copy.domain !== 'terminal' || ['finalReply', 'quietCompletion'].includes(copy.type)) &&
78
+ (copy.turnToken === null || copy.receiptId === null || copy.targetId !== null))) {
79
+ throw new TypeError('invalid C2 effect descriptor identity')
80
+ }
81
+ return copy
82
+ }
83
+
84
+ function exactJson(value) {
85
+ const bytes = JSON.stringify(value)
86
+ if (typeof bytes !== 'string') throw new TypeError('C2 journal value is not JSON')
87
+ const copy = JSON.parse(bytes)
88
+ if (!isDeepStrictEqual(value, copy)) throw new TypeError('C2 journal value is not exact JSON')
89
+ function check(item) {
90
+ if (item && typeof item === 'object') {
91
+ if (Object.hasOwn(item, '__proto__')) throw new TypeError('unsafe C2 journal key')
92
+ for (const child of Object.values(item)) check(child)
93
+ }
94
+ }
95
+ check(copy)
96
+ return copy
97
+ }
98
+
99
+ function frame(record, maxBytes = MAX_RECORD_BYTES) {
100
+ const payload = Buffer.from(JSON.stringify(record))
101
+ if (!payload.length || payload.length > maxBytes) throw new RangeError('C2 journal record too large')
102
+ const result = Buffer.allocUnsafe(FRAME_HEADER + payload.length)
103
+ result.writeUInt32BE(payload.length, 0)
104
+ createHash('sha256').update(payload).digest().copy(result, 4)
105
+ payload.copy(result, FRAME_HEADER)
106
+ return result
107
+ }
108
+
109
+ function identity(stat) { return `${stat.dev}:${stat.ino}` }
110
+
111
+ function writeAll(fd, bytes, position) {
112
+ let written = 0
113
+ while (written < bytes.length) {
114
+ const size = fs.writeSync(fd, bytes, written, bytes.length - written, position + written)
115
+ if (size <= 0) throw new Error('short C2 journal write')
116
+ written += size
117
+ }
118
+ }
119
+
120
+ function readAll(fd, bytes, position) {
121
+ let read = 0
122
+ while (read < bytes.length) {
123
+ const size = fs.readSync(fd, bytes, read, bytes.length - read, position + read)
124
+ if (size <= 0) throw new Error('short C2 journal read')
125
+ read += size
126
+ }
127
+ }
128
+
129
+ function validateSnapshots(record, validateState, validateReceipts, validateRelation) {
130
+ const state = exactJson(record.state)
131
+ const receipts = exactJson(record.receipts)
132
+ if (!state || typeof state !== 'object' || Array.isArray(state) ||
133
+ !receipts || typeof receipts !== 'object' || Array.isArray(receipts)) {
134
+ throw new TypeError('C2 journal snapshots must be objects')
135
+ }
136
+ if (validateState(state) !== true || validateReceipts(receipts) !== true ||
137
+ validateRelation(state, receipts) !== true) {
138
+ throw new TypeError('C2 journal projection rejected')
139
+ }
140
+ return { state, receipts }
141
+ }
142
+
143
+ function validateReceiptProgress(before, after) {
144
+ const oldPending = new Map(before.pending.map(item => [item.id, item]))
145
+ const oldCompleted = new Map(before.completed.map(item => [item.id, item]))
146
+ const newPending = new Map(after.pending.map(item => [item.id, item]))
147
+ const newCompleted = new Map(after.completed.map(item => [item.id, item]))
148
+ for (const [receiptId, item] of oldCompleted) {
149
+ if (!isDeepStrictEqual(newCompleted.get(receiptId), item)) {
150
+ throw new Error('C2 completed receipt cannot disappear or change')
151
+ }
152
+ }
153
+ for (const [receiptId, item] of oldPending) {
154
+ const current = newPending.get(receiptId)
155
+ if (!current) {
156
+ if (!newCompleted.has(receiptId)) throw new Error('C2 pending receipt cannot disappear')
157
+ continue
158
+ }
159
+ const { notifiedAt: oldNotice, ...oldStable } = item
160
+ const { notifiedAt: newNotice, ...newStable } = current
161
+ if (!isDeepStrictEqual(oldStable, newStable) ||
162
+ (oldNotice !== undefined && oldNotice !== newNotice)) {
163
+ throw new Error('C2 pending receipt identity cannot change')
164
+ }
165
+ }
166
+ for (const receiptId of newPending.keys()) {
167
+ if (oldCompleted.has(receiptId)) throw new Error('C2 completed receipt cannot be reopened')
168
+ }
169
+ for (const receiptId of newCompleted.keys()) {
170
+ if (!oldPending.has(receiptId) && !oldCompleted.has(receiptId)) {
171
+ throw new Error('C2 receipt cannot complete without prior acceptance')
172
+ }
173
+ }
174
+ }
175
+
176
+ function validateRegistration(value, receipts, registrations) {
177
+ if (!value || typeof value !== 'object' || Array.isArray(value) ||
178
+ Object.getPrototypeOf(value) !== Object.prototype ||
179
+ Reflect.ownKeys(value).length !== REGISTRATION_FIELDS.size ||
180
+ Reflect.ownKeys(value).some(key => typeof key !== 'string' || !REGISTRATION_FIELDS.has(key) ||
181
+ !Object.getOwnPropertyDescriptor(value, key)?.enumerable ||
182
+ !Object.hasOwn(Object.getOwnPropertyDescriptor(value, key), 'value'))) {
183
+ throw new TypeError('invalid C2 callback registration')
184
+ }
185
+ const registration = exactJson(value)
186
+ if (registration.id !== callbackRegistrationId(registration) ||
187
+ !Number.isSafeInteger(registration.registeredAt) || registration.registeredAt <= 0 ||
188
+ registrations.has(registration.id)) {
189
+ throw new Error('invalid or duplicate C2 callback registration')
190
+ }
191
+ const receipt = receipts.pending.find(item => item.id === registration.receiptId)
192
+ if (!receipt) throw new Error('C2 callback registration requires matching pending receipt')
193
+ validatePersistentReceipts({ pending: [receipt], completed: [] })
194
+ if (/^local:/i.test(receipt.sourceId) || Buffer.byteLength(receipt.sourceId) > 256 ||
195
+ Buffer.byteLength(receipt.chatId) > 256) {
196
+ throw new Error('C2 callback registration requires durable source receipt')
197
+ }
198
+ return registration
199
+ }
200
+
201
+ function noticeFields(value, names) {
202
+ if (!value || typeof value !== 'object' || Array.isArray(value) ||
203
+ Object.getPrototypeOf(value) !== Object.prototype ||
204
+ Reflect.ownKeys(value).length !== names.length ||
205
+ Reflect.ownKeys(value).some(key => typeof key !== 'string' || !names.includes(key) ||
206
+ !Object.getOwnPropertyDescriptor(value, key)?.enumerable ||
207
+ !Object.hasOwn(Object.getOwnPropertyDescriptor(value, key), 'value'))) {
208
+ throw new TypeError('invalid C2 notice fields')
209
+ }
210
+ return value
211
+ }
212
+
213
+ function noticeIdentity(receiptId, chatId) {
214
+ if (typeof receiptId !== 'string' || !RECEIPT_ID.test(receiptId) ||
215
+ typeof chatId !== 'string' || !chatId || Buffer.byteLength(chatId) > 256) {
216
+ throw new TypeError('invalid C2 notice target')
217
+ }
218
+ }
219
+
220
+ function noticeTime(value) {
221
+ if (!Number.isSafeInteger(value) || value <= 0) throw new TypeError('invalid C2 notice time')
222
+ }
223
+
224
+ function runnerClosedId(receiptId, generation) {
225
+ if (!RECEIPT_ID.test(receiptId || '') || !GENERATION.test(generation || '')) {
226
+ throw new TypeError('invalid C2 runner closure identity')
227
+ }
228
+ return `sbot-c2-runner-closed-v1:${receiptId}:${generation}`
229
+ }
230
+
231
+ function runnerNotStartedId(receiptId, generation) {
232
+ if (!RECEIPT_ID.test(receiptId || '') || !GENERATION.test(generation || '')) {
233
+ throw new TypeError('invalid C2 runner not-started identity')
234
+ }
235
+ return `sbot-c2-runner-not-started-v1:${receiptId}:${generation}`
236
+ }
237
+
238
+ function completedReceipt(receipts, receiptId, settledAt) {
239
+ return { pending: receipts.pending.filter(item => item.id !== receiptId),
240
+ completed: [...receipts.completed, { id: receiptId, settledAt }] }
241
+ }
242
+
243
+ function completedBatch(receipts, receiptIds, settledAt) {
244
+ const ids = new Set(receiptIds)
245
+ return { pending: receipts.pending.filter(item => !ids.has(item.id)),
246
+ completed: [...receipts.completed,
247
+ ...receiptIds.map(id => ({ id, settledAt }))] }
248
+ }
249
+
250
+ function terminalEffectForReceipt(descriptors, receiptId) {
251
+ return [...descriptors.values()].some(item =>
252
+ (item.domain === 'terminal' && ['finalReply', 'quietCompletion'].includes(item.type) ||
253
+ item.domain === 'control' && item.type === 'stopAck') &&
254
+ item.receiptId === receiptId)
255
+ }
256
+
257
+ function terminalBatchOwnsReceipt(state, receiptId) {
258
+ return Object.values(state.lanes.terminal || {}).some(lane =>
259
+ lane.finalized === true && lane.batchReceiptIds?.includes(receiptId))
260
+ }
261
+
262
+ function validateStopAckAuthority(state, receipts, effects, descriptors, attempts) {
263
+ for (const descriptor of descriptors.values()) {
264
+ if (descriptor.domain !== 'control' || descriptor.type !== 'stopAck') continue
265
+ const status = effects.get(descriptor.effectId)
266
+ const pending = receipts.pending.find(item => item.id === descriptor.receiptId)
267
+ const completed = receipts.completed.some(item => item.id === descriptor.receiptId)
268
+ if (descriptor.targetId !== `receipt:control:${descriptor.receiptId}` ||
269
+ descriptor.turnToken !== null ||
270
+ (status === 'acked' ? !completed || pending || !attempts.has(descriptor.effectId) :
271
+ !pending || completed || pending.chatId !== descriptor.scope ||
272
+ pending.source !== 'message' || pending.quiet ||
273
+ state.lanes.stopIntent?.[descriptor.scope] !== undefined)) {
274
+ throw new Error('C2 stop acknowledgement lacks exact durable receipt authority')
275
+ }
276
+ }
277
+ }
278
+
279
+ function stopAckAttemptAllowed(state, receipts, descriptor) {
280
+ if (descriptor?.domain !== 'control' || descriptor.type !== 'stopAck') return true
281
+ const receipt = receipts.pending.find(item => item.id === descriptor.receiptId)
282
+ return !!receipt && receipt.chatId === descriptor.scope &&
283
+ receipt.source === 'message' && receipt.quiet === false &&
284
+ descriptor.targetId === `receipt:control:${receipt.id}` &&
285
+ state.lanes.stopIntent?.[descriptor.scope] === undefined &&
286
+ state.lanes.turn?.[descriptor.scope]?.active == null &&
287
+ state.lanes.handoff?.[descriptor.scope]?.phase === 'idle' &&
288
+ (state.lanes.queue?.[descriptor.scope]?.queue?.length ?? 0) === 0
289
+ }
290
+
291
+ function stopAckSettlementCandidate(record, authority, requireLocal) {
292
+ const { state, receipts, effects, attempts, descriptors, notices, local,
293
+ validateState, validateReceipts, validateRelation } = authority
294
+ if (Object.keys(record).sort().join(',') !==
295
+ 'chatId,cursor,effectId,kind,receiptId,settledAt' ||
296
+ record.kind !== 'stopAckSettlement' || record.cursor !== state.seq) {
297
+ throw new Error('invalid C2 stop acknowledgement settlement')
298
+ }
299
+ id(record.effectId, 'effect ID', 512)
300
+ noticeIdentity(record.receiptId, record.chatId)
301
+ noticeTime(record.settledAt)
302
+ const descriptor = descriptors.get(record.effectId)
303
+ const receipt = receipts.pending.find(item => item.id === record.receiptId)
304
+ if (!descriptor || descriptor.domain !== 'control' || descriptor.type !== 'stopAck' ||
305
+ descriptor.scope !== record.chatId || descriptor.receiptId !== record.receiptId ||
306
+ descriptor.targetId !== `receipt:control:${record.receiptId}` ||
307
+ descriptor.turnToken !== null || !receipt || receipt.chatId !== record.chatId ||
308
+ receipt.source !== 'message' || receipt.quiet || notices.has(record.receiptId) ||
309
+ effects.get(record.effectId) !== 'attempting' ||
310
+ !attempts.has(record.effectId) || record.settledAt < attempts.get(record.effectId) ||
311
+ requireLocal && !local.has(record.effectId) ||
312
+ !stopAckAttemptAllowed(state, receipts, descriptor)) {
313
+ throw new Error('C2 stop acknowledgement lacks confirmed local attempt')
314
+ }
315
+ const candidate = completedReceipt(receipts, record.receiptId, record.settledAt)
316
+ validateSnapshots({ state, receipts: candidate }, validateState,
317
+ validateReceipts, validateRelation)
318
+ validateReceiptProgress(receipts, candidate)
319
+ return candidate
320
+ }
321
+
322
+ function terminalAttemptAllowed(state, receipts, effects, descriptor) {
323
+ if (descriptor?.domain !== 'terminal' || descriptor.type !== 'finalReply') return true
324
+ const receipt = receipts.pending.find(item => item.id === descriptor.receiptId)
325
+ const handoff = state.lanes.handoff?.[descriptor.scope]
326
+ const lane = state.lanes.terminal?.[descriptor.scope]
327
+ return !!receipt && receipt.chatId === descriptor.scope && !receipt.quiet &&
328
+ lane?.finalized === true && lane.outcome?.silent === false &&
329
+ lane.pendingEffectIds?.at(-1) === descriptor.effectId &&
330
+ lane.pendingEffectIds.slice(0, -1).every(id => effects.get(id) === 'acked') &&
331
+ state.lanes.turn?.[descriptor.scope]?.active === descriptor.turnToken &&
332
+ handoff?.phase === 'running' && handoff.turnToken === descriptor.turnToken
333
+ }
334
+
335
+ function queueNoticeAttemptAllowed(state, receipts, descriptor) {
336
+ if (descriptor?.domain !== 'queue' || descriptor.type !== 'queueNotice') return true
337
+ const receipt = receipts.pending.find(item => item.id === descriptor.receiptId)
338
+ const queue = state.lanes.queue?.[descriptor.scope]?.queue
339
+ const turn = state.lanes.turn?.[descriptor.scope]
340
+ const handoff = state.lanes.handoff?.[descriptor.scope]
341
+ return !!receipt && receipt.chatId === descriptor.scope &&
342
+ receipt.source === 'message' && receipt.quiet === false &&
343
+ descriptor.targetId === null && descriptor.turnToken === null &&
344
+ Array.isArray(queue) && queue.some(item =>
345
+ item.id === `receipt:job:${receipt.id}` && item.quiet === false &&
346
+ item.polled === false && item.batch === undefined) &&
347
+ Number.isSafeInteger(turn?.active) && turn.active > 0 &&
348
+ handoff?.phase === 'running' && handoff.turnToken === turn.active
349
+ }
350
+
351
+ function runnerAcceptanceAuthority(effectId, registrationId, authority,
352
+ { effectStatus = 'acked', requireRunning = true } = {}) {
353
+ const { state, receipts, effects, attempts, descriptors, registrations } = authority
354
+ id(effectId, 'effect ID', 512)
355
+ if (typeof registrationId !== 'string' || !REGISTRATION_ID.test(registrationId)) {
356
+ throw new TypeError('invalid C2 runner acceptance registration')
357
+ }
358
+ const descriptor = descriptors.get(effectId)
359
+ const registration = registrations.get(registrationId)
360
+ const receipt = [...receipts.pending, ...receipts.completed]
361
+ .find(item => item.id === descriptor?.receiptId)
362
+ if (effects.get(effectId) !== effectStatus || !attempts.has(effectId) ||
363
+ descriptor?.domain !== 'atomicHandoff' || descriptor.type !== 'dispatch' ||
364
+ !RECEIPT_ID.test(descriptor.receiptId || '') ||
365
+ !Number.isSafeInteger(descriptor.turnToken) || descriptor.turnToken <= 0 ||
366
+ !receipt || (receipt.chatId !== undefined && receipt.chatId !== descriptor.scope) ||
367
+ registration?.receiptId !== descriptor.receiptId ||
368
+ registration.domain !== 'atomicHandoff' || registration.kind !== 'runnerAccepted' ||
369
+ registration.ordinal !== 0 ||
370
+ registration.registeredAt > attempts.get(effectId) ||
371
+ (requireRunning && (state.lanes.turn?.[descriptor.scope]?.active !== descriptor.turnToken ||
372
+ state.lanes.handoff?.[descriptor.scope]?.phase !== 'running' ||
373
+ state.lanes.handoff[descriptor.scope].turnToken !== descriptor.turnToken ||
374
+ !receipts.pending.some(item => item.id === descriptor.receiptId &&
375
+ item.chatId === descriptor.scope)))) {
376
+ throw new Error('C2 runner acceptance lacks exact durable dispatch')
377
+ }
378
+ return { descriptor, registration }
379
+ }
380
+
381
+ function runnerClosureCandidate(record, authority, requireRunning = true) {
382
+ if (Object.keys(record).sort().join(',') !==
383
+ 'chatId,closedAt,cursor,dispatchEffectId,evidenceId,generation,kind,receiptId,turnToken' ||
384
+ record.kind !== 'runnerClosed' || record.cursor !== authority.state.seq ||
385
+ record.evidenceId !== runnerClosedId(record.receiptId, record.generation) ||
386
+ typeof record.chatId !== 'string' || !record.chatId ||
387
+ !Number.isSafeInteger(record.turnToken) || record.turnToken <= 0 ||
388
+ authority.closures.has(record.evidenceId)) {
389
+ throw new Error('invalid C2 runner closure record')
390
+ }
391
+ noticeTime(record.closedAt)
392
+ const registrationId = authority.acceptances.get(record.dispatchEffectId)
393
+ const { descriptor, registration } = runnerAcceptanceAuthority(
394
+ record.dispatchEffectId, registrationId, authority, { requireRunning })
395
+ if (descriptor.receiptId !== record.receiptId || descriptor.scope !== record.chatId ||
396
+ descriptor.turnToken !== record.turnToken ||
397
+ registration.generation !== record.generation ||
398
+ record.closedAt < authority.attempts.get(record.dispatchEffectId)) {
399
+ throw new Error('C2 runner closure differs from accepted dispatch')
400
+ }
401
+ return { receiptId: record.receiptId, chatId: record.chatId,
402
+ turnToken: record.turnToken, generation: record.generation,
403
+ dispatchEffectId: record.dispatchEffectId, closedAt: record.closedAt }
404
+ }
405
+
406
+ function runnerNotStartedCandidate(record, authority, requireRunning = true) {
407
+ if (Object.keys(record).sort().join(',') !==
408
+ 'chatId,cursor,dispatchEffectId,evidenceId,generation,kind,receiptId,resolvedAt,turnToken' ||
409
+ record.kind !== 'runnerNotStarted' || record.cursor !== authority.state.seq ||
410
+ record.evidenceId !== runnerNotStartedId(record.receiptId, record.generation) ||
411
+ typeof record.chatId !== 'string' || !record.chatId ||
412
+ !Number.isSafeInteger(record.turnToken) || record.turnToken <= 0 ||
413
+ authority.notStarts.has(record.evidenceId)) {
414
+ throw new Error('invalid C2 runner not-started record')
415
+ }
416
+ noticeTime(record.resolvedAt)
417
+ const descriptor = authority.descriptors.get(record.dispatchEffectId)
418
+ const registrationId = callbackRegistrationId({ receiptId: record.receiptId,
419
+ generation: record.generation, domain: 'atomicHandoff', kind: 'runnerAccepted' })
420
+ const registration = authority.registrations.get(registrationId)
421
+ if (authority.effects.get(record.dispatchEffectId) !== (requireRunning ? 'attempting' : 'notStarted') ||
422
+ !authority.attempts.has(record.dispatchEffectId) ||
423
+ record.resolvedAt < authority.attempts.get(record.dispatchEffectId) ||
424
+ descriptor?.domain !== 'atomicHandoff' || descriptor.type !== 'dispatch' ||
425
+ descriptor.scope !== record.chatId || descriptor.receiptId !== record.receiptId ||
426
+ descriptor.turnToken !== record.turnToken ||
427
+ registration?.receiptId !== record.receiptId ||
428
+ registration.generation !== record.generation ||
429
+ registration.domain !== 'atomicHandoff' || registration.kind !== 'runnerAccepted' ||
430
+ registration.ordinal !== 0 ||
431
+ registration.registeredAt > authority.attempts.get(record.dispatchEffectId) ||
432
+ authority.acceptances.has(record.dispatchEffectId) ||
433
+ [...authority.closures.values()].some(item => item.dispatchEffectId === record.dispatchEffectId) ||
434
+ !(requireRunning ? authority.receipts.pending :
435
+ [...authority.receipts.pending, ...authority.receipts.completed])
436
+ .some(item => item.id === record.receiptId &&
437
+ (item.chatId === undefined || item.chatId === record.chatId))) {
438
+ throw new Error('C2 runner not-started proof differs from dispatch')
439
+ }
440
+ if (requireRunning) {
441
+ const turn = authority.state.lanes.turn?.[record.chatId]
442
+ const handoff = authority.state.lanes.handoff?.[record.chatId]
443
+ if (turn?.epoch !== record.turnToken || turn.active !== record.turnToken ||
444
+ handoff?.phase !== 'running' || handoff.turnToken !== record.turnToken ||
445
+ handoff.attempt !== 0 || handoff.nextJobId !== null ||
446
+ !Array.isArray(handoff.acceptedReceiptIds) || handoff.acceptedReceiptIds.length ||
447
+ ['terminal', 'session', 'outbound', 'cardCleanup', 'stopIntent']
448
+ .some(domain => Object.hasOwn(authority.state.lanes[domain] || {}, record.chatId))) {
449
+ throw new Error('C2 runner not-started proof has active side effects')
450
+ }
451
+ }
452
+ return { receiptId: record.receiptId, chatId: record.chatId,
453
+ turnToken: record.turnToken, generation: record.generation,
454
+ dispatchEffectId: record.dispatchEffectId, resolvedAt: record.resolvedAt }
455
+ }
456
+
457
+ function terminalSettlementCandidate(record, authority, requireLocal) {
458
+ const { state, receipts, effects, attempts, descriptors, notices, local,
459
+ validateState, validateReceipts, validateRelation } = authority
460
+ const batchRecord = Object.hasOwn(record, 'batchReceiptIds')
461
+ if (Object.keys(record).sort().join(',') !==
462
+ (batchRecord
463
+ ? 'batchReceiptIds,chatId,cursor,effectId,kind,receiptId,settledAt,status,turnToken'
464
+ : 'chatId,cursor,effectId,kind,receiptId,settledAt,status,turnToken') ||
465
+ record.kind !== 'terminalSettlement' || record.status !== 'acked' ||
466
+ record.cursor !== state.seq) throw new Error('invalid C2 terminal settlement record')
467
+ id(record.effectId, 'effect ID', 512)
468
+ noticeIdentity(record.receiptId, record.chatId)
469
+ noticeTime(record.settledAt)
470
+ if (!Number.isSafeInteger(record.turnToken) || record.turnToken <= 0) {
471
+ throw new Error('invalid C2 terminal settlement turn token')
472
+ }
473
+ const descriptor = descriptors.get(record.effectId)
474
+ const receipt = receipts.pending.find(item => item.id === record.receiptId)
475
+ const lane = state.lanes.terminal?.[record.chatId]
476
+ const handoff = state.lanes.handoff?.[record.chatId]
477
+ const receiptIds = lane?.batchReceiptIds || [record.receiptId]
478
+ if (batchRecord !== !!lane?.batchReceiptIds ||
479
+ !Array.isArray(receiptIds) || !receiptIds.length ||
480
+ new Set(receiptIds).size !== receiptIds.length ||
481
+ !receiptIds.includes(record.receiptId) ||
482
+ lane?.replyTargetReceiptId !== undefined &&
483
+ lane.replyTargetReceiptId !== record.receiptId ||
484
+ batchRecord && !isDeepStrictEqual(record.batchReceiptIds, receiptIds) ||
485
+ receiptIds.some(id => !RECEIPT_ID.test(id) ||
486
+ receipts.pending.find(item => item.id === id)?.chatId !== record.chatId ||
487
+ notices.has(id))) {
488
+ throw new Error('C2 terminal batch settlement receipt authority invalid')
489
+ }
490
+ if (!descriptor || descriptor.domain !== 'terminal' ||
491
+ !['finalReply', 'quietCompletion'].includes(descriptor.type) ||
492
+ descriptor.scope !== record.chatId ||
493
+ descriptor.turnToken !== record.turnToken ||
494
+ descriptor.receiptId !== record.receiptId || descriptor.targetId !== null ||
495
+ !receipt || receipt.chatId !== record.chatId || notices.has(record.receiptId) ||
496
+ !lane || lane.finalized !== true || !Array.isArray(lane.pendingEffectIds) ||
497
+ lane.pendingEffectIds.at(-1) !== record.effectId ||
498
+ !lane.pendingEffectIds.slice(0, -1).every(effectId => effects.get(effectId) === 'acked') ||
499
+ state.lanes.turn?.[record.chatId]?.active !== null ||
500
+ !handoff || !['idle', 'handoff'].includes(handoff.phase) ||
501
+ handoff.turnToken !== record.turnToken ||
502
+ handoff.nextJobId === record.receiptId ||
503
+ handoff.acceptedReceiptIds?.includes(record.receiptId)) {
504
+ throw new Error('C2 terminal settlement lacks released effect and receipt authority')
505
+ }
506
+ const shouldQuiet = receipt.quiet || lane.outcome?.silent === true
507
+ if (descriptor.type === 'quietCompletion' ?
508
+ !shouldQuiet || effects.get(record.effectId) !== 'pending' ||
509
+ attempts.has(record.effectId) :
510
+ shouldQuiet || effects.get(record.effectId) !== 'attempting' ||
511
+ !attempts.has(record.effectId) ||
512
+ record.settledAt < attempts.get(record.effectId) ||
513
+ requireLocal && !local.has(record.effectId)) {
514
+ throw new Error('C2 terminal settlement effect attempt invalid')
515
+ }
516
+ const candidate = completedBatch(receipts, receiptIds, record.settledAt)
517
+ validateSnapshots({ state, receipts: candidate }, validateState, validateReceipts,
518
+ validateRelation)
519
+ validateReceiptProgress(receipts, candidate)
520
+ return candidate
521
+ }
522
+
523
+ class C2DurableJournal {
524
+ #file
525
+ #validateState
526
+ #validateReceipts
527
+ #validateRelation
528
+ #cursor = 0
529
+ #state
530
+ #receipts
531
+ #eventIds = new Set()
532
+ #effects = new Map()
533
+ #effectAttempts = new Map()
534
+ #effectDescriptors = new Map()
535
+ #terminalSettlements = new Map()
536
+ #registrations = new Map()
537
+ #runnerAcceptances = new Map()
538
+ #runnerClosures = new Map()
539
+ #runnerNotStarts = new Map()
540
+ #notices = new Map()
541
+ // Only the process that durably began a notice may confirm its ACK. A
542
+ // restarted process sees an attempt as ambiguous and must never resend it.
543
+ #localNoticeIntents = new Set()
544
+ // An effect ACK belongs only to the process that fsynced its attempt.
545
+ #localEffectIntents = new Set()
546
+ #offset = 0
547
+ #fileIdentity = null
548
+ #blocked = false
549
+
550
+ constructor(file, { validateState, validateReceipts, validateRelation } = {}) {
551
+ if (typeof file !== 'string' || !path.isAbsolute(file) ||
552
+ typeof validateState !== 'function' || typeof validateReceipts !== 'function' ||
553
+ typeof validateRelation !== 'function') {
554
+ throw new TypeError('C2 journal needs an absolute file and strict projection validators')
555
+ }
556
+ this.#file = file
557
+ this.#validateState = validateState
558
+ this.#validateReceipts = validateReceipts
559
+ this.#validateRelation = validateRelation
560
+ }
561
+
562
+ // Explicit install/migration step only. Opening an absent journal never
563
+ // initializes an empty authority or silently adopts an older checkpoint.
564
+ static initialize(file, { state, receipts }, options) {
565
+ const journal = new C2DurableJournal(file, options)
566
+ const snapshots = validateSnapshots({ state, receipts }, journal.#validateState,
567
+ journal.#validateReceipts, journal.#validateRelation)
568
+ if (snapshots.state.seq !== 0) throw new TypeError('C2 journal genesis must have cursor zero')
569
+ const genesis = frame({ kind: 'genesis', ...snapshots })
570
+ const fd = fs.openSync(file, 'wx', 0o600)
571
+ try {
572
+ writeAll(fd, MAGIC, 0)
573
+ writeAll(fd, genesis, MAGIC.length)
574
+ fs.fsyncSync(fd)
575
+ const dirFd = fs.openSync(path.dirname(file), 'r')
576
+ try { fs.fsyncSync(dirFd) } finally { fs.closeSync(dirFd) }
577
+ } finally { fs.closeSync(fd) }
578
+ return C2DurableJournal.open(file, options)
579
+ }
580
+
581
+ static open(file, options) {
582
+ const journal = new C2DurableJournal(file, options)
583
+ journal.#replay()
584
+ return journal
585
+ }
586
+
587
+ get blocked() { return this.#blocked }
588
+ get cursor() { return this.#cursor }
589
+ get byteLength() { return this.#offset }
590
+ hasEvent(eventId) { return this.#eventIds.has(id(eventId, 'event ID')) }
591
+ effectStatus(effectId) { return this.#effects.get(id(effectId, 'effect ID', 512)) ?? null }
592
+ effectDescriptor(effectId) {
593
+ const value = this.#effectDescriptors.get(id(effectId, 'effect ID', 512))
594
+ return value ? structuredClone(value) : null
595
+ }
596
+ terminalSettlement(effectId) {
597
+ const value = this.#terminalSettlements.get(id(effectId, 'effect ID', 512))
598
+ return value ? { effectId, ...structuredClone(value) } : null
599
+ }
600
+ noticeStatus(receiptId) {
601
+ if (typeof receiptId !== 'string' || !RECEIPT_ID.test(receiptId)) {
602
+ throw new TypeError('invalid C2 notice receipt ID')
603
+ }
604
+ return this.#notices.get(receiptId)?.status ?? null
605
+ }
606
+ lookupRegistration(registrationId) {
607
+ if (typeof registrationId !== 'string' || !REGISTRATION_ID.test(registrationId)) {
608
+ throw new TypeError('invalid C2 callback registration ID')
609
+ }
610
+ const registration = this.#registrations.get(registrationId)
611
+ return registration ? structuredClone(registration) : null
612
+ }
613
+ lookupRunnerClosed(evidenceId) {
614
+ if (typeof evidenceId !== 'string' || !RUNNER_CLOSED_ID.test(evidenceId)) {
615
+ throw new TypeError('invalid C2 runner closure evidence ID')
616
+ }
617
+ const proof = this.#runnerClosures.get(evidenceId)
618
+ return proof ? { evidenceId, ...structuredClone(proof) } : null
619
+ }
620
+ runnerClosedProofs() {
621
+ return [...this.#runnerClosures].map(([evidenceId, proof]) =>
622
+ ({ evidenceId, ...structuredClone(proof) }))
623
+ }
624
+ lookupRunnerNotStarted(evidenceId) {
625
+ if (typeof evidenceId !== 'string' || !RUNNER_NOT_STARTED_ID.test(evidenceId)) {
626
+ throw new TypeError('invalid C2 runner not-started evidence ID')
627
+ }
628
+ const proof = this.#runnerNotStarts.get(evidenceId)
629
+ return proof ? { evidenceId, ...structuredClone(proof) } : null
630
+ }
631
+ runnerNotStartedProofs() {
632
+ return [...this.#runnerNotStarts].map(([evidenceId, proof]) =>
633
+ ({ evidenceId, ...structuredClone(proof) }))
634
+ }
635
+ snapshot() {
636
+ return { cursor: this.#cursor, state: structuredClone(this.#state),
637
+ receipts: structuredClone(this.#receipts), eventCount: this.#eventIds.size,
638
+ effects: new Map(this.#effects), notices: new Map(this.#notices) }
639
+ }
640
+
641
+ #settlementAuthority() {
642
+ return { state: this.#state, receipts: this.#receipts, effects: this.#effects,
643
+ attempts: this.#effectAttempts, descriptors: this.#effectDescriptors,
644
+ notices: this.#notices, local: this.#localEffectIntents,
645
+ validateState: this.#validateState, validateReceipts: this.#validateReceipts,
646
+ validateRelation: this.#validateRelation }
647
+ }
648
+
649
+ #runnerAuthority() {
650
+ return { state: this.#state, receipts: this.#receipts,
651
+ effects: this.#effects, attempts: this.#effectAttempts,
652
+ descriptors: this.#effectDescriptors, registrations: this.#registrations,
653
+ acceptances: this.#runnerAcceptances, closures: this.#runnerClosures }
654
+ }
655
+
656
+ #notStartedAuthority() {
657
+ return { ...this.#runnerAuthority(), notStarts: this.#runnerNotStarts }
658
+ }
659
+
660
+ appendTransition({ eventId, before, after, state, receipts, effectIds = [], effectDescriptors }) {
661
+ if (this.#blocked) throw new Error('C2 journal needs reopen after write failure')
662
+ if ([...this.#notices.values()].some(item => item.status === 'attempting' || item.status === 'unknown')) {
663
+ throw new Error('C2 transition blocked by ambiguous notice')
664
+ }
665
+ id(eventId, 'event ID')
666
+ if (this.#eventIds.has(eventId)) throw new Error('C2 event already committed')
667
+ if (before !== this.#cursor || after !== before + 1 || !Number.isSafeInteger(after)) {
668
+ throw new Error('C2 journal cursor discontinuity')
669
+ }
670
+ if (!Array.isArray(effectIds) || effectIds.length > 4096) throw new TypeError('invalid C2 effects')
671
+ for (const effectId of effectIds) {
672
+ id(effectId, 'effect ID', 512)
673
+ if (this.#effects.has(effectId)) throw new Error('C2 effect already committed')
674
+ }
675
+ if (new Set(effectIds).size !== effectIds.length) throw new Error('duplicate C2 effect ID')
676
+ let descriptors
677
+ if (effectDescriptors !== undefined) {
678
+ if (!Array.isArray(effectDescriptors) || effectDescriptors.length !== effectIds.length) {
679
+ throw new TypeError('C2 effect descriptors must match effect IDs')
680
+ }
681
+ descriptors = effectDescriptors.map((raw, index) => {
682
+ const descriptor = effectDescriptor(raw)
683
+ if (descriptor.effectId !== effectIds[index]) {
684
+ throw new Error('C2 effect descriptor ID or order mismatch')
685
+ }
686
+ return descriptor
687
+ })
688
+ }
689
+ const snapshots = validateSnapshots({ state, receipts }, this.#validateState,
690
+ this.#validateReceipts, this.#validateRelation)
691
+ if (snapshots.state.seq !== after) throw new Error('C2 state cursor mismatch')
692
+ validateReceiptProgress(this.#receipts, snapshots.receipts)
693
+ const nextEffects = new Map(this.#effects)
694
+ const nextDescriptors = new Map(this.#effectDescriptors)
695
+ for (const effectId of effectIds) nextEffects.set(effectId, 'pending')
696
+ for (const descriptor of descriptors || []) {
697
+ nextDescriptors.set(descriptor.effectId, descriptor)
698
+ }
699
+ validateStopAckAuthority(snapshots.state, snapshots.receipts,
700
+ nextEffects, nextDescriptors, this.#effectAttempts)
701
+ const record = { kind: 'transition', eventId, before, after, ...snapshots,
702
+ effectIds: [...effectIds], ...(descriptors ? { effectDescriptors: descriptors } : {}) }
703
+ this.#append(record)
704
+ this.#apply(record)
705
+ return this.snapshot()
706
+ }
707
+
708
+ // Legacy confirmation for effects with no durable external attempt. It does
709
+ // not grant permission to perform an external send. External effect runners
710
+ // must use beginEffectAttempt/recordEffectOutcome instead.
711
+ recordEffect(effectId, status) {
712
+ if (this.#blocked) throw new Error('C2 journal needs reopen after write failure')
713
+ id(effectId, 'effect ID', 512)
714
+ if (!['acked', 'unknown'].includes(status) || this.#effects.get(effectId) !== 'pending' ||
715
+ ['finalReply', 'quietCompletion', 'stopAck', 'queueNotice'].includes(this.#effectDescriptors.get(effectId)?.type)) {
716
+ throw new Error('invalid C2 effect confirmation')
717
+ }
718
+ const record = { kind: 'effect', effectId, status, cursor: this.#cursor }
719
+ this.#append(record)
720
+ this.#apply(record)
721
+ return status
722
+ }
723
+
724
+ // Resolve the external target and verify the active turn before this fsync.
725
+ // Only after it succeeds may the caller attempt the effect. A replayed
726
+ // attempting status is ambiguous and can never authorize another send.
727
+ beginEffectAttempt(value) {
728
+ if (this.#blocked) throw new Error('C2 journal needs reopen after write failure')
729
+ noticeFields(value, ['effectId', 'attemptedAt'])
730
+ const { effectId, attemptedAt } = value
731
+ id(effectId, 'effect ID', 512)
732
+ noticeTime(attemptedAt)
733
+ if (this.#effects.get(effectId) !== 'pending' || this.#effectAttempts.has(effectId) ||
734
+ this.#effectDescriptors.get(effectId)?.type === 'quietCompletion' ||
735
+ !terminalAttemptAllowed(this.#state, this.#receipts, this.#effects,
736
+ this.#effectDescriptors.get(effectId)) ||
737
+ !queueNoticeAttemptAllowed(this.#state, this.#receipts,
738
+ this.#effectDescriptors.get(effectId)) ||
739
+ !stopAckAttemptAllowed(this.#state, this.#receipts,
740
+ this.#effectDescriptors.get(effectId))) {
741
+ throw new Error('C2 effect has no unattempted durable authority')
742
+ }
743
+ const record = { kind: 'effectIntent', cursor: this.#cursor, effectId, attemptedAt }
744
+ this.#append(record)
745
+ this.#apply(record)
746
+ this.#localEffectIntents.add(effectId)
747
+ return 'attempting'
748
+ }
749
+
750
+ // ACK or unknown must be persisted by the process that began the attempt.
751
+ // An outcome write failure blocks this writer; reopen retains ambiguity.
752
+ recordEffectOutcome(value) {
753
+ if (this.#blocked) throw new Error('C2 journal needs reopen after write failure')
754
+ const keys = value?.registrationId === undefined
755
+ ? ['effectId', 'status', 'resolvedAt']
756
+ : ['effectId', 'status', 'resolvedAt', 'registrationId']
757
+ noticeFields(value, keys)
758
+ const { effectId, status, resolvedAt, registrationId } = value
759
+ id(effectId, 'effect ID', 512)
760
+ noticeTime(resolvedAt)
761
+ if (!['acked', 'unknown'].includes(status) ||
762
+ status === 'acked' && ['finalReply', 'stopAck'].includes(this.#effectDescriptors.get(effectId)?.type) ||
763
+ !this.#localEffectIntents.has(effectId) ||
764
+ this.#effects.get(effectId) !== 'attempting' ||
765
+ resolvedAt < this.#effectAttempts.get(effectId)) {
766
+ throw new Error('C2 effect has no current local attempt')
767
+ }
768
+ if (registrationId !== undefined) {
769
+ if (status !== 'acked' || this.#runnerAcceptances.has(effectId)) {
770
+ throw new Error('C2 runner acceptance must ACK one dispatch')
771
+ }
772
+ runnerAcceptanceAuthority(effectId, registrationId, this.#runnerAuthority(),
773
+ { effectStatus: 'attempting' })
774
+ }
775
+ const record = { kind: 'effectOutcome', cursor: this.#cursor, effectId,
776
+ status, resolvedAt, ...(registrationId === undefined ? {} : { registrationId }) }
777
+ this.#append(record)
778
+ this.#apply(record)
779
+ this.#localEffectIntents.delete(effectId)
780
+ return status
781
+ }
782
+
783
+ // This fsynced record is a host assertion that the accepted runner has
784
+ // closed. It never authorizes a new model launch or outward effect. The host
785
+ // must prove process exit/close before calling and keep the single-writer
786
+ // lease; the journal validates exact durable acceptance and active turn.
787
+ recordRunnerClosed({ receiptId, chatId, turnToken, generation,
788
+ dispatchEffectId, closedAt } = {}) {
789
+ if (this.#blocked) throw new Error('C2 journal needs reopen after write failure')
790
+ const evidenceId = runnerClosedId(receiptId, generation)
791
+ const record = { kind: 'runnerClosed', cursor: this.#cursor,
792
+ evidenceId, receiptId, chatId, turnToken, generation,
793
+ dispatchEffectId, closedAt }
794
+ runnerClosureCandidate(record, this.#runnerAuthority())
795
+ this.#append(record)
796
+ this.#apply(record)
797
+ return evidenceId
798
+ }
799
+
800
+ // Only the process which began this dispatch attempt may assert that its
801
+ // child never spawned. The independent status survives a crash before the
802
+ // Rust lane transition; it is neither an ACK nor an unknown external result.
803
+ recordRunnerNotStarted({ receiptId, chatId, turnToken, generation,
804
+ dispatchEffectId, resolvedAt } = {}) {
805
+ if (this.#blocked) throw new Error('C2 journal needs reopen after write failure')
806
+ if (!this.#localEffectIntents.has(dispatchEffectId)) {
807
+ throw new Error('C2 runner not-started proof lacks local attempt')
808
+ }
809
+ const evidenceId = runnerNotStartedId(receiptId, generation)
810
+ const record = { kind: 'runnerNotStarted', cursor: this.#cursor,
811
+ evidenceId, receiptId, chatId, turnToken, generation,
812
+ dispatchEffectId, resolvedAt }
813
+ runnerNotStartedCandidate(record, this.#notStartedAuthority())
814
+ this.#append(record)
815
+ this.#apply(record)
816
+ this.#localEffectIntents.delete(dispatchEffectId)
817
+ return evidenceId
818
+ }
819
+
820
+ // Final reply delivery (or quiet zero-call completion) and receipt closure
821
+ // share one fsynced record. The old turn must first release its queue/handoff
822
+ // authority while the receipt remains pending. A successor may begin only
823
+ // after this settlement and terminal prune.
824
+ settleTerminalEffect(value) {
825
+ if (this.#blocked) throw new Error('C2 journal needs reopen after write failure')
826
+ noticeFields(value, value?.batchReceiptIds === undefined
827
+ ? ['effectId', 'receiptId', 'chatId', 'turnToken', 'status', 'settledAt']
828
+ : ['effectId', 'receiptId', 'chatId', 'turnToken', 'status', 'settledAt',
829
+ 'batchReceiptIds'])
830
+ const record = { kind: 'terminalSettlement', cursor: this.#cursor, ...value }
831
+ terminalSettlementCandidate(record, this.#settlementAuthority(), true)
832
+ this.#append(record)
833
+ this.#apply(record)
834
+ this.#localEffectIntents.delete(value.effectId)
835
+ return 'acked'
836
+ }
837
+
838
+ // The stop reply and its distinct control receipt close in one fsynced
839
+ // record after a confirmed attempt in this process.
840
+ settleStopAck({ effectId, receiptId, chatId, settledAt } = {}) {
841
+ if (this.#blocked) throw new Error('C2 journal needs reopen after write failure')
842
+ const record = { kind: 'stopAckSettlement', cursor: this.#cursor,
843
+ effectId, receiptId, chatId, settledAt }
844
+ stopAckSettlementCandidate(record, this.#settlementAuthority(), true)
845
+ this.#append(record)
846
+ this.#apply(record)
847
+ this.#localEffectIntents.delete(effectId)
848
+ return 'acked'
849
+ }
850
+
851
+ // The caller must resolve the platform reply target before this fsync and
852
+ // must not send unless this call succeeds. An attempt left on disk after a
853
+ // crash is ambiguous; beginNotice will never authorize a second send.
854
+ beginNotice(value) {
855
+ if (this.#blocked) throw new Error('C2 journal needs reopen after write failure')
856
+ noticeFields(value, ['receiptId', 'chatId', 'attemptedAt'])
857
+ const { receiptId, chatId, attemptedAt } = value
858
+ noticeIdentity(receiptId, chatId)
859
+ noticeTime(attemptedAt)
860
+ const receipt = this.#receipts.pending.find(item => item.id === receiptId)
861
+ if (!receipt || receipt.chatId !== chatId || receipt.quiet ||
862
+ this.#notices.has(receiptId) ||
863
+ terminalEffectForReceipt(this.#effectDescriptors, receiptId) ||
864
+ terminalBatchOwnsReceipt(this.#state, receiptId)) {
865
+ throw new Error('C2 notice requires an unattempted nonquiet pending receipt in this group')
866
+ }
867
+ validatePersistentReceipts({ pending: [receipt], completed: [] })
868
+ // Reject before sending if current state still owns the receipt (queue or
869
+ // handoff). Completion must already be legal at the instant of intent.
870
+ const candidate = completedReceipt(this.#receipts, receiptId, attemptedAt)
871
+ validateSnapshots({ state: this.#state, receipts: candidate }, this.#validateState,
872
+ this.#validateReceipts, this.#validateRelation)
873
+ const record = { kind: 'noticeIntent', cursor: this.#cursor, receiptId, chatId, attemptedAt }
874
+ this.#append(record)
875
+ this.#apply(record)
876
+ this.#localNoticeIntents.add(receiptId)
877
+ return 'attempting'
878
+ }
879
+
880
+ // An acknowledged reply and receipt completion share one checksummed,
881
+ // fsynced record. A failed write leaves the journal blocked; reopening may
882
+ // reveal acked or an ambiguous attempt, never permission to resend.
883
+ recordNoticeOutcome(value) {
884
+ if (this.#blocked) throw new Error('C2 journal needs reopen after write failure')
885
+ const names = value?.status === 'acked'
886
+ ? ['receiptId', 'chatId', 'status', 'settledAt']
887
+ : ['receiptId', 'chatId', 'status', 'resolvedAt']
888
+ noticeFields(value, names)
889
+ const { receiptId, chatId, status } = value
890
+ noticeIdentity(receiptId, chatId)
891
+ if (!['acked', 'unknown'].includes(status) || !this.#localNoticeIntents.has(receiptId) ||
892
+ this.#notices.get(receiptId)?.status !== 'attempting' ||
893
+ this.#notices.get(receiptId)?.chatId !== chatId) {
894
+ throw new Error('C2 notice has no current local attempt')
895
+ }
896
+ const at = status === 'acked' ? value.settledAt : value.resolvedAt
897
+ noticeTime(at)
898
+ if (at < this.#notices.get(receiptId).attemptedAt) throw new Error('C2 notice time regressed')
899
+ if (status === 'acked') {
900
+ const candidate = completedReceipt(this.#receipts, receiptId, at)
901
+ validateSnapshots({ state: this.#state, receipts: candidate }, this.#validateState,
902
+ this.#validateReceipts, this.#validateRelation)
903
+ validateReceiptProgress(this.#receipts, candidate)
904
+ }
905
+ const record = { kind: 'noticeOutcome', cursor: this.#cursor, receiptId, chatId,
906
+ status, ...(status === 'acked' ? { settledAt: at } : { resolvedAt: at }) }
907
+ this.#append(record)
908
+ this.#apply(record)
909
+ this.#localNoticeIntents.delete(receiptId)
910
+ return status
911
+ }
912
+
913
+ // A quiet receipt has no external effect, so it can be settled in one
914
+ // durable record once queue/handoff authority has been cleared.
915
+ settleQuietReceipt(value) {
916
+ if (this.#blocked) throw new Error('C2 journal needs reopen after write failure')
917
+ noticeFields(value, ['receiptId', 'chatId', 'settledAt'])
918
+ const { receiptId, chatId, settledAt } = value
919
+ noticeIdentity(receiptId, chatId)
920
+ noticeTime(settledAt)
921
+ const receipt = this.#receipts.pending.find(item => item.id === receiptId)
922
+ if (!receipt || receipt.chatId !== chatId || receipt.quiet !== true ||
923
+ this.#notices.has(receiptId) ||
924
+ terminalEffectForReceipt(this.#effectDescriptors, receiptId)) {
925
+ throw new Error('C2 quiet settlement requires a quiet pending receipt in this group')
926
+ }
927
+ const candidate = completedReceipt(this.#receipts, receiptId, settledAt)
928
+ validateSnapshots({ state: this.#state, receipts: candidate }, this.#validateState,
929
+ this.#validateReceipts, this.#validateRelation)
930
+ validateReceiptProgress(this.#receipts, candidate)
931
+ const record = { kind: 'quietSettlement', cursor: this.#cursor, receiptId, chatId, settledAt }
932
+ this.#append(record)
933
+ this.#apply(record)
934
+ return 'settled'
935
+ }
936
+
937
+ // A callback must be registered while its accepted receipt is pending.
938
+ // The record and fsync complete before the caller may expose the callback.
939
+ registerCallback(value) {
940
+ if (this.#blocked) throw new Error('C2 journal needs reopen after write failure')
941
+ const registration = validateRegistration(value, this.#receipts, this.#registrations)
942
+ if (this.#notices.has(registration.receiptId)) {
943
+ throw new Error('C2 callback cannot register against a notice attempt')
944
+ }
945
+ const record = { kind: 'registration', cursor: this.#cursor, registration }
946
+ this.#append(record)
947
+ this.#apply(record)
948
+ return structuredClone(registration)
949
+ }
950
+
951
+ // Explicit offline maintenance under the daemon's single-writer lock. The
952
+ // checkpoint keeps every event tombstone and every unresolved effect/notice;
953
+ // it only removes repeated historical state and receipt snapshots. A local
954
+ // notice sender must finish before replacement, since its ACK authority is
955
+ // deliberately not transferable to a reopened journal.
956
+ compactMaintenance() {
957
+ if (this.#blocked) throw new Error('C2 journal needs reopen after write failure')
958
+ if (this.#localNoticeIntents.size) throw new Error('C2 notice send is in flight')
959
+ if (this.#localEffectIntents.size) throw new Error('C2 effect send is in flight')
960
+ if ([...this.#effects.values()].some(status => status === 'pending')) {
961
+ throw new Error('C2 effect may be in flight')
962
+ }
963
+ const old = fs.lstatSync(this.#file, { bigint: true })
964
+ if (!old.isFile() || identity(old) !== this.#fileIdentity ||
965
+ old.size !== BigInt(this.#offset)) {
966
+ this.#blocked = true
967
+ throw new Error('C2 journal changed outside writer')
968
+ }
969
+ const record = { kind: 'checkpoint', cursor: this.#cursor,
970
+ state: this.#state, receipts: this.#receipts,
971
+ eventIds: [...this.#eventIds], effects: [...this.#effects],
972
+ effectAttempts: [...this.#effectAttempts],
973
+ effectDescriptors: [...this.#effectDescriptors],
974
+ terminalSettlements: [...this.#terminalSettlements],
975
+ runnerAcceptances: [...this.#runnerAcceptances],
976
+ runnerClosures: [...this.#runnerClosures],
977
+ runnerNotStarts: [...this.#runnerNotStarts],
978
+ registrations: [...this.#registrations.values()], notices: [...this.#notices] }
979
+ const bytes = frame(record, MAX_CHECKPOINT_BYTES)
980
+ const dir = path.dirname(this.#file)
981
+ const temp = path.join(dir, `.${path.basename(this.#file)}.compact-${randomUUID()}`)
982
+ let renamed = false
983
+ try {
984
+ const fd = fs.openSync(temp, 'wx', 0o600)
985
+ try {
986
+ writeAll(fd, MAGIC, 0)
987
+ writeAll(fd, bytes, MAGIC.length)
988
+ fs.fsyncSync(fd)
989
+ } finally { fs.closeSync(fd) }
990
+ const verified = C2DurableJournal.open(temp, {
991
+ validateState: this.#validateState,
992
+ validateReceipts: this.#validateReceipts,
993
+ validateRelation: this.#validateRelation,
994
+ })
995
+ if (verified.#cursor !== this.#cursor ||
996
+ !isDeepStrictEqual(verified.#state, this.#state) ||
997
+ !isDeepStrictEqual(verified.#receipts, this.#receipts) ||
998
+ !isDeepStrictEqual(verified.#eventIds, this.#eventIds) ||
999
+ !isDeepStrictEqual(verified.#effects, this.#effects) ||
1000
+ !isDeepStrictEqual(verified.#effectAttempts, this.#effectAttempts) ||
1001
+ !isDeepStrictEqual(verified.#effectDescriptors, this.#effectDescriptors) ||
1002
+ !isDeepStrictEqual(verified.#terminalSettlements, this.#terminalSettlements) ||
1003
+ !isDeepStrictEqual(verified.#runnerAcceptances, this.#runnerAcceptances) ||
1004
+ !isDeepStrictEqual(verified.#runnerClosures, this.#runnerClosures) ||
1005
+ !isDeepStrictEqual(verified.#runnerNotStarts, this.#runnerNotStarts) ||
1006
+ !isDeepStrictEqual(verified.#registrations, this.#registrations) ||
1007
+ !isDeepStrictEqual(verified.#notices, this.#notices)) {
1008
+ throw new Error('C2 compacted authority differs from source')
1009
+ }
1010
+ const beforeBytes = this.#offset
1011
+ fs.renameSync(temp, this.#file)
1012
+ renamed = true
1013
+ const dirFd = fs.openSync(dir, 'r')
1014
+ try { fs.fsyncSync(dirFd) } finally { fs.closeSync(dirFd) }
1015
+ const current = fs.statSync(this.#file, { bigint: true })
1016
+ this.#offset = MAGIC.length + bytes.length
1017
+ this.#fileIdentity = identity(current)
1018
+ return { beforeBytes, afterBytes: this.#offset, cursor: this.#cursor,
1019
+ eventCount: this.#eventIds.size }
1020
+ } catch (error) {
1021
+ if (renamed) this.#blocked = true
1022
+ throw error
1023
+ } finally {
1024
+ if (!renamed) { try { fs.unlinkSync(temp) } catch (error) { if (error.code !== 'ENOENT') throw error } }
1025
+ }
1026
+ }
1027
+
1028
+ #append(record) {
1029
+ const bytes = frame(record)
1030
+ let fd
1031
+ try {
1032
+ fd = fs.openSync(this.#file, 'r+')
1033
+ const stat = fs.fstatSync(fd, { bigint: true })
1034
+ if (stat.size !== BigInt(this.#offset) || identity(stat) !== this.#fileIdentity) {
1035
+ throw new Error('C2 journal changed outside writer')
1036
+ }
1037
+ writeAll(fd, bytes, this.#offset)
1038
+ fs.fsyncSync(fd)
1039
+ this.#offset += bytes.length
1040
+ } catch (error) {
1041
+ this.#blocked = true
1042
+ throw error
1043
+ } finally { if (fd !== undefined) fs.closeSync(fd) }
1044
+ }
1045
+
1046
+ #apply(record) {
1047
+ if (record.kind === 'checkpoint') {
1048
+ const keys = Object.keys(record).sort().join(',')
1049
+ if (this.#state !== undefined || ![
1050
+ 'cursor,effects,eventIds,kind,notices,receipts,registrations,state',
1051
+ 'cursor,effectAttempts,effects,eventIds,kind,notices,receipts,registrations,state',
1052
+ 'cursor,effectAttempts,effectDescriptors,effects,eventIds,kind,notices,receipts,registrations,state',
1053
+ 'cursor,effectAttempts,effectDescriptors,effects,eventIds,kind,notices,receipts,registrations,state,terminalSettlements',
1054
+ 'cursor,effectAttempts,effectDescriptors,effects,eventIds,kind,notices,receipts,registrations,runnerAcceptances,runnerClosures,state,terminalSettlements',
1055
+ 'cursor,effectAttempts,effectDescriptors,effects,eventIds,kind,notices,receipts,registrations,runnerAcceptances,runnerClosures,runnerNotStarts,state,terminalSettlements',
1056
+ ].includes(keys) ||
1057
+ !Number.isSafeInteger(record.cursor) || record.cursor < 0) {
1058
+ throw new Error('invalid C2 journal checkpoint')
1059
+ }
1060
+ const snapshots = validateSnapshots(record, this.#validateState,
1061
+ this.#validateReceipts, this.#validateRelation)
1062
+ if (snapshots.state.seq !== record.cursor ||
1063
+ !Array.isArray(record.eventIds) || record.eventIds.length !== record.cursor ||
1064
+ !Array.isArray(record.effects) || !Array.isArray(record.registrations) ||
1065
+ !Array.isArray(record.notices) ||
1066
+ (record.effectAttempts !== undefined && !Array.isArray(record.effectAttempts)) ||
1067
+ (record.effectDescriptors !== undefined && !Array.isArray(record.effectDescriptors)) ||
1068
+ (record.terminalSettlements !== undefined && !Array.isArray(record.terminalSettlements)) ||
1069
+ (record.runnerAcceptances !== undefined && !Array.isArray(record.runnerAcceptances)) ||
1070
+ (record.runnerClosures !== undefined && !Array.isArray(record.runnerClosures)) ||
1071
+ (record.runnerNotStarts !== undefined && !Array.isArray(record.runnerNotStarts)) ||
1072
+ [record.eventIds, record.effects, record.effectAttempts || [],
1073
+ record.effectDescriptors || [], record.terminalSettlements || [],
1074
+ record.runnerAcceptances || [], record.runnerClosures || [],
1075
+ record.runnerNotStarts || [],
1076
+ record.registrations, record.notices]
1077
+ .some(items => items.length > MAX_CHECKPOINT_ITEMS)) {
1078
+ throw new Error('invalid C2 checkpoint index')
1079
+ }
1080
+ const events = new Set()
1081
+ for (const eventId of record.eventIds) {
1082
+ id(eventId, 'event ID')
1083
+ if (events.has(eventId)) throw new Error('duplicate C2 checkpoint event')
1084
+ events.add(eventId)
1085
+ }
1086
+ if (snapshots.state.lastEventId !== undefined &&
1087
+ !events.has(snapshots.state.lastEventId)) throw new Error('C2 checkpoint last event missing')
1088
+ const effects = new Map()
1089
+ for (const item of record.effects) {
1090
+ if (!Array.isArray(item) || item.length !== 2 ||
1091
+ !['pending', 'attempting', 'acked', 'unknown', 'notStarted'].includes(item[1])) {
1092
+ throw new Error('invalid C2 checkpoint effect')
1093
+ }
1094
+ id(item[0], 'effect ID', 512)
1095
+ if (effects.has(item[0])) throw new Error('duplicate C2 checkpoint effect')
1096
+ effects.set(item[0], item[1])
1097
+ }
1098
+ const effectAttempts = new Map()
1099
+ for (const item of record.effectAttempts || []) {
1100
+ if (!Array.isArray(item) || item.length !== 2) {
1101
+ throw new Error('invalid C2 checkpoint effect attempt')
1102
+ }
1103
+ id(item[0], 'effect ID', 512)
1104
+ noticeTime(item[1])
1105
+ if (!['attempting', 'acked', 'unknown', 'notStarted'].includes(effects.get(item[0])) ||
1106
+ effectAttempts.has(item[0])) {
1107
+ throw new Error('invalid C2 checkpoint effect attempt')
1108
+ }
1109
+ effectAttempts.set(item[0], item[1])
1110
+ }
1111
+ if ([...effects].some(([effectId, status]) =>
1112
+ status === 'attempting' && !effectAttempts.has(effectId))) {
1113
+ throw new Error('C2 checkpoint attempting effect lacks attempt')
1114
+ }
1115
+ const effectDescriptors = new Map()
1116
+ for (const item of record.effectDescriptors || []) {
1117
+ if (!Array.isArray(item) || item.length !== 2) {
1118
+ throw new Error('invalid C2 checkpoint effect descriptor')
1119
+ }
1120
+ const descriptor = effectDescriptor(item[1])
1121
+ if (item[0] !== descriptor.effectId || !effects.has(item[0]) ||
1122
+ effectDescriptors.has(item[0])) {
1123
+ throw new Error('invalid C2 checkpoint effect descriptor')
1124
+ }
1125
+ effectDescriptors.set(item[0], descriptor)
1126
+ }
1127
+ const receipts = new Set([...snapshots.receipts.pending,
1128
+ ...snapshots.receipts.completed].map(item => item.id))
1129
+ const registrations = new Map()
1130
+ for (const raw of record.registrations) {
1131
+ const registration = exactJson(raw)
1132
+ if (!registration || typeof registration !== 'object' || Array.isArray(registration) ||
1133
+ Object.keys(registration).length !== REGISTRATION_FIELDS.size ||
1134
+ Object.keys(registration).some(key => !REGISTRATION_FIELDS.has(key)) ||
1135
+ registration.id !== callbackRegistrationId(registration) ||
1136
+ !Number.isSafeInteger(registration.registeredAt) || registration.registeredAt <= 0 ||
1137
+ !receipts.has(registration.receiptId) || registrations.has(registration.id)) {
1138
+ throw new Error('invalid C2 checkpoint registration')
1139
+ }
1140
+ registrations.set(registration.id, registration)
1141
+ }
1142
+ const runnerAcceptances = new Map()
1143
+ for (const entry of record.runnerAcceptances || []) {
1144
+ if (!Array.isArray(entry) || entry.length !== 2 ||
1145
+ runnerAcceptances.has(entry[0])) {
1146
+ throw new Error('invalid C2 checkpoint runner acceptance')
1147
+ }
1148
+ runnerAcceptanceAuthority(entry[0], entry[1], {
1149
+ state: snapshots.state, receipts: snapshots.receipts, effects,
1150
+ attempts: effectAttempts, descriptors: effectDescriptors, registrations,
1151
+ }, { requireRunning: false })
1152
+ runnerAcceptances.set(entry[0], entry[1])
1153
+ }
1154
+ const runnerClosures = new Map()
1155
+ for (const entry of record.runnerClosures || []) {
1156
+ if (!Array.isArray(entry) || entry.length !== 2 ||
1157
+ runnerClosures.has(entry[0])) {
1158
+ throw new Error('invalid C2 checkpoint runner closure')
1159
+ }
1160
+ const raw = exactJson(entry[1])
1161
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw) ||
1162
+ Object.keys(raw).sort().join(',') !==
1163
+ 'chatId,closedAt,dispatchEffectId,generation,receiptId,turnToken') {
1164
+ throw new Error('invalid C2 checkpoint runner closure fields')
1165
+ }
1166
+ const proof = runnerClosureCandidate({ kind: 'runnerClosed',
1167
+ cursor: record.cursor, evidenceId: entry[0], ...raw }, {
1168
+ state: snapshots.state, receipts: snapshots.receipts, effects,
1169
+ attempts: effectAttempts, descriptors: effectDescriptors, registrations,
1170
+ acceptances: runnerAcceptances, closures: runnerClosures,
1171
+ }, false)
1172
+ runnerClosures.set(entry[0], proof)
1173
+ }
1174
+ const runnerNotStarts = new Map()
1175
+ for (const entry of record.runnerNotStarts || []) {
1176
+ if (!Array.isArray(entry) || entry.length !== 2 || runnerNotStarts.has(entry[0])) {
1177
+ throw new Error('invalid C2 checkpoint runner not-started proof')
1178
+ }
1179
+ const raw = exactJson(entry[1])
1180
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw) ||
1181
+ Object.keys(raw).sort().join(',') !==
1182
+ 'chatId,dispatchEffectId,generation,receiptId,resolvedAt,turnToken') {
1183
+ throw new Error('invalid C2 checkpoint runner not-started fields')
1184
+ }
1185
+ const proof = runnerNotStartedCandidate({ kind: 'runnerNotStarted',
1186
+ cursor: record.cursor, evidenceId: entry[0], ...raw }, {
1187
+ state: snapshots.state, receipts: snapshots.receipts, effects,
1188
+ attempts: effectAttempts, descriptors: effectDescriptors, registrations,
1189
+ acceptances: runnerAcceptances, closures: runnerClosures,
1190
+ notStarts: runnerNotStarts,
1191
+ }, false)
1192
+ runnerNotStarts.set(entry[0], proof)
1193
+ }
1194
+ if ([...effects].some(([effectId, status]) => status === 'notStarted' &&
1195
+ ![...runnerNotStarts.values()].some(proof => proof.dispatchEffectId === effectId))) {
1196
+ throw new Error('C2 checkpoint not-started effect lacks proof')
1197
+ }
1198
+ const notices = new Map()
1199
+ const pendingIds = new Set(snapshots.receipts.pending.map(item => item.id))
1200
+ const completedIds = new Set(snapshots.receipts.completed.map(item => item.id))
1201
+ for (const entry of record.notices) {
1202
+ if (!Array.isArray(entry) || entry.length !== 2) throw new Error('invalid C2 checkpoint notice')
1203
+ const [receiptId, raw] = entry
1204
+ const notice = exactJson(raw)
1205
+ if (!notice || typeof notice !== 'object' || Array.isArray(notice)) {
1206
+ throw new Error('invalid C2 checkpoint notice')
1207
+ }
1208
+ noticeIdentity(receiptId, notice.chatId)
1209
+ const keys = Object.keys(notice).sort().join(',')
1210
+ if (notice.status === 'quiet-settled') {
1211
+ if (keys !== 'chatId,status' || !completedIds.has(receiptId)) {
1212
+ throw new Error('invalid C2 checkpoint quiet notice')
1213
+ }
1214
+ } else if (['attempting', 'acked', 'unknown'].includes(notice.status)) {
1215
+ if (keys !== 'attemptedAt,chatId,status' ||
1216
+ (notice.status === 'acked' ? !completedIds.has(receiptId) : !pendingIds.has(receiptId))) {
1217
+ throw new Error('invalid C2 checkpoint notice state')
1218
+ }
1219
+ noticeTime(notice.attemptedAt)
1220
+ } else throw new Error('invalid C2 checkpoint notice status')
1221
+ const pending = snapshots.receipts.pending.find(item => item.id === receiptId)
1222
+ if (pending && pending.chatId !== notice.chatId || notices.has(receiptId)) {
1223
+ throw new Error('invalid C2 checkpoint notice target')
1224
+ }
1225
+ notices.set(receiptId, notice)
1226
+ }
1227
+ const terminalSettlements = new Map()
1228
+ const settledReceipts = new Set()
1229
+ for (const entry of record.terminalSettlements || []) {
1230
+ if (!Array.isArray(entry) || entry.length !== 2) {
1231
+ throw new Error('invalid C2 checkpoint terminal settlement')
1232
+ }
1233
+ const [effectId, raw] = entry
1234
+ id(effectId, 'effect ID', 512)
1235
+ const item = exactJson(raw)
1236
+ if (!item || typeof item !== 'object' || Array.isArray(item) ||
1237
+ !['chatId,receiptId,settledAt,turnToken',
1238
+ 'batchReceiptIds,chatId,receiptId,settledAt,turnToken']
1239
+ .includes(Object.keys(item).sort().join(','))) {
1240
+ throw new Error('invalid C2 checkpoint terminal settlement')
1241
+ }
1242
+ noticeIdentity(item.receiptId, item.chatId)
1243
+ noticeTime(item.settledAt)
1244
+ const descriptor = effectDescriptors.get(effectId)
1245
+ const settledIds = item.batchReceiptIds || [item.receiptId]
1246
+ if (!Array.isArray(settledIds) || !settledIds.length ||
1247
+ new Set(settledIds).size !== settledIds.length ||
1248
+ !settledIds.includes(item.receiptId) ||
1249
+ item.batchReceiptIds && item.batchReceiptIds.length < 2 ||
1250
+ !Number.isSafeInteger(item.turnToken) || item.turnToken <= 0 ||
1251
+ !descriptor || descriptor.domain !== 'terminal' ||
1252
+ !['finalReply', 'quietCompletion'].includes(descriptor.type) ||
1253
+ descriptor.scope !== item.chatId || descriptor.receiptId !== item.receiptId ||
1254
+ descriptor.turnToken !== item.turnToken || effects.get(effectId) !== 'acked' ||
1255
+ (descriptor.type === 'finalReply') !== effectAttempts.has(effectId) ||
1256
+ settledIds.some(id => !RECEIPT_ID.test(id) ||
1257
+ !snapshots.receipts.completed.some(receipt =>
1258
+ receipt.id === id && receipt.settledAt === item.settledAt) ||
1259
+ notices.has(id) || settledReceipts.has(id)) ||
1260
+ terminalSettlements.has(effectId)) {
1261
+ throw new Error('invalid C2 checkpoint terminal settlement')
1262
+ }
1263
+ terminalSettlements.set(effectId, item)
1264
+ for (const id of settledIds) settledReceipts.add(id)
1265
+ }
1266
+ for (const [effectId, descriptor] of effectDescriptors) {
1267
+ if (descriptor.domain !== 'terminal' ||
1268
+ !['finalReply', 'quietCompletion'].includes(descriptor.type)) continue
1269
+ const status = effects.get(effectId)
1270
+ if (status === 'acked' ? !terminalSettlements.has(effectId) :
1271
+ !snapshots.receipts.pending.some(receipt =>
1272
+ receipt.id === descriptor.receiptId && receipt.chatId === descriptor.scope)) {
1273
+ throw new Error('C2 checkpoint terminal effect lacks atomic settlement authority')
1274
+ }
1275
+ }
1276
+ validateStopAckAuthority(snapshots.state, snapshots.receipts, effects,
1277
+ effectDescriptors, effectAttempts)
1278
+ this.#state = snapshots.state
1279
+ this.#receipts = snapshots.receipts
1280
+ this.#cursor = record.cursor
1281
+ this.#eventIds = events
1282
+ this.#effects = effects
1283
+ this.#effectAttempts = effectAttempts
1284
+ this.#effectDescriptors = effectDescriptors
1285
+ this.#terminalSettlements = terminalSettlements
1286
+ this.#registrations = registrations
1287
+ this.#runnerAcceptances = runnerAcceptances
1288
+ this.#runnerClosures = runnerClosures
1289
+ this.#runnerNotStarts = runnerNotStarts
1290
+ this.#notices = notices
1291
+ return
1292
+ }
1293
+ if (record.kind === 'genesis') {
1294
+ if (this.#state !== undefined) throw new Error('duplicate C2 journal genesis')
1295
+ const snapshots = validateSnapshots(record, this.#validateState,
1296
+ this.#validateReceipts, this.#validateRelation)
1297
+ if (snapshots.state.seq !== 0 || Object.keys(record).sort().join(',') !== 'kind,receipts,state') {
1298
+ throw new Error('invalid C2 journal genesis')
1299
+ }
1300
+ this.#state = snapshots.state
1301
+ this.#receipts = snapshots.receipts
1302
+ return
1303
+ }
1304
+ if (this.#state === undefined) throw new Error('C2 journal lacks genesis')
1305
+ if (record.kind === 'transition') {
1306
+ if ([...this.#notices.values()].some(item => item.status === 'attempting' || item.status === 'unknown')) {
1307
+ throw new Error('C2 transition blocked by ambiguous notice')
1308
+ }
1309
+ id(record.eventId, 'event ID')
1310
+ const keys = Object.keys(record).sort().join(',')
1311
+ if (record.before !== this.#cursor || record.after !== this.#cursor + 1 ||
1312
+ !Number.isSafeInteger(record.after) || this.#eventIds.has(record.eventId) ||
1313
+ !Array.isArray(record.effectIds) || record.effectIds.length > 4096 ||
1314
+ !['after,before,effectIds,eventId,kind,receipts,state',
1315
+ 'after,before,effectDescriptors,effectIds,eventId,kind,receipts,state'].includes(keys)) {
1316
+ throw new Error('invalid C2 journal transition')
1317
+ }
1318
+ const snapshots = validateSnapshots(record, this.#validateState,
1319
+ this.#validateReceipts, this.#validateRelation)
1320
+ if (snapshots.state.seq !== record.after) throw new Error('C2 journal state cursor mismatch')
1321
+ validateReceiptProgress(this.#receipts, snapshots.receipts)
1322
+ const unique = new Set(record.effectIds)
1323
+ if (unique.size !== record.effectIds.length) throw new Error('duplicate C2 journal effects')
1324
+ for (const effectId of unique) {
1325
+ id(effectId, 'effect ID', 512)
1326
+ if (this.#effects.has(effectId)) throw new Error('duplicate C2 journal effect')
1327
+ }
1328
+ let descriptors = []
1329
+ if (record.effectDescriptors !== undefined) {
1330
+ if (!Array.isArray(record.effectDescriptors) ||
1331
+ record.effectDescriptors.length !== record.effectIds.length) {
1332
+ throw new Error('invalid C2 journal effect descriptors')
1333
+ }
1334
+ descriptors = record.effectDescriptors.map((raw, index) => {
1335
+ const descriptor = effectDescriptor(raw)
1336
+ if (descriptor.effectId !== record.effectIds[index]) {
1337
+ throw new Error('C2 journal effect descriptor mismatch')
1338
+ }
1339
+ return descriptor
1340
+ })
1341
+ }
1342
+ this.#state = snapshots.state
1343
+ this.#receipts = snapshots.receipts
1344
+ this.#cursor = record.after
1345
+ this.#eventIds.add(record.eventId)
1346
+ for (const effectId of unique) this.#effects.set(effectId, 'pending')
1347
+ for (const descriptor of descriptors) this.#effectDescriptors.set(descriptor.effectId, descriptor)
1348
+ validateStopAckAuthority(this.#state, this.#receipts, this.#effects,
1349
+ this.#effectDescriptors, this.#effectAttempts)
1350
+ return
1351
+ }
1352
+ if (record.kind === 'effect') {
1353
+ id(record.effectId, 'effect ID', 512)
1354
+ if (!['acked', 'unknown'].includes(record.status) || record.cursor !== this.#cursor ||
1355
+ this.#effects.get(record.effectId) !== 'pending' ||
1356
+ ['finalReply', 'quietCompletion', 'stopAck', 'queueNotice'].includes(this.#effectDescriptors.get(record.effectId)?.type) ||
1357
+ Object.keys(record).sort().join(',') !== 'cursor,effectId,kind,status') {
1358
+ throw new Error('invalid C2 journal effect confirmation')
1359
+ }
1360
+ this.#effects.set(record.effectId, record.status)
1361
+ return
1362
+ }
1363
+ if (record.kind === 'effectIntent') {
1364
+ if (Object.keys(record).sort().join(',') !== 'attemptedAt,cursor,effectId,kind' ||
1365
+ record.cursor !== this.#cursor) throw new Error('invalid C2 effect intent record')
1366
+ id(record.effectId, 'effect ID', 512)
1367
+ noticeTime(record.attemptedAt)
1368
+ if (this.#effects.get(record.effectId) !== 'pending' ||
1369
+ this.#effectAttempts.has(record.effectId) ||
1370
+ this.#effectDescriptors.get(record.effectId)?.type === 'quietCompletion' ||
1371
+ !terminalAttemptAllowed(this.#state, this.#receipts, this.#effects,
1372
+ this.#effectDescriptors.get(record.effectId)) ||
1373
+ !queueNoticeAttemptAllowed(this.#state, this.#receipts,
1374
+ this.#effectDescriptors.get(record.effectId)) ||
1375
+ !stopAckAttemptAllowed(this.#state, this.#receipts,
1376
+ this.#effectDescriptors.get(record.effectId))) {
1377
+ throw new Error('invalid C2 effect intent target')
1378
+ }
1379
+ this.#effects.set(record.effectId, 'attempting')
1380
+ this.#effectAttempts.set(record.effectId, record.attemptedAt)
1381
+ return
1382
+ }
1383
+ if (record.kind === 'effectOutcome') {
1384
+ if (!['cursor,effectId,kind,resolvedAt,status',
1385
+ 'cursor,effectId,kind,registrationId,resolvedAt,status']
1386
+ .includes(Object.keys(record).sort().join(',')) ||
1387
+ record.cursor !== this.#cursor ||
1388
+ !['acked', 'unknown'].includes(record.status)) {
1389
+ throw new Error('invalid C2 effect outcome record')
1390
+ }
1391
+ id(record.effectId, 'effect ID', 512)
1392
+ noticeTime(record.resolvedAt)
1393
+ if (this.#effects.get(record.effectId) !== 'attempting' ||
1394
+ record.status === 'acked' &&
1395
+ ['finalReply', 'stopAck'].includes(this.#effectDescriptors.get(record.effectId)?.type) ||
1396
+ record.resolvedAt < this.#effectAttempts.get(record.effectId)) {
1397
+ throw new Error('invalid C2 effect outcome target')
1398
+ }
1399
+ if (record.registrationId !== undefined) {
1400
+ if (record.status !== 'acked' || this.#runnerAcceptances.has(record.effectId)) {
1401
+ throw new Error('invalid C2 runner acceptance outcome')
1402
+ }
1403
+ runnerAcceptanceAuthority(record.effectId, record.registrationId,
1404
+ this.#runnerAuthority(), { effectStatus: 'attempting' })
1405
+ this.#runnerAcceptances.set(record.effectId, record.registrationId)
1406
+ }
1407
+ this.#effects.set(record.effectId, record.status)
1408
+ return
1409
+ }
1410
+ if (record.kind === 'runnerClosed') {
1411
+ const proof = runnerClosureCandidate(record, this.#runnerAuthority())
1412
+ this.#runnerClosures.set(record.evidenceId, proof)
1413
+ return
1414
+ }
1415
+ if (record.kind === 'runnerNotStarted') {
1416
+ const proof = runnerNotStartedCandidate(record, this.#notStartedAuthority())
1417
+ this.#runnerNotStarts.set(record.evidenceId, proof)
1418
+ this.#effects.set(record.dispatchEffectId, 'notStarted')
1419
+ return
1420
+ }
1421
+ if (record.kind === 'terminalSettlement') {
1422
+ const candidate = terminalSettlementCandidate(record, this.#settlementAuthority(), false)
1423
+ if (this.#terminalSettlements.has(record.effectId)) {
1424
+ throw new Error('duplicate C2 terminal settlement')
1425
+ }
1426
+ this.#effects.set(record.effectId, 'acked')
1427
+ this.#receipts = candidate
1428
+ this.#terminalSettlements.set(record.effectId, { receiptId: record.receiptId,
1429
+ chatId: record.chatId, turnToken: record.turnToken, settledAt: record.settledAt,
1430
+ ...(record.batchReceiptIds === undefined ? {} : {
1431
+ batchReceiptIds: [...record.batchReceiptIds] }) })
1432
+ return
1433
+ }
1434
+ if (record.kind === 'stopAckSettlement') {
1435
+ const candidate = stopAckSettlementCandidate(record, this.#settlementAuthority(), false)
1436
+ this.#effects.set(record.effectId, 'acked')
1437
+ this.#receipts = candidate
1438
+ validateStopAckAuthority(this.#state, this.#receipts, this.#effects,
1439
+ this.#effectDescriptors, this.#effectAttempts)
1440
+ return
1441
+ }
1442
+ if (record.kind === 'registration') {
1443
+ if (record.cursor !== this.#cursor ||
1444
+ Object.keys(record).sort().join(',') !== 'cursor,kind,registration') {
1445
+ throw new Error('invalid C2 journal registration record')
1446
+ }
1447
+ const registration = validateRegistration(record.registration, this.#receipts,
1448
+ this.#registrations)
1449
+ if (this.#notices.has(registration.receiptId)) {
1450
+ throw new Error('C2 callback cannot register against a notice attempt')
1451
+ }
1452
+ this.#registrations.set(registration.id, registration)
1453
+ return
1454
+ }
1455
+ if (record.kind === 'noticeIntent') {
1456
+ if (Object.keys(record).sort().join(',') !== 'attemptedAt,chatId,cursor,kind,receiptId' ||
1457
+ record.cursor !== this.#cursor) throw new Error('invalid C2 notice intent record')
1458
+ noticeIdentity(record.receiptId, record.chatId)
1459
+ noticeTime(record.attemptedAt)
1460
+ const receipt = this.#receipts.pending.find(item => item.id === record.receiptId)
1461
+ if (!receipt || receipt.chatId !== record.chatId || receipt.quiet ||
1462
+ this.#notices.has(record.receiptId) ||
1463
+ terminalEffectForReceipt(this.#effectDescriptors, record.receiptId) ||
1464
+ terminalBatchOwnsReceipt(this.#state, record.receiptId)) {
1465
+ throw new Error('invalid C2 notice intent target')
1466
+ }
1467
+ validatePersistentReceipts({ pending: [receipt], completed: [] })
1468
+ validateSnapshots({ state: this.#state,
1469
+ receipts: completedReceipt(this.#receipts, record.receiptId, record.attemptedAt) },
1470
+ this.#validateState, this.#validateReceipts, this.#validateRelation)
1471
+ this.#notices.set(record.receiptId, { chatId: record.chatId,
1472
+ status: 'attempting', attemptedAt: record.attemptedAt })
1473
+ return
1474
+ }
1475
+ if (record.kind === 'noticeOutcome') {
1476
+ const names = record.status === 'acked'
1477
+ ? 'chatId,cursor,kind,receiptId,settledAt,status'
1478
+ : 'chatId,cursor,kind,receiptId,resolvedAt,status'
1479
+ if (Object.keys(record).sort().join(',') !== names || record.cursor !== this.#cursor ||
1480
+ !['acked', 'unknown'].includes(record.status)) throw new Error('invalid C2 notice outcome record')
1481
+ noticeIdentity(record.receiptId, record.chatId)
1482
+ const notice = this.#notices.get(record.receiptId)
1483
+ if (!notice || notice.chatId !== record.chatId || notice.status !== 'attempting') {
1484
+ throw new Error('invalid C2 notice outcome target')
1485
+ }
1486
+ const at = record.status === 'acked' ? record.settledAt : record.resolvedAt
1487
+ noticeTime(at)
1488
+ if (at < notice.attemptedAt) throw new Error('C2 notice time regressed')
1489
+ if (record.status === 'acked') {
1490
+ const candidate = completedReceipt(this.#receipts, record.receiptId, at)
1491
+ validateSnapshots({ state: this.#state, receipts: candidate }, this.#validateState,
1492
+ this.#validateReceipts, this.#validateRelation)
1493
+ validateReceiptProgress(this.#receipts, candidate)
1494
+ this.#receipts = candidate
1495
+ }
1496
+ this.#notices.set(record.receiptId, { ...notice, status: record.status })
1497
+ return
1498
+ }
1499
+ if (record.kind === 'quietSettlement') {
1500
+ if (Object.keys(record).sort().join(',') !== 'chatId,cursor,kind,receiptId,settledAt' ||
1501
+ record.cursor !== this.#cursor) throw new Error('invalid C2 quiet settlement record')
1502
+ noticeIdentity(record.receiptId, record.chatId)
1503
+ noticeTime(record.settledAt)
1504
+ const receipt = this.#receipts.pending.find(item => item.id === record.receiptId)
1505
+ if (!receipt || receipt.chatId !== record.chatId || receipt.quiet !== true ||
1506
+ this.#notices.has(record.receiptId) ||
1507
+ terminalEffectForReceipt(this.#effectDescriptors, record.receiptId)) {
1508
+ throw new Error('invalid C2 quiet settlement target')
1509
+ }
1510
+ const candidate = completedReceipt(this.#receipts, record.receiptId, record.settledAt)
1511
+ validateSnapshots({ state: this.#state, receipts: candidate }, this.#validateState,
1512
+ this.#validateReceipts, this.#validateRelation)
1513
+ validateReceiptProgress(this.#receipts, candidate)
1514
+ this.#receipts = candidate
1515
+ this.#notices.set(record.receiptId, { chatId: record.chatId, status: 'quiet-settled' })
1516
+ return
1517
+ }
1518
+ throw new Error('unknown C2 journal record')
1519
+ }
1520
+
1521
+ #replay() {
1522
+ const fd = fs.openSync(this.#file, 'r+')
1523
+ try {
1524
+ const size = fs.fstatSync(fd).size
1525
+ if (size < MAGIC.length) throw new Error('truncated C2 journal header')
1526
+ const magic = Buffer.alloc(MAGIC.length)
1527
+ readAll(fd, magic, 0)
1528
+ if (!magic.equals(MAGIC)) throw new Error('invalid C2 journal header')
1529
+ let offset = MAGIC.length
1530
+ while (offset < size) {
1531
+ if (size - offset < FRAME_HEADER) break
1532
+ const header = Buffer.alloc(FRAME_HEADER)
1533
+ readAll(fd, header, offset)
1534
+ const length = header.readUInt32BE(0)
1535
+ if (!length || length > MAX_CHECKPOINT_BYTES) throw new Error('invalid C2 journal frame length')
1536
+ if (size - offset - FRAME_HEADER < length) break
1537
+ const payload = Buffer.alloc(length)
1538
+ readAll(fd, payload, offset + FRAME_HEADER)
1539
+ const hash = createHash('sha256').update(payload).digest()
1540
+ if (!hash.equals(header.subarray(4))) throw new Error('C2 journal checksum mismatch')
1541
+ let record
1542
+ try { record = JSON.parse(payload.toString('utf8')) } catch { throw new Error('invalid C2 journal JSON') }
1543
+ if (length > MAX_RECORD_BYTES && record.kind !== 'checkpoint') {
1544
+ throw new Error('oversized C2 journal record')
1545
+ }
1546
+ this.#apply(record)
1547
+ offset += FRAME_HEADER + length
1548
+ }
1549
+ if (this.#state === undefined) throw new Error('C2 journal lacks complete genesis')
1550
+ if (offset < size) {
1551
+ fs.ftruncateSync(fd, offset)
1552
+ fs.fsyncSync(fd)
1553
+ }
1554
+ this.#offset = offset
1555
+ this.#fileIdentity = identity(fs.fstatSync(fd, { bigint: true }))
1556
+ } finally { fs.closeSync(fd) }
1557
+ }
1558
+ }
1559
+
1560
+ module.exports = { C2DurableJournal }