sintbot 1.0.16 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.json +99 -0
- package/CHANGELOG.json +99 -0
- package/README.md +20 -158
- package/README.zh-CN.md +20 -166
- package/THIRD-PARTY-NOTICES.md +394 -0
- package/bin/sintbot-v2.js +363 -0
- package/docs/AI-ASSISTED-INSTALL.md +11 -35
- package/docs/install.md +61 -167
- package/docs/zh-CN/AI-ASSISTED-INSTALL.md +11 -35
- package/docs/zh-CN/install.md +61 -165
- package/install.sh +84 -0
- package/package.json +73 -37
- package/src/agent/claudecode.js +149 -108
- package/src/agent/codexapp.js +458 -0
- package/src/agent/codexcode.js +210 -107
- package/src/agent/errtag.js +34 -0
- package/src/agent/privatecore.js +790 -0
- package/src/agent/privatecore.wasm +0 -0
- package/src/agent/runner.js +10 -8
- package/src/agent/winexec.js +35 -5
- package/src/c2install.js +117 -0
- package/src/config.js +2 -1
- package/src/core/c2cardeffectbridge.js +51 -0
- package/src/core/c2cardterminalpath.js +216 -0
- package/src/core/c2durableingress.js +148 -0
- package/src/core/c2durablejournal.js +1560 -0
- package/src/core/c2durableruntime.js +2361 -0
- package/src/core/c2eventidentity.js +90 -0
- package/src/core/c2globalfault.js +401 -0
- package/src/core/c2localeffectexecutor.js +193 -0
- package/src/core/c2payloadhandles.js +333 -0
- package/src/core/c2persistrelation.js +179 -0
- package/src/core/c2persiststate.js +342 -0
- package/src/core/c2platformrecoverysender.js +76 -0
- package/src/core/c2recoverygate.js +385 -0
- package/src/core/c2runnerinput.js +63 -0
- package/src/core/c2servicestartgate.js +103 -0
- package/src/core/c2sessionbridge.js +72 -0
- package/src/core/c2sourcequarantine.js +30 -0
- package/src/core/c2startuprecovery.js +105 -0
- package/src/core/c2successorbridge.js +151 -0
- package/src/core/c2terminaleffectbridge.js +154 -0
- package/src/core/c2terminalprune.js +108 -0
- package/src/core/c2turnprojection.js +177 -0
- package/src/core/enginecheckpoint.js +211 -0
- package/src/core/engineeffectcursor.js +394 -0
- package/src/core/enginestepcoordinator.js +183 -0
- package/src/core/store.js +52 -8
- package/src/core/v2engine.js +1469 -0
- package/src/platform/feishu.js +324 -67
- package/src/plugins.js +1 -9
- package/assets/design-styles/acid-pop.md +0 -155
- package/assets/design-styles/archive-index.md +0 -161
- package/assets/design-styles/art-deco.md +0 -164
- package/assets/design-styles/cinematic-noir.md +0 -178
- package/assets/design-styles/editorial.md +0 -121
- package/assets/design-styles/extra/README.md +0 -23
- package/assets/design-styles/extra/constructivism.md +0 -146
- package/assets/design-styles/extra/neo-brutalism.md +0 -175
- package/assets/design-styles/extra/new-wave.md +0 -166
- package/assets/design-styles/extra/nordic-material.md +0 -162
- package/assets/design-styles/extra/paris-couture.md +0 -162
- package/assets/design-styles/extra/tropical-modern.md +0 -168
- package/assets/design-styles/extra/ulm-functional.md +0 -182
- package/assets/design-styles/extra/wa-shikimen.md +0 -160
- package/assets/design-styles/extra/y2k-chrome.md +0 -172
- package/assets/design-styles/geometric-pop.md +0 -122
- package/assets/design-styles/hand-drawn.md +0 -117
- package/assets/design-styles/islamic-geometry.md +0 -155
- package/assets/design-styles/liquid-glass.md +0 -183
- package/assets/design-styles/neo-chinese.md +0 -136
- package/assets/design-styles/precision-mono.md +0 -156
- package/assets/design-styles/quiet-luxury.md +0 -163
- package/assets/design-styles/riso-print.md +0 -116
- package/assets/design-styles/soft-friendly.md +0 -174
- package/assets/design-styles/swiss-grid.md +0 -156
- package/assets/design-styles/tech-dark.md +0 -182
- package/assets/favicon.svg +0 -6
- package/assets/logo.svg +0 -24
- package/bin/sbot.js +0 -4716
- package/docs/slack-app-manifest.yaml +0 -104
- package/sbot.config.example.json +0 -235
- package/src/agent/askonce.js +0 -76
- package/src/agent/claudemodels.js +0 -60
- package/src/agent/claudestatus.js +0 -74
- package/src/agent/claudeusage.js +0 -200
- package/src/agent/codexstatus.js +0 -181
- package/src/agent/imagen.js +0 -458
- package/src/agent/quotaerr.js +0 -44
- package/src/agent/transienterr.js +0 -32
- package/src/cdp.js +0 -254
- package/src/cli-secret.js +0 -36
- package/src/clilang.js +0 -31
- package/src/core/cron.js +0 -182
- package/src/core/cronfire.js +0 -120
- package/src/core/cronrun.js +0 -116
- package/src/core/engine.js +0 -2758
- package/src/core/interfaces.js +0 -114
- package/src/core/issues.js +0 -185
- package/src/core/ledger.js +0 -30
- package/src/core/opsalert.js +0 -214
- package/src/core/pending.js +0 -123
- package/src/core/proxy.js +0 -424
- package/src/core/quotaalert.js +0 -79
- package/src/core/quotaerr.js +0 -55
- package/src/core/scheduler.js +0 -59
- package/src/design.js +0 -5161
- package/src/gitrepo.js +0 -150
- package/src/health.js +0 -353
- package/src/installdeps.js +0 -72
- package/src/leftover.js +0 -174
- package/src/platform/boardnotice.js +0 -77
- package/src/platform/boardops.js +0 -36
- package/src/platform/boardwatch.js +0 -447
- package/src/platform/cli.js +0 -45
- package/src/platform/slack.js +0 -1289
- package/src/platform/takeovercard.js +0 -37
- package/src/platform/wecom.js +0 -938
- package/src/platform/wecomcli.js +0 -128
- package/src/platform/wecomgate.js +0 -108
- package/src/platform/wecomname.js +0 -43
- package/src/platform/wecompoll.js +0 -241
- package/src/preview.js +0 -581
- package/src/projectmemory.js +0 -175
- package/src/service.js +0 -317
- package/src/ui/auth.js +0 -31
- package/src/ui/browser.html +0 -392
- package/src/ui/browserroutes.js +0 -234
- package/src/ui/console.html +0 -5526
- package/src/ui/dashboard.js +0 -168
- package/src/ui/feishulogin.js +0 -136
- package/src/ui/server.js +0 -2314
- package/src/ui/sessions.js +0 -78
- package/src/ui/takeover.js +0 -108
- package/src/updater.js +0 -243
- package/src/upgrade.js +0 -35
- 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 }
|