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,105 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { isDeepStrictEqual } = require('node:util')
|
|
4
|
+
const { validatePersistentRelation, validatePersistentReceipts } = require('./c2persistrelation')
|
|
5
|
+
|
|
6
|
+
const JOB_PREFIX = 'receipt:job:'
|
|
7
|
+
const MEMBER_PREFIX = 'receipt:member:'
|
|
8
|
+
|
|
9
|
+
function queuedIds(queue) {
|
|
10
|
+
const jobIds = queue.map(job => job.id)
|
|
11
|
+
const receiptIds = []
|
|
12
|
+
const seen = new Set()
|
|
13
|
+
for (const job of queue) {
|
|
14
|
+
for (const id of [job.id.slice(JOB_PREFIX.length),
|
|
15
|
+
...(job.batch || []).map(member => member.id.slice(MEMBER_PREFIX.length))]) {
|
|
16
|
+
if (!seen.has(id)) { seen.add(id); receiptIds.push(id) }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return { jobIds, receiptIds }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Startup has no job bodies, reply contexts, controllers or runner handles.
|
|
23
|
+
// Keep ingress shut and persist queue interruption before handing receipts to
|
|
24
|
+
// the existing notification protocol. This routine never sends or settles one.
|
|
25
|
+
function recoverMissingPayload({ gate, journal, coordinator, scope = null } = {}) {
|
|
26
|
+
if (!gate || typeof gate.close !== 'function' || typeof gate.isClosed !== 'function' ||
|
|
27
|
+
!journal || typeof journal.snapshot !== 'function' || typeof journal.hasEvent !== 'function' ||
|
|
28
|
+
!coordinator || typeof coordinator.snapshot !== 'function' ||
|
|
29
|
+
typeof coordinator.transition !== 'function') {
|
|
30
|
+
throw new TypeError('C2 startup recovery needs gate, journal and coordinator')
|
|
31
|
+
}
|
|
32
|
+
if (scope !== null && (typeof scope !== 'string' ||
|
|
33
|
+
!/^[A-Za-z0-9_-]{1,128}$/.test(scope) || gate.scope !== scope)) {
|
|
34
|
+
throw new TypeError('C2 scoped startup recovery needs matching scoped gate')
|
|
35
|
+
}
|
|
36
|
+
gate.close()
|
|
37
|
+
if (gate.isClosed() !== true) throw new Error('C2 startup ingress is not closed')
|
|
38
|
+
|
|
39
|
+
const initial = journal.snapshot()
|
|
40
|
+
validatePersistentReceipts(initial.receipts)
|
|
41
|
+
validatePersistentRelation(initial.state, initial.receipts)
|
|
42
|
+
if (initial.cursor !== initial.state.seq ||
|
|
43
|
+
!isDeepStrictEqual(coordinator.snapshot(), initial.state)) {
|
|
44
|
+
throw new Error('C2 startup state differs from durable authority')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const interrupted = []
|
|
48
|
+
const scopes = scope === null ? Object.keys(initial.state.lanes.queue || {}).sort() : [scope]
|
|
49
|
+
for (const scope of scopes) {
|
|
50
|
+
const prior = journal.snapshot()
|
|
51
|
+
const queue = prior.state.lanes.queue?.[scope]?.queue
|
|
52
|
+
if (queue === undefined && scopes.length === 1 && gate.scope === scope) continue
|
|
53
|
+
if (!Array.isArray(queue)) throw new Error('C2 startup queue scope changed')
|
|
54
|
+
if (!queue.length) continue
|
|
55
|
+
validatePersistentRelation(prior.state, prior.receipts)
|
|
56
|
+
if (prior.cursor !== prior.state.seq ||
|
|
57
|
+
!isDeepStrictEqual(coordinator.snapshot(), prior.state)) {
|
|
58
|
+
throw new Error('C2 startup cursor changed during recovery')
|
|
59
|
+
}
|
|
60
|
+
const expected = queuedIds(queue)
|
|
61
|
+
const eventId = `sbot-c2-startup-queue-v1:${scope}:${prior.cursor}`
|
|
62
|
+
const expectedEffectId = `sbot-c2-effect-v1:${Buffer.byteLength(eventId)}:${eventId}:0`
|
|
63
|
+
if (Buffer.byteLength(eventId) > 256 || journal.hasEvent(eventId)) {
|
|
64
|
+
throw new Error('C2 startup queue event identity conflict')
|
|
65
|
+
}
|
|
66
|
+
const candidateState = structuredClone(prior.state)
|
|
67
|
+
candidateState.seq++
|
|
68
|
+
candidateState.lastEventId = eventId
|
|
69
|
+
candidateState.lanes.queue[scope].queue = []
|
|
70
|
+
const result = coordinator.transition({ id: eventId, domain: 'queue', scope,
|
|
71
|
+
payload: { type: 'recoverMissingPayload' }, validate(candidate) {
|
|
72
|
+
if (!isDeepStrictEqual(candidate.state, candidateState) ||
|
|
73
|
+
candidate.effects?.length !== 1 ||
|
|
74
|
+
candidate.effects[0].type !== 'interruptMissingPayload' ||
|
|
75
|
+
candidate.effects[0].effectId !== expectedEffectId ||
|
|
76
|
+
!isDeepStrictEqual(candidate.effects[0].jobIds, expected.jobIds) ||
|
|
77
|
+
!isDeepStrictEqual(candidate.effects[0].receiptIds, expected.receiptIds) ||
|
|
78
|
+
candidate.decision?.runLen !== 0 || candidate.decision?.visibleQueue !== 0) {
|
|
79
|
+
throw new Error('C2 startup queue interruption mismatch')
|
|
80
|
+
}
|
|
81
|
+
} })
|
|
82
|
+
if (!journal.hasEvent(eventId) || journal.cursor !== prior.cursor + 1 ||
|
|
83
|
+
!isDeepStrictEqual(coordinator.snapshot(), journal.snapshot().state) ||
|
|
84
|
+
!isDeepStrictEqual(result.state, journal.snapshot().state)) {
|
|
85
|
+
throw new Error('C2 startup queue interruption was not durably committed')
|
|
86
|
+
}
|
|
87
|
+
interrupted.push(...expected.receiptIds)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const final = journal.snapshot()
|
|
91
|
+
validatePersistentRelation(final.state, final.receipts)
|
|
92
|
+
if (!isDeepStrictEqual(coordinator.snapshot(), final.state) ||
|
|
93
|
+
(scope === null
|
|
94
|
+
? Object.values(final.state.lanes.queue || {}).some(lane => lane.queue.length)
|
|
95
|
+
: (final.state.lanes.queue?.[scope]?.queue?.length || 0) > 0)) {
|
|
96
|
+
throw new Error('C2 startup queue remains unresolved')
|
|
97
|
+
}
|
|
98
|
+
const pending = final.receipts.pending.filter(item => scope === null || item.chatId === scope)
|
|
99
|
+
.map(item => ({ id: item.id,
|
|
100
|
+
chatId: item.chatId, quiet: item.quiet }))
|
|
101
|
+
.sort((a, b) => a.chatId.localeCompare(b.chatId) || a.id.localeCompare(b.id))
|
|
102
|
+
return { interruptedReceiptIds: [...new Set(interrupted)], pending, ingressClosed: true }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { recoverMissingPayload }
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Same-process continuation after a settled v2 terminal effect. The prior
|
|
4
|
+
// answer is already externally visible; only internal handoff effects may be
|
|
5
|
+
// acknowledged here. A crash loses raw queued jobs and startup recovery must
|
|
6
|
+
// interrupt them rather than replaying a model run.
|
|
7
|
+
const { isDeepStrictEqual } = require('node:util')
|
|
8
|
+
const { C2DurableRuntime } = require('./c2durableruntime')
|
|
9
|
+
const { C2DurableJournal } = require('./c2durablejournal')
|
|
10
|
+
const { C2PayloadHandles } = require('./c2payloadhandles')
|
|
11
|
+
|
|
12
|
+
const RECEIPT_ID = /^[0-9a-f]{32}$/
|
|
13
|
+
|
|
14
|
+
function continueC2Successor({ runtime, journal, payloadHandles, chatId,
|
|
15
|
+
priorReceiptId, priorTurnToken, terminalResult, generation,
|
|
16
|
+
assertWriterLock, now = Date.now } = {}) {
|
|
17
|
+
if (!(runtime instanceof C2DurableRuntime) ||
|
|
18
|
+
!(journal instanceof C2DurableJournal) ||
|
|
19
|
+
!(payloadHandles instanceof C2PayloadHandles) ||
|
|
20
|
+
typeof chatId !== 'string' || !chatId ||
|
|
21
|
+
!RECEIPT_ID.test(priorReceiptId || '') ||
|
|
22
|
+
!Number.isSafeInteger(priorTurnToken) || priorTurnToken <= 0 ||
|
|
23
|
+
!RECEIPT_ID.test(generation || '') ||
|
|
24
|
+
typeof assertWriterLock !== 'function' ||
|
|
25
|
+
assertWriterLock.constructor?.name === 'AsyncFunction' ||
|
|
26
|
+
typeof now !== 'function' || now.constructor?.name === 'AsyncFunction') {
|
|
27
|
+
throw new TypeError('C2 successor bridge needs exact live authority')
|
|
28
|
+
}
|
|
29
|
+
const locked = () => {
|
|
30
|
+
if (assertWriterLock() !== true) throw new Error('C2 successor lost writer lock')
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
locked()
|
|
34
|
+
const before = runtime.snapshot()
|
|
35
|
+
if (runtime.blocked || journal.blocked || payloadHandles.blocked ||
|
|
36
|
+
!isDeepStrictEqual(before, journal.snapshot()) ||
|
|
37
|
+
terminalResult?.receiptId !== priorReceiptId ||
|
|
38
|
+
!RECEIPT_ID.test(terminalResult?.nextReceiptId || '') ||
|
|
39
|
+
typeof terminalResult?.terminalEffectId !== 'string' ||
|
|
40
|
+
!Array.isArray(terminalResult.pendingEffects) ||
|
|
41
|
+
terminalResult.pendingEffects.length !== 2 ||
|
|
42
|
+
new Set(terminalResult.pendingEffects).size !== 2) {
|
|
43
|
+
throw new Error('C2 successor lacks settled prior terminal authority')
|
|
44
|
+
}
|
|
45
|
+
const nextReceiptId = terminalResult.nextReceiptId
|
|
46
|
+
const settlement = journal.terminalSettlement(terminalResult.terminalEffectId)
|
|
47
|
+
const terminal = journal.effectDescriptor(terminalResult.terminalEffectId)
|
|
48
|
+
const [holdId, scheduleId] = terminalResult.pendingEffects
|
|
49
|
+
const hold = journal.effectDescriptor(holdId)
|
|
50
|
+
const schedule = journal.effectDescriptor(scheduleId)
|
|
51
|
+
const handoff = before.state?.lanes?.handoff?.[chatId]
|
|
52
|
+
const turn = before.state?.lanes?.turn?.[chatId]
|
|
53
|
+
const nextReceipt = before.receipts?.pending?.find(item => item.id === nextReceiptId)
|
|
54
|
+
if (settlement?.receiptId !== priorReceiptId ||
|
|
55
|
+
settlement.chatId !== chatId || settlement.turnToken !== priorTurnToken ||
|
|
56
|
+
terminal?.receiptId !== priorReceiptId || terminal.scope !== chatId ||
|
|
57
|
+
!['finalReply', 'quietCompletion'].includes(terminal.type) ||
|
|
58
|
+
journal.effectStatus(terminalResult.terminalEffectId) !== 'acked' ||
|
|
59
|
+
!before.receipts.completed.some(item => item.id === priorReceiptId) ||
|
|
60
|
+
before.receipts.pending.some(item => item.id === priorReceiptId) ||
|
|
61
|
+
nextReceipt?.chatId !== chatId || nextReceipt.source !== 'message' ||
|
|
62
|
+
handoff?.phase !== 'handoff' || handoff.turnToken !== priorTurnToken ||
|
|
63
|
+
handoff.nextJobId !== nextReceiptId || turn?.active !== null ||
|
|
64
|
+
hold?.domain !== 'atomicHandoff' || hold.scope !== chatId ||
|
|
65
|
+
hold.type !== 'holdHandoff' || hold.turnToken !== priorTurnToken ||
|
|
66
|
+
hold.receiptId !== null || hold.targetId !== null ||
|
|
67
|
+
schedule?.domain !== 'atomicHandoff' || schedule.scope !== chatId ||
|
|
68
|
+
schedule.type !== 'scheduleDispatch' ||
|
|
69
|
+
schedule.turnToken !== priorTurnToken ||
|
|
70
|
+
schedule.receiptId !== nextReceiptId || schedule.targetId !== null ||
|
|
71
|
+
journal.effectStatus(holdId) !== 'pending' ||
|
|
72
|
+
journal.effectStatus(scheduleId) !== 'pending') {
|
|
73
|
+
throw new Error('C2 successor effects differ from durable handoff')
|
|
74
|
+
}
|
|
75
|
+
payloadHandles.peekHandoff(nextReceiptId)
|
|
76
|
+
locked()
|
|
77
|
+
runtime.recordEffect(holdId, 'acked')
|
|
78
|
+
if (journal.effectStatus(holdId) !== 'acked' ||
|
|
79
|
+
!isDeepStrictEqual(runtime.snapshot(), journal.snapshot())) {
|
|
80
|
+
throw new Error('C2 successor hold ACK differs from runtime')
|
|
81
|
+
}
|
|
82
|
+
locked()
|
|
83
|
+
runtime.recordEffect(scheduleId, 'acked')
|
|
84
|
+
if (journal.effectStatus(scheduleId) !== 'acked' ||
|
|
85
|
+
!isDeepStrictEqual(runtime.snapshot(), journal.snapshot())) {
|
|
86
|
+
throw new Error('C2 successor schedule ACK differs from runtime')
|
|
87
|
+
}
|
|
88
|
+
const registeredAt = now()
|
|
89
|
+
if (!Number.isSafeInteger(registeredAt) || registeredAt <= 0) {
|
|
90
|
+
throw new TypeError('C2 successor callback time invalid')
|
|
91
|
+
}
|
|
92
|
+
locked()
|
|
93
|
+
runtime.registerHandoffBeginCallback({ receiptId: nextReceiptId,
|
|
94
|
+
generation, registeredAt })
|
|
95
|
+
locked()
|
|
96
|
+
const begun = runtime.commitHandoffBegin({ receiptId: nextReceiptId,
|
|
97
|
+
generation, priorTurnToken, payloadHandles })
|
|
98
|
+
const [clear, dispatch] = begun.effects || []
|
|
99
|
+
const token = begun.decision?.turnToken
|
|
100
|
+
if (!Number.isSafeInteger(token) || token <= priorTurnToken ||
|
|
101
|
+
!Array.isArray(begun.jobReceiptIds) || begun.jobReceiptIds.length === 0 ||
|
|
102
|
+
!begun.jobReceiptIds.includes(nextReceiptId) ||
|
|
103
|
+
begun.effects.length !== 2 || clear?.type !== 'clearHandoff' ||
|
|
104
|
+
dispatch?.type !== 'dispatch' || dispatch.jobId !== nextReceiptId ||
|
|
105
|
+
dispatch.turnToken !== token ||
|
|
106
|
+
journal.effectDescriptor(clear.effectId)?.type !== 'clearHandoff' ||
|
|
107
|
+
journal.effectDescriptor(dispatch.effectId)?.type !== 'dispatch' ||
|
|
108
|
+
journal.effectStatus(clear.effectId) !== 'pending' ||
|
|
109
|
+
journal.effectStatus(dispatch.effectId) !== 'pending' ||
|
|
110
|
+
!isDeepStrictEqual(runtime.snapshot(), journal.snapshot())) {
|
|
111
|
+
throw new Error('C2 successor begin differs from durable dispatch')
|
|
112
|
+
}
|
|
113
|
+
locked()
|
|
114
|
+
runtime.recordEffect(clear.effectId, 'acked')
|
|
115
|
+
if (journal.effectStatus(clear.effectId) !== 'acked' ||
|
|
116
|
+
!isDeepStrictEqual(runtime.snapshot(), journal.snapshot())) {
|
|
117
|
+
throw new Error('C2 successor clear ACK differs from runtime')
|
|
118
|
+
}
|
|
119
|
+
locked()
|
|
120
|
+
const jobs = payloadHandles.consumeCommittedDispatch({ eventId: begun.eventId,
|
|
121
|
+
turnToken: token, receiptIds: begun.jobReceiptIds })
|
|
122
|
+
if (jobs.length !== begun.jobReceiptIds.length ||
|
|
123
|
+
jobs.some((job, index) => job.msg?._c2ReceiptId !== begun.jobReceiptIds[index] ||
|
|
124
|
+
job.msg.chatId !== chatId)) {
|
|
125
|
+
throw new Error('C2 successor jobs differ from durable dispatch')
|
|
126
|
+
}
|
|
127
|
+
const after = runtime.snapshot()
|
|
128
|
+
const queue = after.state?.lanes?.queue?.[chatId]?.queue
|
|
129
|
+
if (!Array.isArray(queue) ||
|
|
130
|
+
!queue.every(item => typeof item.id === 'string' &&
|
|
131
|
+
/^receipt:job:[0-9a-f]{32}$/.test(item.id)) ||
|
|
132
|
+
after.state?.lanes?.turn?.[chatId]?.active !== token ||
|
|
133
|
+
after.state?.lanes?.handoff?.[chatId]?.phase !== 'running' ||
|
|
134
|
+
after.state.lanes.handoff[chatId].turnToken !== token ||
|
|
135
|
+
after.effects.get(dispatch.effectId) !== 'pending') {
|
|
136
|
+
throw new Error('C2 successor dispatch is no longer live')
|
|
137
|
+
}
|
|
138
|
+
locked()
|
|
139
|
+
return Object.freeze({ receiptId: nextReceiptId, turnToken: token,
|
|
140
|
+
effectId: dispatch.effectId, eventId: begun.eventId, cursor: begun.cursor,
|
|
141
|
+
jobReceiptIds: Object.freeze([...begun.jobReceiptIds]),
|
|
142
|
+
batchReceiptIds: Object.freeze([...begun.batchReceiptIds]),
|
|
143
|
+
jobs: Object.freeze([...jobs]),
|
|
144
|
+
queuedReceiptIds: Object.freeze(queue.map(item => item.id.slice('receipt:job:'.length))) })
|
|
145
|
+
} catch (error) {
|
|
146
|
+
runtime.close()
|
|
147
|
+
throw error
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { continueC2Successor }
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Opt-in host bridge for one single-receipt terminal effect. The three callback
|
|
4
|
+
// identities are fsynced before a runner completion callback may be exposed.
|
|
5
|
+
// Text and platform targets stay in the caller's process; the runtime/journal
|
|
6
|
+
// receive only the v2 decision and stable effect/receipt identities.
|
|
7
|
+
const { C2DurableRuntime } = require('./c2durableruntime')
|
|
8
|
+
const { C2PayloadHandles } = require('./c2payloadhandles')
|
|
9
|
+
|
|
10
|
+
const RECEIPT_ID = /^[0-9a-f]{32}$/
|
|
11
|
+
const GENERATION = /^[0-9a-f]{32}$/
|
|
12
|
+
const LOCAL_EFFECTS = new Set(['markTainted', 'clearTainted',
|
|
13
|
+
'clearExhausted', 'verifyTurn'])
|
|
14
|
+
|
|
15
|
+
// The legacy final delivery keeps the answer after the last tool when one
|
|
16
|
+
// exists, otherwise the full streamed text. Codex may emit several assistant
|
|
17
|
+
// messages while result.text contains only the last one. Keep this projection
|
|
18
|
+
// process-local: the durable terminal payload is validated by the core and
|
|
19
|
+
// never stores model text in the journal.
|
|
20
|
+
function terminalReplyText(payload) {
|
|
21
|
+
if (!payload || typeof payload.resultText !== 'string' ||
|
|
22
|
+
typeof payload.bufferText !== 'string' ||
|
|
23
|
+
typeof payload.answerText !== 'string') {
|
|
24
|
+
throw new TypeError('C2 terminal reply text fields invalid')
|
|
25
|
+
}
|
|
26
|
+
return (payload.answerText.trim() ? payload.answerText : payload.bufferText) ||
|
|
27
|
+
payload.resultText
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function prepareTerminalEffectBridge({ runtime, payloadHandles, receiptId, chatId,
|
|
31
|
+
turnToken, generation, verifyGeneration, applyLocalEffect, assertWriterLock,
|
|
32
|
+
now = Date.now } = {}) {
|
|
33
|
+
if (!(runtime instanceof C2DurableRuntime) ||
|
|
34
|
+
!(payloadHandles instanceof C2PayloadHandles) || payloadHandles.blocked ||
|
|
35
|
+
!RECEIPT_ID.test(receiptId) || typeof chatId !== 'string' || !chatId ||
|
|
36
|
+
!Number.isSafeInteger(turnToken) || turnToken <= 0 ||
|
|
37
|
+
!GENERATION.test(generation) || typeof verifyGeneration !== 'function' ||
|
|
38
|
+
verifyGeneration.constructor?.name === 'AsyncFunction' ||
|
|
39
|
+
typeof applyLocalEffect !== 'function' ||
|
|
40
|
+
applyLocalEffect.constructor?.name === 'AsyncFunction' ||
|
|
41
|
+
typeof now !== 'function') {
|
|
42
|
+
throw new TypeError('C2 terminal bridge needs exact live authority')
|
|
43
|
+
}
|
|
44
|
+
const snapshot = runtime.snapshot()
|
|
45
|
+
if (runtime.blocked ||
|
|
46
|
+
snapshot.receipts?.pending?.find(item => item.id === receiptId)?.chatId !== chatId ||
|
|
47
|
+
snapshot.state?.lanes?.turn?.[chatId]?.active !== turnToken ||
|
|
48
|
+
snapshot.state?.lanes?.handoff?.[chatId]?.phase !== 'running' ||
|
|
49
|
+
snapshot.state.lanes.handoff[chatId].turnToken !== turnToken ||
|
|
50
|
+
verifyGeneration({ chatId, receiptId, turnToken, generation }) !== true) {
|
|
51
|
+
throw new Error('C2 terminal bridge has no live running receipt')
|
|
52
|
+
}
|
|
53
|
+
const stamp = () => {
|
|
54
|
+
const value = now()
|
|
55
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
56
|
+
throw new TypeError('C2 terminal bridge time invalid')
|
|
57
|
+
}
|
|
58
|
+
return value
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
runtime.registerTerminalCallback({ receiptId, generation,
|
|
62
|
+
registeredAt: stamp() })
|
|
63
|
+
runtime.registerReleaseRequestedCallback({ receiptId, generation,
|
|
64
|
+
registeredAt: stamp() })
|
|
65
|
+
runtime.registerQueueReleaseCallback({ receiptId, generation,
|
|
66
|
+
registeredAt: stamp() })
|
|
67
|
+
} catch (error) { runtime.close(); throw error }
|
|
68
|
+
|
|
69
|
+
let completed = false
|
|
70
|
+
return Object.freeze({
|
|
71
|
+
async complete({ payload, replyText, resolveTarget, sendReply,
|
|
72
|
+
confirmReply, paused = false } = {}) {
|
|
73
|
+
if (completed) throw new Error('C2 terminal bridge already used')
|
|
74
|
+
completed = true
|
|
75
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload) ||
|
|
76
|
+
payload.type !== 'finalizeEffectCommit' ||
|
|
77
|
+
payload.receiptId !== receiptId || payload.turnToken !== turnToken ||
|
|
78
|
+
typeof payload.resultText !== 'string' ||
|
|
79
|
+
typeof paused !== 'boolean') {
|
|
80
|
+
runtime.close()
|
|
81
|
+
throw new TypeError('C2 terminal bridge payload differs from running turn')
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const terminal = runtime.commitTerminalEffect({ receiptId,
|
|
85
|
+
generation, turnToken, payload })
|
|
86
|
+
const final = terminal.effects.at(-1)
|
|
87
|
+
if (!final || final.effectId !== terminal.terminalEffectId ||
|
|
88
|
+
!['finalReply', 'quietCompletion'].includes(final.type)) {
|
|
89
|
+
throw new Error('C2 terminal bridge has no exact terminal effect')
|
|
90
|
+
}
|
|
91
|
+
for (const effect of terminal.effects.slice(0, -1)) {
|
|
92
|
+
if (!LOCAL_EFFECTS.has(effect.type)) {
|
|
93
|
+
throw new Error('C2 terminal bridge cannot apply unknown local effect')
|
|
94
|
+
}
|
|
95
|
+
if (typeof applyLocalEffect.claim === 'function') {
|
|
96
|
+
runtime.performLocalTerminalEffect({ effect, receiptId, chatId,
|
|
97
|
+
turnToken, generation, verifyGeneration, assertWriterLock,
|
|
98
|
+
applyLocalEffect })
|
|
99
|
+
} else {
|
|
100
|
+
// Older isolated callers retain the development seam. Production
|
|
101
|
+
// must supply the claimed, write-ahead executor above.
|
|
102
|
+
if (applyLocalEffect(effect) !== true) {
|
|
103
|
+
throw new Error('C2 terminal local effect lacks confirmed projection')
|
|
104
|
+
}
|
|
105
|
+
runtime.recordEffect(effect.effectId, 'acked')
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const effect = { effectId: final.effectId, receiptId, chatId,
|
|
109
|
+
turnToken, generation, verifyGeneration }
|
|
110
|
+
if (final.type === 'quietCompletion') {
|
|
111
|
+
if (replyText !== undefined || resolveTarget !== undefined ||
|
|
112
|
+
sendReply !== undefined || confirmReply !== undefined) {
|
|
113
|
+
throw new Error('C2 quiet terminal may not send a reply')
|
|
114
|
+
}
|
|
115
|
+
await runtime.executeTerminalEffect(effect)
|
|
116
|
+
} else {
|
|
117
|
+
if (typeof replyText !== 'string' || !replyText ||
|
|
118
|
+
replyText !== terminalReplyText(payload) ||
|
|
119
|
+
typeof resolveTarget !== 'function' ||
|
|
120
|
+
resolveTarget.constructor?.name === 'AsyncFunction' ||
|
|
121
|
+
typeof sendReply !== 'function' ||
|
|
122
|
+
typeof confirmReply !== 'function' ||
|
|
123
|
+
confirmReply.constructor?.name === 'AsyncFunction') {
|
|
124
|
+
throw new TypeError('C2 final reply needs exact process-local text and sender')
|
|
125
|
+
}
|
|
126
|
+
await runtime.executeTerminalEffect({ ...effect, resolveTarget,
|
|
127
|
+
perform: ({ target }) => sendReply(target, replyText),
|
|
128
|
+
confirm: confirmReply })
|
|
129
|
+
}
|
|
130
|
+
const requested = runtime.commitReleaseRequested({ receiptId,
|
|
131
|
+
generation, turnToken, payloadHandles })
|
|
132
|
+
if (requested.effects.length !== 1 ||
|
|
133
|
+
requested.effects[0].type !== 'attemptQueueRelease') {
|
|
134
|
+
throw new Error('C2 terminal bridge release request differs')
|
|
135
|
+
}
|
|
136
|
+
// This is an internal queue instruction. The actual queue transition
|
|
137
|
+
// follows immediately; a crash in between leaves ingress closed.
|
|
138
|
+
runtime.recordEffect(requested.effects[0].effectId, 'acked')
|
|
139
|
+
const released = runtime.commitQueueRelease({ receiptId, generation,
|
|
140
|
+
turnToken, paused, payloadHandles })
|
|
141
|
+
const settlement = runtime.settleTerminalEffect({ effectId: final.effectId,
|
|
142
|
+
receiptId, chatId, turnToken, generation })
|
|
143
|
+
if (settlement.status !== 'acked' || !settlement.pruned) {
|
|
144
|
+
throw new Error('C2 terminal bridge settlement incomplete')
|
|
145
|
+
}
|
|
146
|
+
return { terminalEffectId: final.effectId, receiptId,
|
|
147
|
+
nextReceiptId: released.decision.nextJobId,
|
|
148
|
+
pendingEffects: released.effects.map(item => item.effectId) }
|
|
149
|
+
} catch (error) { runtime.close(); throw error }
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = { prepareTerminalEffectBridge, terminalReplyText }
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { createHash } = require('node:crypto')
|
|
4
|
+
const { isDeepStrictEqual } = require('node:util')
|
|
5
|
+
const { validatePersistentRelation } = require('./c2persistrelation')
|
|
6
|
+
|
|
7
|
+
// Development-only cleanup after every terminal intent has a durable ACK.
|
|
8
|
+
// A pending or unknown effect keeps the lane for operator review.
|
|
9
|
+
function pruneConfirmedTerminal({ journal, coordinator, scope } = {}) {
|
|
10
|
+
if (!journal || typeof journal.snapshot !== 'function' ||
|
|
11
|
+
typeof journal.effectStatus !== 'function' || typeof journal.hasEvent !== 'function' ||
|
|
12
|
+
!coordinator || typeof coordinator.snapshot !== 'function' ||
|
|
13
|
+
typeof coordinator.transition !== 'function' ||
|
|
14
|
+
typeof scope !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(scope)) {
|
|
15
|
+
throw new TypeError('C2 terminal prune needs durable journal, coordinator and scope')
|
|
16
|
+
}
|
|
17
|
+
const durable = journal.snapshot()
|
|
18
|
+
validatePersistentRelation(durable.state, durable.receipts)
|
|
19
|
+
if (durable.cursor !== durable.state.seq ||
|
|
20
|
+
!isDeepStrictEqual(coordinator.snapshot(), durable.state)) {
|
|
21
|
+
throw new Error('C2 terminal prune state differs from authority')
|
|
22
|
+
}
|
|
23
|
+
const lane = durable.state.lanes.terminal?.[scope]
|
|
24
|
+
if (!lane) return false
|
|
25
|
+
if (!lane.finalized || typeof lane.finalEventId !== 'string' ||
|
|
26
|
+
!Array.isArray(lane.pendingEffectIds)) throw new Error('C2 terminal lane is not final')
|
|
27
|
+
for (const effectId of lane.pendingEffectIds) {
|
|
28
|
+
if (journal.effectStatus(effectId) !== 'acked') {
|
|
29
|
+
throw new Error('C2 terminal effect lacks durable ACK')
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// A v2 runner-close session is not settled by queue release. The terminal
|
|
33
|
+
// prune is the first reducer cursor after the external final effect and
|
|
34
|
+
// receipt have been atomically ACKed. Bind that cursor to the exact closed
|
|
35
|
+
// child so restart recovery can finish a crash between ACK and prune.
|
|
36
|
+
const finalEffectId = lane.pendingEffectIds.at(-1)
|
|
37
|
+
const finalDescriptor = journal.effectDescriptor(finalEffectId)
|
|
38
|
+
const v2Final = finalDescriptor?.domain === 'terminal' &&
|
|
39
|
+
['finalReply', 'quietCompletion'].includes(finalDescriptor.type)
|
|
40
|
+
const session = durable.state.lanes.session?.[scope]
|
|
41
|
+
if (v2Final && !session) {
|
|
42
|
+
throw new Error('C2 terminal prune lacks closed session lane')
|
|
43
|
+
}
|
|
44
|
+
let sessionProof = null
|
|
45
|
+
if (session) {
|
|
46
|
+
const descriptor = finalDescriptor
|
|
47
|
+
const settlement = journal.terminalSettlement(finalEffectId)
|
|
48
|
+
const handoff = durable.state.lanes.handoff?.[scope]
|
|
49
|
+
const turn = durable.state.lanes.turn?.[scope]
|
|
50
|
+
const closed = typeof journal.runnerClosedProofs === 'function'
|
|
51
|
+
? journal.runnerClosedProofs().filter(proof =>
|
|
52
|
+
proof.receiptId === descriptor?.receiptId && proof.chatId === scope &&
|
|
53
|
+
proof.turnToken === descriptor?.turnToken) : []
|
|
54
|
+
const proof = closed.length === 1 ? closed[0] : null
|
|
55
|
+
const completed = durable.receipts.completed.find(item =>
|
|
56
|
+
item.id === descriptor?.receiptId)
|
|
57
|
+
if (session.settled !== false || session.closeSeen !== true ||
|
|
58
|
+
session.epoch !== descriptor?.turnToken ||
|
|
59
|
+
turn?.epoch !== descriptor?.turnToken || turn?.active !== null ||
|
|
60
|
+
!handoff || !['idle', 'handoff'].includes(handoff.phase) ||
|
|
61
|
+
handoff.turnToken !== descriptor?.turnToken ||
|
|
62
|
+
descriptor?.domain !== 'terminal' || descriptor.scope !== scope ||
|
|
63
|
+
!['finalReply', 'quietCompletion'].includes(descriptor.type) ||
|
|
64
|
+
settlement?.receiptId !== descriptor.receiptId ||
|
|
65
|
+
settlement?.chatId !== scope ||
|
|
66
|
+
settlement?.turnToken !== descriptor.turnToken ||
|
|
67
|
+
completed?.settledAt !== settlement?.settledAt ||
|
|
68
|
+
!isDeepStrictEqual(settlement?.batchReceiptIds, lane.batchReceiptIds) ||
|
|
69
|
+
(lane.batchReceiptIds || [descriptor.receiptId]).some(id =>
|
|
70
|
+
!durable.receipts.completed.some(item =>
|
|
71
|
+
item.id === id && item.settledAt === settlement?.settledAt)) ||
|
|
72
|
+
!proof || !/^[0-9a-f]{32}$/.test(proof.generation || '') ||
|
|
73
|
+
proof.evidenceId !==
|
|
74
|
+
`sbot-c2-runner-closed-v1:${descriptor.receiptId}:${proof.generation}` ||
|
|
75
|
+
journal.lookupRunnerClosed(proof.evidenceId)?.dispatchEffectId !==
|
|
76
|
+
proof.dispatchEffectId ||
|
|
77
|
+
journal.effectStatus(proof.dispatchEffectId) !== 'acked' ||
|
|
78
|
+
!journal.hasEvent(`sbot-c2-session-close-v1:${descriptor.receiptId}:${proof.generation}`)) {
|
|
79
|
+
throw new Error('C2 terminal prune lacks settled receipt and closed session proof')
|
|
80
|
+
}
|
|
81
|
+
sessionProof = { receiptId: descriptor.receiptId,
|
|
82
|
+
turnToken: descriptor.turnToken, runnerGeneration: proof.generation,
|
|
83
|
+
closeEvidenceId: proof.evidenceId }
|
|
84
|
+
}
|
|
85
|
+
const eventId = `sbot-c2-terminal-prune-v1:${createHash('sha256')
|
|
86
|
+
.update(scope).update('\0').update(lane.finalEventId).digest('hex')}`
|
|
87
|
+
if (journal.hasEvent(eventId)) throw new Error('C2 terminal prune identity already committed')
|
|
88
|
+
const result = coordinator.transition({ id: eventId, domain: 'terminal', scope,
|
|
89
|
+
payload: { type: 'prune', finalEventId: lane.finalEventId,
|
|
90
|
+
effectsConfirmed: true, confirmedEffectIds: [...lane.pendingEffectIds],
|
|
91
|
+
...(lane.batchReceiptIds ? { batchReceiptIds: [...lane.batchReceiptIds],
|
|
92
|
+
replyTargetReceiptId: lane.replyTargetReceiptId } : {}),
|
|
93
|
+
...(sessionProof || {}) },
|
|
94
|
+
validate(candidate) {
|
|
95
|
+
if (candidate.effects?.length !== 0 || candidate.decision?.pruned !== true ||
|
|
96
|
+
candidate.state?.lanes?.terminal?.[scope] !== undefined ||
|
|
97
|
+
sessionProof && candidate.state?.lanes?.session?.[scope]?.settled !== true) {
|
|
98
|
+
throw new Error('C2 terminal prune returned invalid decision')
|
|
99
|
+
}
|
|
100
|
+
} })
|
|
101
|
+
if (journal.cursor !== durable.cursor + 1 ||
|
|
102
|
+
!isDeepStrictEqual(result.state, journal.snapshot().state)) {
|
|
103
|
+
throw new Error('C2 terminal prune was not durably committed')
|
|
104
|
+
}
|
|
105
|
+
return true
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = { pruneConfirmedTerminal }
|