sintbot 1.0.16 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/CHANGELOG.en.json +99 -0
  2. package/CHANGELOG.json +99 -0
  3. package/README.md +20 -158
  4. package/README.zh-CN.md +20 -166
  5. package/THIRD-PARTY-NOTICES.md +394 -0
  6. package/bin/sintbot-v2.js +363 -0
  7. package/docs/AI-ASSISTED-INSTALL.md +11 -35
  8. package/docs/install.md +61 -167
  9. package/docs/zh-CN/AI-ASSISTED-INSTALL.md +11 -35
  10. package/docs/zh-CN/install.md +61 -165
  11. package/install.sh +84 -0
  12. package/package.json +73 -37
  13. package/src/agent/claudecode.js +149 -108
  14. package/src/agent/codexapp.js +458 -0
  15. package/src/agent/codexcode.js +210 -107
  16. package/src/agent/errtag.js +34 -0
  17. package/src/agent/privatecore.js +790 -0
  18. package/src/agent/privatecore.wasm +0 -0
  19. package/src/agent/runner.js +10 -8
  20. package/src/agent/winexec.js +35 -5
  21. package/src/c2install.js +117 -0
  22. package/src/config.js +2 -1
  23. package/src/core/c2cardeffectbridge.js +51 -0
  24. package/src/core/c2cardterminalpath.js +216 -0
  25. package/src/core/c2durableingress.js +148 -0
  26. package/src/core/c2durablejournal.js +1560 -0
  27. package/src/core/c2durableruntime.js +2361 -0
  28. package/src/core/c2eventidentity.js +90 -0
  29. package/src/core/c2globalfault.js +401 -0
  30. package/src/core/c2localeffectexecutor.js +193 -0
  31. package/src/core/c2payloadhandles.js +333 -0
  32. package/src/core/c2persistrelation.js +179 -0
  33. package/src/core/c2persiststate.js +342 -0
  34. package/src/core/c2platformrecoverysender.js +76 -0
  35. package/src/core/c2recoverygate.js +385 -0
  36. package/src/core/c2runnerinput.js +63 -0
  37. package/src/core/c2servicestartgate.js +103 -0
  38. package/src/core/c2sessionbridge.js +72 -0
  39. package/src/core/c2sourcequarantine.js +30 -0
  40. package/src/core/c2startuprecovery.js +105 -0
  41. package/src/core/c2successorbridge.js +151 -0
  42. package/src/core/c2terminaleffectbridge.js +154 -0
  43. package/src/core/c2terminalprune.js +108 -0
  44. package/src/core/c2turnprojection.js +177 -0
  45. package/src/core/enginecheckpoint.js +211 -0
  46. package/src/core/engineeffectcursor.js +394 -0
  47. package/src/core/enginestepcoordinator.js +183 -0
  48. package/src/core/store.js +52 -8
  49. package/src/core/v2engine.js +1469 -0
  50. package/src/platform/feishu.js +324 -67
  51. package/src/plugins.js +1 -9
  52. package/assets/design-styles/acid-pop.md +0 -155
  53. package/assets/design-styles/archive-index.md +0 -161
  54. package/assets/design-styles/art-deco.md +0 -164
  55. package/assets/design-styles/cinematic-noir.md +0 -178
  56. package/assets/design-styles/editorial.md +0 -121
  57. package/assets/design-styles/extra/README.md +0 -23
  58. package/assets/design-styles/extra/constructivism.md +0 -146
  59. package/assets/design-styles/extra/neo-brutalism.md +0 -175
  60. package/assets/design-styles/extra/new-wave.md +0 -166
  61. package/assets/design-styles/extra/nordic-material.md +0 -162
  62. package/assets/design-styles/extra/paris-couture.md +0 -162
  63. package/assets/design-styles/extra/tropical-modern.md +0 -168
  64. package/assets/design-styles/extra/ulm-functional.md +0 -182
  65. package/assets/design-styles/extra/wa-shikimen.md +0 -160
  66. package/assets/design-styles/extra/y2k-chrome.md +0 -172
  67. package/assets/design-styles/geometric-pop.md +0 -122
  68. package/assets/design-styles/hand-drawn.md +0 -117
  69. package/assets/design-styles/islamic-geometry.md +0 -155
  70. package/assets/design-styles/liquid-glass.md +0 -183
  71. package/assets/design-styles/neo-chinese.md +0 -136
  72. package/assets/design-styles/precision-mono.md +0 -156
  73. package/assets/design-styles/quiet-luxury.md +0 -163
  74. package/assets/design-styles/riso-print.md +0 -116
  75. package/assets/design-styles/soft-friendly.md +0 -174
  76. package/assets/design-styles/swiss-grid.md +0 -156
  77. package/assets/design-styles/tech-dark.md +0 -182
  78. package/assets/favicon.svg +0 -6
  79. package/assets/logo.svg +0 -24
  80. package/bin/sbot.js +0 -4716
  81. package/docs/slack-app-manifest.yaml +0 -104
  82. package/sbot.config.example.json +0 -235
  83. package/src/agent/askonce.js +0 -76
  84. package/src/agent/claudemodels.js +0 -60
  85. package/src/agent/claudestatus.js +0 -74
  86. package/src/agent/claudeusage.js +0 -200
  87. package/src/agent/codexstatus.js +0 -181
  88. package/src/agent/imagen.js +0 -458
  89. package/src/agent/quotaerr.js +0 -44
  90. package/src/agent/transienterr.js +0 -32
  91. package/src/cdp.js +0 -254
  92. package/src/cli-secret.js +0 -36
  93. package/src/clilang.js +0 -31
  94. package/src/core/cron.js +0 -182
  95. package/src/core/cronfire.js +0 -120
  96. package/src/core/cronrun.js +0 -116
  97. package/src/core/engine.js +0 -2758
  98. package/src/core/interfaces.js +0 -114
  99. package/src/core/issues.js +0 -185
  100. package/src/core/ledger.js +0 -30
  101. package/src/core/opsalert.js +0 -214
  102. package/src/core/pending.js +0 -123
  103. package/src/core/proxy.js +0 -424
  104. package/src/core/quotaalert.js +0 -79
  105. package/src/core/quotaerr.js +0 -55
  106. package/src/core/scheduler.js +0 -59
  107. package/src/design.js +0 -5161
  108. package/src/gitrepo.js +0 -150
  109. package/src/health.js +0 -353
  110. package/src/installdeps.js +0 -72
  111. package/src/leftover.js +0 -174
  112. package/src/platform/boardnotice.js +0 -77
  113. package/src/platform/boardops.js +0 -36
  114. package/src/platform/boardwatch.js +0 -447
  115. package/src/platform/cli.js +0 -45
  116. package/src/platform/slack.js +0 -1289
  117. package/src/platform/takeovercard.js +0 -37
  118. package/src/platform/wecom.js +0 -938
  119. package/src/platform/wecomcli.js +0 -128
  120. package/src/platform/wecomgate.js +0 -108
  121. package/src/platform/wecomname.js +0 -43
  122. package/src/platform/wecompoll.js +0 -241
  123. package/src/preview.js +0 -581
  124. package/src/projectmemory.js +0 -175
  125. package/src/service.js +0 -317
  126. package/src/ui/auth.js +0 -31
  127. package/src/ui/browser.html +0 -392
  128. package/src/ui/browserroutes.js +0 -234
  129. package/src/ui/console.html +0 -5526
  130. package/src/ui/dashboard.js +0 -168
  131. package/src/ui/feishulogin.js +0 -136
  132. package/src/ui/server.js +0 -2314
  133. package/src/ui/sessions.js +0 -78
  134. package/src/ui/takeover.js +0 -108
  135. package/src/updater.js +0 -243
  136. package/src/upgrade.js +0 -35
  137. package/src/usage.js +0 -144
@@ -0,0 +1,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 }