sintbot 1.0.15 → 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 (136) hide show
  1. package/CHANGELOG.en.json +118 -0
  2. package/CHANGELOG.json +118 -0
  3. package/README.md +20 -156
  4. package/README.zh-CN.md +20 -164
  5. package/THIRD-PARTY-NOTICES.md +394 -0
  6. package/bin/sintbot-v2.js +363 -0
  7. package/docs/AI-ASSISTED-INSTALL.md +19 -0
  8. package/docs/install.md +93 -0
  9. package/docs/zh-CN/AI-ASSISTED-INSTALL.md +19 -0
  10. package/docs/zh-CN/install.md +93 -0
  11. package/install.sh +84 -0
  12. package/package.json +74 -34
  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 -4711
  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/clilang.js +0 -31
  93. package/src/core/cron.js +0 -182
  94. package/src/core/cronfire.js +0 -120
  95. package/src/core/cronrun.js +0 -116
  96. package/src/core/engine.js +0 -2762
  97. package/src/core/interfaces.js +0 -114
  98. package/src/core/issues.js +0 -185
  99. package/src/core/ledger.js +0 -30
  100. package/src/core/opsalert.js +0 -214
  101. package/src/core/pending.js +0 -123
  102. package/src/core/proxy.js +0 -424
  103. package/src/core/quotaalert.js +0 -79
  104. package/src/core/quotaerr.js +0 -55
  105. package/src/core/scheduler.js +0 -59
  106. package/src/design.js +0 -5161
  107. package/src/gitrepo.js +0 -150
  108. package/src/health.js +0 -353
  109. package/src/installdeps.js +0 -72
  110. package/src/leftover.js +0 -174
  111. package/src/platform/boardnotice.js +0 -77
  112. package/src/platform/boardops.js +0 -36
  113. package/src/platform/boardwatch.js +0 -447
  114. package/src/platform/cli.js +0 -45
  115. package/src/platform/slack.js +0 -1289
  116. package/src/platform/takeovercard.js +0 -37
  117. package/src/platform/wecom.js +0 -938
  118. package/src/platform/wecomcli.js +0 -128
  119. package/src/platform/wecomgate.js +0 -108
  120. package/src/platform/wecomname.js +0 -43
  121. package/src/platform/wecompoll.js +0 -241
  122. package/src/preview.js +0 -581
  123. package/src/projectmemory.js +0 -175
  124. package/src/service.js +0 -317
  125. package/src/ui/auth.js +0 -31
  126. package/src/ui/browser.html +0 -392
  127. package/src/ui/browserroutes.js +0 -234
  128. package/src/ui/console.html +0 -5494
  129. package/src/ui/dashboard.js +0 -168
  130. package/src/ui/feishulogin.js +0 -136
  131. package/src/ui/server.js +0 -2314
  132. package/src/ui/sessions.js +0 -78
  133. package/src/ui/takeover.js +0 -108
  134. package/src/updater.js +0 -243
  135. package/src/upgrade.js +0 -35
  136. package/src/usage.js +0 -144
@@ -0,0 +1,2361 @@
1
+ 'use strict'
2
+
3
+ const { createHash } = require('node:crypto')
4
+ const { isDeepStrictEqual } = require('node:util')
5
+ const { C2DurableIngress } = require('./c2durableingress')
6
+ const { C2DurableJournal } = require('./c2durablejournal')
7
+ const { platformMessageEventId, callbackRegistrationId, callbackEventId } = require('./c2eventidentity')
8
+ const { C2PayloadHandles } = require('./c2payloadhandles')
9
+ const { EngineStepCoordinator } = require('./enginestepcoordinator')
10
+ const { engineTerminalStep } = require('../agent/privatecore')
11
+ const { recoverAndOpen } = require('./c2recoverygate')
12
+ const { pruneConfirmedTerminal } = require('./c2terminalprune')
13
+ const { prepareClosedSessionEvent } = require('./c2sessionbridge')
14
+
15
+ function positiveLimit(value, label) {
16
+ if (!Number.isSafeInteger(value) || value <= 0) throw new TypeError(`C2 ${label} required`)
17
+ return value
18
+ }
19
+
20
+ const RECEIPT_ID = /^[0-9a-f]{32}$/
21
+ const GENERATION = /^[0-9a-f]{32}$/
22
+ const CARD_OPERATION_KINDS = new Set(['card.create', 'message.reply',
23
+ 'message.create', 'cardElement.content', 'card.update', 'card.settings',
24
+ 'message.delete'])
25
+
26
+ function cardOperationIdentity({ receiptId, turnToken, generation, operation }) {
27
+ if (!operation || typeof operation !== 'object' || Array.isArray(operation) ||
28
+ !CARD_OPERATION_KINDS.has(operation.kind) ||
29
+ typeof operation.operationId !== 'string' || !operation.operationId ||
30
+ Buffer.byteLength(operation.operationId) > 512 ||
31
+ typeof operation.invoke !== 'function' ||
32
+ (operation.kind !== 'card.create' &&
33
+ (typeof operation.cardId !== 'string' || !operation.cardId)) ||
34
+ (['message.reply', 'message.create', 'message.delete', 'card.create']
35
+ .includes(operation.kind) &&
36
+ (typeof operation.chatId !== 'string' || !operation.chatId)) ||
37
+ (operation.kind === 'message.reply' &&
38
+ (typeof operation.replyToMsgId !== 'string' || !operation.replyToMsgId)) ||
39
+ (operation.kind === 'message.delete' &&
40
+ (typeof operation.msgId !== 'string' || !operation.msgId))) {
41
+ throw new TypeError('C2 card operation identity invalid')
42
+ }
43
+ // The Feishu adapter alone supplies a one-call invoke. Hash the stable
44
+ // operation identity with this turn generation; no card/body/target enters
45
+ // the Rust event or durable journal.
46
+ const parts = [receiptId, turnToken, generation, operation.kind,
47
+ operation.operationId]
48
+ const hash = createHash('sha256').update('sbot-c2-card-operation-v1\0')
49
+ for (const part of parts) {
50
+ const bytes = Buffer.from(String(part), 'utf8')
51
+ hash.update(String(bytes.length)).update(':').update(bytes)
52
+ }
53
+ const operationId = hash.digest('hex')
54
+ return { operationId,
55
+ eventId: `sbot-c2-card-v1:${receiptId}:${turnToken}:${operationId}` }
56
+ }
57
+
58
+ function cardTargetDigest({ chatId, cardId, msgId }) {
59
+ if (![chatId, cardId, msgId].every(value =>
60
+ typeof value === 'string' && value.length > 0)) {
61
+ throw new TypeError('C2 card cleanup target invalid')
62
+ }
63
+ const hash = createHash('sha256').update('sbot-c2-card-target-v1\0')
64
+ for (const value of [chatId, cardId, msgId]) {
65
+ const bytes = Buffer.from(value, 'utf8')
66
+ hash.update(String(bytes.length)).update(':').update(bytes)
67
+ }
68
+ return hash.digest('hex')
69
+ }
70
+
71
+ function queuedPayloads(snapshot, scope, handles) {
72
+ if (!(handles instanceof C2PayloadHandles) ||
73
+ handles.blocked || !Array.isArray(snapshot.state.lanes.queue?.[scope]?.queue)) {
74
+ throw new Error('C2 atomic release needs verified live queue handles')
75
+ }
76
+ const pending = new Map(snapshot.receipts.pending.map(item => [item.id, item]))
77
+ const queue = snapshot.state.lanes.queue[scope].queue
78
+ const accepted = []
79
+ const seen = new Set()
80
+ const add = id => { if (!seen.has(id)) { seen.add(id); accepted.push(id) } }
81
+ for (const item of queue) {
82
+ const id = item.id?.slice('receipt:job:'.length)
83
+ if (!RECEIPT_ID.test(id) || item.id !== `receipt:job:${id}`) {
84
+ throw new Error('C2 atomic release has unstable queue ID')
85
+ }
86
+ const receipt = pending.get(id)
87
+ const job = handles.requireQueued(id)
88
+ if (!receipt || receipt.chatId !== scope || job?.msg?._c2ReceiptId !== id ||
89
+ job.msg.chatId !== scope || (job.msg.receiptSourceId || job.msg.msgId) !== receipt.sourceId) {
90
+ throw new Error('C2 atomic release queue handle differs from durable receipt')
91
+ }
92
+ add(id)
93
+ if (item.batch) {
94
+ if (!Array.isArray(job.batch) || job.batch.length !== item.batch.length) {
95
+ throw new Error('C2 atomic release batch handle missing')
96
+ }
97
+ for (let index = 0; index < item.batch.length; index++) {
98
+ const memberId = item.batch[index].id?.slice('receipt:member:'.length)
99
+ const member = job.batch[index]
100
+ const memberReceipt = pending.get(memberId)
101
+ if (!RECEIPT_ID.test(memberId) ||
102
+ item.batch[index].id !== `receipt:member:${memberId}` ||
103
+ !memberReceipt || memberReceipt.chatId !== scope ||
104
+ member?._c2ReceiptId !== memberId || member.chatId !== scope ||
105
+ (member.receiptSourceId || member.msgId) !== memberReceipt.sourceId) {
106
+ throw new Error('C2 atomic release batch handle differs from durable receipt')
107
+ }
108
+ add(memberId)
109
+ }
110
+ }
111
+ }
112
+ return { queue, accepted }
113
+ }
114
+
115
+ function queueRunLen(queue) {
116
+ if (!queue.length) return 0
117
+ const { quiet, polled } = queue[0]
118
+ const end = queue.findIndex(item => item.quiet !== quiet || item.polled !== polled)
119
+ return end < 0 ? queue.length : end
120
+ }
121
+
122
+ // The model receives top-level jobs, while a retried job may also contain
123
+ // member receipts. Durable terminal settlement must cover every member.
124
+ function queueCohortReceiptIds(items) {
125
+ const ids = []
126
+ for (const item of items) {
127
+ for (const handle of [item.id, ...(item.batch || []).map(member => member.id)]) {
128
+ const prefix = handle.startsWith('receipt:job:')
129
+ ? 'receipt:job:' : 'receipt:member:'
130
+ const id = handle.slice(prefix.length)
131
+ if (!RECEIPT_ID.test(id) || handle !== `${prefix}${id}`) {
132
+ throw new Error('C2 queue cohort has unstable receipt handle')
133
+ }
134
+ if (!ids.includes(id)) ids.push(id)
135
+ }
136
+ }
137
+ return ids
138
+ }
139
+
140
+ // Development-only v2 authority. The caller must hold the daemon's single
141
+ // writer lock, supply an explicitly opened/initialized journal, and own all
142
+ // process-local payloads. This façade never saves a body or reply context.
143
+ class C2DurableRuntime {
144
+ #journal
145
+ #coordinator
146
+ #ingress = null
147
+ #resolveTarget
148
+ #send
149
+ #now
150
+ #maxEventCount
151
+ #maxJournalBytes
152
+ #maxCompletionEvents
153
+ #maxCompletionBytes
154
+ #assertWriterLock
155
+ #singleNoticePerGroup
156
+ #opening = false
157
+ #closed = true
158
+ #effectAuthorityReady = false
159
+ #capacitySealed = false
160
+ #scheduledHandoffs = new Map()
161
+ // A dispatch receipt is only a live capability in this process. Reopen
162
+ // cannot reconstruct its body or prove which pending sibling was running.
163
+ #activeRuns = new Map()
164
+ #terminalAwaitingSettlement = new Map()
165
+ #externalEffectInFlight = false
166
+ #gate = {
167
+ close: () => { this.#closed = true },
168
+ open: () => { this.#closed = false },
169
+ isClosed: () => this.#closed,
170
+ }
171
+
172
+ constructor({ journal, resolveTarget, send, now = Date.now,
173
+ maxEventCount, maxJournalBytes, maxCompletionEvents, maxCompletionBytes,
174
+ assertWriterLock = null, singleNoticePerGroup = false } = {}) {
175
+ if (!(journal instanceof C2DurableJournal) || typeof journal.snapshot !== 'function' ||
176
+ typeof journal.appendTransition !== 'function' ||
177
+ typeof journal.byteLength !== 'number' || journal.byteLength <= 0 ||
178
+ typeof resolveTarget !== 'function' || typeof send !== 'function' ||
179
+ typeof now !== 'function' ||
180
+ (assertWriterLock !== null &&
181
+ (typeof assertWriterLock !== 'function' ||
182
+ assertWriterLock.constructor?.name === 'AsyncFunction')) ||
183
+ typeof singleNoticePerGroup !== 'boolean') {
184
+ throw new TypeError('C2 runtime needs an explicit journal and recovery sender')
185
+ }
186
+ this.#journal = journal
187
+ this.#coordinator = new EngineStepCoordinator({ journal })
188
+ this.#resolveTarget = resolveTarget
189
+ this.#send = send
190
+ this.#now = now
191
+ this.#assertWriterLock = assertWriterLock
192
+ this.#singleNoticePerGroup = singleNoticePerGroup
193
+ this.#maxEventCount = positiveLimit(maxEventCount, 'event limit')
194
+ this.#maxJournalBytes = positiveLimit(maxJournalBytes, 'journal byte limit')
195
+ this.#maxCompletionEvents = positiveLimit(maxCompletionEvents ?? maxEventCount,
196
+ 'completion event reserve')
197
+ this.#maxCompletionBytes = positiveLimit(maxCompletionBytes ?? maxJournalBytes,
198
+ 'completion byte reserve')
199
+ if (!Number.isSafeInteger(this.#maxEventCount + this.#maxCompletionEvents) ||
200
+ !Number.isSafeInteger(this.#maxJournalBytes + this.#maxCompletionBytes)) {
201
+ throw new RangeError('C2 authority limits exceed safe integer range')
202
+ }
203
+ }
204
+
205
+ get blocked() { return this.#closed || this.#opening || this.#externalEffectInFlight || !this.#ingress ||
206
+ this.#ingress.blocked || this.#journal.blocked }
207
+
208
+ hasRecoveryPolicy({ assertWriterLock, singleNoticePerGroup } = {}) {
209
+ return typeof assertWriterLock === 'function' &&
210
+ this.#assertWriterLock === assertWriterLock &&
211
+ this.#singleNoticePerGroup === singleNoticePerGroup
212
+ }
213
+
214
+ // A scope-labelled gate must not be mistaken for a scoped runtime lease.
215
+ // The current callbacks and failure catches still close the whole runtime,
216
+ // while open() discards every group's live capabilities. Keep this contract
217
+ // visibly closed until all effect/callback paths can fence one group only.
218
+ scopedGate(chatId) {
219
+ if (typeof chatId !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(chatId) ||
220
+ ['__proto__', 'constructor', 'prototype'].includes(chatId)) {
221
+ throw new TypeError('C2 scoped runtime chat ID invalid')
222
+ }
223
+ const unavailable = () => {
224
+ throw new Error('C2 scoped runtime close/open unavailable; global authority unchanged')
225
+ }
226
+ return Object.freeze({ scope: chatId, close: unavailable, open: unavailable,
227
+ isClosed: () => true })
228
+ }
229
+
230
+ closeScope(chatId) { this.scopedGate(chatId).close() }
231
+
232
+ async recoverScope(chatId) { this.scopedGate(chatId).open() }
233
+
234
+ #capacity() {
235
+ const snapshot = this.#journal.snapshot()
236
+ if (!Number.isSafeInteger(snapshot.eventCount) || snapshot.eventCount < 0 ||
237
+ !Number.isSafeInteger(this.#journal.byteLength) || this.#journal.byteLength <= 0) {
238
+ this.close()
239
+ throw new Error('C2 durable authority capacity unreadable')
240
+ }
241
+ if (snapshot.eventCount >= this.#maxEventCount ||
242
+ this.#journal.byteLength >= this.#maxJournalBytes) {
243
+ this.#sealForCapacity()
244
+ throw new Error('C2 durable authority capacity reached; ingress closed')
245
+ }
246
+ return snapshot
247
+ }
248
+
249
+ #completionCapacity() {
250
+ if (!this.#effectAuthorityReady || this.#opening || this.#externalEffectInFlight ||
251
+ this.#journal.blocked ||
252
+ (this.#closed && !this.#capacitySealed)) {
253
+ throw new Error('C2 durable completion authority closed')
254
+ }
255
+ const snapshot = this.#journal.snapshot()
256
+ if (snapshot.eventCount >= this.#maxEventCount + this.#maxCompletionEvents ||
257
+ this.#journal.byteLength >= this.#maxJournalBytes + this.#maxCompletionBytes) {
258
+ this.#sealForCapacity()
259
+ throw new Error('C2 durable completion reserve exhausted; manual recovery required')
260
+ }
261
+ return snapshot
262
+ }
263
+
264
+ async open() {
265
+ if (this.#externalEffectInFlight) throw new Error('C2 external effect still in flight')
266
+ if (this.#opening) throw new Error('C2 runtime startup already in progress')
267
+ if (!this.blocked) return this.snapshot()
268
+ this.#opening = true
269
+ this.#ingress = null
270
+ this.#effectAuthorityReady = false
271
+ this.#capacitySealed = false
272
+ this.#scheduledHandoffs.clear()
273
+ this.#activeRuns.clear()
274
+ this.#terminalAwaitingSettlement.clear()
275
+ this.#gate.close()
276
+ try {
277
+ // Capacity limits new ingress. Recovery must still settle already
278
+ // accepted receipts before deciding whether new work may enter.
279
+ await recoverAndOpen({ gate: this.#gate, journal: this.#journal,
280
+ coordinator: this.#coordinator, resolveTarget: this.#resolveTarget,
281
+ send: this.#send, now: this.#now,
282
+ assertWriterLock: this.#assertWriterLock,
283
+ singleNoticePerGroup: this.#singleNoticePerGroup })
284
+ this.#capacity()
285
+ this.#ingress = C2DurableIngress.resumeAfterVerifiedRecovery({
286
+ journal: this.#journal, coordinator: this.#coordinator, gate: this.#gate })
287
+ this.#effectAuthorityReady = true
288
+ return this.snapshot()
289
+ } catch (error) {
290
+ this.close()
291
+ throw error
292
+ } finally { this.#opening = false }
293
+ }
294
+
295
+ close() {
296
+ this.#gate.close()
297
+ this.#ingress = null
298
+ this.#effectAuthorityReady = false
299
+ this.#capacitySealed = false
300
+ this.#scheduledHandoffs.clear()
301
+ this.#activeRuns.clear()
302
+ this.#terminalAwaitingSettlement.clear()
303
+ }
304
+
305
+ #sealForCapacity() {
306
+ this.#gate.close()
307
+ this.#ingress = null
308
+ this.#capacitySealed = true
309
+ }
310
+
311
+ enqueueMessage(options) {
312
+ if (this.blocked) throw new Error('C2 durable runtime ingress closed')
313
+ this.#capacity()
314
+ const result = this.#ingress.enqueueMessage(options)
315
+ if (this.#journal.snapshot().eventCount >= this.#maxEventCount ||
316
+ this.#journal.byteLength >= this.#maxJournalBytes) this.#sealForCapacity()
317
+ return result
318
+ }
319
+
320
+ // A visible busy receipt remains pending after its queue acknowledgement.
321
+ // The effect descriptor and target identity are durable before any send;
322
+ // only the same process may turn one attempted call into an ACK.
323
+ prepareQueueNotice({ receiptId, chatId, payloadHandles } = {}) {
324
+ const before = this.#completionCapacity()
325
+ if (!RECEIPT_ID.test(receiptId || '') ||
326
+ typeof chatId !== 'string' || !chatId ||
327
+ !(payloadHandles instanceof C2PayloadHandles) || payloadHandles.blocked) {
328
+ throw new TypeError('C2 queue notice identity invalid')
329
+ }
330
+ const receipt = before.receipts.pending.find(item => item.id === receiptId)
331
+ const queue = before.state.lanes.queue?.[chatId]?.queue
332
+ const index = queue?.findIndex(item => item.id === `receipt:job:${receiptId}`)
333
+ const witness = this.#activeRuns.get(chatId)
334
+ const item = index >= 0 ? queue[index] : null
335
+ const target = payloadHandles.requireQueued(receiptId)?.msg
336
+ const visibleQueue = queue?.filter(job => !job.quiet && !job.polled).length
337
+ const eventId = `sbot-c2-queue-notice-v1:${receiptId}`
338
+ if (!receipt || receipt.chatId !== chatId || receipt.source !== 'message' ||
339
+ receipt.quiet || !Array.isArray(queue) || index < 0 ||
340
+ item.quiet || item.polled || item.batch !== undefined ||
341
+ !Number.isSafeInteger(visibleQueue) || visibleQueue <= 0 ||
342
+ target?._c2ReceiptId !== receiptId || target.chatId !== chatId ||
343
+ (target.receiptSourceId || target.msgId) !== receipt.sourceId ||
344
+ target.replyCtx?.chatId !== chatId ||
345
+ target.replyCtx.messageId !== receipt.sourceId ||
346
+ !witness || before.state.lanes.turn?.[chatId]?.active !== witness.turnToken ||
347
+ before.state.lanes.handoff?.[chatId]?.phase !== 'running' ||
348
+ before.state.lanes.handoff[chatId].turnToken !== witness.turnToken ||
349
+ this.#journal.hasEvent(eventId) ||
350
+ !isDeepStrictEqual(this.#coordinator.snapshot(), before.state)) {
351
+ throw new Error('C2 queue notice lacks exact pending busy receipt')
352
+ }
353
+ try {
354
+ const prepared = this.#coordinator.prepare({ id: eventId, domain: 'queue',
355
+ scope: chatId, payload: { type: 'prepareQueueNotice', receiptId,
356
+ queueIndex: index, visibleQueue },
357
+ validate: candidate => {
358
+ const effect = candidate.effects?.[0]
359
+ const descriptor = candidate.effectDescriptors?.[0]
360
+ if (candidate.cursor !== before.cursor + 1 ||
361
+ candidate.state?.seq !== candidate.cursor ||
362
+ !isDeepStrictEqual(candidate.state.lanes, before.state.lanes) ||
363
+ candidate.effects?.length !== 1 || effect?.type !== 'queueNotice' ||
364
+ effect.receiptId !== receiptId || effect.visibleQueue !== visibleQueue ||
365
+ descriptor?.effectId !== effect.effectId ||
366
+ descriptor.domain !== 'queue' || descriptor.scope !== chatId ||
367
+ descriptor.type !== 'queueNotice' || descriptor.turnToken !== null ||
368
+ descriptor.receiptId !== receiptId || descriptor.targetId !== null ||
369
+ candidate.decision?.receiptId !== receiptId ||
370
+ candidate.decision.queueIndex !== index ||
371
+ candidate.decision.visibleQueue !== visibleQueue ||
372
+ candidate.decision.receiptSettlement !== false) {
373
+ throw new Error('C2 queue notice result invalid')
374
+ }
375
+ } })
376
+ const result = prepared.commit()
377
+ return { effectId: result.effects[0].effectId, receiptId, chatId,
378
+ queueIndex: index, visibleQueue }
379
+ } catch (error) { this.close(); throw error }
380
+ }
381
+
382
+ async executeQueueNotice({ effectId, receiptId, chatId, payloadHandles,
383
+ assertWriterLock, perform, confirm } = {}) {
384
+ const before = this.#completionCapacity()
385
+ const descriptor = this.#journal.effectDescriptor(effectId)
386
+ const receipt = before.receipts.pending.find(item => item.id === receiptId)
387
+ const queue = before.state.lanes.queue?.[chatId]?.queue
388
+ const target = payloadHandles instanceof C2PayloadHandles &&
389
+ !payloadHandles.blocked ? payloadHandles.requireQueued(receiptId)?.msg : null
390
+ if (!RECEIPT_ID.test(receiptId || '') || !receipt ||
391
+ receipt.chatId !== chatId || receipt.source !== 'message' || receipt.quiet ||
392
+ descriptor?.domain !== 'queue' || descriptor.scope !== chatId ||
393
+ descriptor.type !== 'queueNotice' || descriptor.receiptId !== receiptId ||
394
+ descriptor.turnToken !== null || descriptor.targetId !== null ||
395
+ this.#journal.effectStatus(effectId) !== 'pending' ||
396
+ !Array.isArray(queue) || !queue.some(item =>
397
+ item.id === `receipt:job:${receiptId}` && !item.quiet && !item.polled &&
398
+ item.batch === undefined) ||
399
+ target?._c2ReceiptId !== receiptId || target.chatId !== chatId ||
400
+ (target.receiptSourceId || target.msgId) !== receipt.sourceId ||
401
+ target.replyCtx?.chatId !== chatId ||
402
+ target.replyCtx.messageId !== receipt.sourceId ||
403
+ typeof assertWriterLock !== 'function' ||
404
+ assertWriterLock.constructor?.name === 'AsyncFunction' ||
405
+ typeof perform !== 'function' || typeof confirm !== 'function' ||
406
+ confirm.constructor?.name === 'AsyncFunction' ||
407
+ !isDeepStrictEqual(this.#coordinator.snapshot(), before.state) ||
408
+ assertWriterLock() !== true) {
409
+ throw new Error('C2 queue notice lacks exact live target and effect')
410
+ }
411
+ const attemptedAt = this.#now()
412
+ if (!Number.isSafeInteger(attemptedAt) || attemptedAt <= 0) {
413
+ throw new TypeError('C2 queue notice time invalid')
414
+ }
415
+ this.#externalEffectInFlight = true
416
+ try {
417
+ this.#journal.beginEffectAttempt({ effectId, attemptedAt })
418
+ let confirmed = false
419
+ let response
420
+ try {
421
+ response = await perform({ effectId, target: target.replyCtx })
422
+ confirmed = confirm(response) === true &&
423
+ this.#effectAuthorityReady && assertWriterLock() === true
424
+ } catch { /* The platform call may have succeeded. */ }
425
+ const now = this.#now()
426
+ const resolvedAt = Number.isSafeInteger(now) && now >= attemptedAt
427
+ ? now : attemptedAt
428
+ this.#journal.recordEffectOutcome({ effectId,
429
+ status: confirmed ? 'acked' : 'unknown', resolvedAt })
430
+ if (!confirmed) throw new Error('C2 queue notice outcome unknown')
431
+ return { effectId, receiptId, status: 'acked', response }
432
+ } catch (error) { this.close(); throw error }
433
+ finally { this.#externalEffectInFlight = false }
434
+ }
435
+
436
+ #retainActiveRun(scope, result, receiptId, jobReceiptIds, batchReceiptIds) {
437
+ const turnToken = result.decision?.turnToken
438
+ const dispatch = result.effects?.find(item => item.type === 'dispatch')
439
+ const descriptor = dispatch && this.#journal.effectDescriptor(dispatch.effectId)
440
+ const durable = this.#journal.snapshot()
441
+ if (!RECEIPT_ID.test(receiptId) || !Array.isArray(jobReceiptIds) ||
442
+ !Array.isArray(batchReceiptIds) || !jobReceiptIds.includes(receiptId) ||
443
+ !batchReceiptIds.includes(receiptId) ||
444
+ !Number.isSafeInteger(turnToken) || turnToken <= 0 ||
445
+ descriptor?.type !== 'dispatch' || descriptor.domain !== 'atomicHandoff' ||
446
+ descriptor.scope !== scope || descriptor.turnToken !== turnToken ||
447
+ descriptor.receiptId !== receiptId ||
448
+ durable.state.lastEventId !== result.eventId ||
449
+ durable.state.lanes.turn?.[scope]?.active !== turnToken ||
450
+ durable.state.lanes.handoff?.[scope]?.phase !== 'running' ||
451
+ durable.state.lanes.handoff[scope].turnToken !== turnToken ||
452
+ !isDeepStrictEqual(durable.state.lanes.handoff[scope].acceptedReceiptIds,
453
+ batchReceiptIds.length > 1 ? batchReceiptIds : [])) {
454
+ throw new Error('C2 running receipt witness differs from durable dispatch')
455
+ }
456
+ this.#activeRuns.set(scope, { receiptId, turnToken, eventId: result.eventId,
457
+ dispatchEffectId: dispatch.effectId, jobReceiptIds: [...jobReceiptIds],
458
+ batchReceiptIds: [...batchReceiptIds] })
459
+ }
460
+
461
+ // One accepted queue head becomes a running turn under a single fsynced
462
+ // cursor. Raw jobs remain in the process-local registry until the caller
463
+ // consumes the committed dispatch; nothing may run before this returns.
464
+ commitInitialDispatch({ receiptId, payloadHandles } = {}) {
465
+ this.#completionCapacity()
466
+ if (!RECEIPT_ID.test(receiptId)) throw new TypeError('C2 initial dispatch receipt invalid')
467
+ const before = this.#journal.snapshot()
468
+ const receipt = before.receipts.pending.find(item => item.id === receiptId)
469
+ if (!receipt) throw new Error('C2 initial dispatch lacks pending receipt')
470
+ const scope = receipt.chatId
471
+ if (this.#terminalAwaitingSettlement.has(scope)) {
472
+ throw new Error('C2 prior terminal receipt has not settled')
473
+ }
474
+ const { queue } = queuedPayloads(before, scope, payloadHandles)
475
+ if (!queue.length ||
476
+ before.state.lanes.handoff?.[scope]?.phase !== undefined &&
477
+ before.state.lanes.handoff[scope].phase !== 'idle' ||
478
+ before.state.lanes.turn?.[scope]?.active != null ||
479
+ !isDeepStrictEqual(this.#coordinator.snapshot(), before.state)) {
480
+ throw new Error('C2 initial dispatch differs from durable idle queue')
481
+ }
482
+ const runLen = queueRunLen(queue)
483
+ const taken = queue.slice(0, runLen)
484
+ const remaining = queue.slice(runLen)
485
+ const jobReceiptIds = taken.map(item => item.id.slice('receipt:job:'.length))
486
+ const batchReceiptIds = queueCohortReceiptIds(taken)
487
+ const expectedAnchor = [...taken].reverse().find(item => !item.synthetic)?.id ||
488
+ taken.at(-1)?.id
489
+ if (expectedAnchor !== `receipt:job:${receiptId}`) {
490
+ throw new Error('C2 initial dispatch anchor differs from queue decision')
491
+ }
492
+ const id = platformMessageEventId({ receiptId, domain: 'atomicHandoff',
493
+ kind: 'initialDispatchCommit', lookupReceipt: key =>
494
+ before.receipts.pending.find(item => item.id === key) || null })
495
+ if (this.#journal.hasEvent(id)) throw new Error('C2 initial dispatch already committed')
496
+ try {
497
+ const prepared = this.#coordinator.prepare({ id, domain: 'atomicHandoff', scope,
498
+ payload: { type: 'initialDispatchCommit' },
499
+ validate: candidate => {
500
+ const token = candidate.decision?.turnToken
501
+ const dispatch = candidate.decision?.queueDispatch
502
+ const effect = candidate.effects?.[0]
503
+ if (candidate.cursor !== before.cursor + 1 || candidate.state?.seq !== candidate.cursor ||
504
+ !Number.isSafeInteger(token) || token <= 0 ||
505
+ candidate.state.lanes.turn?.[scope]?.active !== token ||
506
+ candidate.state.lanes.handoff?.[scope]?.phase !== 'running' ||
507
+ candidate.state.lanes.handoff[scope].turnToken !== token ||
508
+ !isDeepStrictEqual(candidate.state.lanes.queue?.[scope]?.queue, remaining) ||
509
+ dispatch?.type !== 'dispatch' ||
510
+ !taken.some(item => item.id === dispatch.anchor) ||
511
+ !isDeepStrictEqual(dispatch.batch, taken.flatMap(item =>
512
+ item.batch?.map(member => member.id) || [item.id])) ||
513
+ !isDeepStrictEqual(candidate.decision.jobReceiptIds, jobReceiptIds) ||
514
+ !isDeepStrictEqual(candidate.decision.batchReceiptIds, batchReceiptIds) ||
515
+ !isDeepStrictEqual(candidate.state.lanes.handoff[scope].acceptedReceiptIds,
516
+ batchReceiptIds.length > 1 ? batchReceiptIds : []) ||
517
+ candidate.decision.queueRunLen !== queueRunLen(remaining) ||
518
+ candidate.decision.visibleQueue !== remaining.filter(item =>
519
+ !item.quiet && !item.polled).length ||
520
+ candidate.effects.length !== 1 || effect.type !== 'dispatch' ||
521
+ effect.origin !== 'queue' || effect.turnToken !== token ||
522
+ effect.effectId !== `sbot-c2-effect-v1:${Buffer.byteLength(id)}:${id}:0`) {
523
+ throw new Error('C2 initial dispatch result invalid')
524
+ }
525
+ } })
526
+ const result = prepared.commit()
527
+ this.#retainActiveRun(scope, result, receiptId, jobReceiptIds, batchReceiptIds)
528
+ if (this.#journal.snapshot().eventCount >= this.#maxEventCount ||
529
+ this.#journal.byteLength >= this.#maxJournalBytes) this.#sealForCapacity()
530
+ return { ...result, jobReceiptIds, batchReceiptIds }
531
+ } catch (error) { this.close(); throw error }
532
+ }
533
+
534
+ // The model process may only start after its dispatch effect has an fsynced
535
+ // attempt. A trusted runner hook confirms acceptance before any turn event
536
+ // reaches Engine. Losing either side leaves an ambiguous attempt closed on
537
+ // restart; no process relaunch is inferred from a pending receipt.
538
+ beginRunnerStart({ receiptId, chatId, turnToken, generation,
539
+ verifyGeneration } = {}) {
540
+ const before = this.#completionCapacity()
541
+ if (!RECEIPT_ID.test(receiptId) || typeof chatId !== 'string' || !chatId ||
542
+ !Number.isSafeInteger(turnToken) || turnToken <= 0 ||
543
+ !GENERATION.test(generation) || typeof verifyGeneration !== 'function' ||
544
+ verifyGeneration.constructor?.name === 'AsyncFunction') {
545
+ throw new TypeError('C2 runner start identity invalid')
546
+ }
547
+ const witness = this.#activeRuns.get(chatId)
548
+ const receipt = before.receipts.pending.find(item => item.id === receiptId)
549
+ const effectId = witness?.dispatchEffectId
550
+ const descriptor = effectId && this.#journal.effectDescriptor(effectId)
551
+ if (!receipt || receipt.chatId !== chatId || witness?.receiptId !== receiptId ||
552
+ witness?.turnToken !== turnToken ||
553
+ !this.#matchesRunningReceipts(before, witness, chatId, receiptId, turnToken) ||
554
+ descriptor?.domain !== 'atomicHandoff' || descriptor.type !== 'dispatch' ||
555
+ descriptor.scope !== chatId || descriptor.receiptId !== receiptId ||
556
+ descriptor.turnToken !== turnToken ||
557
+ this.#journal.effectStatus(effectId) !== 'pending' ||
558
+ before.state.lanes.turn?.[chatId]?.active !== turnToken ||
559
+ before.state.lanes.handoff?.[chatId]?.phase !== 'running' ||
560
+ before.state.lanes.handoff[chatId].turnToken !== turnToken ||
561
+ !isDeepStrictEqual(this.#coordinator.snapshot(), before.state) ||
562
+ verifyGeneration({ chatId, receiptId, turnToken, generation }) !== true) {
563
+ throw new Error('C2 runner start lacks exact live dispatch authority')
564
+ }
565
+ const attemptedAt = this.#now()
566
+ if (!Number.isSafeInteger(attemptedAt) || attemptedAt <= 0) {
567
+ throw new TypeError('C2 runner start time invalid')
568
+ }
569
+ const registrationId = callbackRegistrationId({ receiptId, generation,
570
+ domain: 'atomicHandoff', kind: 'runnerAccepted' })
571
+ try {
572
+ this.#journal.registerCallback({ id: registrationId, receiptId, generation,
573
+ domain: 'atomicHandoff', kind: 'runnerAccepted', ordinal: 0,
574
+ registeredAt: attemptedAt })
575
+ this.#journal.beginEffectAttempt({ effectId, attemptedAt })
576
+ } catch (error) { this.close(); throw error }
577
+
578
+ let phase = 'attempting'
579
+ const onSpawnFailed = (failure) => {
580
+ if (phase !== 'attempting' || failure?.backend !== 'codex-exec' ||
581
+ failure.code !== 'ENOENT' || failure.pid !== null ||
582
+ !isDeepStrictEqual(witness.jobReceiptIds, [receiptId]) ||
583
+ !isDeepStrictEqual(witness.batchReceiptIds, [receiptId]) ||
584
+ verifyGeneration({ chatId, receiptId, turnToken, generation }) !== true ||
585
+ this.#assertWriterLock?.() !== true) {
586
+ throw new Error('C2 runner not-started lacks exact single-receipt witness')
587
+ }
588
+ const current = this.#journal.snapshot()
589
+ if (current.state.lanes.turn?.[chatId]?.active !== turnToken ||
590
+ current.state.lanes.handoff?.[chatId]?.phase !== 'running' ||
591
+ this.#journal.effectStatus(effectId) !== 'attempting' ||
592
+ !isDeepStrictEqual(this.#coordinator.snapshot(), current.state)) {
593
+ throw new Error('C2 runner not-started authority changed')
594
+ }
595
+ try {
596
+ const now = this.#now()
597
+ const evidenceId = this.#journal.recordRunnerNotStarted({ receiptId, chatId,
598
+ turnToken, generation, dispatchEffectId: effectId,
599
+ resolvedAt: Number.isSafeInteger(now) && now >= attemptedAt ? now : attemptedAt })
600
+ phase = 'notStarted'
601
+ const eventId = `sbot-c2-runner-not-started-commit-v1:${receiptId}:${turnToken}:${generation}`
602
+ const result = this.#coordinator.transition({ id: eventId,
603
+ domain: 'runnerStartFailure', scope: chatId,
604
+ payload: { type: 'abortUnstartedDispatch', receiptId, turnToken,
605
+ runnerGeneration: generation, dispatchEffectId: effectId,
606
+ notStartedEvidenceId: evidenceId },
607
+ validate: candidate => {
608
+ if (candidate.effects.length ||
609
+ !isDeepStrictEqual(candidate.decision,
610
+ { abandonedReceiptId: receiptId, requiresStartupRecovery: true,
611
+ scope: chatId, dispatchEffectId: effectId })) {
612
+ throw new Error('C2 runner not-started decision invalid')
613
+ }
614
+ } })
615
+ if (result.decision.requiresStartupRecovery !== true) {
616
+ throw new Error('C2 runner not-started recovery not required')
617
+ }
618
+ this.close()
619
+ return true
620
+ } catch (error) { this.close(); throw error }
621
+ }
622
+ const markUnknown = () => {
623
+ if (phase !== 'attempting') return false
624
+ phase = 'unknown'
625
+ try {
626
+ const now = this.#now()
627
+ this.#journal.recordEffectOutcome({ effectId, status: 'unknown',
628
+ resolvedAt: Number.isSafeInteger(now) && now >= attemptedAt ? now : attemptedAt })
629
+ } finally { this.close() }
630
+ return true
631
+ }
632
+ const onTurnAccepted = (accepted) => {
633
+ if (phase !== 'attempting') throw new Error('C2 runner start callback already resolved')
634
+ const registered = this.#journal.lookupRegistration(registrationId)
635
+ const current = this.#journal.snapshot()
636
+ const active = this.#activeRuns.get(chatId)
637
+ const validChannel = ['claude-code', 'codex-exec', 'codex-app-server']
638
+ .includes(accepted?.channel)
639
+ const validIdentity = typeof accepted?.sessionId === 'string' &&
640
+ accepted.sessionId.length > 0 &&
641
+ (accepted.channel !== 'codex-app-server' ||
642
+ typeof accepted.turnId === 'string' && accepted.turnId.length > 0)
643
+ if (!validChannel || !validIdentity || !this.#effectAuthorityReady ||
644
+ registered?.generation !== generation || registered?.receiptId !== receiptId ||
645
+ active?.receiptId !== receiptId || active.turnToken !== turnToken ||
646
+ current.state.lanes.turn?.[chatId]?.active !== turnToken ||
647
+ current.state.lanes.handoff?.[chatId]?.phase !== 'running' ||
648
+ current.state.lanes.handoff[chatId].turnToken !== turnToken ||
649
+ verifyGeneration({ chatId, receiptId, turnToken, generation }) !== true) {
650
+ markUnknown()
651
+ throw new Error('C2 runner acceptance differs from live dispatch')
652
+ }
653
+ const now = this.#now()
654
+ try {
655
+ this.#journal.recordEffectOutcome({ effectId, status: 'acked',
656
+ resolvedAt: Number.isSafeInteger(now) && now >= attemptedAt ? now : attemptedAt,
657
+ registrationId })
658
+ phase = 'acked'
659
+ return { effectId, status: 'acked' }
660
+ } catch (error) { this.close(); throw error }
661
+ }
662
+ return { effectId, onTurnAccepted, onSpawnFailed, markUnknown }
663
+ }
664
+
665
+ // A trusted runner wrapper calls this only after its process/turn close is
666
+ // observed. The synchronous host witness is not persisted verbatim: only
667
+ // exact receipt/generation/turn/accepted-dispatch identity is fsynced. A
668
+ // future global-fault transition may require this ID, but must separately
669
+ // reconcile every external effect before mutating active lanes.
670
+ confirmRunnerClosed({ receiptId, chatId, turnToken, generation,
671
+ verifyGeneration, assertRunnerClosed } = {}) {
672
+ const before = this.#completionCapacity()
673
+ if (!RECEIPT_ID.test(receiptId) || typeof chatId !== 'string' || !chatId ||
674
+ !Number.isSafeInteger(turnToken) || turnToken <= 0 ||
675
+ !GENERATION.test(generation) || typeof verifyGeneration !== 'function' ||
676
+ verifyGeneration.constructor?.name === 'AsyncFunction' ||
677
+ typeof assertRunnerClosed !== 'function' ||
678
+ assertRunnerClosed.constructor?.name === 'AsyncFunction') {
679
+ throw new TypeError('C2 runner close identity invalid')
680
+ }
681
+ const witness = this.#activeRuns.get(chatId)
682
+ const effectId = witness?.dispatchEffectId
683
+ const receipt = before.receipts.pending.find(item => item.id === receiptId)
684
+ const descriptor = effectId && this.#journal.effectDescriptor(effectId)
685
+ if (!receipt || receipt.chatId !== chatId || witness?.receiptId !== receiptId ||
686
+ witness?.turnToken !== turnToken ||
687
+ !this.#matchesRunningReceipts(before, witness, chatId, receiptId, turnToken) ||
688
+ descriptor?.domain !== 'atomicHandoff' || descriptor.type !== 'dispatch' ||
689
+ descriptor.scope !== chatId || descriptor.receiptId !== receiptId ||
690
+ descriptor.turnToken !== turnToken ||
691
+ this.#journal.effectStatus(effectId) !== 'acked' ||
692
+ before.state.lanes.turn?.[chatId]?.active !== turnToken ||
693
+ before.state.lanes.handoff?.[chatId]?.phase !== 'running' ||
694
+ before.state.lanes.handoff[chatId].turnToken !== turnToken ||
695
+ !isDeepStrictEqual(this.#coordinator.snapshot(), before.state) ||
696
+ verifyGeneration({ chatId, receiptId, turnToken, generation }) !== true ||
697
+ assertRunnerClosed({ chatId, receiptId, turnToken, generation }) !== true) {
698
+ throw new Error('C2 runner close lacks exact stopped runner authority')
699
+ }
700
+ try {
701
+ const closedAt = this.#now()
702
+ const evidenceId = this.#journal.recordRunnerClosed({ receiptId, chatId,
703
+ turnToken, generation, dispatchEffectId: effectId, closedAt })
704
+ if (this.#journal.byteLength >= this.#maxJournalBytes) this.#sealForCapacity()
705
+ return { evidenceId, receiptId, chatId, turnToken,
706
+ runnerGeneration: generation, runnerClosed: true }
707
+ } catch (error) { this.close(); throw error }
708
+ }
709
+
710
+ // One closed runner may write its process-local session ID only after a
711
+ // generation-bound reducer decision and fsynced effect attempts. Neither
712
+ // the session ID nor Store content enters the journal. A failed Store write
713
+ // or outcome ACK is ambiguous and seals this authority for reconciliation.
714
+ commitClosedRunnerSession({ receiptId, chatId, turnToken, generation,
715
+ sessionId = null, aborted, verifyGeneration, assertWriterLock, store,
716
+ usageDecision = null } = {}) {
717
+ const before = this.#completionCapacity()
718
+ if (!store || typeof store.setSessionAtomic !== 'function' ||
719
+ store.setSessionAtomic.constructor?.name === 'AsyncFunction' ||
720
+ typeof store.getSession !== 'function' ||
721
+ typeof assertWriterLock !== 'function' ||
722
+ assertWriterLock.constructor?.name === 'AsyncFunction' ||
723
+ (sessionId !== null && (typeof sessionId !== 'string' || !sessionId)) ||
724
+ (usageDecision !== null && (sessionId === null ||
725
+ typeof store.getSessionStats !== 'function' || !usageDecision ||
726
+ usageDecision.write !== true || !usageDecision.stats ||
727
+ typeof usageDecision.stats !== 'object' ||
728
+ usageDecision.stats.sessionId !== sessionId))) {
729
+ throw new TypeError('C2 closed session needs synchronous Store authority')
730
+ }
731
+ let locked = false
732
+ try { locked = assertWriterLock() === true } catch { /* lost lock is fatal */ }
733
+ if (!locked) {
734
+ this.close()
735
+ throw new Error('C2 closed session lost single-writer lock')
736
+ }
737
+ const witness = this.#activeRuns.get(chatId)
738
+ if (witness?.receiptId !== receiptId || witness.turnToken !== turnToken ||
739
+ !isDeepStrictEqual(this.#coordinator.snapshot(), before.state)) {
740
+ throw new Error('C2 closed session lacks live running dispatch')
741
+ }
742
+ const prepared = prepareClosedSessionEvent({ journal: this.#journal,
743
+ receiptId, chatId, turnToken, generation, sessionId, aborted,
744
+ verifyGeneration })
745
+ let result
746
+ let attemptedAt = null
747
+ const attempted = []
748
+ try {
749
+ result = this.#coordinator.prepare(prepared).commit()
750
+ if (assertWriterLock() !== true) {
751
+ throw new Error('C2 closed session lost single-writer lock')
752
+ }
753
+ if (result.effects.length) {
754
+ this.#externalEffectInFlight = true
755
+ attemptedAt = this.#now()
756
+ if (!Number.isSafeInteger(attemptedAt) || attemptedAt <= 0) {
757
+ throw new TypeError('C2 closed session attempt time invalid')
758
+ }
759
+ for (const effect of result.effects) {
760
+ this.#journal.beginEffectAttempt({ effectId: effect.effectId, attemptedAt })
761
+ attempted.push(effect.effectId)
762
+ }
763
+ if (assertWriterLock() !== true) {
764
+ throw new Error('C2 closed session lost single-writer lock')
765
+ }
766
+ const saved = store.setSessionAtomic(chatId, sessionId,
767
+ { tainted: aborted === true,
768
+ ...(usageDecision === null ? {} : { sessionStats: usageDecision.stats }) })
769
+ if (saved && typeof saved.then === 'function' ||
770
+ store.getSession(chatId) !== sessionId ||
771
+ (usageDecision !== null &&
772
+ !isDeepStrictEqual(store.getSessionStats?.(chatId), usageDecision.stats)) ||
773
+ assertWriterLock() !== true) {
774
+ throw new Error('C2 closed session Store write unconfirmed')
775
+ }
776
+ const resolvedAt = Math.max(attemptedAt, this.#now())
777
+ for (const effectId of attempted) {
778
+ this.#journal.recordEffectOutcome({ effectId, status: 'acked', resolvedAt })
779
+ }
780
+ }
781
+ if (this.#journal.snapshot().eventCount >= this.#maxEventCount ||
782
+ this.#journal.byteLength >= this.#maxJournalBytes) this.#sealForCapacity()
783
+ return { cursor: result.cursor, eventId: result.eventId,
784
+ effectIds: result.effects.map(effect => effect.effectId),
785
+ sessionWritten: sessionId !== null }
786
+ } catch (error) {
787
+ if (attemptedAt !== null) {
788
+ for (const effectId of attempted) {
789
+ if (this.#journal.effectStatus(effectId) !== 'attempting') continue
790
+ try {
791
+ this.#journal.recordEffectOutcome({ effectId, status: 'unknown',
792
+ resolvedAt: Math.max(attemptedAt, this.#now()) })
793
+ } catch { /* a failed journal write remains ambiguous on disk */ }
794
+ }
795
+ }
796
+ this.close()
797
+ throw error
798
+ } finally { this.#externalEffectInFlight = false }
799
+ }
800
+
801
+ registerReleaseRequestedCallback({ receiptId, generation, registeredAt } = {}) {
802
+ this.#completionCapacity()
803
+ const id = callbackRegistrationId({ receiptId, generation,
804
+ domain: 'atomicHandoff', kind: 'releaseRequestedCommit' })
805
+ const result = this.#journal.registerCallback({ id, receiptId, generation,
806
+ domain: 'atomicHandoff', kind: 'releaseRequestedCommit', ordinal: 0,
807
+ registeredAt })
808
+ if (this.#journal.byteLength >= this.#maxJournalBytes) this.#sealForCapacity()
809
+ return result
810
+ }
811
+
812
+ // The runner completion callback is registered before exposure. Its request
813
+ // invalidates the old turn and enters releasing in one durable transition.
814
+ commitReleaseRequested({ receiptId, generation, turnToken, payloadHandles } = {}) {
815
+ this.#completionCapacity()
816
+ if (!RECEIPT_ID.test(receiptId) || !GENERATION.test(generation) ||
817
+ !Number.isSafeInteger(turnToken) || turnToken <= 0) {
818
+ throw new TypeError('C2 release request identity invalid')
819
+ }
820
+ const before = this.#journal.snapshot()
821
+ const receipt = before.receipts.pending.find(item => item.id === receiptId)
822
+ if (!receipt) throw new Error('C2 release request lacks pending turn receipt')
823
+ const scope = receipt.chatId
824
+ const handoff = before.state.lanes.handoff?.[scope]
825
+ const turn = before.state.lanes.turn?.[scope]
826
+ const terminal = this.#terminalAwaitingSettlement.get(scope)
827
+ if (terminal && (terminal.receiptId !== receiptId ||
828
+ terminal.turnToken !== turnToken || !terminal.ready)) {
829
+ throw new Error('C2 terminal effect must finish before release request')
830
+ }
831
+ if (handoff?.phase !== 'running' || handoff.turnToken !== turnToken ||
832
+ turn?.active !== turnToken ||
833
+ !isDeepStrictEqual(this.#coordinator.snapshot(), before.state)) {
834
+ throw new Error('C2 release request is not durably running')
835
+ }
836
+ const { accepted } = queuedPayloads(before, scope, payloadHandles)
837
+ const id = callbackEventId({ receiptId, generation,
838
+ domain: 'atomicHandoff', kind: 'releaseRequestedCommit',
839
+ lookupReceipt: key => before.receipts.pending.find(item => item.id === key) || null,
840
+ lookupRegistration: key => this.#journal.lookupRegistration(key) })
841
+ if (this.#journal.hasEvent(id)) throw new Error('C2 release request already committed')
842
+ try {
843
+ const prepared = this.#coordinator.prepare({ id, domain: 'atomicHandoff', scope,
844
+ payload: { type: 'releaseRequestedCommit', turnToken,
845
+ acceptedReceiptIds: accepted },
846
+ validate: candidate => {
847
+ const effect = candidate.effects?.[0]
848
+ if (candidate.cursor !== before.cursor + 1 || candidate.state?.seq !== candidate.cursor ||
849
+ candidate.state.lanes.turn?.[scope]?.active !== null ||
850
+ candidate.state.lanes.handoff?.[scope]?.phase !== 'releasing' ||
851
+ candidate.state.lanes.handoff[scope].turnToken !== turnToken ||
852
+ !isDeepStrictEqual(candidate.state.lanes.handoff[scope].acceptedReceiptIds, accepted) ||
853
+ !isDeepStrictEqual(candidate.state.lanes.queue?.[scope]?.queue,
854
+ before.state.lanes.queue?.[scope]?.queue) ||
855
+ candidate.decision?.turnToken !== turnToken ||
856
+ !isDeepStrictEqual(candidate.decision?.acceptedReceiptIds, accepted) ||
857
+ candidate.effects.length !== 1 || effect.type !== 'attemptQueueRelease' ||
858
+ effect.origin !== 'handoff' ||
859
+ effect.effectId !== `sbot-c2-effect-v1:${Buffer.byteLength(id)}:${id}:0`) {
860
+ throw new Error('C2 release request result invalid')
861
+ }
862
+ } })
863
+ const result = prepared.commit()
864
+ this.#activeRuns.delete(scope)
865
+ if (this.#journal.snapshot().eventCount >= this.#maxEventCount ||
866
+ this.#journal.byteLength >= this.#maxJournalBytes) this.#sealForCapacity()
867
+ return result
868
+ } catch (error) { this.close(); throw error }
869
+ }
870
+
871
+ registerHandoffBeginCallback({ receiptId, generation, registeredAt } = {}) {
872
+ this.#completionCapacity()
873
+ const id = callbackRegistrationId({ receiptId, generation,
874
+ domain: 'atomicHandoff', kind: 'handoffBeginCommit' })
875
+ const result = this.#journal.registerCallback({ id, receiptId, generation,
876
+ domain: 'atomicHandoff', kind: 'handoffBeginCommit', ordinal: 0,
877
+ registeredAt })
878
+ if (this.#journal.byteLength >= this.#maxJournalBytes) this.#sealForCapacity()
879
+ return result
880
+ }
881
+
882
+ // A successor is only eligible in the same process that committed its
883
+ // queue release. The journal cannot recreate the removed jobs' raw bodies;
884
+ // a reopen has no scheduled capability and must interrupt instead.
885
+ commitHandoffBegin({ receiptId, generation, priorTurnToken, payloadHandles } = {}) {
886
+ this.#completionCapacity()
887
+ if (!RECEIPT_ID.test(receiptId) || !GENERATION.test(generation) ||
888
+ !Number.isSafeInteger(priorTurnToken) || priorTurnToken <= 0 ||
889
+ !(payloadHandles instanceof C2PayloadHandles) || payloadHandles.blocked ||
890
+ typeof payloadHandles.peekHandoff !== 'function') {
891
+ throw new TypeError('C2 successor dispatch identity or handles invalid')
892
+ }
893
+ const before = this.#journal.snapshot()
894
+ const receipt = before.receipts.pending.find(item => item.id === receiptId)
895
+ if (!receipt) throw new Error('C2 successor dispatch lacks pending receipt')
896
+ const scope = receipt.chatId
897
+ if (this.#terminalAwaitingSettlement.has(scope)) {
898
+ throw new Error('C2 prior terminal receipt has not settled')
899
+ }
900
+ const scheduled = this.#scheduledHandoffs.get(scope)
901
+ const handoff = before.state.lanes.handoff?.[scope]
902
+ if (!scheduled || scheduled.nextJobId !== receiptId ||
903
+ scheduled.cursor > before.cursor || !this.#journal.hasEvent(scheduled.eventId) ||
904
+ scheduled.priorTurnToken !== priorTurnToken ||
905
+ handoff?.phase !== 'handoff' || handoff.turnToken !== priorTurnToken ||
906
+ handoff.nextJobId !== receiptId || before.state.lanes.turn?.[scope]?.active !== null ||
907
+ this.#journal.effectStatus(scheduled.scheduleEffectId) !== 'acked' ||
908
+ !isDeepStrictEqual(this.#coordinator.snapshot(), before.state)) {
909
+ throw new Error('C2 successor dispatch lacks committed scheduled handoff')
910
+ }
911
+ payloadHandles.peekHandoff(receiptId)
912
+ const liveIds = new Set(payloadHandles.snapshot().receiptIds)
913
+ if (!scheduled.jobReceiptIds.length ||
914
+ !scheduled.jobReceiptIds.every(id => liveIds.has(id)) ||
915
+ !isDeepStrictEqual(handoff.acceptedReceiptIds, scheduled.batchReceiptIds)) {
916
+ throw new Error('C2 successor dispatch payload handles missing')
917
+ }
918
+ const id = callbackEventId({ receiptId, generation,
919
+ domain: 'atomicHandoff', kind: 'handoffBeginCommit',
920
+ lookupReceipt: key => before.receipts.pending.find(item => item.id === key) || null,
921
+ lookupRegistration: key => this.#journal.lookupRegistration(key) })
922
+ if (this.#journal.hasEvent(id)) throw new Error('C2 successor dispatch already committed')
923
+ try {
924
+ const prepared = this.#coordinator.prepare({ id, domain: 'atomicHandoff', scope,
925
+ payload: { type: 'handoffBeginCommit', priorTurnToken, nextJobId: receiptId,
926
+ batchReceiptIds: [...scheduled.batchReceiptIds] },
927
+ validate: candidate => {
928
+ const token = candidate.decision?.turnToken
929
+ const [clear, dispatch] = candidate.effects || []
930
+ if (candidate.cursor !== before.cursor + 1 || candidate.state?.seq !== candidate.cursor ||
931
+ !Number.isSafeInteger(token) || token <= priorTurnToken ||
932
+ candidate.decision.nextJobId !== receiptId ||
933
+ !isDeepStrictEqual(candidate.decision.batchReceiptIds,
934
+ scheduled.batchReceiptIds) ||
935
+ candidate.state.lanes.turn?.[scope]?.active !== token ||
936
+ candidate.state.lanes.handoff?.[scope]?.phase !== 'running' ||
937
+ candidate.state.lanes.handoff[scope].turnToken !== token ||
938
+ candidate.state.lanes.handoff[scope].nextJobId !== null ||
939
+ !isDeepStrictEqual(candidate.state.lanes.handoff[scope].acceptedReceiptIds,
940
+ scheduled.batchReceiptIds.length > 1 ? scheduled.batchReceiptIds : []) ||
941
+ !isDeepStrictEqual(candidate.state.lanes.queue?.[scope]?.queue,
942
+ before.state.lanes.queue?.[scope]?.queue) ||
943
+ candidate.effects.length !== 2 || clear.type !== 'clearHandoff' ||
944
+ clear.origin !== 'handoff' || dispatch.type !== 'dispatch' ||
945
+ dispatch.origin !== 'handoff' || dispatch.jobId !== receiptId ||
946
+ dispatch.turnToken !== token ||
947
+ [clear, dispatch].some((effect, index) =>
948
+ effect.effectId !== `sbot-c2-effect-v1:${Buffer.byteLength(id)}:${id}:${index}`)) {
949
+ throw new Error('C2 successor dispatch result invalid')
950
+ }
951
+ } })
952
+ const result = prepared.commit()
953
+ this.#retainActiveRun(scope, result, receiptId,
954
+ scheduled.jobReceiptIds, scheduled.batchReceiptIds)
955
+ this.#scheduledHandoffs.delete(scope)
956
+ if (this.#journal.snapshot().eventCount >= this.#maxEventCount ||
957
+ this.#journal.byteLength >= this.#maxJournalBytes) this.#sealForCapacity()
958
+ return { ...result, jobReceiptIds: [...scheduled.jobReceiptIds],
959
+ batchReceiptIds: [...scheduled.batchReceiptIds] }
960
+ } catch (error) { this.close(); throw error }
961
+ }
962
+
963
+ // Register the callback identity before exposing a runner-close or queue
964
+ // release callback. Registration alone does not advance reducer state.
965
+ registerQueueReleaseCallback({ receiptId, generation, registeredAt } = {}) {
966
+ this.#completionCapacity()
967
+ const id = callbackRegistrationId({ receiptId, generation,
968
+ domain: 'atomicHandoff', kind: 'queueReleaseCommit' })
969
+ const result = this.#journal.registerCallback({ id, receiptId, generation,
970
+ domain: 'atomicHandoff', kind: 'queueReleaseCommit', ordinal: 0,
971
+ registeredAt })
972
+ if (this.#journal.byteLength >= this.#maxJournalBytes) this.#sealForCapacity()
973
+ return result
974
+ }
975
+
976
+ // The handoff lane must already be durably in `releasing` for this token.
977
+ // This call commits one queue+handoff transition. It never dispatches or
978
+ // acknowledges the returned effects; the caller owns those later stages.
979
+ commitQueueRelease({ receiptId, generation, turnToken, paused, payloadHandles } = {}) {
980
+ this.#completionCapacity()
981
+ if (!RECEIPT_ID.test(receiptId) || !GENERATION.test(generation) ||
982
+ !Number.isSafeInteger(turnToken) || turnToken <= 0 || typeof paused !== 'boolean') {
983
+ throw new TypeError('C2 atomic release identity invalid')
984
+ }
985
+ const before = this.#journal.snapshot()
986
+ const receipt = before.receipts.pending.find(item => item.id === receiptId)
987
+ if (!receipt) throw new Error('C2 atomic release lacks pending turn receipt')
988
+ const scope = receipt.chatId
989
+ const handoff = before.state.lanes.handoff?.[scope]
990
+ const terminal = this.#terminalAwaitingSettlement.get(scope)
991
+ if (terminal && (terminal.receiptId !== receiptId ||
992
+ terminal.turnToken !== turnToken || !terminal.ready)) {
993
+ throw new Error('C2 terminal effect must finish before queue release')
994
+ }
995
+ if (!handoff || handoff.phase !== 'releasing' || handoff.turnToken !== turnToken ||
996
+ !isDeepStrictEqual(this.#coordinator.snapshot(), before.state)) {
997
+ throw new Error('C2 atomic release is not durably releasing')
998
+ }
999
+ const { queue, accepted } = queuedPayloads(before, scope, payloadHandles)
1000
+ if (!isDeepStrictEqual(handoff.acceptedReceiptIds, accepted)) {
1001
+ throw new Error('C2 atomic release accepted receipts differ from queue')
1002
+ }
1003
+ const releasedCount = paused ? queue.length : queueRunLen(queue)
1004
+ const remaining = queue.slice(releasedCount)
1005
+ const id = callbackEventId({ receiptId, generation,
1006
+ domain: 'atomicHandoff', kind: 'queueReleaseCommit',
1007
+ lookupReceipt: key => before.receipts.pending.find(item => item.id === key) || null,
1008
+ lookupRegistration: key => this.#journal.lookupRegistration(key) })
1009
+ if (this.#journal.hasEvent(id)) throw new Error('C2 atomic release already committed')
1010
+ try {
1011
+ const prepared = this.#coordinator.prepare({ id, domain: 'atomicHandoff', scope,
1012
+ payload: { type: 'queueReleaseCommit', turnToken, paused },
1013
+ validate: candidate => {
1014
+ if (candidate.cursor !== before.cursor + 1 ||
1015
+ candidate.state?.seq !== candidate.cursor ||
1016
+ !isDeepStrictEqual(candidate.state.lanes.queue?.[scope]?.queue, remaining) ||
1017
+ candidate.decision?.queueRunLen !== queueRunLen(remaining) ||
1018
+ !isDeepStrictEqual(candidate.decision?.jobReceiptIds,
1019
+ paused ? [] : queue.slice(0, releasedCount).map(item =>
1020
+ item.id.slice('receipt:job:'.length))) ||
1021
+ !isDeepStrictEqual(candidate.decision?.batchReceiptIds,
1022
+ paused ? [] : queueCohortReceiptIds(queue.slice(0, releasedCount))) ||
1023
+ candidate.decision?.visibleQueue !== remaining.filter(item =>
1024
+ !item.quiet && !item.polled).length ||
1025
+ !Array.isArray(candidate.effects) ||
1026
+ candidate.effects.some((effect, index) =>
1027
+ effect.effectId !== `sbot-c2-effect-v1:${Buffer.byteLength(id)}:${id}:${index}`)) {
1028
+ throw new Error('C2 atomic release result invalid')
1029
+ }
1030
+ const release = candidate.decision.queueRelease
1031
+ const next = candidate.decision.nextJobId
1032
+ if (paused) {
1033
+ if (next !== null || candidate.state.lanes.handoff?.[scope]?.phase !== 'idle' ||
1034
+ candidate.state.lanes.handoff[scope].nextJobId !== null ||
1035
+ candidate.effects.some(effect => effect.type !== 'dropQueued' || effect.origin !== 'queue') ||
1036
+ (queue.length > 0 && (candidate.effects.length !== 1 ||
1037
+ release?.type !== 'dropQueued' ||
1038
+ !isDeepStrictEqual(release.batch, queue.map(item => item.id)))) ||
1039
+ (queue.length === 0 && (release !== null || candidate.effects.length))) {
1040
+ throw new Error('C2 atomic paused release mismatch')
1041
+ }
1042
+ } else if (next !== null) {
1043
+ const anchor = release?.anchor
1044
+ if (release?.type !== 'dispatch' ||
1045
+ !queue.slice(0, releasedCount).some(item => item.id === anchor) ||
1046
+ next !== anchor.slice('receipt:job:'.length) ||
1047
+ candidate.state.lanes.handoff?.[scope]?.phase !== 'handoff' ||
1048
+ candidate.state.lanes.handoff[scope].nextJobId !== next ||
1049
+ !isDeepStrictEqual(candidate.state.lanes.handoff[scope].acceptedReceiptIds,
1050
+ candidate.decision.batchReceiptIds) ||
1051
+ !isDeepStrictEqual(candidate.effects.map(effect =>
1052
+ [effect.type, effect.origin, effect.jobId ?? null]),
1053
+ [['holdHandoff', 'handoff', null], ['scheduleDispatch', 'handoff', next]])) {
1054
+ throw new Error('C2 atomic next job mismatch')
1055
+ }
1056
+ } else if (release !== null || candidate.effects.length ||
1057
+ candidate.state.lanes.handoff?.[scope]?.phase !== 'idle') {
1058
+ throw new Error('C2 atomic empty release mismatch')
1059
+ }
1060
+ } })
1061
+ const result = prepared.commit()
1062
+ const terminal = this.#terminalAwaitingSettlement.get(scope)
1063
+ if (terminal?.receiptId === receiptId && terminal.turnToken === turnToken) {
1064
+ terminal.releaseCommitted = true
1065
+ }
1066
+ const nextJobId = result.decision.nextJobId
1067
+ if (nextJobId) {
1068
+ const effect = result.effects.find(item =>
1069
+ item.type === 'scheduleDispatch' && item.jobId === nextJobId)
1070
+ if (!effect) throw new Error('C2 atomic release schedule effect missing')
1071
+ this.#scheduledHandoffs.set(scope, { eventId: id, cursor: result.cursor,
1072
+ nextJobId, priorTurnToken: turnToken, scheduleEffectId: effect.effectId,
1073
+ jobReceiptIds: queue.slice(0, releasedCount).map(item =>
1074
+ item.id.slice('receipt:job:'.length)),
1075
+ batchReceiptIds: queueCohortReceiptIds(queue.slice(0, releasedCount)) })
1076
+ } else this.#scheduledHandoffs.delete(scope)
1077
+ if (this.#journal.snapshot().eventCount >= this.#maxEventCount ||
1078
+ this.#journal.byteLength >= this.#maxJournalBytes) this.#sealForCapacity()
1079
+ return { ...result, releasedJobReceiptIds: queue.slice(0, releasedCount).map(item =>
1080
+ item.id.slice('receipt:job:'.length)) }
1081
+ } catch (error) {
1082
+ this.close()
1083
+ throw error
1084
+ }
1085
+ }
1086
+
1087
+ // Isolated v2 terminal boundary. The callback is registered before the
1088
+ // runner is exposed, and only this process can prove which pending receipt
1089
+ // was actually dispatched. No text or reply context enters the journal.
1090
+ registerTerminalCallback({ receiptId, generation, registeredAt } = {}) {
1091
+ this.#completionCapacity()
1092
+ const id = callbackRegistrationId({ receiptId, generation,
1093
+ domain: 'terminal', kind: 'finalizeEffectCommit' })
1094
+ const result = this.#journal.registerCallback({ id, receiptId, generation,
1095
+ domain: 'terminal', kind: 'finalizeEffectCommit', ordinal: 0, registeredAt })
1096
+ if (this.#journal.byteLength >= this.#maxJournalBytes) this.#sealForCapacity()
1097
+ return result
1098
+ }
1099
+
1100
+ commitTerminalEffect({ receiptId, generation, turnToken, payload } = {}) {
1101
+ this.#completionCapacity()
1102
+ if (!RECEIPT_ID.test(receiptId) || !GENERATION.test(generation) ||
1103
+ !Number.isSafeInteger(turnToken) || turnToken <= 0 ||
1104
+ !payload || typeof payload !== 'object' || Array.isArray(payload) ||
1105
+ payload.type !== 'finalizeEffectCommit' || payload.receiptId !== receiptId ||
1106
+ payload.turnToken !== turnToken) {
1107
+ throw new TypeError('C2 terminal identity invalid')
1108
+ }
1109
+ const before = this.#journal.snapshot()
1110
+ const receipt = before.receipts.pending.find(item => item.id === receiptId)
1111
+ const scope = receipt?.chatId
1112
+ const witness = this.#activeRuns.get(scope)
1113
+ const batchReceiptIds = witness?.batchReceiptIds?.length > 1
1114
+ ? witness.batchReceiptIds : null
1115
+ if (!receipt || payload.quiet !== receipt.quiet ||
1116
+ witness?.receiptId !== receiptId || witness.turnToken !== turnToken ||
1117
+ !isDeepStrictEqual(before.state.lanes.handoff?.[scope]?.acceptedReceiptIds,
1118
+ batchReceiptIds || []) ||
1119
+ (batchReceiptIds
1120
+ ? !isDeepStrictEqual(payload.batchReceiptIds, batchReceiptIds) ||
1121
+ payload.replyTargetReceiptId !== receiptId ||
1122
+ batchReceiptIds.some(id =>
1123
+ before.receipts.pending.find(item => item.id === id)?.chatId !== scope)
1124
+ : payload.batchReceiptIds !== undefined ||
1125
+ payload.replyTargetReceiptId !== undefined) ||
1126
+ this.#journal.effectStatus(witness.dispatchEffectId) !== 'acked' ||
1127
+ before.state.lanes.turn?.[scope]?.active !== turnToken ||
1128
+ before.state.lanes.handoff?.[scope]?.phase !== 'running' ||
1129
+ before.state.lanes.handoff[scope].turnToken !== turnToken ||
1130
+ !isDeepStrictEqual(this.#coordinator.snapshot(), before.state)) {
1131
+ throw new Error('C2 terminal lacks exact running receipt authority')
1132
+ }
1133
+ const id = callbackEventId({ receiptId, generation,
1134
+ domain: 'terminal', kind: 'finalizeEffectCommit',
1135
+ lookupReceipt: key => before.receipts.pending.find(item => item.id === key) || null,
1136
+ lookupRegistration: key => this.#journal.lookupRegistration(key) })
1137
+ if (this.#journal.hasEvent(id)) throw new Error('C2 terminal already committed')
1138
+ try {
1139
+ const prepared = this.#coordinator.prepare({ id, domain: 'terminal', scope,
1140
+ payload, validate: candidate => {
1141
+ const effect = candidate.effects?.at(-1)
1142
+ const descriptor = candidate.effectDescriptors?.at(-1)
1143
+ const terminal = candidate.state?.lanes?.terminal?.[scope]
1144
+ const expectedType = receipt.quiet || terminal?.outcome?.silent
1145
+ ? 'quietCompletion' : 'finalReply'
1146
+ if (candidate.cursor !== before.cursor + 1 ||
1147
+ candidate.state?.seq !== candidate.cursor ||
1148
+ terminal?.finalized !== true || terminal.pendingEffectIds?.at(-1) !== effect?.effectId ||
1149
+ !isDeepStrictEqual(terminal?.batchReceiptIds,
1150
+ batchReceiptIds || undefined) ||
1151
+ terminal?.replyTargetReceiptId !==
1152
+ (batchReceiptIds ? receiptId : undefined) ||
1153
+ !isDeepStrictEqual(candidate.decision?.batchReceiptIds,
1154
+ batchReceiptIds || undefined) ||
1155
+ candidate.decision?.replyTargetReceiptId !==
1156
+ (batchReceiptIds ? receiptId : undefined) ||
1157
+ !['quietCompletion', 'finalReply'].includes(effect?.type) ||
1158
+ effect.type !== expectedType || descriptor?.type !== effect.type ||
1159
+ descriptor.domain !== 'terminal' || descriptor.scope !== scope ||
1160
+ descriptor.turnToken !== turnToken || descriptor.receiptId !== receiptId ||
1161
+ descriptor.targetId !== null) {
1162
+ throw new Error('C2 terminal effect result invalid')
1163
+ }
1164
+ } })
1165
+ const result = prepared.commit()
1166
+ this.#terminalAwaitingSettlement.set(scope, { receiptId, turnToken,
1167
+ generation, effectId: result.effects.at(-1).effectId,
1168
+ batchReceiptIds: batchReceiptIds ? [...batchReceiptIds] : null,
1169
+ type: result.effects.at(-1).type, ready: false, releaseCommitted: false })
1170
+ if (this.#journal.snapshot().eventCount >= this.#maxEventCount ||
1171
+ this.#journal.byteLength >= this.#maxJournalBytes) this.#sealForCapacity()
1172
+ return { ...result, terminalEffectId: result.effects.at(-1).effectId }
1173
+ } catch (error) { this.close(); throw error }
1174
+ }
1175
+
1176
+ // The local Store/verification projection is an external effect too: its
1177
+ // attempt is durable before the host writes, and only a fresh process-local
1178
+ // claim may turn the observed write into an ACK.
1179
+ performLocalTerminalEffect({ effect, receiptId, chatId, turnToken,
1180
+ generation, verifyGeneration, assertWriterLock, applyLocalEffect } = {}) {
1181
+ const before = this.#completionCapacity()
1182
+ const effectId = effect?.effectId
1183
+ const descriptor = this.#journal.effectDescriptor(effectId)
1184
+ const capability = this.#terminalAwaitingSettlement.get(chatId)
1185
+ const terminal = before.state.lanes.terminal?.[chatId]
1186
+ if (!effect || typeof effect !== 'object' || Array.isArray(effect) ||
1187
+ !['clearTainted', 'clearExhausted', 'verifyTurn'].includes(effect.type) ||
1188
+ !RECEIPT_ID.test(receiptId || '') || !GENERATION.test(generation || '') ||
1189
+ typeof chatId !== 'string' || !chatId ||
1190
+ !Number.isSafeInteger(turnToken) || turnToken <= 0 ||
1191
+ typeof verifyGeneration !== 'function' ||
1192
+ verifyGeneration.constructor?.name === 'AsyncFunction' ||
1193
+ typeof assertWriterLock !== 'function' ||
1194
+ assertWriterLock.constructor?.name === 'AsyncFunction' ||
1195
+ typeof applyLocalEffect !== 'function' ||
1196
+ typeof applyLocalEffect.claim !== 'function' ||
1197
+ applyLocalEffect.constructor?.name === 'AsyncFunction' ||
1198
+ assertWriterLock() !== true ||
1199
+ verifyGeneration({ chatId, receiptId, turnToken, generation }) !== true ||
1200
+ before.receipts.pending.find(item => item.id === receiptId)?.chatId !== chatId ||
1201
+ before.state.lanes.turn?.[chatId]?.active !== turnToken ||
1202
+ before.state.lanes.handoff?.[chatId]?.phase !== 'running' ||
1203
+ before.state.lanes.handoff[chatId].turnToken !== turnToken ||
1204
+ capability?.receiptId !== receiptId || capability.turnToken !== turnToken ||
1205
+ capability.generation !== generation || capability.ready ||
1206
+ terminal?.finalized !== true ||
1207
+ !terminal.pendingEffectIds?.includes(effectId) ||
1208
+ descriptor?.domain !== 'terminal' || descriptor.scope !== chatId ||
1209
+ descriptor.type !== effect.type || descriptor.receiptId !== receiptId ||
1210
+ descriptor.turnToken !== turnToken ||
1211
+ this.#journal.effectStatus(effectId) !== 'pending') {
1212
+ throw new Error('C2 local effect lacks exact live authority')
1213
+ }
1214
+ const attemptedAt = this.#now()
1215
+ if (!Number.isSafeInteger(attemptedAt) || attemptedAt <= 0) {
1216
+ throw new TypeError('C2 local effect time invalid')
1217
+ }
1218
+ this.#externalEffectInFlight = true
1219
+ try {
1220
+ this.#journal.beginEffectAttempt({ effectId, attemptedAt })
1221
+ let confirmed = false
1222
+ try {
1223
+ applyLocalEffect.claim(effectId)
1224
+ confirmed = applyLocalEffect(effect) === true &&
1225
+ assertWriterLock() === true &&
1226
+ verifyGeneration({ chatId, receiptId, turnToken, generation }) === true
1227
+ } catch { /* A write may have happened; its outcome is ambiguous. */ }
1228
+ const now = this.#now()
1229
+ const resolvedAt = Number.isSafeInteger(now) && now >= attemptedAt
1230
+ ? now : attemptedAt
1231
+ this.#journal.recordEffectOutcome({ effectId,
1232
+ status: confirmed ? 'acked' : 'unknown', resolvedAt })
1233
+ if (!confirmed) {
1234
+ throw new Error('C2 local effect outcome unknown; manual recovery required')
1235
+ }
1236
+ return { effectId, status: 'acked' }
1237
+ } catch (error) { this.close(); throw error }
1238
+ finally { this.#externalEffectInFlight = false }
1239
+ }
1240
+
1241
+ // Classify a closed runner that cannot be delivered. This is an internal
1242
+ // zero-effect transition: the receipt and running turn remain unresolved,
1243
+ // and this process closes ingress for explicit fault recovery. No error
1244
+ // text, session ID, or reply target is written into the journal.
1245
+ commitUnresolvedClassification({ receiptId, generation, turnToken, status,
1246
+ payload, verifyGeneration, assertWriterLock } = {}) {
1247
+ this.#completionCapacity()
1248
+ if (!RECEIPT_ID.test(receiptId || '') || !GENERATION.test(generation || '') ||
1249
+ !Number.isSafeInteger(turnToken) || turnToken <= 0 ||
1250
+ !['error', 'empty', 'stopped'].includes(status) ||
1251
+ !payload || typeof payload !== 'object' || Array.isArray(payload) ||
1252
+ payload.type !== 'finalizeEffectCommit' || payload.receiptId !== receiptId ||
1253
+ payload.turnToken !== turnToken ||
1254
+ typeof verifyGeneration !== 'function' ||
1255
+ verifyGeneration.constructor?.name === 'AsyncFunction' ||
1256
+ typeof assertWriterLock !== 'function' ||
1257
+ assertWriterLock.constructor?.name === 'AsyncFunction') {
1258
+ throw new TypeError('C2 unresolved classification needs closed runner identity')
1259
+ }
1260
+ try {
1261
+ const before = this.#journal.snapshot()
1262
+ const receipt = before.receipts.pending.find(item => item.id === receiptId)
1263
+ const scope = receipt?.chatId
1264
+ const witness = this.#activeRuns.get(scope)
1265
+ const closeEvidenceId = `sbot-c2-runner-closed-v1:${receiptId}:${generation}`
1266
+ const proof = this.#journal.lookupRunnerClosed(closeEvidenceId)
1267
+ const dispatch = proof && this.#journal.effectDescriptor(proof.dispatchEffectId)
1268
+ const registrationId = callbackRegistrationId({ receiptId, generation,
1269
+ domain: 'terminal', kind: 'finalizeEffectCommit' })
1270
+ const registration = this.#journal.lookupRegistration(registrationId)
1271
+ const cohortIds = witness?.batchReceiptIds || []
1272
+ const batch = cohortIds.length > 1
1273
+ if (this.blocked || assertWriterLock() !== true || !receipt ||
1274
+ payload.quiet !== receipt.quiet ||
1275
+ witness?.receiptId !== receiptId || witness.turnToken !== turnToken ||
1276
+ !witness.jobReceiptIds.includes(receiptId) ||
1277
+ !isDeepStrictEqual(witness.jobReceiptIds, cohortIds) ||
1278
+ !isDeepStrictEqual(before.state.lanes.handoff?.[scope]?.acceptedReceiptIds,
1279
+ batch ? cohortIds : []) ||
1280
+ (batch
1281
+ ? !isDeepStrictEqual(payload.batchReceiptIds, cohortIds) ||
1282
+ payload.replyTargetReceiptId !== receiptId ||
1283
+ cohortIds.some(id =>
1284
+ before.receipts.pending.find(item => item.id === id)?.chatId !== scope)
1285
+ : payload.batchReceiptIds !== undefined ||
1286
+ payload.replyTargetReceiptId !== undefined) ||
1287
+ proof?.receiptId !== receiptId || proof.chatId !== scope ||
1288
+ proof.turnToken !== turnToken || proof.generation !== generation ||
1289
+ dispatch?.type !== 'dispatch' || dispatch.scope !== scope ||
1290
+ dispatch.receiptId !== receiptId || dispatch.turnToken !== turnToken ||
1291
+ this.#journal.effectStatus(proof.dispatchEffectId) !== 'acked' ||
1292
+ registration?.receiptId !== receiptId ||
1293
+ registration.generation !== generation ||
1294
+ registration.domain !== 'terminal' ||
1295
+ registration.kind !== 'finalizeEffectCommit' ||
1296
+ !this.#journal.hasEvent(`sbot-c2-session-close-v1:${receiptId}:${generation}`) ||
1297
+ before.state.lanes.session?.[scope]?.closeSeen !== true ||
1298
+ before.state.lanes.turn?.[scope]?.active !== turnToken ||
1299
+ before.state.lanes.handoff?.[scope]?.phase !== 'running' ||
1300
+ before.state.lanes.handoff[scope].turnToken !== turnToken ||
1301
+ !isDeepStrictEqual(this.#coordinator.snapshot(), before.state) ||
1302
+ [...before.effects].some(([effectId, effectStatus]) =>
1303
+ cohortIds.includes(this.#journal.effectDescriptor(effectId)?.receiptId) &&
1304
+ effectStatus !== 'acked') ||
1305
+ verifyGeneration({ chatId: scope, receiptId, turnToken,
1306
+ generation }) !== true) {
1307
+ throw new Error('C2 unresolved runner lacks settled durable proof')
1308
+ }
1309
+ const eventId = `sbot-c2-unresolved-v1:${receiptId}:${turnToken}:${generation}:${status}`
1310
+ if (this.#journal.hasEvent(eventId)) throw new Error('C2 unresolved already classified')
1311
+ const terminalPayload = { ...payload, type: 'finalize' }
1312
+ delete terminalPayload.receiptId
1313
+ delete terminalPayload.turnToken
1314
+ delete terminalPayload.quiet
1315
+ if (engineTerminalStep({ finalized: false, outcome: null },
1316
+ terminalPayload).state.outcome.status !== status) {
1317
+ throw new Error('C2 unresolved status differs from runner projection')
1318
+ }
1319
+ const prepared = this.#coordinator.prepare({ id: eventId,
1320
+ domain: 'terminal', scope,
1321
+ payload: { ...payload, type: 'classifyUnresolved',
1322
+ runnerGeneration: generation,
1323
+ runnerClosed: true, closeEvidenceId },
1324
+ validate: candidate => {
1325
+ if (candidate.eventId !== eventId ||
1326
+ candidate.cursor !== before.cursor + 1 ||
1327
+ candidate.decision?.outcome?.status !== status ||
1328
+ candidate.decision?.sealed !== true ||
1329
+ candidate.decision?.requiresFaultRecovery !== true ||
1330
+ !isDeepStrictEqual(candidate.decision?.batchReceiptIds,
1331
+ batch ? cohortIds : undefined) ||
1332
+ candidate.decision?.replyTargetReceiptId !==
1333
+ (batch ? receiptId : undefined) ||
1334
+ !Array.isArray(candidate.effects) || candidate.effects.length !== 0 ||
1335
+ !Array.isArray(candidate.effectDescriptors) ||
1336
+ candidate.effectDescriptors.length !== 0 ||
1337
+ !isDeepStrictEqual(candidate.state?.lanes, before.state.lanes)) {
1338
+ throw new Error('C2 unresolved classification changed running state')
1339
+ }
1340
+ } })
1341
+ if (assertWriterLock() !== true) {
1342
+ prepared.cancel()
1343
+ throw new Error('C2 unresolved lost single-writer lock before commit')
1344
+ }
1345
+ const result = prepared.commit()
1346
+ if (!this.#journal.hasEvent(eventId) ||
1347
+ !isDeepStrictEqual(this.#journal.snapshot().state, result.state)) {
1348
+ throw new Error('C2 unresolved durable classification unverified')
1349
+ }
1350
+ this.close()
1351
+ return Object.freeze({ eventId, cursor: result.cursor, status,
1352
+ ingressClosed: true, deliveryWithheld: true })
1353
+ } catch (error) { this.close(); throw error }
1354
+ }
1355
+
1356
+ // A recalled queued message has no outward result. The queue decision and
1357
+ // quiet receipt settlement share one journal record; only after fsync may
1358
+ // its process-local body be discarded. This deliberately supports one
1359
+ // ordinary queued job, not a coalesced batch or synthetic source.
1360
+ commitQueuedCancellation({ receiptId, chatId, control, payloadHandles,
1361
+ assertWriterLock } = {}) {
1362
+ this.#completionCapacity()
1363
+ if (!RECEIPT_ID.test(receiptId || '') || typeof chatId !== 'string' || !chatId ||
1364
+ !['recall', 'stop'].includes(control) ||
1365
+ !(payloadHandles instanceof C2PayloadHandles) ||
1366
+ typeof assertWriterLock !== 'function' ||
1367
+ assertWriterLock.constructor?.name === 'AsyncFunction') {
1368
+ throw new TypeError('C2 queued recall identity invalid')
1369
+ }
1370
+ try {
1371
+ const before = this.#journal.snapshot()
1372
+ const receipt = before.receipts.pending.find(item => item.id === receiptId)
1373
+ const queue = before.state.lanes.queue?.[chatId]?.queue
1374
+ const handle = payloadHandles.requireQueued(receiptId)
1375
+ const index = queue?.findIndex(item => item.id === `receipt:job:${receiptId}`)
1376
+ if (this.blocked || assertWriterLock() !== true ||
1377
+ receipt?.chatId !== chatId || receipt.source !== 'message' ||
1378
+ index < 0 || !Array.isArray(queue) || queue[index]?.batch ||
1379
+ handle?.msg?._c2ReceiptId !== receiptId ||
1380
+ handle.msg.msgId !== receipt.sourceId ||
1381
+ !isDeepStrictEqual(this.#coordinator.snapshot(), before.state) ||
1382
+ [...before.effects].some(([id, status]) =>
1383
+ this.#journal.effectDescriptor(id)?.receiptId === receiptId &&
1384
+ status !== 'acked') ||
1385
+ this.#journal.noticeStatus(receiptId) !== null) {
1386
+ throw new Error('C2 queued recall differs from durable queue')
1387
+ }
1388
+ const settledAt = this.#now()
1389
+ if (!Number.isSafeInteger(settledAt) || settledAt <= 0) {
1390
+ throw new TypeError('C2 queued recall time invalid')
1391
+ }
1392
+ const eventId = `sbot-c2-queue-drop-v1:${receiptId}:${control}`
1393
+ const receipts = {
1394
+ pending: before.receipts.pending.filter(item => item.id !== receiptId),
1395
+ completed: [...before.receipts.completed, { id: receiptId, settledAt }],
1396
+ }
1397
+ const expectedQueue = [...queue.slice(0, index), ...queue.slice(index + 1)]
1398
+ const prepared = this.#coordinator.prepare({ id: eventId, domain: 'queue',
1399
+ scope: chatId, payload: { type: 'dropQueuedReceipt', receiptId,
1400
+ control }, receipts,
1401
+ validate: candidate => {
1402
+ if (candidate.cursor !== before.cursor + 1 ||
1403
+ candidate.decision?.receiptId !== receiptId ||
1404
+ candidate.decision?.control !== control ||
1405
+ candidate.decision?.position !== 'job' ||
1406
+ candidate.decision?.quietSettlement !== true ||
1407
+ candidate.decision?.sealed !== false ||
1408
+ candidate.effects?.length !== 0 ||
1409
+ candidate.effectDescriptors?.length !== 0 ||
1410
+ !isDeepStrictEqual(candidate.state?.lanes?.queue?.[chatId]?.queue,
1411
+ expectedQueue)) {
1412
+ throw new Error('C2 queued recall decision invalid')
1413
+ }
1414
+ } })
1415
+ if (assertWriterLock() !== true) {
1416
+ prepared.cancel()
1417
+ throw new Error('C2 queued recall lost writer lock')
1418
+ }
1419
+ const result = prepared.commit()
1420
+ payloadHandles.dropDurablyCancelled(receiptId, eventId, control)
1421
+ if (this.#journal.snapshot().eventCount >= this.#maxEventCount ||
1422
+ this.#journal.byteLength >= this.#maxJournalBytes) this.#sealForCapacity()
1423
+ return { eventId, cursor: result.cursor, receiptId, status: 'cancelled',
1424
+ control }
1425
+ } catch (error) { this.close(); throw error }
1426
+ }
1427
+
1428
+ #cardEffects(before, receiptId, chatId, turnToken, cleanupEffectId = null) {
1429
+ const prior = []
1430
+ for (const [id, status] of before.effects) {
1431
+ const descriptor = this.#journal.effectDescriptor(id)
1432
+ if (descriptor?.receiptId !== receiptId) continue
1433
+ if (status !== 'acked' || descriptor.scope !== chatId ||
1434
+ descriptor.turnToken !== turnToken && descriptor.domain === 'outbound' ||
1435
+ ['outbound', 'terminal'].includes(descriptor.domain) &&
1436
+ (descriptor.domain !== 'outbound' ||
1437
+ !CARD_OPERATION_KINDS.has(descriptor.type))) {
1438
+ throw new Error('C2 card cleanup has unsettled or foreign effect')
1439
+ }
1440
+ if (descriptor.domain === 'outbound') {
1441
+ if (id === cleanupEffectId && descriptor.type === 'message.delete') continue
1442
+ if (descriptor.type === 'message.delete') {
1443
+ throw new Error('C2 card cleanup has unexpected delete effect')
1444
+ }
1445
+ prior.push(id)
1446
+ }
1447
+ }
1448
+ return prior
1449
+ }
1450
+
1451
+ #cardCleanupIdentity({ receiptId, chatId, turnToken, generation, target }) {
1452
+ if (!RECEIPT_ID.test(receiptId || '') || !GENERATION.test(generation || '') ||
1453
+ !Number.isSafeInteger(turnToken) || turnToken <= 0 ||
1454
+ target?.chatId !== chatId) throw new TypeError('C2 card cleanup identity invalid')
1455
+ const cardTargetDigestValue = cardTargetDigest(target)
1456
+ const { operationId: deleteOperationId, eventId } = cardOperationIdentity({
1457
+ receiptId, turnToken, generation,
1458
+ operation: { kind: 'message.delete', operationId: `${receiptId}:message.delete`,
1459
+ chatId, cardId: target.cardId, msgId: target.msgId, invoke: () => {} } })
1460
+ const cleanupEffectId = `sbot-c2-effect-v1:${Buffer.byteLength(eventId)}:${eventId}:0`
1461
+ return { receiptId, turnToken, runnerGeneration: generation,
1462
+ cardTargetDigest: cardTargetDigestValue, deleteOperationId,
1463
+ cleanupEffectId }
1464
+ }
1465
+
1466
+ commitCardCleanupIntent({ receiptId, chatId, turnToken, generation, target,
1467
+ verifyGeneration, assertWriterLock } = {}) {
1468
+ const before = this.#completionCapacity()
1469
+ try {
1470
+ const identity = this.#cardCleanupIdentity({ receiptId, chatId,
1471
+ turnToken, generation, target })
1472
+ const witness = this.#activeRuns.get(chatId)
1473
+ const session = before.state.lanes.session?.[chatId]
1474
+ const prior = this.#cardEffects(before, receiptId, chatId, turnToken)
1475
+ const cardOperations = prior.length
1476
+ if (this.blocked || assertWriterLock?.() !== true ||
1477
+ verifyGeneration?.({ chatId, receiptId, turnToken, generation }) !== true ||
1478
+ before.receipts.pending.find(item => item.id === receiptId)?.chatId !== chatId ||
1479
+ witness?.receiptId !== receiptId || witness.turnToken !== turnToken ||
1480
+ !witness.jobReceiptIds.includes(receiptId) ||
1481
+ !isDeepStrictEqual(witness.jobReceiptIds, witness.batchReceiptIds) ||
1482
+ !isDeepStrictEqual(before.state.lanes.handoff?.[chatId]?.acceptedReceiptIds,
1483
+ witness.batchReceiptIds.length > 1 ? witness.batchReceiptIds : []) ||
1484
+ witness.batchReceiptIds.some(id =>
1485
+ before.receipts.pending.find(item => item.id === id)?.chatId !== chatId) ||
1486
+ before.state.lanes.turn?.[chatId]?.active !== turnToken ||
1487
+ before.state.lanes.handoff?.[chatId]?.phase !== 'running' ||
1488
+ session?.epoch !== turnToken || session.closeSeen !== true ||
1489
+ session.settled !== false ||
1490
+ !this.#journal.hasEvent(`sbot-c2-session-close-v1:${receiptId}:${generation}`) ||
1491
+ !this.#journal.lookupRunnerClosed(`sbot-c2-runner-closed-v1:${receiptId}:${generation}`) ||
1492
+ cardOperations <= 0 ||
1493
+ before.state.lanes.cardCleanup?.[chatId] !== undefined ||
1494
+ !isDeepStrictEqual(this.#coordinator.snapshot(), before.state)) {
1495
+ throw new Error('C2 card cleanup intent lacks closed turn and ACKed card')
1496
+ }
1497
+ const eventId = `sbot-c2-card-cleanup-intent-v1:${receiptId}:${turnToken}:${generation}:${identity.cardTargetDigest}:${identity.deleteOperationId}`
1498
+ const lane = { phase: 'intent', ...identity, cardOperations }
1499
+ const prepared = this.#coordinator.prepare({ id: eventId, domain: 'outbound',
1500
+ scope: chatId, payload: { type: 'cardCleanupIntent', receiptId, turnToken,
1501
+ runnerGeneration: generation, cardTargetDigest: identity.cardTargetDigest,
1502
+ deleteOperationId: identity.deleteOperationId, cardOperations },
1503
+ validate: candidate => {
1504
+ if (candidate.cursor !== before.cursor + 1 ||
1505
+ candidate.effects?.length !== 0 ||
1506
+ candidate.effectDescriptors?.length !== 0 ||
1507
+ !isDeepStrictEqual(candidate.state?.lanes?.cardCleanup?.[chatId], lane) ||
1508
+ candidate.decision?.cleanupEffectId !== identity.cleanupEffectId) {
1509
+ throw new Error('C2 card cleanup intent decision invalid')
1510
+ }
1511
+ } })
1512
+ if (assertWriterLock() !== true) {
1513
+ prepared.cancel(); throw new Error('C2 card cleanup intent lost writer lock')
1514
+ }
1515
+ prepared.commit()
1516
+ return { ...identity, cardOperations, targetDigest: identity.cardTargetDigest }
1517
+ } catch (error) { this.close(); throw error }
1518
+ }
1519
+
1520
+ commitCardCleanupConfirmed({ receiptId, chatId, turnToken, generation, target,
1521
+ proof, verifyGeneration, assertWriterLock } = {}) {
1522
+ const before = this.#completionCapacity()
1523
+ try {
1524
+ const identity = this.#cardCleanupIdentity({ receiptId, chatId,
1525
+ turnToken, generation, target })
1526
+ const lane = before.state.lanes.cardCleanup?.[chatId]
1527
+ const prior = this.#cardEffects(before, receiptId, chatId, turnToken,
1528
+ identity.cleanupEffectId)
1529
+ const descriptor = this.#journal.effectDescriptor(identity.cleanupEffectId)
1530
+ if (this.blocked || assertWriterLock?.() !== true ||
1531
+ verifyGeneration?.({ chatId, receiptId, turnToken, generation }) !== true ||
1532
+ !isDeepStrictEqual(proof, { ...identity,
1533
+ cardOperations: prior.length, targetDigest: identity.cardTargetDigest }) ||
1534
+ !isDeepStrictEqual(lane, { phase: 'intent', ...identity,
1535
+ cardOperations: prior.length }) ||
1536
+ descriptor?.domain !== 'outbound' || descriptor.type !== 'message.delete' ||
1537
+ descriptor.scope !== chatId || descriptor.receiptId !== receiptId ||
1538
+ descriptor.turnToken !== turnToken ||
1539
+ this.#journal.effectStatus(identity.cleanupEffectId) !== 'acked' ||
1540
+ !isDeepStrictEqual(this.#coordinator.snapshot(), before.state)) {
1541
+ throw new Error('C2 card cleanup delete ACK proof invalid')
1542
+ }
1543
+ const eventId = `sbot-c2-card-cleanup-confirm-v1:${receiptId}:${turnToken}:${generation}:${identity.cardTargetDigest}:${identity.deleteOperationId}`
1544
+ const payload = { type: 'cardCleanupConfirmed', receiptId, turnToken,
1545
+ runnerGeneration: generation, cardTargetDigest: identity.cardTargetDigest,
1546
+ deleteOperationId: identity.deleteOperationId,
1547
+ cardOperations: prior.length, cleanupEffectId: identity.cleanupEffectId,
1548
+ cleanupAcked: true }
1549
+ const prepared = this.#coordinator.prepare({ id: eventId, domain: 'outbound',
1550
+ scope: chatId, payload, validate: candidate => {
1551
+ if (candidate.cursor !== before.cursor + 1 ||
1552
+ candidate.effects?.length !== 0 ||
1553
+ candidate.effectDescriptors?.length !== 0 ||
1554
+ !isDeepStrictEqual(candidate.state?.lanes?.cardCleanup?.[chatId],
1555
+ { ...lane, phase: 'acked' }) ||
1556
+ candidate.decision?.cleanupAcked !== true) {
1557
+ throw new Error('C2 card cleanup confirmation decision invalid')
1558
+ }
1559
+ } })
1560
+ if (assertWriterLock() !== true) {
1561
+ prepared.cancel(); throw new Error('C2 card cleanup confirmation lost writer lock')
1562
+ }
1563
+ prepared.commit()
1564
+ return { cardOperations: prior.length,
1565
+ cardTargetDigest: identity.cardTargetDigest,
1566
+ cleanupEffectId: identity.cleanupEffectId }
1567
+ } catch (error) { this.close(); throw error }
1568
+ }
1569
+
1570
+ #cancellationCardFields(before, { receiptId, chatId, turnToken, generation,
1571
+ cardCleanup }) {
1572
+ const lane = before.state.lanes.cardCleanup?.[chatId]
1573
+ if (cardCleanup === null || cardCleanup === undefined) {
1574
+ if (lane !== undefined ||
1575
+ this.#cardEffects(before, receiptId, chatId, turnToken).length !== 0) {
1576
+ throw new Error('C2 cancellation has visible card without cleanup proof')
1577
+ }
1578
+ return { cardOperations: 0 }
1579
+ }
1580
+ const { cardOperations, cardTargetDigest: digest, cleanupEffectId } = cardCleanup
1581
+ const descriptor = this.#journal.effectDescriptor(cleanupEffectId)
1582
+ const prior = this.#cardEffects(before, receiptId, chatId, turnToken,
1583
+ cleanupEffectId)
1584
+ if (!Number.isSafeInteger(cardOperations) || cardOperations <= 0 ||
1585
+ prior.length !== cardOperations ||
1586
+ !isDeepStrictEqual(lane, { phase: 'acked', receiptId, turnToken,
1587
+ runnerGeneration: generation, cardTargetDigest: digest,
1588
+ deleteOperationId: lane?.deleteOperationId,
1589
+ cleanupEffectId, cardOperations }) ||
1590
+ descriptor?.domain !== 'outbound' || descriptor.type !== 'message.delete' ||
1591
+ descriptor.scope !== chatId || descriptor.receiptId !== receiptId ||
1592
+ descriptor.turnToken !== turnToken ||
1593
+ this.#journal.effectStatus(cleanupEffectId) !== 'acked') {
1594
+ throw new Error('C2 cancellation card cleanup proof invalid')
1595
+ }
1596
+ return { cardOperations, cardTargetDigest: digest, cleanupEffectId }
1597
+ }
1598
+
1599
+ #matchesRunningReceipts(before, witness, chatId, receiptId, turnToken) {
1600
+ const handoff = before.state.lanes.handoff?.[chatId]
1601
+ const ids = witness?.batchReceiptIds
1602
+ return witness?.receiptId === receiptId && witness.turnToken === turnToken &&
1603
+ handoff?.phase === 'running' && handoff.turnToken === turnToken &&
1604
+ Array.isArray(ids) && ids.length >= 1 && ids.includes(receiptId) &&
1605
+ new Set(ids).size === ids.length &&
1606
+ ids.every(id => RECEIPT_ID.test(id) &&
1607
+ before.receipts.pending.some(item => item.id === id &&
1608
+ item.chatId === chatId && item.source === 'message')) &&
1609
+ isDeepStrictEqual(handoff.acceptedReceiptIds,
1610
+ ids.length > 1 ? ids : [])
1611
+ }
1612
+
1613
+ #runningControlReceipts(before, witness, chatId, receiptId, turnToken) {
1614
+ if (!this.#matchesRunningReceipts(before, witness, chatId, receiptId,
1615
+ turnToken)) {
1616
+ throw new Error('C2 running control lacks exact batch receipts')
1617
+ }
1618
+ const ids = witness.batchReceiptIds
1619
+ return { ids, batchFields: ids.length > 1 ? { batchReceiptIds: ids } : {} }
1620
+ }
1621
+
1622
+ // A running recall is allowed only after the exact child has stopped and
1623
+ // the dispatch/session closure are durable. Visible card effects require
1624
+ // the separately ACKed deletion proof before quiet settlement.
1625
+ commitRunningCancellation({ receiptId, chatId, turnToken, generation, control,
1626
+ cardCleanup = null, verifyGeneration, assertWriterLock } = {}) {
1627
+ this.#completionCapacity()
1628
+ if (!RECEIPT_ID.test(receiptId || '') || typeof chatId !== 'string' || !chatId ||
1629
+ !Number.isSafeInteger(turnToken) || turnToken <= 0 ||
1630
+ !['recall', 'stop'].includes(control) ||
1631
+ !GENERATION.test(generation || '') ||
1632
+ typeof verifyGeneration !== 'function' ||
1633
+ verifyGeneration.constructor?.name === 'AsyncFunction' ||
1634
+ typeof assertWriterLock !== 'function' ||
1635
+ assertWriterLock.constructor?.name === 'AsyncFunction') {
1636
+ throw new TypeError('C2 running recall identity invalid')
1637
+ }
1638
+ try {
1639
+ const before = this.#journal.snapshot()
1640
+ const receipt = before.receipts.pending.find(item => item.id === receiptId)
1641
+ const witness = this.#activeRuns.get(chatId)
1642
+ const { ids: runningIds, batchFields } = this.#runningControlReceipts(
1643
+ before, witness, chatId, receiptId, turnToken)
1644
+ const closeEvidenceId = `sbot-c2-runner-closed-v1:${receiptId}:${generation}`
1645
+ const proof = this.#journal.lookupRunnerClosed(closeEvidenceId)
1646
+ const dispatch = proof && this.#journal.effectDescriptor(proof.dispatchEffectId)
1647
+ const session = before.state.lanes.session?.[chatId]
1648
+ const cardFields = this.#cancellationCardFields(before, { receiptId,
1649
+ chatId, turnToken, generation, cardCleanup })
1650
+ if (this.blocked || assertWriterLock() !== true ||
1651
+ receipt?.source !== 'message' || receipt.chatId !== chatId ||
1652
+ witness?.receiptId !== receiptId || witness.turnToken !== turnToken ||
1653
+ proof?.receiptId !== receiptId || proof.chatId !== chatId ||
1654
+ proof.turnToken !== turnToken || proof.generation !== generation ||
1655
+ dispatch?.type !== 'dispatch' || dispatch.scope !== chatId ||
1656
+ dispatch.receiptId !== receiptId || dispatch.turnToken !== turnToken ||
1657
+ this.#journal.effectStatus(proof.dispatchEffectId) !== 'acked' ||
1658
+ !this.#journal.hasEvent(`sbot-c2-session-close-v1:${receiptId}:${generation}`) ||
1659
+ session?.epoch !== turnToken || session.closeSeen !== true ||
1660
+ session.settled !== false ||
1661
+ before.state.lanes.turn?.[chatId]?.active !== turnToken ||
1662
+ before.state.lanes.handoff?.[chatId]?.phase !== 'running' ||
1663
+ before.state.lanes.handoff[chatId].turnToken !== turnToken ||
1664
+ !isDeepStrictEqual(this.#coordinator.snapshot(), before.state) ||
1665
+ [...before.effects].some(([id, status]) => {
1666
+ const descriptor = this.#journal.effectDescriptor(id)
1667
+ return runningIds.includes(descriptor?.receiptId) &&
1668
+ (status !== 'acked' || descriptor.domain === 'terminal')
1669
+ }) ||
1670
+ verifyGeneration({ chatId, receiptId, turnToken, generation }) !== true) {
1671
+ throw new Error('C2 running recall lacks closed, unsettled turn proof')
1672
+ }
1673
+ const settledAt = this.#now()
1674
+ if (!Number.isSafeInteger(settledAt) || settledAt <= 0) {
1675
+ throw new TypeError('C2 running recall time invalid')
1676
+ }
1677
+ const eventId = `sbot-c2-cancel-v1:${receiptId}:${turnToken}:${generation}:${control}`
1678
+ const settled = new Set(runningIds)
1679
+ const receipts = {
1680
+ pending: before.receipts.pending.filter(item => !settled.has(item.id)),
1681
+ completed: [...before.receipts.completed,
1682
+ ...runningIds.map(id => ({ id, settledAt }))],
1683
+ }
1684
+ const prepared = this.#coordinator.prepare({ id: eventId,
1685
+ domain: 'terminal', scope: chatId,
1686
+ payload: { type: 'cancelRunningTurn', receiptId, turnToken,
1687
+ runnerGeneration: generation, runnerClosed: true,
1688
+ closeEvidenceId, control, ...cardFields, ...batchFields }, receipts,
1689
+ validate: candidate => {
1690
+ if (candidate.cursor !== before.cursor + 1 ||
1691
+ candidate.decision?.receiptId !== receiptId ||
1692
+ !isDeepStrictEqual(candidate.decision?.batchReceiptIds,
1693
+ batchFields.batchReceiptIds) ||
1694
+ candidate.decision?.turnToken !== turnToken ||
1695
+ candidate.decision?.control !== control ||
1696
+ candidate.decision?.quietSettlement !== true ||
1697
+ candidate.decision?.cardCleanupRequired !== false ||
1698
+ candidate.decision?.sealed !== false ||
1699
+ candidate.effects?.length !== 0 ||
1700
+ candidate.effectDescriptors?.length !== 0 ||
1701
+ candidate.state?.lanes?.turn?.[chatId]?.active !== null ||
1702
+ candidate.state?.lanes?.handoff?.[chatId]?.phase !== 'idle' ||
1703
+ candidate.state?.lanes?.session?.[chatId]?.settled !== true) {
1704
+ throw new Error('C2 running recall decision invalid')
1705
+ }
1706
+ } })
1707
+ if (assertWriterLock() !== true) {
1708
+ prepared.cancel()
1709
+ throw new Error('C2 running recall lost writer lock')
1710
+ }
1711
+ const result = prepared.commit()
1712
+ this.#activeRuns.delete(chatId)
1713
+ if (this.#journal.snapshot().eventCount >= this.#maxEventCount ||
1714
+ this.#journal.byteLength >= this.#maxJournalBytes) this.#sealForCapacity()
1715
+ return { eventId, cursor: result.cursor, receiptId, status: 'cancelled',
1716
+ control, cancelledReceiptIds: runningIds }
1717
+ } catch (error) { this.close(); throw error }
1718
+ }
1719
+
1720
+ // A hard timer has no outward acknowledgement. Its exact child closure,
1721
+ // pending receipt, ACKed effects and quiet settlement form one transition.
1722
+ commitHardTimeoutRelease({ receiptId, chatId, turnToken, generation,
1723
+ timeoutProof, cardCleanup = null, verifyGeneration, assertWriterLock } = {}) {
1724
+ const before = this.#completionCapacity()
1725
+ if (!RECEIPT_ID.test(receiptId || '') || typeof chatId !== 'string' || !chatId ||
1726
+ !Number.isSafeInteger(turnToken) || turnToken <= 0 ||
1727
+ !GENERATION.test(generation || '') ||
1728
+ typeof verifyGeneration !== 'function' ||
1729
+ typeof assertWriterLock !== 'function' ||
1730
+ timeoutProof?.receiptId !== receiptId ||
1731
+ timeoutProof.turnToken !== turnToken ||
1732
+ timeoutProof.generation !== generation ||
1733
+ !Number.isSafeInteger(timeoutProof.deadlineAt) ||
1734
+ !Number.isSafeInteger(timeoutProof.elapsedAt) ||
1735
+ timeoutProof.deadlineAt <= 0 ||
1736
+ timeoutProof.elapsedAt < timeoutProof.deadlineAt ||
1737
+ this.#now() < timeoutProof.deadlineAt) {
1738
+ throw new TypeError('C2 hard timeout timer proof invalid')
1739
+ }
1740
+ try {
1741
+ const receipt = before.receipts.pending.find(item => item.id === receiptId)
1742
+ const witness = this.#activeRuns.get(chatId)
1743
+ const { ids: runningIds, batchFields } = this.#runningControlReceipts(
1744
+ before, witness, chatId, receiptId, turnToken)
1745
+ const closeEvidenceId = `sbot-c2-runner-closed-v1:${receiptId}:${generation}`
1746
+ const proof = this.#journal.lookupRunnerClosed(closeEvidenceId)
1747
+ const dispatch = proof && this.#journal.effectDescriptor(proof.dispatchEffectId)
1748
+ const session = before.state.lanes.session?.[chatId]
1749
+ const cardFields = this.#cancellationCardFields(before, { receiptId,
1750
+ chatId, turnToken, generation, cardCleanup })
1751
+ if (this.blocked || assertWriterLock() !== true ||
1752
+ receipt?.source !== 'message' || receipt.chatId !== chatId ||
1753
+ witness?.receiptId !== receiptId || witness.turnToken !== turnToken ||
1754
+ proof?.receiptId !== receiptId || proof.chatId !== chatId ||
1755
+ proof.turnToken !== turnToken || proof.generation !== generation ||
1756
+ dispatch?.type !== 'dispatch' || dispatch.scope !== chatId ||
1757
+ dispatch.receiptId !== receiptId || dispatch.turnToken !== turnToken ||
1758
+ this.#journal.effectStatus(proof.dispatchEffectId) !== 'acked' ||
1759
+ !this.#journal.hasEvent(`sbot-c2-session-close-v1:${receiptId}:${generation}`) ||
1760
+ session?.epoch !== turnToken || session.closeSeen !== true ||
1761
+ session.settled !== false ||
1762
+ before.state.lanes.turn?.[chatId]?.active !== turnToken ||
1763
+ before.state.lanes.handoff?.[chatId]?.phase !== 'running' ||
1764
+ before.state.lanes.handoff[chatId].turnToken !== turnToken ||
1765
+ before.state.lanes.stopIntent?.[chatId] !== undefined ||
1766
+ !isDeepStrictEqual(this.#coordinator.snapshot(), before.state) ||
1767
+ [...before.effects].some(([id, status]) => {
1768
+ const descriptor = this.#journal.effectDescriptor(id)
1769
+ return runningIds.includes(descriptor?.receiptId) &&
1770
+ (status !== 'acked' || descriptor.domain === 'terminal')
1771
+ }) ||
1772
+ verifyGeneration({ chatId, receiptId, turnToken, generation }) !== true) {
1773
+ throw new Error('C2 hard timeout lacks closed reconciled turn proof')
1774
+ }
1775
+ const settledAt = this.#now()
1776
+ if (!Number.isSafeInteger(settledAt) || settledAt <= 0) {
1777
+ throw new TypeError('C2 hard timeout time invalid')
1778
+ }
1779
+ const eventId = `sbot-c2-hard-timeout-release-v1:${receiptId}:${turnToken}:${generation}`
1780
+ const queue = before.state.lanes.queue?.[chatId]?.queue
1781
+ const settled = new Set(runningIds)
1782
+ const receipts = { pending: before.receipts.pending.filter(item => !settled.has(item.id)),
1783
+ completed: [...before.receipts.completed,
1784
+ ...runningIds.map(id => ({ id, settledAt }))] }
1785
+ const prepared = this.#coordinator.prepare({ id: eventId,
1786
+ domain: 'terminal', scope: chatId,
1787
+ payload: { type: 'hardTimeoutReleaseCommit', receiptId, turnToken,
1788
+ runnerGeneration: generation, timeoutElapsed: true, runnerClosed: true,
1789
+ closeEvidenceId, externalEffectsReconciled: true, ...cardFields,
1790
+ ...batchFields },
1791
+ receipts, validate: candidate => {
1792
+ if (candidate.cursor !== before.cursor + 1 ||
1793
+ candidate.decision?.receiptId !== receiptId ||
1794
+ !isDeepStrictEqual(candidate.decision?.batchReceiptIds,
1795
+ batchFields.batchReceiptIds) ||
1796
+ candidate.decision?.turnToken !== turnToken ||
1797
+ candidate.decision?.outcome?.status !== 'timeout' ||
1798
+ candidate.decision?.outcome?.silent !== true ||
1799
+ candidate.decision?.quietSettlement !== true ||
1800
+ candidate.decision?.cardOperations !== cardFields.cardOperations ||
1801
+ candidate.decision?.sealed !== false ||
1802
+ candidate.effects?.length !== 0 ||
1803
+ candidate.effectDescriptors?.length !== 0 ||
1804
+ candidate.state?.lanes?.turn?.[chatId]?.active !== null ||
1805
+ candidate.state?.lanes?.handoff?.[chatId]?.phase !== 'idle' ||
1806
+ candidate.state?.lanes?.session?.[chatId]?.settled !== true ||
1807
+ !isDeepStrictEqual(candidate.state?.lanes?.queue?.[chatId]?.queue, queue)) {
1808
+ throw new Error('C2 hard timeout release decision invalid')
1809
+ }
1810
+ } })
1811
+ if (assertWriterLock() !== true) {
1812
+ prepared.cancel(); throw new Error('C2 hard timeout lost writer lock')
1813
+ }
1814
+ const result = prepared.commit()
1815
+ this.#activeRuns.delete(chatId)
1816
+ if (this.#journal.snapshot().eventCount >= this.#maxEventCount ||
1817
+ this.#journal.byteLength >= this.#maxJournalBytes) this.#sealForCapacity()
1818
+ return { eventId, cursor: result.cursor, receiptId, status: 'timeout',
1819
+ settledReceiptIds: runningIds }
1820
+ } catch (error) { this.close(); throw error }
1821
+ }
1822
+
1823
+ // Persist the separate /stop command receipt and its binding to the live
1824
+ // turn before asking the child to stop. A crash after this point leaves
1825
+ // startup closed rather than losing the command or replaying the runner.
1826
+ commitStopIntent({ controlReceipt, runningReceiptId, chatId, turnToken,
1827
+ generation, cardTarget = null, assertWriterLock } = {}) {
1828
+ const before = this.#capacity()
1829
+ const controlReceiptId = controlReceipt?.id
1830
+ if (!RECEIPT_ID.test(controlReceiptId || '') ||
1831
+ !RECEIPT_ID.test(runningReceiptId || '') ||
1832
+ controlReceiptId === runningReceiptId ||
1833
+ typeof chatId !== 'string' || !chatId ||
1834
+ !Number.isSafeInteger(turnToken) || turnToken <= 0 ||
1835
+ !GENERATION.test(generation || '') ||
1836
+ typeof assertWriterLock !== 'function' ||
1837
+ assertWriterLock.constructor?.name === 'AsyncFunction') {
1838
+ throw new TypeError('C2 stop intent identity invalid')
1839
+ }
1840
+ try {
1841
+ const running = before.receipts.pending.find(item => item.id === runningReceiptId)
1842
+ const witness = this.#activeRuns.get(chatId)
1843
+ const { ids: runningIds, batchFields } = this.#runningControlReceipts(
1844
+ before, witness, chatId, runningReceiptId, turnToken)
1845
+ const registered = this.#journal.lookupRegistration(callbackRegistrationId({
1846
+ receiptId: runningReceiptId, generation,
1847
+ domain: 'atomicHandoff', kind: 'runnerAccepted' }))
1848
+ const cardOperations = this.#cardEffects(before, runningReceiptId,
1849
+ chatId, turnToken).length
1850
+ if (this.blocked || assertWriterLock() !== true ||
1851
+ before.receipts.pending.some(item => item.id === controlReceiptId) ||
1852
+ before.receipts.completed.some(item => item.id === controlReceiptId) ||
1853
+ controlReceipt.source !== 'message' || controlReceipt.chatId !== chatId ||
1854
+ !/^om_[A-Za-z0-9_-]{1,124}$/.test(controlReceipt.sourceId || '') ||
1855
+ controlReceipt.quiet !== false ||
1856
+ running?.chatId !== chatId || running.source !== 'message' ||
1857
+ witness?.receiptId !== runningReceiptId || witness.turnToken !== turnToken ||
1858
+ registered?.receiptId !== runningReceiptId ||
1859
+ registered.generation !== generation ||
1860
+ this.#journal.effectStatus(witness.dispatchEffectId) !== 'acked' ||
1861
+ before.state.lanes.turn?.[chatId]?.active !== turnToken ||
1862
+ before.state.lanes.handoff?.[chatId]?.phase !== 'running' ||
1863
+ before.state.lanes.stopIntent?.[chatId] !== undefined ||
1864
+ (cardOperations > 0 &&
1865
+ (cardTarget?.chatId !== chatId || !cardTarget.cardId || !cardTarget.msgId)) ||
1866
+ [...before.effects].some(([id, status]) => {
1867
+ const descriptor = this.#journal.effectDescriptor(id)
1868
+ return runningIds.includes(descriptor?.receiptId) &&
1869
+ (status !== 'acked' || descriptor.domain === 'terminal')
1870
+ }) ||
1871
+ !isDeepStrictEqual(this.#coordinator.snapshot(), before.state)) {
1872
+ throw new Error('C2 stop intent differs from running durable turn')
1873
+ }
1874
+ const eventId = `sbot-c2-stop-intent-v1:${controlReceiptId}:${runningReceiptId}:${turnToken}:${generation}`
1875
+ const receipts = { pending: [...before.receipts.pending, controlReceipt],
1876
+ completed: [...before.receipts.completed] }
1877
+ const prepared = this.#coordinator.prepare({ id: eventId, domain: 'control',
1878
+ scope: chatId, payload: { type: 'stopIntent', controlReceiptId,
1879
+ runningReceiptId, turnToken, runnerGeneration: generation,
1880
+ cardOperations, ...batchFields }, receipts,
1881
+ validate: candidate => {
1882
+ if (candidate.cursor !== before.cursor + 1 ||
1883
+ candidate.effects?.length !== 0 ||
1884
+ candidate.effectDescriptors?.length !== 0 ||
1885
+ candidate.decision?.controlReceiptId !== controlReceiptId ||
1886
+ candidate.decision?.runningReceiptId !== runningReceiptId ||
1887
+ !isDeepStrictEqual(candidate.decision?.batchReceiptIds,
1888
+ batchFields.batchReceiptIds) ||
1889
+ candidate.decision?.turnToken !== turnToken ||
1890
+ !isDeepStrictEqual(candidate.state?.lanes?.stopIntent?.[chatId],
1891
+ { phase: 'intent', controlReceiptId, runningReceiptId,
1892
+ turnToken, runnerGeneration: generation, ...batchFields })) {
1893
+ throw new Error('C2 stop intent decision invalid')
1894
+ }
1895
+ } })
1896
+ if (assertWriterLock() !== true) {
1897
+ prepared.cancel()
1898
+ throw new Error('C2 stop intent lost writer lock')
1899
+ }
1900
+ const result = prepared.commit()
1901
+ return { eventId, cursor: result.cursor, controlReceiptId }
1902
+ } catch (error) { this.close(); throw error }
1903
+ }
1904
+
1905
+ commitStopAll({ controlReceiptId, runningReceiptId, chatId, turnToken,
1906
+ generation, queuedReceiptIds, payloadHandles, cardCleanup = null, verifyGeneration,
1907
+ assertWriterLock } = {}) {
1908
+ const before = this.#completionCapacity()
1909
+ if (!RECEIPT_ID.test(controlReceiptId || '') ||
1910
+ !RECEIPT_ID.test(runningReceiptId || '') ||
1911
+ controlReceiptId === runningReceiptId ||
1912
+ typeof chatId !== 'string' || !chatId ||
1913
+ !Number.isSafeInteger(turnToken) || turnToken <= 0 ||
1914
+ !GENERATION.test(generation || '') ||
1915
+ !Array.isArray(queuedReceiptIds) ||
1916
+ new Set(queuedReceiptIds).size !== queuedReceiptIds.length ||
1917
+ !(payloadHandles instanceof C2PayloadHandles) ||
1918
+ typeof verifyGeneration !== 'function' ||
1919
+ typeof assertWriterLock !== 'function') {
1920
+ throw new TypeError('C2 stop-all identity invalid')
1921
+ }
1922
+ try {
1923
+ const control = before.receipts.pending.find(item => item.id === controlReceiptId)
1924
+ const running = before.receipts.pending.find(item => item.id === runningReceiptId)
1925
+ const intent = before.state.lanes.stopIntent?.[chatId]
1926
+ const witness = this.#activeRuns.get(chatId)
1927
+ const { ids: runningIds, batchFields } = this.#runningControlReceipts(
1928
+ before, witness, chatId, runningReceiptId, turnToken)
1929
+ const closeEvidenceId = `sbot-c2-runner-closed-v1:${runningReceiptId}:${generation}`
1930
+ const proof = this.#journal.lookupRunnerClosed(closeEvidenceId)
1931
+ const dispatch = proof && this.#journal.effectDescriptor(proof.dispatchEffectId)
1932
+ const session = before.state.lanes.session?.[chatId]
1933
+ const cardFields = this.#cancellationCardFields(before, {
1934
+ receiptId: runningReceiptId, chatId, turnToken, generation, cardCleanup })
1935
+ const queued = queuedPayloads(before, chatId, payloadHandles)
1936
+ if (this.blocked || assertWriterLock() !== true ||
1937
+ control?.chatId !== chatId || control.source !== 'message' || control.quiet ||
1938
+ running?.chatId !== chatId || running.source !== 'message' ||
1939
+ !isDeepStrictEqual(intent, { phase: 'intent', controlReceiptId,
1940
+ runningReceiptId, turnToken, runnerGeneration: generation,
1941
+ ...batchFields }) ||
1942
+ witness?.receiptId !== runningReceiptId || witness.turnToken !== turnToken ||
1943
+ proof?.receiptId !== runningReceiptId || proof.chatId !== chatId ||
1944
+ proof.turnToken !== turnToken || proof.generation !== generation ||
1945
+ dispatch?.type !== 'dispatch' || dispatch.scope !== chatId ||
1946
+ dispatch.receiptId !== runningReceiptId ||
1947
+ this.#journal.effectStatus(proof.dispatchEffectId) !== 'acked' ||
1948
+ !this.#journal.hasEvent(`sbot-c2-session-close-v1:${runningReceiptId}:${generation}`) ||
1949
+ session?.epoch !== turnToken || session.closeSeen !== true ||
1950
+ session.settled !== false ||
1951
+ before.state.lanes.turn?.[chatId]?.active !== turnToken ||
1952
+ before.state.lanes.handoff?.[chatId]?.phase !== 'running' ||
1953
+ !isDeepStrictEqual(queued.accepted, queuedReceiptIds) ||
1954
+ queued.queue.some(item => item.batch) ||
1955
+ [...before.effects].some(([id, status]) => {
1956
+ const descriptor = this.#journal.effectDescriptor(id)
1957
+ return [...runningIds, controlReceiptId, ...queuedReceiptIds]
1958
+ .includes(descriptor?.receiptId) &&
1959
+ (status !== 'acked' || descriptor.domain === 'terminal')
1960
+ }) ||
1961
+ !isDeepStrictEqual(this.#coordinator.snapshot(), before.state) ||
1962
+ verifyGeneration({ chatId, receiptId: runningReceiptId,
1963
+ turnToken, generation }) !== true) {
1964
+ throw new Error('C2 stop-all lacks exact closed turn and queue proof')
1965
+ }
1966
+ const settledAt = this.#now()
1967
+ if (!Number.isSafeInteger(settledAt) || settledAt <= 0) {
1968
+ throw new TypeError('C2 stop-all time invalid')
1969
+ }
1970
+ const all = new Set([...runningIds, ...queuedReceiptIds])
1971
+ const receipts = { pending: before.receipts.pending.filter(item => !all.has(item.id)),
1972
+ completed: [...before.receipts.completed,
1973
+ ...[...all].map(id => ({ id, settledAt }))] }
1974
+ const controlTargetId = `receipt:control:${controlReceiptId}`
1975
+ const eventId = `sbot-c2-stop-all-v1:${controlReceiptId}:${runningReceiptId}:${turnToken}:${generation}`
1976
+ const effectId = `sbot-c2-effect-v1:${Buffer.byteLength(eventId)}:${eventId}:0`
1977
+ const prepared = this.#coordinator.prepare({ id: eventId, domain: 'control',
1978
+ scope: chatId, payload: { type: 'stopAllCommit', controlReceiptId,
1979
+ controlTargetId, runningReceiptId, turnToken, runnerGeneration: generation,
1980
+ runnerClosed: true, closeEvidenceId, queuedReceiptIds,
1981
+ ...cardFields, ...batchFields }, receipts,
1982
+ validate: candidate => {
1983
+ if (candidate.cursor !== before.cursor + 1 ||
1984
+ candidate.decision?.controlReceiptId !== controlReceiptId ||
1985
+ candidate.decision?.runningReceiptId !== runningReceiptId ||
1986
+ !isDeepStrictEqual(candidate.decision?.batchReceiptIds,
1987
+ batchFields.batchReceiptIds) ||
1988
+ !isDeepStrictEqual(candidate.decision?.queuedReceiptIds, queuedReceiptIds) ||
1989
+ candidate.decision?.stopAckEffectId !== effectId ||
1990
+ candidate.decision?.controlTargetId !== controlTargetId ||
1991
+ candidate.decision?.quietSettlement !== true ||
1992
+ candidate.effects?.length !== 1 ||
1993
+ !isDeepStrictEqual(candidate.effects[0],
1994
+ { type: 'stopAck', controlReceiptId, targetId: controlTargetId, effectId }) ||
1995
+ !isDeepStrictEqual(candidate.effectDescriptors?.[0],
1996
+ { effectId, domain: 'control', scope: chatId, type: 'stopAck',
1997
+ turnToken: null, receiptId: controlReceiptId,
1998
+ targetId: controlTargetId }) ||
1999
+ candidate.state?.lanes?.stopIntent?.[chatId] !== undefined ||
2000
+ candidate.state?.lanes?.turn?.[chatId]?.active !== null ||
2001
+ candidate.state?.lanes?.handoff?.[chatId]?.phase !== 'idle' ||
2002
+ candidate.state?.lanes?.queue?.[chatId]?.queue?.length !== 0) {
2003
+ throw new Error('C2 stop-all decision invalid')
2004
+ }
2005
+ } })
2006
+ if (assertWriterLock() !== true) {
2007
+ prepared.cancel()
2008
+ throw new Error('C2 stop-all lost writer lock')
2009
+ }
2010
+ const result = prepared.commit()
2011
+ payloadHandles.dropDurablyStopped(queuedReceiptIds, eventId)
2012
+ this.#activeRuns.delete(chatId)
2013
+ return { eventId, cursor: result.cursor, effectId, controlReceiptId,
2014
+ cancelledReceiptIds: [...all] }
2015
+ } catch (error) { this.close(); throw error }
2016
+ }
2017
+
2018
+ // One stop confirmation uses its own accepted command receipt. The Feishu
2019
+ // call is allowed only after the stop-all transition and durable attempt.
2020
+ async executeStopAck({ effectId, controlReceiptId, chatId, target,
2021
+ perform, confirm, assertWriterLock } = {}) {
2022
+ const before = this.#completionCapacity()
2023
+ const receipt = before.receipts.pending.find(item => item.id === controlReceiptId)
2024
+ const descriptor = this.#journal.effectDescriptor(effectId)
2025
+ if (!RECEIPT_ID.test(controlReceiptId || '') ||
2026
+ typeof chatId !== 'string' || !chatId ||
2027
+ !target || typeof target !== 'object' || Array.isArray(target) ||
2028
+ target.receiptId !== controlReceiptId || target.chatId !== chatId ||
2029
+ target.sourceId !== receipt?.sourceId ||
2030
+ target.replyCtx?.chatId !== chatId ||
2031
+ target.replyCtx?.messageId !== receipt?.sourceId ||
2032
+ typeof perform !== 'function' || typeof confirm !== 'function' ||
2033
+ typeof assertWriterLock !== 'function' ||
2034
+ descriptor?.domain !== 'control' || descriptor.type !== 'stopAck' ||
2035
+ descriptor.scope !== chatId || descriptor.receiptId !== controlReceiptId ||
2036
+ descriptor.targetId !== `receipt:control:${controlReceiptId}` ||
2037
+ descriptor.turnToken !== null ||
2038
+ this.#journal.effectStatus(effectId) !== 'pending' ||
2039
+ before.state.lanes.stopIntent?.[chatId] !== undefined ||
2040
+ before.state.lanes.turn?.[chatId]?.active !== null ||
2041
+ before.state.lanes.handoff?.[chatId]?.phase !== 'idle' ||
2042
+ before.state.lanes.queue?.[chatId]?.queue?.length !== 0 ||
2043
+ !isDeepStrictEqual(this.#coordinator.snapshot(), before.state) ||
2044
+ assertWriterLock() !== true) {
2045
+ this.close()
2046
+ throw new Error('C2 stop acknowledgement lacks exact durable target and effect')
2047
+ }
2048
+ const attemptedAt = this.#now()
2049
+ if (!Number.isSafeInteger(attemptedAt) || attemptedAt <= 0) {
2050
+ this.close()
2051
+ throw new TypeError('C2 stop acknowledgement time invalid')
2052
+ }
2053
+ this.#externalEffectInFlight = true
2054
+ try {
2055
+ this.#journal.beginEffectAttempt({ effectId, attemptedAt })
2056
+ let confirmed = false
2057
+ let response
2058
+ try {
2059
+ response = await perform(target)
2060
+ confirmed = confirm(response) === true &&
2061
+ this.#effectAuthorityReady && assertWriterLock() === true
2062
+ } catch { /* The platform outcome may have succeeded. */ }
2063
+ const now = this.#now()
2064
+ const resolvedAt = Number.isSafeInteger(now) && now > 0
2065
+ ? Math.max(attemptedAt, now) : attemptedAt
2066
+ if (!confirmed) {
2067
+ if (assertWriterLock() === true) {
2068
+ this.#journal.recordEffectOutcome({ effectId, status: 'unknown',
2069
+ resolvedAt })
2070
+ }
2071
+ this.close()
2072
+ throw new Error('C2 stop acknowledgement outcome unknown')
2073
+ }
2074
+ this.#journal.settleStopAck({ effectId, receiptId: controlReceiptId,
2075
+ chatId, settledAt: resolvedAt })
2076
+ if (this.#journal.byteLength >= this.#maxJournalBytes) this.#sealForCapacity()
2077
+ return { effectId, receiptId: controlReceiptId, status: 'acked', response }
2078
+ } catch (error) { this.close(); throw error }
2079
+ finally { this.#externalEffectInFlight = false }
2080
+ }
2081
+
2082
+ // Development-only terminal executor. The normal Engine path never calls
2083
+ // this yet. It executes one externally visible reply only after a durable
2084
+ // attempt; a lost ACK leaves the receipt and effect ambiguous on restart.
2085
+ async executeTerminalEffect({ effectId, receiptId, chatId, turnToken, generation,
2086
+ verifyGeneration, resolveTarget, perform, confirm } = {}) {
2087
+ const before = this.#completionCapacity()
2088
+ if (typeof effectId !== 'string' || !RECEIPT_ID.test(receiptId) ||
2089
+ typeof chatId !== 'string' || !chatId || !GENERATION.test(generation) ||
2090
+ !Number.isSafeInteger(turnToken) || turnToken <= 0 ||
2091
+ typeof verifyGeneration !== 'function' ||
2092
+ verifyGeneration.constructor?.name === 'AsyncFunction') {
2093
+ throw new TypeError('C2 terminal effect identity invalid')
2094
+ }
2095
+ const receipt = before.receipts.pending.find(item => item.id === receiptId)
2096
+ const descriptor = this.#journal.effectDescriptor(effectId)
2097
+ const witness = this.#activeRuns.get(chatId)
2098
+ const terminalCapability = this.#terminalAwaitingSettlement.get(chatId)
2099
+ const terminal = before.state.lanes.terminal?.[chatId]
2100
+ const runningReceiptIds = terminal?.batchReceiptIds || [receiptId]
2101
+ const expectedType = receipt?.quiet || terminal?.outcome?.silent
2102
+ ? 'quietCompletion' : 'finalReply'
2103
+ const registration = this.#journal.lookupRegistration(callbackRegistrationId({ receiptId,
2104
+ generation, domain: 'terminal', kind: 'finalizeEffectCommit' }))
2105
+ if (!receipt || receipt.chatId !== chatId ||
2106
+ witness?.receiptId !== receiptId || witness.turnToken !== turnToken ||
2107
+ !isDeepStrictEqual(witness.batchReceiptIds, runningReceiptIds) ||
2108
+ terminal?.replyTargetReceiptId !==
2109
+ (runningReceiptIds.length > 1 ? receiptId : undefined) ||
2110
+ runningReceiptIds.some(id =>
2111
+ before.receipts.pending.find(item => item.id === id)?.chatId !== chatId) ||
2112
+ terminalCapability?.effectId !== effectId ||
2113
+ terminalCapability.receiptId !== receiptId ||
2114
+ terminalCapability.turnToken !== turnToken ||
2115
+ terminalCapability.generation !== generation || terminalCapability.ready ||
2116
+ before.state.lanes.turn?.[chatId]?.active !== turnToken ||
2117
+ before.state.lanes.handoff?.[chatId]?.phase !== 'running' ||
2118
+ before.state.lanes.handoff[chatId].turnToken !== turnToken ||
2119
+ terminal?.finalized !== true ||
2120
+ terminal.pendingEffectIds?.at(-1) !== effectId ||
2121
+ !terminal.pendingEffectIds.slice(0, -1).every(id =>
2122
+ this.#journal.effectStatus(id) === 'acked') ||
2123
+ descriptor?.domain !== 'terminal' || descriptor.scope !== chatId ||
2124
+ descriptor.type !== expectedType || descriptor.turnToken !== turnToken ||
2125
+ descriptor.receiptId !== receiptId || descriptor.targetId !== null ||
2126
+ this.#journal.effectStatus(effectId) !== 'pending' ||
2127
+ registration?.generation !== generation || registration.receiptId !== receiptId ||
2128
+ verifyGeneration({ chatId, receiptId, turnToken, generation }) !== true) {
2129
+ throw new Error('C2 terminal effect lacks exact live authority')
2130
+ }
2131
+ if (expectedType === 'quietCompletion') {
2132
+ if (resolveTarget !== undefined || perform !== undefined || confirm !== undefined) {
2133
+ throw new Error('C2 quiet completion cannot perform an external effect')
2134
+ }
2135
+ terminalCapability.ready = true
2136
+ return { effectId, status: 'ready-to-settle', external: false }
2137
+ }
2138
+ if (typeof resolveTarget !== 'function' ||
2139
+ resolveTarget.constructor?.name === 'AsyncFunction' ||
2140
+ typeof perform !== 'function' || typeof confirm !== 'function' ||
2141
+ confirm.constructor?.name === 'AsyncFunction') {
2142
+ throw new TypeError('C2 final reply needs one confirmed platform operation')
2143
+ }
2144
+ const target = resolveTarget({ id: receipt.id, chatId: receipt.chatId,
2145
+ source: receipt.source, sourceId: receipt.sourceId })
2146
+ if (!target || typeof target !== 'object' || Array.isArray(target) ||
2147
+ typeof target.then === 'function' || target.receiptId !== receiptId ||
2148
+ target.chatId !== chatId || target.sourceId !== receipt.sourceId ||
2149
+ target.replyCtx == null) {
2150
+ throw new Error('C2 final reply target differs from durable receipt')
2151
+ }
2152
+ const attemptedAt = this.#now()
2153
+ if (!Number.isSafeInteger(attemptedAt) || attemptedAt <= 0) {
2154
+ throw new TypeError('C2 terminal effect time invalid')
2155
+ }
2156
+ this.#externalEffectInFlight = true
2157
+ try {
2158
+ this.#journal.beginEffectAttempt({ effectId, attemptedAt })
2159
+ let confirmed = false
2160
+ try {
2161
+ const reply = await perform({ effectId, target })
2162
+ confirmed = confirm(reply) === true &&
2163
+ this.#effectAuthorityReady &&
2164
+ verifyGeneration({ chatId, receiptId, turnToken, generation }) === true
2165
+ } catch { /* External outcome is unknown after the durable attempt. */ }
2166
+ const now = this.#now()
2167
+ const resolvedAt = Number.isSafeInteger(now) && now > 0
2168
+ ? Math.max(attemptedAt, now) : attemptedAt
2169
+ if (!confirmed) {
2170
+ this.#journal.recordEffectOutcome({ effectId, status: 'unknown', resolvedAt })
2171
+ this.close()
2172
+ throw new Error('C2 final reply outcome unknown; manual recovery required')
2173
+ }
2174
+ terminalCapability.ready = true
2175
+ return { effectId, status: 'confirmed-pending-release', external: true }
2176
+ } catch (error) {
2177
+ this.close()
2178
+ throw error
2179
+ } finally { this.#externalEffectInFlight = false }
2180
+ }
2181
+
2182
+ // The runner-close and queue-release callbacks still need the pending
2183
+ // receipt. Only after both durable transitions may reply ACK and receipt
2184
+ // completion share one record. Successor dispatch remains held until prune.
2185
+ settleTerminalEffect({ effectId, receiptId, chatId, turnToken, generation } = {}) {
2186
+ const before = this.#completionCapacity()
2187
+ if (typeof effectId !== 'string' || !RECEIPT_ID.test(receiptId) ||
2188
+ typeof chatId !== 'string' || !chatId || !GENERATION.test(generation) ||
2189
+ !Number.isSafeInteger(turnToken) || turnToken <= 0) {
2190
+ throw new TypeError('C2 terminal settlement identity invalid')
2191
+ }
2192
+ const capability = this.#terminalAwaitingSettlement.get(chatId)
2193
+ const handoff = before.state.lanes.handoff?.[chatId]
2194
+ const receipt = before.receipts.pending.find(item => item.id === receiptId)
2195
+ const batchReceiptIds = capability?.batchReceiptIds
2196
+ const session = before.state.lanes.session?.[chatId]
2197
+ if (!capability || capability.effectId !== effectId ||
2198
+ capability.receiptId !== receiptId || capability.turnToken !== turnToken ||
2199
+ capability.generation !== generation || !capability.ready ||
2200
+ !capability.releaseCommitted || !receipt || receipt.chatId !== chatId ||
2201
+ session?.epoch !== turnToken || session.closeSeen !== true ||
2202
+ session.settled !== false ||
2203
+ !this.#journal.hasEvent(`sbot-c2-session-close-v1:${receiptId}:${generation}`) ||
2204
+ (batchReceiptIds !== null &&
2205
+ (!isDeepStrictEqual(before.state.lanes.terminal?.[chatId]?.batchReceiptIds,
2206
+ batchReceiptIds) ||
2207
+ before.state.lanes.terminal[chatId].replyTargetReceiptId !== receiptId ||
2208
+ batchReceiptIds.some(id =>
2209
+ before.receipts.pending.find(item => item.id === id)?.chatId !== chatId))) ||
2210
+ before.state.lanes.turn?.[chatId]?.active !== null ||
2211
+ !['idle', 'handoff'].includes(handoff?.phase) ||
2212
+ handoff.turnToken !== turnToken ||
2213
+ !isDeepStrictEqual(this.#coordinator.snapshot(), before.state)) {
2214
+ throw new Error('C2 terminal settlement requires confirmed release')
2215
+ }
2216
+ const settledAt = this.#now()
2217
+ if (!Number.isSafeInteger(settledAt) || settledAt <= 0) {
2218
+ throw new TypeError('C2 terminal settlement time invalid')
2219
+ }
2220
+ try {
2221
+ const result = this.#journal.settleTerminalEffect({ effectId, receiptId,
2222
+ chatId, turnToken, status: 'acked', settledAt,
2223
+ ...(batchReceiptIds ? { batchReceiptIds: [...batchReceiptIds] } : {}) })
2224
+ pruneConfirmedTerminal({ journal: this.#journal,
2225
+ coordinator: this.#coordinator, scope: chatId })
2226
+ this.#terminalAwaitingSettlement.delete(chatId)
2227
+ if (this.#journal.byteLength >= this.#maxJournalBytes) this.#sealForCapacity()
2228
+ return { effectId, status: result, receiptId, pruned: true }
2229
+ } catch (error) { this.close(); throw error }
2230
+ }
2231
+
2232
+ // Development-only boundary for one Feishu card SDK operation. The adapter
2233
+ // supplies a stable operationId and a single-use invoke. This path never
2234
+ // authorizes the final text reply or the normal Engine runner.
2235
+ async performOutboundCardOperation({ receiptId, chatId, turnToken, generation,
2236
+ operation, verifyGeneration, confirm } = {}) {
2237
+ const before = this.#completionCapacity()
2238
+ if (!RECEIPT_ID.test(receiptId) || typeof chatId !== 'string' || !chatId ||
2239
+ !GENERATION.test(generation) || !Number.isSafeInteger(turnToken) ||
2240
+ turnToken <= 0 || typeof verifyGeneration !== 'function' ||
2241
+ verifyGeneration.constructor?.name === 'AsyncFunction' ||
2242
+ typeof confirm !== 'function' || confirm.constructor?.name === 'AsyncFunction') {
2243
+ throw new TypeError('C2 outbound card identity invalid')
2244
+ }
2245
+ const { operationId, eventId } = cardOperationIdentity({ receiptId,
2246
+ turnToken, generation, operation })
2247
+ const receipt = before.receipts.pending.find(item => item.id === receiptId)
2248
+ const witness = this.#activeRuns.get(chatId)
2249
+ const cleanupLane = before.state.lanes.cardCleanup?.[chatId]
2250
+ if (!receipt || receipt.chatId !== chatId ||
2251
+ (before.state.lanes.stopIntent?.[chatId] &&
2252
+ !(operation.kind === 'message.delete' && cleanupLane?.phase === 'intent')) ||
2253
+ (cleanupLane &&
2254
+ !(operation.kind === 'message.delete' && cleanupLane.phase === 'intent' &&
2255
+ cleanupLane.receiptId === receiptId &&
2256
+ cleanupLane.turnToken === turnToken &&
2257
+ cleanupLane.deleteOperationId === operationId)) ||
2258
+ (operation.chatId !== undefined && operation.chatId !== chatId) ||
2259
+ witness?.receiptId !== receiptId || witness.turnToken !== turnToken ||
2260
+ !witness.jobReceiptIds.includes(receiptId) ||
2261
+ !isDeepStrictEqual(witness.jobReceiptIds, witness.batchReceiptIds) ||
2262
+ !isDeepStrictEqual(before.state.lanes.handoff?.[chatId]?.acceptedReceiptIds,
2263
+ witness.batchReceiptIds.length > 1 ? witness.batchReceiptIds : []) ||
2264
+ witness.batchReceiptIds.some(id =>
2265
+ before.receipts.pending.find(item => item.id === id)?.chatId !== chatId) ||
2266
+ this.#journal.effectStatus(witness.dispatchEffectId) !== 'acked' ||
2267
+ before.state.lanes.turn?.[chatId]?.active !== turnToken ||
2268
+ before.state.lanes.handoff?.[chatId]?.phase !== 'running' ||
2269
+ before.state.lanes.handoff[chatId].turnToken !== turnToken ||
2270
+ this.#terminalAwaitingSettlement.has(chatId) ||
2271
+ !isDeepStrictEqual(this.#coordinator.snapshot(), before.state) ||
2272
+ verifyGeneration({ chatId, receiptId, turnToken, generation }) !== true) {
2273
+ throw new Error('C2 outbound card lacks exact live running receipt authority')
2274
+ }
2275
+ if (this.#journal.hasEvent(eventId)) {
2276
+ throw new Error('C2 outbound card operation already committed')
2277
+ }
2278
+ const effectId = `sbot-c2-effect-v1:${Buffer.byteLength(eventId)}:${eventId}:0`
2279
+ try {
2280
+ const prepared = this.#coordinator.prepare({ id: eventId,
2281
+ domain: 'outbound', scope: chatId,
2282
+ payload: { type: 'prepareCardOperation', kind: operation.kind,
2283
+ receiptId, turnToken, operationId },
2284
+ validate: candidate => {
2285
+ const descriptor = candidate.effectDescriptors?.[0]
2286
+ const effect = candidate.effects?.[0]
2287
+ if (candidate.cursor !== before.cursor + 1 ||
2288
+ candidate.state?.seq !== candidate.cursor ||
2289
+ !isDeepStrictEqual(candidate.state.lanes, before.state.lanes) ||
2290
+ !isDeepStrictEqual(candidate.decision, {
2291
+ type: operation.kind, operationId, receiptId, turnToken }) ||
2292
+ candidate.effects.length !== 1 || effect?.type !== operation.kind ||
2293
+ effect.operationId !== operationId ||
2294
+ effect.turnToken !== turnToken || effect.effectId !== effectId ||
2295
+ !isDeepStrictEqual(descriptor, { effectId, domain: 'outbound',
2296
+ scope: chatId, type: operation.kind, turnToken, receiptId,
2297
+ targetId: null })) {
2298
+ throw new Error('C2 outbound card result invalid')
2299
+ }
2300
+ } })
2301
+ prepared.commit()
2302
+ } catch (error) { this.close(); throw error }
2303
+
2304
+ const attemptedAt = this.#now()
2305
+ if (!Number.isSafeInteger(attemptedAt) || attemptedAt <= 0) {
2306
+ this.close()
2307
+ throw new TypeError('C2 outbound card time invalid')
2308
+ }
2309
+ this.#externalEffectInFlight = true
2310
+ try {
2311
+ this.#journal.beginEffectAttempt({ effectId, attemptedAt })
2312
+ let response
2313
+ let confirmed = false
2314
+ try {
2315
+ response = await operation.invoke()
2316
+ confirmed = confirm(response, operation) === true &&
2317
+ this.#effectAuthorityReady &&
2318
+ verifyGeneration({ chatId, receiptId, turnToken, generation }) === true
2319
+ } catch { /* The SDK call may have succeeded; never retry this ID. */ }
2320
+ const now = this.#now()
2321
+ const resolvedAt = Number.isSafeInteger(now) && now > 0
2322
+ ? Math.max(attemptedAt, now) : attemptedAt
2323
+ this.#journal.recordEffectOutcome({ effectId,
2324
+ status: confirmed ? 'acked' : 'unknown', resolvedAt })
2325
+ if (!confirmed) {
2326
+ this.close()
2327
+ throw new Error('C2 outbound card outcome unknown; manual recovery required')
2328
+ }
2329
+ if (this.#journal.byteLength >= this.#maxJournalBytes) this.#sealForCapacity()
2330
+ return response
2331
+ } catch (error) { this.close(); throw error }
2332
+ finally { this.#externalEffectInFlight = false }
2333
+ }
2334
+
2335
+ recordEffect(effectId, status) {
2336
+ if (!this.#effectAuthorityReady || this.#opening || this.#externalEffectInFlight ||
2337
+ this.#journal.blocked) {
2338
+ throw new Error('C2 durable runtime effect authority closed')
2339
+ }
2340
+ const descriptor = this.#journal.effectDescriptor(effectId)
2341
+ if (descriptor?.type === 'scheduleDispatch' &&
2342
+ this.#terminalAwaitingSettlement.has(descriptor.scope)) {
2343
+ throw new Error('C2 successor schedule awaits prior terminal settlement')
2344
+ }
2345
+ const result = this.#journal.recordEffect(effectId, status)
2346
+ if (this.#journal.byteLength >= this.#maxJournalBytes) this.#sealForCapacity()
2347
+ return result
2348
+ }
2349
+
2350
+ snapshot() { return this.#journal.snapshot() }
2351
+
2352
+ scopeState(domain, scope) {
2353
+ if (typeof domain !== 'string' || typeof scope !== 'string' || !domain || !scope) {
2354
+ throw new TypeError('C2 scope required')
2355
+ }
2356
+ const lane = this.#journal.snapshot().state.lanes[domain]?.[scope]
2357
+ return lane === undefined ? null : structuredClone(lane)
2358
+ }
2359
+ }
2360
+
2361
+ module.exports = { C2DurableRuntime }