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,1469 @@
1
+ // Dedicated durable v2 host. The binary and journal own turn decisions;
2
+ // this class owns process-local payloads, runner handles and platform I/O.
3
+ // No legacy Engine, retry/failover scheduler or v1 reducer is loaded here.
4
+ 'use strict'
5
+ const { randomBytes } = require('node:crypto')
6
+ const { isDeepStrictEqual } = require('node:util')
7
+ const { redact } = require('./redact')
8
+ const { fmtStamp, normalizeTz } = require('./time')
9
+ const { settleUsage, manifestPolicy, manifestHeader, manifestContext,
10
+ systemAppendix, turnPrompt, engineTerminalStep, engineQueueStep } = require('../agent/privatecore')
11
+ const { taskId } = require('./enginecheckpoint')
12
+ const { prepareCardTerminalPath } = require('./c2cardterminalpath')
13
+ const { terminalReplyText } = require('./c2terminaleffectbridge')
14
+ const { createC2TurnProjection } = require('./c2turnprojection')
15
+ const { prepareC2RunnerInput } = require('./c2runnerinput')
16
+ const { continueC2Successor } = require('./c2successorbridge')
17
+ const NO_REPLY = 'NO_REPLY'
18
+ const SYSTEM_APPENDIX = systemAppendix(NO_REPLY)
19
+ const V2_NORMAL_ROUTE_MISSING = Object.freeze(['complete durable v2 authority'])
20
+ const T = {
21
+ queued: n => n > 1 ? `⏳ 已排队 — 当前任务结束后与前面 ${n - 1} 条一并处理` : '⏳ 已排队 — 当前任务结束后处理',
22
+ stopped: '已停止',
23
+ noActiveTask: '没有正在运行的任务',
24
+ stoppedWithQueue: n => `已停止 — 连同排队的 ${n} 条一并取消`,
25
+ cardFooter: ({ ms, tokens }) => {
26
+ const parts = [`耗时 ${Math.max(0, Math.round(ms / 1000))} 秒`]
27
+ const tokenLabel = n => n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n)
28
+ if (tokens) parts.push(`tokens ↑${tokenLabel((tokens.in || 0) + (tokens.cr || 0) + (tokens.cw || 0))} ↓${tokenLabel(tokens.out || 0)}`)
29
+ return parts.join(' · ')
30
+ },
31
+ }
32
+ function ordinaryV2Cohort(snapshot, jobs, binding, chatId, receiptId) {
33
+ if (!Array.isArray(jobs) || !jobs.length ||
34
+ jobs.some(job => !job || job.msg?.batch || job.msg?.synthetic ||
35
+ job.msg?.cron || job.msg?.polled || job.msg?.quiet ||
36
+ job.msg?.chatId !== chatId ||
37
+ job.msg?.replyCtx?.chatId !== chatId ||
38
+ job.msg?.replyCtx?.messageId !== job.msg?.msgId ||
39
+ typeof job.msg?._c2ReceiptId !== 'string' ||
40
+ !/^[0-9a-f]{32}$/.test(job.msg._c2ReceiptId)) ||
41
+ !isDeepStrictEqual(binding?.receiptIds,
42
+ jobs.map(job => job.msg._c2ReceiptId)) ||
43
+ new Set(binding.receiptIds).size !== binding.receiptIds.length ||
44
+ !isDeepStrictEqual(binding?.batchReceiptIds, binding.receiptIds) ||
45
+ !binding.receiptIds.includes(receiptId) ||
46
+ !isDeepStrictEqual(snapshot.state?.lanes?.handoff?.[chatId]?.acceptedReceiptIds,
47
+ jobs.length > 1 ? binding.receiptIds : []) ||
48
+ jobs.some(job => {
49
+ const receipt = snapshot.receipts?.pending?.find(item =>
50
+ item.id === job.msg._c2ReceiptId)
51
+ return receipt?.chatId !== chatId || receipt.source !== 'message' ||
52
+ receipt.sourceId !== job.msg.msgId || receipt.quiet !== false
53
+ })) return null
54
+ return { jobs, anchor: jobs.find(job => job.msg._c2ReceiptId === receiptId),
55
+ receiptIds: [...binding.receiptIds] }
56
+ }
57
+ function v2QueueSelection(queue) {
58
+ if (!Array.isArray(queue)) throw new TypeError('durable queue must be an array')
59
+ const result = engineQueueStep({ queue }, { type: 'dequeue' })
60
+ const effect = result.effects[0]
61
+ if (!queue.length && result.effects.length === 0) return { jobs: [], anchor: null }
62
+ if (result.effects.length !== 1 || effect.type !== 'dispatch' ||
63
+ !Array.isArray(effect.batch) || !effect.batch.length ||
64
+ !effect.batch.includes(effect.anchor)) throw new Error('binary queue selection invalid')
65
+ const jobs = effect.batch.map(id => queue.find(job => job.id === id))
66
+ if (jobs.some(job => !job)) throw new Error('binary selected unknown queue handle')
67
+ return { jobs, anchor: effect.anchor.slice('receipt:job:'.length) }
68
+ }
69
+ class Engine {
70
+ constructor(cfg, options = {}) {
71
+ const { store, log = () => {}, durableV2Runtime, durableV2Payloads,
72
+ durableV2StartTurn, durableV2ApplyLocalEffect, durableV2VerifyRunnerClosed,
73
+ durableV2AssertWriterLock, durableV2Journal,
74
+ durableV2NormalRoute = true, durableV2StreamFrames = false } = options
75
+ const sync = f => typeof f === 'function' && f.constructor?.name !== 'AsyncFunction'
76
+ if (!cfg || !cfg.agent || !cfg.groups || !cfg.providers ||
77
+ options.receiptJournal || options.unifiedStepHost ||
78
+ !durableV2Runtime || !durableV2Payloads ||
79
+ typeof durableV2Runtime.enqueueMessage !== 'function' ||
80
+ typeof durableV2Runtime.snapshot !== 'function' ||
81
+ typeof durableV2Runtime.close !== 'function' ||
82
+ typeof durableV2Runtime.beginRunnerStart !== 'function' ||
83
+ typeof durableV2Runtime.confirmRunnerClosed !== 'function' ||
84
+ typeof durableV2Runtime.commitClosedRunnerSession !== 'function' ||
85
+ typeof durableV2Payloads.preflight !== 'function' ||
86
+ typeof durableV2Payloads.attach !== 'function' ||
87
+ typeof durableV2StartTurn !== 'function' ||
88
+ !sync(durableV2ApplyLocalEffect) || !sync(durableV2VerifyRunnerClosed) ||
89
+ !sync(durableV2AssertWriterLock) ||
90
+ typeof durableV2Journal?.snapshot !== 'function' ||
91
+ typeof store?.getSession !== 'function' ||
92
+ typeof store?.getSessionStats !== 'function' ||
93
+ typeof store?.setSessionAtomic !== 'function' ||
94
+ durableV2NormalRoute !== true || typeof durableV2StreamFrames !== 'boolean') {
95
+ throw new TypeError('dedicated Engine requires complete durable v2 authority')
96
+ }
97
+ Object.assign(this, { cfg, store, log, durableV2Runtime, durableV2Payloads,
98
+ durableV2StartTurn, durableV2ApplyLocalEffect, durableV2VerifyRunnerClosed,
99
+ durableV2AssertWriterLock, durableV2Journal, durableV2NormalRoute,
100
+ durableV2StreamFrames })
101
+ this.projectMemory = options.projectMemory || null
102
+ this.repoInfo = options.repoInfo || null
103
+ this.tz = normalizeTz(cfg.timezone)
104
+ this.rts = new Map()
105
+ }
106
+
107
+ shutdown() {
108
+ this.durableV2Runtime.close()
109
+ let aborted = 0
110
+ for (const rt of this.rts.values()) {
111
+ if (rt.v2Controller) {
112
+ try { rt.v2Controller.abort() } catch {}
113
+ aborted++
114
+ }
115
+ }
116
+ return aborted
117
+ }
118
+
119
+ drain(timeoutMs = 12_000) { return this.drainDurableV2(timeoutMs) }
120
+
121
+ async drainDurableV2(timeoutMs = 12_000) {
122
+ if (!this.durableV2Runtime || !Number.isSafeInteger(timeoutMs) || timeoutMs < 0) {
123
+ throw new TypeError('C2 v2 runner drain needs an exact timeout')
124
+ }
125
+ const active = [...this.rts.values()]
126
+ if (active.some(rt => rt.v2NormalRouteFailure)) return false
127
+ const runs = active.filter(rt => rt.v2RunnerStopPromise || rt.v2Controller)
128
+ const routes = active.filter(rt => rt.v2NormalRouteActive)
129
+ if (!runs.length && !routes.length) return true
130
+ if (runs.some(rt => !rt.v2RunnerStopPromise) ||
131
+ routes.some(rt => !rt.v2NormalRoutePromise)) return false
132
+ let timer
133
+ try {
134
+ const results = await Promise.race([
135
+ Promise.all([...runs.map(rt => rt.v2RunnerStopPromise),
136
+ ...routes.map(rt => rt.v2NormalRoutePromise)]),
137
+ new Promise(resolve => { timer = setTimeout(() => resolve(null), timeoutMs) }),
138
+ ])
139
+ return Array.isArray(results) && results.every(value => value === true)
140
+ } finally { if (timer) clearTimeout(timer) }
141
+ }
142
+
143
+ durableV2ActiveFaultProofs(activeTurns) {
144
+ const journal = this.durableV2Journal
145
+ const runtime = this.durableV2Runtime
146
+ if (!runtime || !journal || !Array.isArray(activeTurns) ||
147
+ activeTurns.length === 0 || activeTurns.length > 32 ||
148
+ !isDeepStrictEqual(runtime.snapshot(), journal.snapshot())) return null
149
+ const snapshot = journal.snapshot()
150
+ const lanes = snapshot.state.lanes
151
+ const expected = []
152
+ for (const chatId of new Set([
153
+ ...Object.keys(lanes.turn || {}), ...Object.keys(lanes.handoff || {}),
154
+ ...Object.keys(lanes.lifecycle || {}),
155
+ ])) {
156
+ const turn = lanes.turn?.[chatId]
157
+ const handoff = lanes.handoff?.[chatId]
158
+ const lifecycle = lanes.lifecycle?.[chatId]
159
+ if (turn?.active != null || handoff?.phase && handoff.phase !== 'idle' ||
160
+ lifecycle?.busy === true || lifecycle?.controller === true) {
161
+ expected.push({ chatId, turnToken: turn?.active ?? handoff?.turnToken ?? null })
162
+ }
163
+ }
164
+ expected.sort((a, b) => a.chatId.localeCompare(b.chatId))
165
+ if (!isDeepStrictEqual(activeTurns, expected)) return null
166
+ const suppliedChats = new Set()
167
+ const proofs = []
168
+ for (const turn of activeTurns) {
169
+ const chatId = turn?.chatId
170
+ const turnToken = turn?.turnToken
171
+ if (typeof chatId !== 'string' || !chatId || suppliedChats.has(chatId) ||
172
+ !Number.isSafeInteger(turnToken) || turnToken <= 0 ||
173
+ snapshot.state.lanes.turn?.[chatId]?.active !== turnToken ||
174
+ snapshot.state.lanes.handoff?.[chatId]?.phase !== 'running' ||
175
+ snapshot.state.lanes.handoff[chatId].turnToken !== turnToken) return null
176
+ suppliedChats.add(chatId)
177
+ const rt = this.rts.get(chatId)
178
+ const binding = rt?.v2DispatchEffect
179
+ const jobs = rt?.v2RunningJobs
180
+ const receiptId = binding?.receiptIds?.[0]
181
+ const generation = rt?.v2RunnerGeneration
182
+ const closeEvidenceId = rt?.v2RunnerCloseEvidenceId
183
+ const durable = closeEvidenceId && journal.lookupRunnerClosed(closeEvidenceId)
184
+ const receipt = snapshot.receipts.pending.find(item => item.id === receiptId)
185
+ const descriptor = binding?.effectId && journal.effectDescriptor(binding.effectId)
186
+ if (!rt || rt.v2TurnToken !== turnToken ||
187
+ rt.v2RunnerStopVerified !== true || rt.v2Controller ||
188
+ binding?.type !== 'dispatch' || binding.turnToken !== turnToken ||
189
+ !Array.isArray(binding.receiptIds) || binding.receiptIds.length !== 1 ||
190
+ !/^[0-9a-f]{32}$/.test(receiptId || '') ||
191
+ !/^[0-9a-f]{32}$/.test(generation || '') ||
192
+ closeEvidenceId !==
193
+ `sbot-c2-runner-closed-v1:${receiptId}:${generation}` ||
194
+ !Array.isArray(jobs) || jobs.length !== 1 ||
195
+ jobs[0]?.msg?._c2ReceiptId !== receiptId ||
196
+ receipt?.chatId !== chatId || receipt.source !== 'message' ||
197
+ jobs[0].msg.msgId !== receipt.sourceId ||
198
+ durable?.receiptId !== receiptId || durable.chatId !== chatId ||
199
+ durable.turnToken !== turnToken || durable.generation !== generation ||
200
+ durable.dispatchEffectId !== binding.effectId ||
201
+ descriptor?.domain !== 'atomicHandoff' || descriptor.type !== 'dispatch' ||
202
+ descriptor.scope !== chatId || descriptor.receiptId !== receiptId ||
203
+ descriptor.turnToken !== turnToken ||
204
+ journal.effectStatus(binding.effectId) !== 'acked') return null
205
+ proofs.push(Object.freeze({ chatId, receiptId, turnToken,
206
+ runnerGeneration: generation, runnerClosed: true, closeEvidenceId }))
207
+ }
208
+ return Object.freeze(proofs)
209
+ }
210
+
211
+ abortDurableV2FaultTurn(turn) {
212
+ const chatId = turn?.chatId
213
+ const turnToken = turn?.turnToken
214
+ const rt = typeof chatId === 'string' ? this.rts.get(chatId) : null
215
+ if (!this.durableV2Runtime || !rt ||
216
+ !Number.isSafeInteger(turnToken) || turnToken <= 0 ||
217
+ rt.v2TurnToken !== turnToken ||
218
+ this.durableV2Journal?.snapshot().state.lanes.turn?.[chatId]?.active !== turnToken) {
219
+ return false
220
+ }
221
+ if (rt.v2Controller) {
222
+ try { rt.v2Controller.abort(); return true } catch { return false }
223
+ }
224
+ return this.durableV2ActiveFaultProofs([turn]) !== null
225
+ }
226
+
227
+ _rt(chatId) {
228
+ if (!this.rts.has(chatId)) this.rts.set(chatId, { chatId })
229
+ return this.rts.get(chatId)
230
+ }
231
+
232
+ baseSkillEnv(chatId) {
233
+ const g = (this.cfg.groups || {})[chatId]
234
+ return { SBOT_CHAT_ID: chatId, ...(g && g.platform ? { SBOT_PLATFORM: g.platform } : {}), TZ: this.tz, ...(this.cfg._path ? { SBOT_CONFIG: this.cfg._path } : {}), ...(this.uiLocalUrl ? { SBOT_UI_URL: this.uiLocalUrl } : {}) }
235
+ }
236
+
237
+ _resolveGroup(chatId) {
238
+ return (this.cfg.groups && this.cfg.groups[chatId]) || this.defaultGroup || null
239
+ }
240
+
241
+ _resolveProvider(chatId, group) {
242
+ const name = this.store.getProvider(chatId) || group.provider || this.cfg.defaultProvider
243
+ return { name, provider: this.cfg.providers[name] }
244
+ }
245
+
246
+ async runDurableV2Message(msg, platform) {
247
+ if (msg?.text?.trim() === '/stop') {
248
+ return this.runDurableV2Stop(msg, platform)
249
+ }
250
+ if (!this.durableV2NormalRoute) {
251
+ const error = new Error('C2 v2 normal route is closed: runner transcript, tool/stream effects, and terminal projection lack durable evidence')
252
+ error.code = 'C2_V2_NORMAL_ROUTE_CLOSED'
253
+ error.missingEvidence = [...V2_NORMAL_ROUTE_MISSING]
254
+ throw error
255
+ }
256
+ const authority = this.durableV2Runtime
257
+ if (!authority || authority.blocked ||
258
+ !msg || typeof msg !== 'object' || msg.synthetic || msg.cron ||
259
+ msg.polled || msg.wantsAck || msg.resetSessionAfter || msg.quiet ||
260
+ msg.batch ||
261
+ msg.receiptSource && msg.receiptSource !== 'message' ||
262
+ typeof msg.text !== 'string' || !msg.text.trim() ||
263
+ msg.text.trim().startsWith('/') ||
264
+ typeof msg.chatId !== 'string' || !msg.chatId ||
265
+ typeof msg.msgId !== 'string' ||
266
+ !/^om_[A-Za-z0-9_-]{1,124}$/.test(msg.msgId) ||
267
+ !platform || platform.features?.cardQuota !== true ||
268
+ typeof platform.reply !== 'function' ||
269
+ typeof platform.replyOnce !== 'function' ||
270
+ typeof platform.createStreamingCard !== 'function') {
271
+ throw new Error('C2 v2 ordinary Feishu message is unsupported or closed')
272
+ }
273
+ const group = this._resolveGroup(msg.chatId)
274
+ const { provider } = group ? this._resolveProvider(msg.chatId, group) : {}
275
+ if (!group || group.paused || group.platform && group.platform !== 'feishu' ||
276
+ !provider || !['claude', 'codex'].includes(provider.runner) ||
277
+ provider.runner === 'codex' && group.codexSteer !== false) {
278
+ throw new Error('C2 v2 ordinary group control is not migrated')
279
+ }
280
+ let locked = false
281
+ try { locked = this.durableV2AssertWriterLock() === true } catch {}
282
+ if (!locked) {
283
+ authority.close()
284
+ throw new Error('C2 v2 ordinary route lost single-writer lock')
285
+ }
286
+ const rt = this._rt(msg.chatId)
287
+ if (rt.v2QueueNoticePromise) await rt.v2QueueNoticePromise
288
+ const before = authority.snapshot()
289
+ const receiptId = taskId({ source: 'message', chatId: msg.chatId,
290
+ sourceId: msg.msgId })
291
+ if (before.receipts.pending.some(item => item.id === receiptId) ||
292
+ before.receipts.completed.some(item => item.id === receiptId)) {
293
+ const duplicate = this.stageDurableV2Message(msg, platform)
294
+ return { receiptId, duplicate: true, staged: false,
295
+ ingressClosed: duplicate.ingressClosed }
296
+ }
297
+ if (rt.v2StopControl || before.state.lanes.stopIntent?.[msg.chatId]) {
298
+ throw new Error('C2 v2 ordinary route is waiting for stop acknowledgement')
299
+ }
300
+ const lane = before.state?.lanes
301
+ const queue = lane?.queue?.[msg.chatId]?.queue
302
+ const busy = lane?.turn?.[msg.chatId]?.active != null ||
303
+ lane?.handoff?.[msg.chatId]?.phase === 'running' ||
304
+ Array.isArray(queue) && queue.length > 0 ||
305
+ rt.v2RunningJobs || rt.v2QueueReceiptIds?.length
306
+ if (busy && rt.v2NormalRouteActive &&
307
+ lane?.turn?.[msg.chatId]?.active === rt.v2TurnToken &&
308
+ lane?.handoff?.[msg.chatId]?.phase === 'running' &&
309
+ rt.v2DispatchEffect?.type === 'dispatch' &&
310
+ rt.v2RunningJobs?.length === 1 &&
311
+ rt.v2RunnerStopVerified !== true &&
312
+ Array.isArray(queue) && queue.length < 2 &&
313
+ queue.every(item => !item.batch &&
314
+ /^receipt:job:[0-9a-f]{32}$/.test(item.id)) &&
315
+ queue.every(item => [...before.effects].some(([effectId, status]) => {
316
+ const descriptor = this.durableV2Journal.effectDescriptor(effectId)
317
+ return status === 'acked' && descriptor?.type === 'queueNotice' &&
318
+ descriptor.receiptId === item.id.slice('receipt:job:'.length)
319
+ })) &&
320
+ isDeepStrictEqual(queue.map(item => item.id.slice('receipt:job:'.length)),
321
+ rt.v2QueueReceiptIds || []) &&
322
+ isDeepStrictEqual(this.durableV2Journal.snapshot(), before)) {
323
+ try { this.durableV2Payloads.auditOrdinaryQueue(msg.chatId,
324
+ rt.v2QueueReceiptIds || []) }
325
+ catch (error) { authority.close(); throw error }
326
+ const queued = this.stageDurableV2Message(msg, platform)
327
+ let attached
328
+ try { attached = this.durableV2Payloads.requireQueued(queued.receiptId) }
329
+ catch (error) { authority.close(); throw error }
330
+ if (!queued.staged || queued.duplicate ||
331
+ attached?.msg?._c2ReceiptId !== queued.receiptId) {
332
+ authority.close()
333
+ throw new Error('C2 v2 queued follow-up lacks exact payload handle')
334
+ }
335
+ try { this.durableV2Payloads.auditOrdinaryQueue(msg.chatId,
336
+ rt.v2QueueReceiptIds) }
337
+ catch (error) { authority.close(); throw error }
338
+ const notice = authority.prepareQueueNotice({ receiptId: queued.receiptId,
339
+ chatId: msg.chatId, payloadHandles: this.durableV2Payloads })
340
+ const noticePromise = authority.executeQueueNotice({
341
+ effectId: notice.effectId, receiptId: queued.receiptId, chatId: msg.chatId,
342
+ payloadHandles: this.durableV2Payloads,
343
+ assertWriterLock: this.durableV2AssertWriterLock,
344
+ perform: ({ target }) => platform.replyOnce(target, T.queued(notice.visibleQueue)),
345
+ confirm: response => response?.code === 0 &&
346
+ /^om_[A-Za-z0-9_-]{1,124}$/.test(response?.data?.message_id || ''),
347
+ })
348
+ rt.v2QueueNoticePromise = noticePromise
349
+ try { await noticePromise } finally {
350
+ if (rt.v2QueueNoticePromise === noticePromise) rt.v2QueueNoticePromise = null
351
+ }
352
+ return { receiptId: queued.receiptId, queued: true, staged: true,
353
+ ingressClosed: !!authority.blocked }
354
+ }
355
+ if (busy) {
356
+ if (rt.v2NormalRouteActive && Array.isArray(queue) && queue.length > 0) {
357
+ try { this.durableV2Payloads.auditOrdinaryQueue(msg.chatId,
358
+ rt.v2QueueReceiptIds || []) }
359
+ catch (error) { authority.close(); throw error }
360
+ }
361
+ if (!rt.v2NormalRouteActive || !Array.isArray(queue) || queue.length === 0) {
362
+ authority.close()
363
+ }
364
+ const error = new Error('C2 v2 busy follow-up route is not migrated')
365
+ error.code = 'C2_V2_BUSY_ROUTE_CLOSED'
366
+ throw error
367
+ }
368
+ const accepted = this.stageDurableV2Message(msg, platform)
369
+ if (!accepted.staged || accepted.duplicate) {
370
+ authority.close()
371
+ throw new Error('C2 v2 ordinary receipt admission changed')
372
+ }
373
+ const dispatched = this.commitDurableV2InitialDispatch({ chatId: msg.chatId,
374
+ receiptId: accepted.receiptId })
375
+ const effectId = dispatched.effectsPending?.[0]
376
+ const identity = { chatId: msg.chatId, receiptId: accepted.receiptId,
377
+ turnToken: dispatched.turnToken, effectId }
378
+ rt.v2NormalRouteActive = true
379
+ const execution = this._drainDurableV2NormalChain(identity)
380
+ rt.v2NormalRoutePromise = execution.then(() => true, () => {
381
+ rt.v2NormalRouteFailure = true
382
+ return false
383
+ })
384
+ try {
385
+ return await execution
386
+ } finally {
387
+ rt.v2NormalRouteActive = false
388
+ rt.v2NormalRoutePromise = null
389
+ }
390
+ }
391
+
392
+ async runDurableV2Stop(msg, platform) {
393
+ const authority = this.durableV2Runtime
394
+ const chatId = msg?.chatId
395
+ const rt = this.rts.get(chatId)
396
+ if (!this.durableV2NormalRoute || !authority ||
397
+ !msg || msg.synthetic || msg.cron || msg.polled || msg.quiet ||
398
+ msg.wantsAck || msg.batch || msg.text?.trim() !== '/stop' ||
399
+ typeof chatId !== 'string' || !chatId ||
400
+ typeof msg.msgId !== 'string' ||
401
+ !/^om_[A-Za-z0-9_-]{1,124}$/.test(msg.msgId) ||
402
+ msg.replyCtx?.chatId !== chatId ||
403
+ msg.replyCtx?.messageId !== msg.msgId ||
404
+ typeof platform?.replyOnce !== 'function' ||
405
+ !rt?.v2NormalRouteActive || !rt.v2RunningJobs?.length ||
406
+ !rt.v2Controller || rt.v2TerminalStarted ||
407
+ rt.v2CancelControl || rt.v2StopControl || rt.v2StreamQuiescing ||
408
+ rt.v2TimeoutProof) {
409
+ throw new Error('C2 v2 /stop is unsupported or closed')
410
+ }
411
+ const group = this._resolveGroup(chatId)
412
+ if (!group || group.paused || group.platform && group.platform !== 'feishu') {
413
+ throw new Error('C2 v2 /stop group control is not migrated')
414
+ }
415
+ const requestedGeneration = rt.v2RunnerGeneration
416
+ const requestedTurnToken = rt.v2TurnToken
417
+ // A busy receipt may already be sending its durable queue notice. Keep
418
+ // that external effect and the stop intent ordered: otherwise the notice
419
+ // can become unknown while the stop transition clears its target queue.
420
+ if (rt.v2QueueNoticePromise) await rt.v2QueueNoticePromise
421
+ if (!rt.v2NormalRouteActive ||
422
+ !rt.v2RunningJobs?.length || !rt.v2Controller ||
423
+ rt.v2TerminalStarted || rt.v2CancelControl || rt.v2StopControl ||
424
+ rt.v2StreamQuiescing || rt.v2TimeoutProof ||
425
+ rt.v2RunnerGeneration !== requestedGeneration ||
426
+ rt.v2TurnToken !== requestedTurnToken) {
427
+ throw new Error('C2 v2 /stop is unsupported or closed')
428
+ }
429
+ const before = authority.snapshot()
430
+ const controlReceipt = { source: 'message', chatId, sourceId: msg.msgId,
431
+ acceptedAt: Date.now(), quiet: false }
432
+ controlReceipt.id = taskId(controlReceipt)
433
+ const runningReceiptId = rt.v2DispatchEffect?.receiptIds?.at(-1)
434
+ const cohort = ordinaryV2Cohort(before, rt.v2RunningJobs,
435
+ rt.v2DispatchEffect, chatId, runningReceiptId)
436
+ const generation = rt.v2RunnerGeneration
437
+ const turnToken = rt.v2TurnToken
438
+ const queue = before.state.lanes.queue?.[chatId]?.queue
439
+ const queuedReceiptIds = queue?.map(item => item.id.slice('receipt:job:'.length))
440
+ if (!cohort || !/^[0-9a-f]{32}$/.test(runningReceiptId || '') ||
441
+ !/^[0-9a-f]{32}$/.test(generation || '') ||
442
+ !Number.isSafeInteger(turnToken) || turnToken <= 0 ||
443
+ !Array.isArray(queue) || queue.some(item => item.batch || item.synthetic ||
444
+ item.quiet || item.polled ||
445
+ !/^receipt:job:[0-9a-f]{32}$/.test(item.id) ||
446
+ before.receipts.pending.find(receipt =>
447
+ receipt.id === item.id.slice('receipt:job:'.length))?.source !== 'message' ||
448
+ ![...before.effects].some(([effectId, status]) => {
449
+ const descriptor = this.durableV2Journal.effectDescriptor(effectId)
450
+ return status === 'acked' && descriptor?.type === 'queueNotice' &&
451
+ descriptor.receiptId === item.id.slice('receipt:job:'.length)
452
+ })) ||
453
+ !isDeepStrictEqual(queuedReceiptIds, rt.v2QueueReceiptIds || []) ||
454
+ before.state.lanes.turn?.[chatId]?.active !== turnToken ||
455
+ before.state.lanes.handoff?.[chatId]?.phase !== 'running' ||
456
+ before.receipts.pending.find(item => item.id === runningReceiptId)?.sourceId !==
457
+ cohort.anchor.msg.msgId ||
458
+ before.receipts.pending.some(item => item.id === controlReceipt.id) ||
459
+ before.receipts.completed.some(item => item.id === controlReceipt.id)) {
460
+ authority.close()
461
+ throw new Error('C2 v2 /stop differs from durable running turn')
462
+ }
463
+ try { this.durableV2Payloads.auditOrdinaryQueue(chatId,
464
+ queuedReceiptIds) }
465
+ catch (error) { authority.close(); throw error }
466
+ let releaseIntentGate = null
467
+ try {
468
+ // Stop admitting process frames, then drain any already prepared card
469
+ // calls before persisting the stop count and aborting the child.
470
+ rt.v2StreamQuiescing = true
471
+ rt.v2StopIntentGate = new Promise(resolve => { releaseIntentGate = resolve })
472
+ try {
473
+ await rt.v2StreamChain
474
+ if (rt.v2CardBinding?.opened) {
475
+ await rt.v2CardBinding.path.flushForTerminal()
476
+ }
477
+ if (authority.blocked || rt.v2CancelControl || rt.v2TerminalStarted ||
478
+ rt.v2RunnerGeneration !== generation || rt.v2TurnToken !== turnToken) {
479
+ throw new Error('C2 v2 /stop card drain lost running turn')
480
+ }
481
+ } catch (error) {
482
+ releaseIntentGate()
483
+ throw error
484
+ }
485
+ if (this.durableV2AssertWriterLock() !== true) throw new Error('writer lock lost')
486
+ const cardTarget = rt.v2CardBinding?.opened
487
+ ? rt.v2CardBinding.path.cancellationTarget() : null
488
+ authority.commitStopIntent({ controlReceipt, runningReceiptId, chatId,
489
+ turnToken, generation, cardTarget,
490
+ assertWriterLock: this.durableV2AssertWriterLock })
491
+ rt.v2StopControl = { controlReceiptId: controlReceipt.id, runningReceiptId,
492
+ turnToken, generation, runningReceiptIds: cohort.receiptIds,
493
+ queuedReceiptIds,
494
+ target: { receiptId: controlReceipt.id, chatId,
495
+ sourceId: msg.msgId, replyCtx: msg.replyCtx },
496
+ platform }
497
+ rt.v2CancelControl = 'stop'
498
+ rt.v2Controller.abort()
499
+ releaseIntentGate()
500
+ const completed = await rt.v2NormalRoutePromise
501
+ if (completed !== true || authority.blocked ||
502
+ !authority.snapshot().receipts.completed.some(item =>
503
+ item.id === controlReceipt.id)) {
504
+ throw new Error('C2 v2 /stop acknowledgement was not durably settled')
505
+ }
506
+ return { status: 'stopped', controlReceiptId: controlReceipt.id,
507
+ cancelledReceiptIds: [...cohort.receiptIds, ...queuedReceiptIds] }
508
+ } catch (error) {
509
+ releaseIntentGate?.()
510
+ rt.v2StopIntentGate = null
511
+ try { rt.v2Controller?.abort() } catch {}
512
+ authority.close(); throw error
513
+ }
514
+ }
515
+
516
+ async _cleanupDurableV2CancelledCard(rt, identity, generation,
517
+ verifyGeneration) {
518
+ if (!rt.v2CardBinding?.opened) return null
519
+ const authority = this.durableV2Runtime
520
+ const binding = rt.v2CardBinding
521
+ if (binding.receiptId !== identity.receiptId ||
522
+ binding.turnToken !== identity.turnToken ||
523
+ binding.generation !== generation) {
524
+ authority.close()
525
+ throw new Error('C2 v2 cancellation card differs from running turn')
526
+ }
527
+ const cleanupBase = { receiptId: identity.receiptId,
528
+ chatId: identity.chatId, turnToken: identity.turnToken, generation,
529
+ verifyGeneration, assertWriterLock: this.durableV2AssertWriterLock }
530
+ const intent = authority.commitCardCleanupIntent({ ...cleanupBase,
531
+ target: binding.path.cancellationTarget() })
532
+ if (await binding.path.recallForCancellation() !== true) {
533
+ authority.close()
534
+ throw new Error('C2 v2 cancellation card delete unconfirmed')
535
+ }
536
+ return authority.commitCardCleanupConfirmed({ ...cleanupBase,
537
+ target: binding.path.cancellationTarget(), proof: intent })
538
+ }
539
+
540
+ async _drainDurableV2NormalChain(initial) {
541
+ const authority = this.durableV2Runtime
542
+ const firstReceiptId = initial.receiptId
543
+ const settled = []
544
+ let identity = initial
545
+ while (identity) {
546
+ let locked = false
547
+ try { locked = this.durableV2AssertWriterLock() === true } catch {}
548
+ if (!locked) {
549
+ authority.close()
550
+ throw new Error('C2 v2 ordinary route lost single-writer lock before runner')
551
+ }
552
+ const started = await this.startDurableV2Runner(identity)
553
+ if (started.startFailureRecovered === true) {
554
+ settled.push({ receiptId: identity.receiptId, status: 'startupNotified' })
555
+ return { receiptId: firstReceiptId, status: settled[0].status,
556
+ settledReceiptIds: settled.map(item => item.receiptId),
557
+ ingressClosed: authority.blocked }
558
+ }
559
+ const rt = this._rt(identity.chatId)
560
+ if (rt.v2QueueNoticePromise) await rt.v2QueueNoticePromise
561
+ if (rt.v2CancelControl === 'recall' || rt.v2CancelControl === 'stop') {
562
+ const control = rt.v2CancelControl
563
+ const generation = rt.v2RunnerGeneration
564
+ if (rt.v2RunnerStopVerified !== true ||
565
+ rt.v2RunnerCloseEvidenceId !==
566
+ `sbot-c2-runner-closed-v1:${identity.receiptId}:${generation}` ||
567
+ rt.v2SessionCloseEventId !==
568
+ `sbot-c2-session-close-v1:${identity.receiptId}:${generation}` ||
569
+ typeof authority.commitRunningCancellation !== 'function') {
570
+ authority.close()
571
+ throw new Error('C2 v2 cancelled runner lacks durable close proof')
572
+ }
573
+ const verifyGeneration = value => value.chatId === identity.chatId &&
574
+ value.receiptId === identity.receiptId &&
575
+ value.turnToken === identity.turnToken &&
576
+ value.generation === generation && rt.v2RunnerGeneration === generation
577
+ const cardCleanup = await this._cleanupDurableV2CancelledCard(
578
+ rt, identity, generation, verifyGeneration)
579
+ const stop = control === 'stop' ? rt.v2StopControl : null
580
+ const cancelled = stop
581
+ ? authority.commitStopAll({ chatId: identity.chatId,
582
+ controlReceiptId: stop.controlReceiptId,
583
+ runningReceiptId: identity.receiptId,
584
+ turnToken: identity.turnToken, generation,
585
+ queuedReceiptIds: stop.queuedReceiptIds,
586
+ cardCleanup,
587
+ payloadHandles: this.durableV2Payloads,
588
+ verifyGeneration, assertWriterLock: this.durableV2AssertWriterLock })
589
+ : authority.commitRunningCancellation({ ...identity, generation,
590
+ control, cardCleanup, verifyGeneration,
591
+ assertWriterLock: this.durableV2AssertWriterLock })
592
+ rt.v2RunnerResult = null
593
+ rt.v2TerminalProjection = null
594
+ rt.v2UsageDecision = null
595
+ rt.v2StartedAt = null
596
+ rt.v2CardBinding = null
597
+ rt.v2StreamRequested = false
598
+ rt.v2StreamQuiescing = false
599
+ rt.v2StreamChain = null
600
+ rt.v2StopIntentGate = null
601
+ rt.v2TimeoutProof = null
602
+ rt.v2RunningJobs = null
603
+ rt.v2RunnerGeneration = null
604
+ rt.v2RunnerCloseEvidenceId = null
605
+ rt.v2SessionCloseEventId = null
606
+ rt.v2DispatchEffect = null
607
+ rt.v2TurnToken = null
608
+ rt.v2CancelControl = null
609
+ if (stop) {
610
+ rt.v2QueueReceiptIds = []
611
+ const text = stop.queuedReceiptIds.length
612
+ ? T.stoppedWithQueue(stop.queuedReceiptIds.length) : T.stopped
613
+ await authority.executeStopAck({ effectId: cancelled.effectId,
614
+ controlReceiptId: stop.controlReceiptId, chatId: identity.chatId,
615
+ target: stop.target,
616
+ perform: target => stop.platform.replyOnce(target.replyCtx, text),
617
+ confirm: response => response?.code === 0 &&
618
+ /^om_[A-Za-z0-9_-]{1,124}$/.test(response?.data?.message_id || ''),
619
+ assertWriterLock: this.durableV2AssertWriterLock })
620
+ rt.v2StopControl = null
621
+ }
622
+ for (const settledReceiptId of cancelled.cancelledReceiptIds ||
623
+ [identity.receiptId]) {
624
+ settled.push({ receiptId: settledReceiptId,
625
+ status: control === 'recall' ? 'recalled' : 'stopped',
626
+ terminalEffectId: null, cancellationEventId: cancelled.eventId })
627
+ }
628
+ if (authority.blocked) throw new Error('C2 v2 cancellation closed ingress')
629
+ const nextReceiptId = this._nextDurableV2NormalAnchor(identity.chatId)
630
+ if (nextReceiptId) {
631
+ const next = this.commitDurableV2InitialDispatch({
632
+ chatId: identity.chatId, receiptId: nextReceiptId })
633
+ identity = { chatId: identity.chatId, receiptId: nextReceiptId,
634
+ turnToken: next.turnToken, effectId: next.effectsPending[0] }
635
+ } else identity = null
636
+ continue
637
+ }
638
+ if (rt.v2TimeoutProof) {
639
+ const generation = rt.v2RunnerGeneration
640
+ const timeoutProof = rt.v2TimeoutProof
641
+ if (rt.v2RunnerStopVerified !== true ||
642
+ rt.v2RunnerCloseEvidenceId !==
643
+ `sbot-c2-runner-closed-v1:${identity.receiptId}:${generation}` ||
644
+ rt.v2SessionCloseEventId !==
645
+ `sbot-c2-session-close-v1:${identity.receiptId}:${generation}` ||
646
+ typeof authority.commitHardTimeoutRelease !== 'function') {
647
+ authority.close()
648
+ throw new Error('C2 v2 hard timeout lacks durable child close proof')
649
+ }
650
+ const verifyGeneration = value => value.chatId === identity.chatId &&
651
+ value.receiptId === identity.receiptId &&
652
+ value.turnToken === identity.turnToken &&
653
+ value.generation === generation && rt.v2RunnerGeneration === generation
654
+ const cardCleanup = await this._cleanupDurableV2CancelledCard(
655
+ rt, identity, generation, verifyGeneration)
656
+ const released = authority.commitHardTimeoutRelease({ ...identity,
657
+ generation, timeoutProof, cardCleanup, verifyGeneration,
658
+ assertWriterLock: this.durableV2AssertWriterLock })
659
+ rt.v2RunnerResult = null
660
+ rt.v2TerminalProjection = null
661
+ rt.v2UsageDecision = null
662
+ rt.v2StartedAt = null
663
+ rt.v2CardBinding = null
664
+ rt.v2StreamRequested = false
665
+ rt.v2StreamQuiescing = false
666
+ rt.v2StreamChain = null
667
+ rt.v2StopIntentGate = null
668
+ rt.v2TimeoutProof = null
669
+ rt.v2RunningJobs = null
670
+ rt.v2RunnerGeneration = null
671
+ rt.v2RunnerCloseEvidenceId = null
672
+ rt.v2SessionCloseEventId = null
673
+ rt.v2DispatchEffect = null
674
+ rt.v2TurnToken = null
675
+ for (const settledReceiptId of released.settledReceiptIds ||
676
+ [identity.receiptId]) {
677
+ settled.push({ receiptId: settledReceiptId,
678
+ status: 'timeout', terminalEffectId: null,
679
+ cancellationEventId: released.eventId })
680
+ }
681
+ if (authority.blocked) throw new Error('C2 v2 hard timeout closed ingress')
682
+ const nextReceiptId = this._nextDurableV2NormalAnchor(identity.chatId)
683
+ if (nextReceiptId) {
684
+ const next = this.commitDurableV2InitialDispatch({
685
+ chatId: identity.chatId, receiptId: nextReceiptId })
686
+ identity = { chatId: identity.chatId, receiptId: nextReceiptId,
687
+ turnToken: next.turnToken, effectId: next.effectsPending[0] }
688
+ } else identity = null
689
+ continue
690
+ }
691
+ const payload = rt.v2TerminalProjection?.payload
692
+ if (!payload) {
693
+ authority.close()
694
+ const error = new Error('C2 v2 ordinary result needs an unresolved-turn policy')
695
+ error.code = 'C2_V2_UNRESOLVED_TURN'
696
+ throw error
697
+ }
698
+ const terminal = { ...payload, type: 'finalize' }
699
+ delete terminal.receiptId
700
+ delete terminal.turnToken
701
+ delete terminal.quiet
702
+ const outcome = engineTerminalStep({ finalized: false, outcome: null },
703
+ terminal).state.outcome
704
+ if (['error', 'empty', 'stopped'].includes(outcome.status)) {
705
+ const classified = this.classifyDurableV2UnresolvedTurn({ ...identity,
706
+ payload })
707
+ const error = new Error('C2 v2 ordinary result requires fault recovery')
708
+ error.code = 'C2_V2_UNRESOLVED_TURN'
709
+ error.classificationEventId = classified.eventId
710
+ throw error
711
+ }
712
+ if (payload.source !== 'result' || payload.sessionMissing ||
713
+ payload.hasQuota || payload.transientNetwork || payload.freshRetried ||
714
+ payload.resumedSessionCurrent ||
715
+ !['ok', 'no-reply'].includes(outcome.status)) {
716
+ authority.close()
717
+ const error = new Error('C2 v2 ordinary result needs a retry or notice decision')
718
+ error.code = 'C2_V2_UNRESOLVED_TURN'
719
+ throw error
720
+ }
721
+ let stillLocked = false
722
+ try { stillLocked = this.durableV2AssertWriterLock() === true } catch {}
723
+ if (!stillLocked) {
724
+ authority.close()
725
+ throw new Error('C2 v2 ordinary route lost single-writer lock before delivery')
726
+ }
727
+ const silent = payload.quiet || outcome.silent
728
+ const completed = await this.completeDurableV2CardTurn({ ...identity, payload,
729
+ ...(silent ? {} : { replyText: terminalReplyText(payload) }) })
730
+ for (const settledReceiptId of completed.settledReceiptIds ||
731
+ [identity.receiptId]) {
732
+ settled.push({ receiptId: settledReceiptId,
733
+ status: silent ? 'silent' : 'answered',
734
+ terminalEffectId: completed.terminalEffectId })
735
+ }
736
+ if (authority.blocked) {
737
+ throw new Error('C2 v2 ordinary route closed during terminal settlement')
738
+ }
739
+ identity = completed.successor
740
+ ? { chatId: initial.chatId, receiptId: completed.successor.receiptId,
741
+ turnToken: completed.successor.turnToken,
742
+ effectId: completed.successor.effectId }
743
+ : null
744
+ }
745
+ return { receiptId: firstReceiptId, status: settled[0].status,
746
+ terminalEffectId: settled[0].terminalEffectId,
747
+ ...(settled[0].cancellationEventId
748
+ ? { cancellationEventId: settled[0].cancellationEventId } : {}),
749
+ settledReceiptIds: settled.map(item => item.receiptId),
750
+ ingressClosed: false }
751
+ }
752
+
753
+ stageDurableV2Message(msg, platform) {
754
+ const authority = this.durableV2Runtime
755
+ const handles = this.durableV2Payloads
756
+ if (!authority || !handles) throw new Error('C2 v2 staging is unavailable')
757
+ if (authority.blocked) throw new Error('C2 v2 ingress is closed')
758
+ if (!msg || typeof msg !== 'object' || msg.synthetic || msg.cron ||
759
+ msg.receiptSource && msg.receiptSource !== 'message' ||
760
+ typeof msg.text !== 'string' || !msg.text.trim() ||
761
+ typeof msg.chatId !== 'string' || typeof msg.msgId !== 'string' ||
762
+ !/^om_[A-Za-z0-9_-]{1,124}$/.test(msg.msgId) ||
763
+ !msg.replyCtx || typeof msg.replyCtx !== 'object' ||
764
+ msg.replyCtx.chatId !== msg.chatId ||
765
+ msg.replyCtx.messageId !== msg.msgId ||
766
+ !platform || typeof platform.reply !== 'function') {
767
+ throw new TypeError('C2 v2 staging needs a platform message and reply target')
768
+ }
769
+ const group = this._resolveGroup(msg.chatId)
770
+ if (!group || group.paused) throw new Error('C2 v2 staging needs an active bound group')
771
+ const acceptedAt = Date.now()
772
+ const receipt = { source: 'message', chatId: msg.chatId, sourceId: msg.msgId,
773
+ acceptedAt, quiet: !!msg.quiet }
774
+ receipt.id = taskId(receipt)
775
+ const prior = authority.snapshot()
776
+ const pendingDuplicate = prior.receipts?.pending?.find(item => item.id === receipt.id)
777
+ const completedDuplicate = prior.receipts?.completed?.some(item => item.id === receipt.id)
778
+ if (pendingDuplicate || completedDuplicate) {
779
+ if (pendingDuplicate && (pendingDuplicate.chatId !== msg.chatId ||
780
+ pendingDuplicate.source !== 'message' || pendingDuplicate.sourceId !== msg.msgId)) {
781
+ authority.close()
782
+ throw new Error('C2 v2 duplicate identity differs from durable receipt')
783
+ }
784
+ return { receiptId: receipt.id, cursor: prior.cursor, staged: false,
785
+ duplicate: true, ingressClosed: false }
786
+ }
787
+ const job = { msg: { ...msg, _c2ReceiptId: receipt.id }, platform, group }
788
+ handles.preflight(receipt, job)
789
+ const flags = { quiet: receipt.quiet, polled: !!msg.polled, synthetic: false,
790
+ wantsAck: !!msg.wantsAck, resetSessionAfter: !!msg.resetSessionAfter,
791
+ emptyRetried: false, freshRetried: false, firstResumed: false, failedOver: false }
792
+ let result
793
+ try { result = authority.enqueueMessage({ receipt, flags }) }
794
+ catch (error) {
795
+ authority.close()
796
+ throw error
797
+ }
798
+ try { handles.attach(receipt.id, job) }
799
+ catch (error) {
800
+ authority.close()
801
+ throw error
802
+ }
803
+ const rt = this._rt(msg.chatId)
804
+ if (!rt.v2QueueReceiptIds) rt.v2QueueReceiptIds = []
805
+ rt.v2QueueReceiptIds.push(receipt.id)
806
+ return { receiptId: receipt.id, cursor: result.cursor, staged: true,
807
+ ingressClosed: !!authority.blocked }
808
+ }
809
+
810
+ _nextDurableV2NormalAnchor(chatId) {
811
+ const authority = this.durableV2Runtime
812
+ const snapshot = authority?.snapshot?.()
813
+ const queue = snapshot?.state?.lanes?.queue?.[chatId]?.queue
814
+ const rt = this._rt(chatId)
815
+ const ids = queue?.map(item => typeof item?.id === 'string'
816
+ ? item.id.slice('receipt:job:'.length) : null)
817
+ if (!authority || authority.blocked || !Array.isArray(queue) ||
818
+ !isDeepStrictEqual(ids, rt.v2QueueReceiptIds || []) ||
819
+ !isDeepStrictEqual(this.durableV2Journal?.snapshot(), snapshot)) {
820
+ authority?.close()
821
+ throw new Error('C2 v2 successor differs from durable queue')
822
+ }
823
+ if (!queue.length) return null
824
+ const taken = v2QueueSelection(queue).jobs
825
+ if (!taken.length || taken.some(item => item.synthetic || item.batch ||
826
+ item.quiet || item.polled ||
827
+ !/^receipt:job:[0-9a-f]{32}$/.test(item.id) ||
828
+ snapshot.receipts.pending.find(receipt =>
829
+ receipt.id === item.id.slice('receipt:job:'.length))?.chatId !== chatId ||
830
+ ![...snapshot.effects].some(([effectId, status]) => {
831
+ const descriptor = this.durableV2Journal.effectDescriptor(effectId)
832
+ return status === 'acked' && descriptor?.type === 'queueNotice' &&
833
+ descriptor.receiptId === item.id.slice('receipt:job:'.length)
834
+ }))) {
835
+ authority.close()
836
+ throw new Error('C2 v2 successor lacks flat acknowledged queue cohort')
837
+ }
838
+ try { this.durableV2Payloads.auditOrdinaryQueue(chatId, ids) }
839
+ catch (error) { authority.close(); throw error }
840
+ return v2QueueSelection(queue).anchor
841
+ }
842
+
843
+ commitDurableV2InitialDispatch({ chatId, receiptId } = {}) {
844
+ const authority = this.durableV2Runtime
845
+ const handles = this.durableV2Payloads
846
+ if (!authority || !handles || authority.blocked ||
847
+ typeof authority.snapshot !== 'function' ||
848
+ typeof authority.commitInitialDispatch !== 'function' ||
849
+ typeof handles.consumeCommittedDispatch !== 'function') {
850
+ throw new Error('C2 v2 initial dispatch is unavailable')
851
+ }
852
+ const before = authority.snapshot()
853
+ const queue = before.state?.lanes?.queue?.[chatId]?.queue
854
+ const rt = this._rt(chatId)
855
+ const takenIds = Array.isArray(queue)
856
+ ? v2QueueSelection(queue).jobs.map(item =>
857
+ typeof item?.id === 'string' ? item.id.slice('receipt:job:'.length) : null) : []
858
+ if (typeof chatId !== 'string' || !chatId ||
859
+ typeof receiptId !== 'string' || !/^[0-9a-f]{32}$/.test(receiptId) ||
860
+ !Array.isArray(queue) || v2QueueSelection(queue).anchor !== receiptId ||
861
+ !isDeepStrictEqual(queue.map(item => item.id.slice('receipt:job:'.length)),
862
+ rt.v2QueueReceiptIds)) {
863
+ authority.close()
864
+ throw new Error('C2 v2 initial dispatch differs from durable queue')
865
+ }
866
+ try {
867
+ handles.auditQueued()
868
+ const result = authority.commitInitialDispatch({ receiptId, payloadHandles: handles })
869
+ const token = result.decision?.turnToken
870
+ const jobs = handles.consumeCommittedDispatch({ eventId: result.eventId,
871
+ turnToken: token, receiptIds: result.jobReceiptIds })
872
+ const after = authority.snapshot()
873
+ const remaining = after.state?.lanes?.queue?.[chatId]?.queue
874
+ if (!Array.isArray(remaining) || !Array.isArray(jobs) ||
875
+ !isDeepStrictEqual(result.jobReceiptIds, takenIds) ||
876
+ jobs.length !== result.jobReceiptIds.length ||
877
+ !isDeepStrictEqual(remaining.map(item => item.id.slice('receipt:job:'.length)),
878
+ queue.slice(result.jobReceiptIds.length).map(item => item.id.slice('receipt:job:'.length)))) {
879
+ throw new Error('C2 v2 initial dispatch host result invalid')
880
+ }
881
+ rt.v2QueueReceiptIds = remaining.map(item => item.id.slice('receipt:job:'.length))
882
+ rt.v2RunningJobs = jobs
883
+ rt.v2TurnToken = token
884
+ // This is only a process-local binding for the next migration step.
885
+ // The durable journal stores IDs/status, not an executable reply target.
886
+ rt.v2DispatchEffect = { effectId: result.effects[0].effectId,
887
+ type: result.effects[0].type, turnToken: token,
888
+ receiptIds: [...result.jobReceiptIds],
889
+ batchReceiptIds: [...result.batchReceiptIds] }
890
+ if (!this.durableV2StartTurn ||
891
+ !ordinaryV2Cohort(after, jobs, rt.v2DispatchEffect, chatId, receiptId)) {
892
+ authority.close()
893
+ }
894
+ return { cursor: result.cursor, turnToken: token,
895
+ dispatchedReceiptIds: [...result.jobReceiptIds],
896
+ effectsPending: result.effects.map(effect => effect.effectId),
897
+ ingressClosed: !!authority.blocked }
898
+ } catch (error) { authority.close(); throw error }
899
+ }
900
+
901
+ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
902
+ const authority = this.durableV2Runtime
903
+ if (!this.durableV2StartTurn || !authority || authority.blocked) {
904
+ throw new Error('C2 v2 runner start is unavailable')
905
+ }
906
+ const rt = typeof chatId === 'string' && chatId ? this._rt(chatId) : null
907
+ const snapshot = authority.snapshot()
908
+ const receipt = snapshot.receipts?.pending?.find(item => item.id === receiptId)
909
+ const binding = rt?.v2DispatchEffect
910
+ const jobs = rt?.v2RunningJobs
911
+ const cohort = ordinaryV2Cohort(snapshot, jobs, binding, chatId, receiptId)
912
+ if (!/^[0-9a-f]{32}$/.test(receiptId || '') ||
913
+ !Number.isSafeInteger(turnToken) || turnToken <= 0 ||
914
+ typeof effectId !== 'string' || !effectId ||
915
+ receipt?.chatId !== chatId ||
916
+ snapshot.state?.lanes?.turn?.[chatId]?.active !== turnToken ||
917
+ snapshot.state?.lanes?.handoff?.[chatId]?.phase !== 'running' ||
918
+ snapshot.state.lanes.handoff[chatId].turnToken !== turnToken ||
919
+ binding?.type !== 'dispatch' || binding.effectId !== effectId ||
920
+ binding.turnToken !== turnToken ||
921
+ !cohort ||
922
+ snapshot.effects?.get(effectId) !== 'pending' ||
923
+ rt.v2RunnerGeneration) {
924
+ authority.close()
925
+ throw new Error('C2 v2 runner start differs from durable dispatch')
926
+ }
927
+ const generation = randomBytes(16).toString('hex')
928
+ rt.v2RunnerGeneration = generation
929
+ rt.v2StartedAt = Date.now()
930
+ rt.v2CardBinding = null
931
+ rt.v2StreamRequested = false
932
+ rt.v2StreamQuiescing = false
933
+ rt.v2StreamChain = Promise.resolve()
934
+ rt.v2StopIntentGate = null
935
+ rt.v2TimeoutProof = null
936
+ const verifyGeneration = value => value.chatId === chatId &&
937
+ value.receiptId === receiptId && value.turnToken === turnToken &&
938
+ value.generation === generation && rt.v2RunnerGeneration === generation
939
+ let capability
940
+ let accepted = false
941
+ let controller = null
942
+ let capTimer = null
943
+ try {
944
+ capability = authority.beginRunnerStart({ receiptId, chatId, turnToken,
945
+ generation, verifyGeneration })
946
+ if (capability.effectId !== effectId) {
947
+ throw new Error('C2 v2 runner effect changed before start')
948
+ }
949
+ const projection = createC2TurnProjection({ receiptId, turnToken,
950
+ quiet: receipt.quiet,
951
+ resumed: !!this.store.getSession(chatId), freshRetried: false,
952
+ ...(cohort.receiptIds.length > 1
953
+ ? { batchReceiptIds: cohort.receiptIds } : {}) })
954
+ const job = cohort.anchor
955
+ const { name, provider } = this._resolveProvider(chatId, job.group)
956
+ if (!provider || (provider.baseUrl && !provider.apiKey)) {
957
+ throw new Error('C2 v2 runner provider unavailable')
958
+ }
959
+ if (provider.runner === 'codex' && job.group.codexSteer !== false) {
960
+ throw new Error('C2 v2 follow-up control has no durable authority')
961
+ }
962
+ const prompt = this._prepareRunnerPrompt({ msg: job.msg, group: job.group,
963
+ platform: job.platform, providerName: name, provider, rt,
964
+ batch: cohort.receiptIds.length > 1 ? cohort.jobs.map(item => item.msg) : null,
965
+ consumeLifecycle: false })
966
+ controller = new AbortController()
967
+ rt.v2Controller = controller
968
+ rt.v2RunnerStopPromise = null
969
+ rt.v2RunnerStopVerified = false
970
+ let streamChain = Promise.resolve()
971
+ let streamFailure = null
972
+ const enqueueFrame = event => {
973
+ if (!this.durableV2StreamFrames || event.type !== 'assistant') return
974
+ if (!event.text || controller.signal.aborted || rt.v2CancelControl ||
975
+ rt.v2StreamQuiescing ||
976
+ authority.scopeState('stopIntent', chatId)) return
977
+ const buffer = projection.streamSnapshot().bufferText
978
+ const trimmed = buffer.trim()
979
+ if (!trimmed || NO_REPLY.startsWith(trimmed)) return
980
+ const text = buffer
981
+ rt.v2StreamRequested = true
982
+ streamChain = streamChain.then(async () => {
983
+ if (rt.v2QueueNoticePromise) await rt.v2QueueNoticePromise
984
+ if (authority.blocked) throw new Error('C2 v2 stream authority closed')
985
+ if (controller.signal.aborted || rt.v2CancelControl ||
986
+ authority.scopeState('stopIntent', chatId)) return
987
+ if (!rt.v2CardBinding) {
988
+ const path = prepareCardTerminalPath({ runtime: authority,
989
+ payloadHandles: this.durableV2Payloads, platform: job.platform,
990
+ replyCtx: job.msg.replyCtx, receiptId, chatId, turnToken,
991
+ generation, verifyGeneration,
992
+ applyLocalEffect: this.durableV2ApplyLocalEffect,
993
+ assertWriterLock: this.durableV2AssertWriterLock,
994
+ redactText: value => redact(value,
995
+ { pii: job.group?.redactPii !== false }),
996
+ sendPlainReply: typeof job.platform.replyOnce === 'function'
997
+ ? (target, value) => job.platform.replyOnce(target.replyCtx, value)
998
+ : null })
999
+ const opened = await path.open()
1000
+ rt.v2CardBinding = { path, receiptId, turnToken,
1001
+ generation, opened }
1002
+ }
1003
+ if (rt.v2CardBinding.opened) rt.v2CardBinding.path.update(text)
1004
+ })
1005
+ rt.v2StreamChain = streamChain
1006
+ streamChain.catch(error => {
1007
+ streamFailure = error
1008
+ try { controller.abort() } catch {}
1009
+ })
1010
+ }
1011
+ const maxMins = Number(this.cfg.agent.maxTurnTimeMins) || 0
1012
+ const timeoutMs = maxMins > 0 ? maxMins * 60_000 : 0
1013
+ const timeoutDeadlineAt = timeoutMs > 0 ? Date.now() + timeoutMs : null
1014
+ if (timeoutDeadlineAt !== null &&
1015
+ (!Number.isSafeInteger(timeoutDeadlineAt) || timeoutDeadlineAt <= 0)) {
1016
+ throw new Error('C2 v2 hard timeout deadline invalid')
1017
+ }
1018
+ capTimer = timeoutDeadlineAt !== null
1019
+ ? setTimeout(() => {
1020
+ const elapsedAt = Date.now()
1021
+ if (rt.v2RunnerGeneration !== generation ||
1022
+ rt.v2Controller !== controller || rt.v2CancelControl ||
1023
+ rt.v2StreamQuiescing) return
1024
+ if (elapsedAt >= timeoutDeadlineAt) {
1025
+ rt.v2TimeoutProof = { receiptId, turnToken, generation,
1026
+ deadlineAt: timeoutDeadlineAt, elapsedAt }
1027
+ }
1028
+ controller.abort()
1029
+ }, timeoutMs) : null
1030
+ const input = prepareC2RunnerInput({ prompt,
1031
+ appendSystemPrompt: SYSTEM_APPENDIX, msg: job.msg, group: job.group,
1032
+ provider, config: this.cfg,
1033
+ sessionId: this.store.getSession(chatId) || null,
1034
+ signal: controller.signal, extraEnv: this.baseSkillEnv(chatId),
1035
+ onDurableEvent: event => {
1036
+ projection.accept(event)
1037
+ enqueueFrame(event)
1038
+ },
1039
+ onTurnAccepted: value => {
1040
+ const ack = capability.onTurnAccepted(value)
1041
+ accepted = true
1042
+ return ack
1043
+ },
1044
+ onSpawnFailed: value => capability.onSpawnFailed(value),
1045
+ log: this.log })
1046
+ let started
1047
+ try { started = this.durableV2StartTurn(input) }
1048
+ catch (error) { started = Promise.reject(error) }
1049
+ const running = Promise.resolve(started)
1050
+ const stopWitness = (result, error) => {
1051
+ let verified = false
1052
+ try {
1053
+ verified = this.durableV2VerifyRunnerClosed?.({ chatId, receiptId,
1054
+ turnToken, generation, result, error }) === true
1055
+ } catch { /* missing or failed close witness stays unverified */ }
1056
+ rt.v2RunnerStopVerified = verified
1057
+ return verified
1058
+ }
1059
+ rt.v2RunnerStopPromise = running.then(
1060
+ result => stopWitness(result, null),
1061
+ error => stopWitness(null, error))
1062
+ let result
1063
+ try {
1064
+ result = await running
1065
+ if (rt.v2QueueNoticePromise) await rt.v2QueueNoticePromise
1066
+ await rt.v2RunnerStopPromise
1067
+ await streamChain
1068
+ if (streamFailure) throw streamFailure
1069
+ if (rt.v2CardBinding?.opened && !rt.v2StreamQuiescing) {
1070
+ await rt.v2CardBinding.path.flushForTerminal()
1071
+ }
1072
+ if (rt.v2StreamQuiescing && rt.v2StopIntentGate) {
1073
+ await rt.v2StopIntentGate
1074
+ }
1075
+ }
1076
+ finally {
1077
+ if (capTimer) clearTimeout(capTimer)
1078
+ if (rt.v2Controller === controller) rt.v2Controller = null
1079
+ }
1080
+ if (authority.blocked && rt.v2RunnerStopVerified !== true) {
1081
+ throw new Error('C2 v2 runner closure is unverified after shutdown')
1082
+ }
1083
+ if (!accepted) throw new Error('C2 v2 runner exited before trusted acceptance')
1084
+ // A wall-clock cap or shutdown may abort after the last result event but
1085
+ // before the child closes. The original result object remains the close
1086
+ // witness; the host projection must still classify the turn as stopped.
1087
+ const settledResult = controller.signal.aborted && result?.aborted === false
1088
+ ? { ...result, aborted: true } : result
1089
+ // Codex reports cumulative session usage. Use the same private-core
1090
+ // arithmetic as the legacy path, without writing session state before
1091
+ // terminal effects have been confirmed.
1092
+ const usage = settledResult?.result?.usage
1093
+ const usageDecision = usage ? settleUsage(
1094
+ this.store.getSessionStats?.(chatId) || null,
1095
+ settledResult?.sessionId || this.store.getSession(chatId) || null,
1096
+ settledResult.result) : null
1097
+ const outputTokens = Number(usageDecision?.tokens?.out) || 0
1098
+ const projected = projection.complete({ runnerResult: settledResult,
1099
+ outputTokens, resumedSessionCurrent: false })
1100
+ if (this.durableV2VerifyRunnerClosed) {
1101
+ const closed = authority.confirmRunnerClosed({ receiptId, chatId,
1102
+ turnToken, generation, verifyGeneration,
1103
+ assertRunnerClosed: () => rt.v2RunnerStopVerified === true })
1104
+ rt.v2RunnerCloseEvidenceId = closed.evidenceId
1105
+ if (usageDecision?.write &&
1106
+ (!projected.display.sessionId ||
1107
+ usageDecision.stats?.sessionId !== projected.display.sessionId)) {
1108
+ throw new Error('C2 v2 usage session identity unresolved after runner close')
1109
+ }
1110
+ const session = authority.commitClosedRunnerSession({ receiptId, chatId,
1111
+ turnToken, generation, verifyGeneration,
1112
+ sessionId: projected.display.sessionId || null,
1113
+ ...(usageDecision?.write ? { usageDecision } : {}),
1114
+ aborted: settledResult.aborted, assertWriterLock: this.durableV2AssertWriterLock,
1115
+ store: this.store })
1116
+ rt.v2SessionCloseEventId = session.eventId
1117
+ }
1118
+ rt.v2RunnerResult = settledResult // process-local only; no reply until terminal effect
1119
+ rt.v2TerminalProjection = projected
1120
+ rt.v2UsageDecision = usageDecision
1121
+ return { effectId, accepted: true, outputHeld: true }
1122
+ } catch (error) {
1123
+ if (capTimer) clearTimeout(capTimer)
1124
+ if (controller && rt.v2Controller === controller) rt.v2Controller = null
1125
+ const proofId = `sbot-c2-runner-not-started-v1:${receiptId}:${generation}`
1126
+ const eventId = `sbot-c2-runner-not-started-commit-v1:${receiptId}:${turnToken}:${generation}`
1127
+ const provenNotStarted = !accepted && error?.code === 'AGENT_BIN_MISSING' &&
1128
+ this.durableV2Journal?.lookupRunnerNotStarted(proofId)?.dispatchEffectId === effectId &&
1129
+ this.durableV2Journal?.hasEvent(eventId) === true
1130
+ if (provenNotStarted) {
1131
+ authority.close()
1132
+ await authority.open()
1133
+ if (authority.snapshot().receipts.pending.some(item => item.id === receiptId) ||
1134
+ !authority.snapshot().receipts.completed.some(item => item.id === receiptId)) {
1135
+ authority.close()
1136
+ throw new Error('C2 v2 unstarted runner receipt remains unsettled')
1137
+ }
1138
+ rt.v2RunnerGeneration = null
1139
+ rt.v2RunningJobs = null
1140
+ rt.v2DispatchEffect = null
1141
+ return { effectId, accepted: false, startFailureRecovered: true }
1142
+ }
1143
+ if (capability && !accepted) {
1144
+ try { capability.markUnknown() } catch { /* journal itself seals on failed write */ }
1145
+ }
1146
+ authority.close()
1147
+ throw error
1148
+ }
1149
+ }
1150
+
1151
+ async completeDurableV2CardTurn({ chatId, receiptId, turnToken, effectId,
1152
+ payload, replyText, paused = false, updates = [] } = {}) {
1153
+ const authority = this.durableV2Runtime
1154
+ const handles = this.durableV2Payloads
1155
+ const rt = typeof chatId === 'string' && chatId ? this._rt(chatId) : null
1156
+ if (!authority || !handles || !this.durableV2StartTurn ||
1157
+ !this.durableV2ApplyLocalEffect || authority.blocked ||
1158
+ typeof paused !== 'boolean' || !Array.isArray(updates) ||
1159
+ !updates.every(text => typeof text === 'string')) {
1160
+ throw new Error('C2 v2 card terminal path is unavailable')
1161
+ }
1162
+ const snapshot = authority.snapshot()
1163
+ const receipt = snapshot.receipts?.pending?.find(item => item.id === receiptId)
1164
+ const binding = rt?.v2DispatchEffect
1165
+ const jobs = rt?.v2RunningJobs
1166
+ const cohort = ordinaryV2Cohort(snapshot, jobs, binding, chatId, receiptId)
1167
+ const job = cohort?.anchor
1168
+ const generation = rt?.v2RunnerGeneration
1169
+ const projected = rt?.v2TerminalProjection
1170
+ const resultText = projected?.payload?.resultText
1171
+ if (!/^[0-9a-f]{32}$/.test(receiptId || '') ||
1172
+ !Number.isSafeInteger(turnToken) || turnToken <= 0 ||
1173
+ typeof effectId !== 'string' || !effectId ||
1174
+ typeof generation !== 'string' || !/^[0-9a-f]{32}$/.test(generation) ||
1175
+ typeof resultText !== 'string' ||
1176
+ rt.v2RunnerCloseEvidenceId !==
1177
+ `sbot-c2-runner-closed-v1:${receiptId}:${generation}` ||
1178
+ rt.v2SessionCloseEventId !==
1179
+ `sbot-c2-session-close-v1:${receiptId}:${generation}` ||
1180
+ receipt?.source !== 'message' || receipt.chatId !== chatId ||
1181
+ snapshot.state?.lanes?.turn?.[chatId]?.active !== turnToken ||
1182
+ snapshot.state?.lanes?.handoff?.[chatId]?.phase !== 'running' ||
1183
+ snapshot.state.lanes.handoff[chatId].turnToken !== turnToken ||
1184
+ binding?.type !== 'dispatch' || binding.effectId !== effectId ||
1185
+ binding.turnToken !== turnToken ||
1186
+ !cohort ||
1187
+ snapshot.effects?.get(effectId) !== 'acked' ||
1188
+ !Number.isSafeInteger(rt.v2StartedAt) || rt.v2StartedAt <= 0 ||
1189
+ job.msg.msgId !== receipt.sourceId ||
1190
+ job.msg.replyCtx?.chatId !== chatId ||
1191
+ job.msg.replyCtx.messageId !== receipt.sourceId ||
1192
+ (typeof job.platform?.createStreamingCard !== 'function' &&
1193
+ typeof job.platform?.replyOnce !== 'function') ||
1194
+ !payload || typeof payload !== 'object' || Array.isArray(payload) ||
1195
+ payload.type !== 'finalizeEffectCommit' ||
1196
+ payload.receiptId !== receiptId || payload.turnToken !== turnToken ||
1197
+ payload.quiet !== receipt.quiet || payload.resultText !== resultText ||
1198
+ (cohort.receiptIds.length > 1 &&
1199
+ (!isDeepStrictEqual(payload.batchReceiptIds, cohort.receiptIds) ||
1200
+ payload.replyTargetReceiptId !== receiptId)) ||
1201
+ !isDeepStrictEqual(payload, projected.payload) ||
1202
+ // The isolated bridge currently implements only a successful final
1203
+ // answer. An abort, provider error, quota or lost-session result needs
1204
+ // its own durable retry/failover/notice decision before any card call.
1205
+ payload.source !== 'result' || payload.aborted || payload.isError ||
1206
+ payload.sessionMissing || payload.hasQuota || payload.transientNetwork ||
1207
+ payload.freshRetried || payload.resumedSessionCurrent ||
1208
+ (replyText !== undefined && replyText !== terminalReplyText(projected.payload)) ||
1209
+ rt.v2TerminalStarted) {
1210
+ authority.close()
1211
+ throw new Error('C2 v2 card terminal differs from accepted turn')
1212
+ }
1213
+ rt.v2TerminalStarted = true
1214
+ const verifyGeneration = value => value.chatId === chatId &&
1215
+ value.receiptId === receiptId && value.turnToken === turnToken &&
1216
+ value.generation === generation && rt.v2RunnerGeneration === generation
1217
+ try {
1218
+ const preopened = rt.v2CardBinding
1219
+ if (preopened && (preopened.receiptId !== receiptId ||
1220
+ preopened.turnToken !== turnToken ||
1221
+ preopened.generation !== generation)) {
1222
+ throw new Error('C2 v2 streamed card differs from terminal turn')
1223
+ }
1224
+ const path = preopened?.path || prepareCardTerminalPath({ runtime: authority,
1225
+ payloadHandles: handles, platform: job.platform,
1226
+ replyCtx: job.msg.replyCtx, receiptId, chatId, turnToken, generation,
1227
+ verifyGeneration, applyLocalEffect: this.durableV2ApplyLocalEffect,
1228
+ assertWriterLock: this.durableV2AssertWriterLock,
1229
+ redactText: text => redact(text, { pii: job.group?.redactPii !== false }),
1230
+ sendPlainReply: typeof job.platform.replyOnce === 'function'
1231
+ ? (target, text) => job.platform.replyOnce(target.replyCtx, text)
1232
+ : null })
1233
+ const suppressCard = (payload.quiet || projected.display.silent) &&
1234
+ projected.display.assistantEvents === 0 && updates.length === 0
1235
+ if (!preopened) await path.open({ suppressCard })
1236
+ for (const text of updates) path.update(text)
1237
+ const footer = T.cardFooter({ ms: Math.max(0, Date.now() - rt.v2StartedAt),
1238
+ tokens: rt.v2UsageDecision?.tokens || null })
1239
+ const completed = await path.complete({ payload, replyText, paused, footer })
1240
+ const after = authority.snapshot()
1241
+ const queue = after.state?.lanes?.queue?.[chatId]?.queue
1242
+ let successor = null
1243
+ if (completed?.nextReceiptId === null) {
1244
+ if (!Array.isArray(completed.pendingEffects) ||
1245
+ completed.pendingEffects.length !== 0 ||
1246
+ !Array.isArray(queue) || queue.length !== 0 ||
1247
+ after.state?.lanes?.turn?.[chatId]?.active !== null ||
1248
+ after.state?.lanes?.handoff?.[chatId]?.phase !== 'idle' ||
1249
+ after.receipts.pending.some(item => item.chatId === chatId) ||
1250
+ !Array.isArray(rt.v2QueueReceiptIds) ||
1251
+ rt.v2QueueReceiptIds.length !== 0) {
1252
+ throw new Error('C2 v2 terminal successor lacks durable dispatch wiring')
1253
+ }
1254
+ } else {
1255
+ if (!this.durableV2Journal || !this.durableV2AssertWriterLock) {
1256
+ throw new Error('C2 v2 terminal successor lacks durable dispatch wiring')
1257
+ }
1258
+ const next = continueC2Successor({ runtime: authority,
1259
+ journal: this.durableV2Journal, payloadHandles: handles,
1260
+ chatId, priorReceiptId: receiptId, priorTurnToken: turnToken,
1261
+ terminalResult: completed, generation: randomBytes(16).toString('hex'),
1262
+ assertWriterLock: this.durableV2AssertWriterLock })
1263
+ if (!ordinaryV2Cohort(authority.snapshot(), next.jobs,
1264
+ { receiptIds: next.jobReceiptIds,
1265
+ batchReceiptIds: next.batchReceiptIds }, chatId, next.receiptId) ||
1266
+ !isDeepStrictEqual(rt.v2QueueReceiptIds,
1267
+ [...next.jobReceiptIds, ...next.queuedReceiptIds])) {
1268
+ throw new Error('C2 v2 successor jobs differ from queued host state')
1269
+ }
1270
+ successor = next
1271
+ }
1272
+ rt.v2RunnerResult = null
1273
+ rt.v2TerminalProjection = null
1274
+ rt.v2UsageDecision = null
1275
+ rt.v2StartedAt = null
1276
+ rt.v2CardBinding = null
1277
+ rt.v2StreamRequested = false
1278
+ rt.v2TimeoutProof = null
1279
+ rt.v2RunningJobs = null
1280
+ rt.v2RunnerGeneration = null
1281
+ rt.v2RunnerCloseEvidenceId = null
1282
+ rt.v2SessionCloseEventId = null
1283
+ rt.v2DispatchEffect = null
1284
+ rt.v2TurnToken = null
1285
+ rt.v2TerminalStarted = false
1286
+ if (successor) {
1287
+ rt.v2QueueReceiptIds = [...successor.queuedReceiptIds]
1288
+ rt.v2RunningJobs = [...successor.jobs]
1289
+ rt.v2TurnToken = successor.turnToken
1290
+ rt.v2DispatchEffect = { effectId: successor.effectId,
1291
+ type: 'dispatch', turnToken: successor.turnToken,
1292
+ receiptIds: [...successor.jobReceiptIds],
1293
+ batchReceiptIds: [...successor.batchReceiptIds] }
1294
+ return { ...completed, settledReceiptIds: cohort.receiptIds,
1295
+ successor: { receiptId: successor.receiptId,
1296
+ turnToken: successor.turnToken, effectId: successor.effectId,
1297
+ cursor: successor.cursor } }
1298
+ }
1299
+ return { ...completed, settledReceiptIds: cohort.receiptIds }
1300
+ } catch (error) { authority.close(); throw error }
1301
+ }
1302
+
1303
+ classifyDurableV2UnresolvedTurn({ chatId, receiptId, turnToken, effectId,
1304
+ payload } = {}) {
1305
+ const authority = this.durableV2Runtime
1306
+ const rt = typeof chatId === 'string' && chatId ? this._rt(chatId) : null
1307
+ if (!authority || authority.blocked || !rt ||
1308
+ typeof authority.commitUnresolvedClassification !== 'function') {
1309
+ throw new Error('C2 v2 unresolved classification is unavailable')
1310
+ }
1311
+ try {
1312
+ const snapshot = authority.snapshot()
1313
+ const receipt = snapshot.receipts?.pending?.find(item => item.id === receiptId)
1314
+ const binding = rt.v2DispatchEffect
1315
+ const jobs = rt.v2RunningJobs
1316
+ const cohort = ordinaryV2Cohort(snapshot, jobs, binding, chatId, receiptId)
1317
+ const generation = rt.v2RunnerGeneration
1318
+ if (!/^[0-9a-f]{32}$/.test(receiptId || '') ||
1319
+ !Number.isSafeInteger(turnToken) || turnToken <= 0 ||
1320
+ typeof effectId !== 'string' || !effectId ||
1321
+ typeof generation !== 'string' || !/^[0-9a-f]{32}$/.test(generation) ||
1322
+ rt.v2RunnerCloseEvidenceId !==
1323
+ `sbot-c2-runner-closed-v1:${receiptId}:${generation}` ||
1324
+ rt.v2SessionCloseEventId !==
1325
+ `sbot-c2-session-close-v1:${receiptId}:${generation}` ||
1326
+ receipt?.source !== 'message' || receipt.chatId !== chatId ||
1327
+ snapshot.state?.lanes?.turn?.[chatId]?.active !== turnToken ||
1328
+ snapshot.state?.lanes?.handoff?.[chatId]?.phase !== 'running' ||
1329
+ snapshot.state.lanes.handoff[chatId].turnToken !== turnToken ||
1330
+ binding?.type !== 'dispatch' || binding.effectId !== effectId ||
1331
+ binding.turnToken !== turnToken ||
1332
+ !cohort ||
1333
+ snapshot.effects?.get(effectId) !== 'acked' ||
1334
+ cohort.anchor.msg.msgId !== receipt.sourceId ||
1335
+ !payload || typeof payload !== 'object' || Array.isArray(payload) ||
1336
+ !isDeepStrictEqual(payload, rt.v2TerminalProjection?.payload) ||
1337
+ rt.v2TerminalStarted) {
1338
+ throw new Error('C2 v2 unresolved classification differs from accepted turn')
1339
+ }
1340
+ const terminal = { ...payload, type: 'finalize' }
1341
+ delete terminal.receiptId
1342
+ delete terminal.turnToken
1343
+ delete terminal.quiet
1344
+ const status = engineTerminalStep({ finalized: false, outcome: null },
1345
+ terminal).state.outcome.status
1346
+ if (!['error', 'empty', 'stopped'].includes(status)) {
1347
+ throw new Error('C2 v2 unresolved classification needs error, empty or stopped outcome')
1348
+ }
1349
+ rt.v2TerminalStarted = true
1350
+ authority.registerTerminalCallback({ receiptId, generation,
1351
+ registeredAt: Date.now() })
1352
+ return authority.commitUnresolvedClassification({ receiptId, generation,
1353
+ turnToken, status, payload,
1354
+ verifyGeneration: value => value.chatId === chatId &&
1355
+ value.receiptId === receiptId && value.turnToken === turnToken &&
1356
+ value.generation === generation && rt.v2RunnerGeneration === generation,
1357
+ assertWriterLock: this.durableV2AssertWriterLock })
1358
+ } catch (error) { authority.close(); throw error }
1359
+ }
1360
+
1361
+ _prepareRunnerPrompt({ msg, group, platform, providerName, provider, batch, rt }) {
1362
+ if (rt.cancelled) throw new Error('C2 v2 cancelled prompt lacks durable control event')
1363
+ const line = m => `[${fmtStamp(m.createTimeMs || Date.now(), this.tz)} ${m.senderName || m.userId || 'user'}] ${(m.text || '').trim()}`
1364
+ const policy = manifestPolicy(group.permissionMode, provider.runner === 'codex',
1365
+ group.allowedTools, group.disallowedTools)
1366
+ const brand = this.cfg.brand || 'SintBot'
1367
+ const info = group.repo && this.repoInfo ? this.repoInfo(group.workdir, group.repo) : null
1368
+ const manifest = [
1369
+ ...manifestHeader({ restrictedV2: true, brand, chatLabel: platform.chatLabel || '项目群',
1370
+ chatId: msg.chatId, groupName: group.name || msg.chatId,
1371
+ workdir: group.workdir, repo: group.repo || '', repoState: info?.state || '',
1372
+ repoOrigin: info?.origin || '未知', pushDenied: policy.pushDenied,
1373
+ providerName, model: group.model || provider.model || '',
1374
+ noVision: provider.vision === false, platformName: platform.chatLabel || '项目群',
1375
+ permissionTier: policy.tier, tz: this.tz, publicUrl: '', uiUrl: '', lanUrl: '' }),
1376
+ ...manifestContext({ restrictedV2: true, brand, isCodex: provider.runner === 'codex',
1377
+ pmem: this.projectMemory ? this.projectMemory(group.workdir,
1378
+ { full: policy.full, isCodex: provider.runner === 'codex' }) : null,
1379
+ unredactedPii: group.redactPii === false,
1380
+ cardPinned: platform.features?.cardPinned === true, plainSend: false }),
1381
+ ].join('\n')
1382
+ // Reinject current supported context each turn. No legacy feature menu,
1383
+ // external-message commands or v1 manifest refresh scheduler is included.
1384
+ return turnPrompt({ lines: batch?.length > 1 ? batch.map(line) : [line(msg)],
1385
+ cancelled: null, compacted: false, manifest, updated: false })
1386
+ }
1387
+
1388
+ handleMessage() { throw new Error('legacy Engine execution path is not enabled') }
1389
+
1390
+ stop(chatId) {
1391
+ const rt = this.rts.get(chatId)
1392
+ return rt?.v2NormalRouteActive ? this.requestDurableV2Stop(chatId) : false
1393
+ }
1394
+
1395
+ requestDurableV2Stop(chatId) {
1396
+ const rt = this.rts.get(chatId)
1397
+ if (!this.durableV2NormalRoute || !rt?.v2NormalRouteActive ||
1398
+ !rt.v2RunningJobs?.length || rt.v2TerminalStarted ||
1399
+ rt.v2CancelControl || rt.v2StopControl || rt.v2StreamQuiescing ||
1400
+ rt.v2TimeoutProof || !rt.v2Controller ||
1401
+ !this.durableV2Runtime || this.durableV2Runtime.blocked) return false
1402
+ try {
1403
+ if (this.durableV2AssertWriterLock() !== true) throw new Error('writer lock lost')
1404
+ const snapshot = this.durableV2Runtime.snapshot()
1405
+ const receiptId = rt.v2DispatchEffect?.receiptIds?.at(-1)
1406
+ if (!ordinaryV2Cohort(snapshot, rt.v2RunningJobs,
1407
+ rt.v2DispatchEffect, chatId, receiptId) ||
1408
+ snapshot.state?.lanes?.turn?.[chatId]?.active !== rt.v2TurnToken ||
1409
+ snapshot.state?.lanes?.handoff?.[chatId]?.phase !== 'running') {
1410
+ throw new Error('console stop differs from durable running cohort')
1411
+ }
1412
+ const controller = rt.v2Controller
1413
+ const generation = rt.v2RunnerGeneration
1414
+ let releaseGate
1415
+ rt.v2StopIntentGate = new Promise(resolve => { releaseGate = resolve })
1416
+ rt.v2StreamQuiescing = true
1417
+ rt.v2CancelControl = 'stop'
1418
+ Promise.resolve().then(async () => {
1419
+ if (rt.v2QueueNoticePromise) await rt.v2QueueNoticePromise
1420
+ await rt.v2StreamChain
1421
+ if (rt.v2CardBinding?.opened) {
1422
+ await rt.v2CardBinding.path.flushForTerminal()
1423
+ }
1424
+ if (rt.v2RunnerGeneration !== generation ||
1425
+ rt.v2TimeoutProof || this.durableV2AssertWriterLock() !== true) {
1426
+ throw new Error('console stop lost exact running turn')
1427
+ }
1428
+ }).catch(error => {
1429
+ this.durableV2Runtime.close()
1430
+ this.log(`group ${chatId}: durable console stop closed: ${error.message}`)
1431
+ }).finally(() => {
1432
+ try { controller.abort() } catch { this.durableV2Runtime.close() }
1433
+ releaseGate()
1434
+ })
1435
+ return true
1436
+ } catch (error) {
1437
+ this.durableV2Runtime.close()
1438
+ this.log(`group ${chatId}: durable stop closed: ${error.message}`)
1439
+ return false
1440
+ }
1441
+ }
1442
+
1443
+ handleRecall({ chatId, msgId } = {}) {
1444
+ if (!chatId || !msgId) return false
1445
+ const rt = this.rts.get(chatId)
1446
+ if (!rt || !this.durableV2NormalRoute || !rt.v2NormalRouteActive) return false
1447
+ const authority = this.durableV2Runtime
1448
+ const receiptId = taskId({ source: 'message', chatId, sourceId: msgId })
1449
+ if (rt.v2QueueReceiptIds?.includes(receiptId)) {
1450
+ try {
1451
+ authority.commitQueuedCancellation({ receiptId, chatId, control: 'recall',
1452
+ payloadHandles: this.durableV2Payloads,
1453
+ assertWriterLock: this.durableV2AssertWriterLock })
1454
+ rt.v2QueueReceiptIds = rt.v2QueueReceiptIds.filter(id => id !== receiptId)
1455
+ return true
1456
+ } catch (error) { authority?.close(); this.log(error.message); return false }
1457
+ }
1458
+ if (rt.v2RunningJobs?.length === 1 &&
1459
+ rt.v2RunningJobs[0]?.msg?.msgId === msgId && !rt.v2TerminalStarted) {
1460
+ rt.v2CancelControl = 'recall'
1461
+ if (rt.v2Controller) { try { rt.v2Controller.abort() } catch { authority?.close() } }
1462
+ return true
1463
+ }
1464
+ return false
1465
+ }
1466
+
1467
+ }
1468
+
1469
+ module.exports = { Engine }