sintbot 1.0.15 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.json +118 -0
- package/CHANGELOG.json +118 -0
- package/README.md +20 -156
- package/README.zh-CN.md +20 -164
- package/THIRD-PARTY-NOTICES.md +394 -0
- package/bin/sintbot-v2.js +363 -0
- package/docs/AI-ASSISTED-INSTALL.md +19 -0
- package/docs/install.md +93 -0
- package/docs/zh-CN/AI-ASSISTED-INSTALL.md +19 -0
- package/docs/zh-CN/install.md +93 -0
- package/install.sh +84 -0
- package/package.json +74 -34
- 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 -4711
- 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/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 -2762
- 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 -5494
- 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,177 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// The three runners expose normalized events only after the child has acted.
|
|
4
|
+
// This process-local projection preserves the legacy terminal text semantics;
|
|
5
|
+
// it is not a pre-tool execution barrier or a persisted model transcript.
|
|
6
|
+
const RECEIPT_ID = /^[0-9a-f]{32}$/
|
|
7
|
+
const NO_REPLY = 'NO_REPLY'
|
|
8
|
+
const { isDeepStrictEqual } = require('node:util')
|
|
9
|
+
|
|
10
|
+
function createC2TurnProjection({ receiptId, turnToken, quiet, resumed = false,
|
|
11
|
+
freshRetried = false, batchReceiptIds } = {}) {
|
|
12
|
+
if (!RECEIPT_ID.test(receiptId || '') ||
|
|
13
|
+
!Number.isSafeInteger(turnToken) || turnToken <= 0 ||
|
|
14
|
+
typeof quiet !== 'boolean' || typeof resumed !== 'boolean' ||
|
|
15
|
+
typeof freshRetried !== 'boolean') {
|
|
16
|
+
throw new TypeError('C2 projection needs a typed running receipt')
|
|
17
|
+
}
|
|
18
|
+
if (batchReceiptIds !== undefined &&
|
|
19
|
+
(!Array.isArray(batchReceiptIds) || batchReceiptIds.length < 2 ||
|
|
20
|
+
new Set(batchReceiptIds).size !== batchReceiptIds.length ||
|
|
21
|
+
!batchReceiptIds.includes(receiptId) ||
|
|
22
|
+
batchReceiptIds.some(id => !RECEIPT_ID.test(id)))) {
|
|
23
|
+
throw new TypeError('C2 projection needs an exact batch receipt cohort')
|
|
24
|
+
}
|
|
25
|
+
let bufferText = ''
|
|
26
|
+
let answerText = ''
|
|
27
|
+
let fullText = ''
|
|
28
|
+
let sessionId = null
|
|
29
|
+
let contextTokens = 0
|
|
30
|
+
let observedResult = null
|
|
31
|
+
let assistantEvents = 0
|
|
32
|
+
let completed = false
|
|
33
|
+
const toolCounts = new Map()
|
|
34
|
+
|
|
35
|
+
const append = (prior, part) => {
|
|
36
|
+
const milestone = /^>>/.test(part)
|
|
37
|
+
const separator = prior && !prior.endsWith('\n') && !part.startsWith('\n') &&
|
|
38
|
+
(milestone || /(^|\n)>>[^\n]*$/.test(prior)) ? '\n' : ''
|
|
39
|
+
return prior + separator + part
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function accept(event) {
|
|
43
|
+
if (completed || observedResult) throw new Error('C2 projection event after result')
|
|
44
|
+
if (!event || typeof event !== 'object' || Array.isArray(event) ||
|
|
45
|
+
typeof event.type !== 'string') {
|
|
46
|
+
throw new TypeError('C2 projection event invalid')
|
|
47
|
+
}
|
|
48
|
+
if (event.sessionId !== undefined && event.sessionId !== null) {
|
|
49
|
+
if (typeof event.sessionId !== 'string' || !event.sessionId) {
|
|
50
|
+
throw new TypeError('C2 projection session invalid')
|
|
51
|
+
}
|
|
52
|
+
sessionId = event.sessionId
|
|
53
|
+
}
|
|
54
|
+
if (event.type === 'assistant') {
|
|
55
|
+
assistantEvents++
|
|
56
|
+
if (event.contextTokens !== undefined) {
|
|
57
|
+
if (!Number.isFinite(event.contextTokens) || event.contextTokens < 0) {
|
|
58
|
+
throw new TypeError('C2 projection context usage invalid')
|
|
59
|
+
}
|
|
60
|
+
contextTokens = event.contextTokens
|
|
61
|
+
}
|
|
62
|
+
if (event.text !== undefined && typeof event.text !== 'string') {
|
|
63
|
+
throw new TypeError('C2 projection assistant text invalid')
|
|
64
|
+
}
|
|
65
|
+
const tools = event.tools === undefined ? [] : event.tools
|
|
66
|
+
if (!Array.isArray(tools) || tools.some(tool => !tool ||
|
|
67
|
+
typeof tool.name !== 'string' || !tool.name)) {
|
|
68
|
+
throw new TypeError('C2 projection tool names invalid')
|
|
69
|
+
}
|
|
70
|
+
if (event.text) {
|
|
71
|
+
fullText = append(fullText, event.text)
|
|
72
|
+
bufferText = append(bufferText, event.text)
|
|
73
|
+
answerText = append(answerText, event.text)
|
|
74
|
+
}
|
|
75
|
+
for (const tool of tools) {
|
|
76
|
+
toolCounts.set(tool.name, (toolCounts.get(tool.name) || 0) + 1)
|
|
77
|
+
}
|
|
78
|
+
// Claude can report text and tool use in one assistant event. Text before
|
|
79
|
+
// the tool is progress, while the final answer begins after that tool.
|
|
80
|
+
if (tools.length) answerText = ''
|
|
81
|
+
} else if (event.type === 'result') {
|
|
82
|
+
if (event.text !== undefined && typeof event.text !== 'string') {
|
|
83
|
+
throw new TypeError('C2 projection result text invalid')
|
|
84
|
+
}
|
|
85
|
+
observedResult = { text: event.text || '', subtype: event.subtype,
|
|
86
|
+
isError: !!event.isError, sessionMissing: !!event.sessionMissing,
|
|
87
|
+
transientKind: event.transientKind, quota: event.quota || null,
|
|
88
|
+
usage: event.usage || null,
|
|
89
|
+
usageCumulative: event.usageCumulative === true }
|
|
90
|
+
} else if (event.type === 'tool_result') {
|
|
91
|
+
// Codex exec emits item.completed(command_execution) as a tool_result.
|
|
92
|
+
// The output is evidence of tool completion, never assistant answer text.
|
|
93
|
+
const results = event.results
|
|
94
|
+
if (!Array.isArray(results) || results.length !== 1 ||
|
|
95
|
+
!results[0] || results[0].type !== 'tool_result' ||
|
|
96
|
+
typeof results[0].content !== 'string' ||
|
|
97
|
+
typeof results[0].is_error !== 'boolean') {
|
|
98
|
+
throw new TypeError('C2 projection tool result invalid')
|
|
99
|
+
}
|
|
100
|
+
} else if (event.type !== 'system' && event.type !== 'user') {
|
|
101
|
+
throw new TypeError('C2 projection event type unknown')
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function complete({ runnerResult, outputTokens, resumedSessionCurrent = false } = {}) {
|
|
106
|
+
if (completed) throw new Error('C2 projection already completed')
|
|
107
|
+
if (!runnerResult || typeof runnerResult !== 'object' ||
|
|
108
|
+
Array.isArray(runnerResult) || typeof runnerResult.aborted !== 'boolean' ||
|
|
109
|
+
!Number.isFinite(outputTokens) || outputTokens < 0 ||
|
|
110
|
+
typeof resumedSessionCurrent !== 'boolean') {
|
|
111
|
+
throw new TypeError('C2 projection needs closed runner and settled output usage')
|
|
112
|
+
}
|
|
113
|
+
const result = runnerResult.result
|
|
114
|
+
if (result != null && (!result || typeof result !== 'object' ||
|
|
115
|
+
Array.isArray(result) || result.type !== 'result')) {
|
|
116
|
+
throw new TypeError('C2 projection closed runner result invalid')
|
|
117
|
+
}
|
|
118
|
+
if (!!result !== !!observedResult ||
|
|
119
|
+
(result && ((result.text || '') !== observedResult.text ||
|
|
120
|
+
result.subtype !== observedResult.subtype ||
|
|
121
|
+
!!result.isError !== observedResult.isError ||
|
|
122
|
+
!!result.sessionMissing !== observedResult.sessionMissing ||
|
|
123
|
+
result.transientKind !== observedResult.transientKind ||
|
|
124
|
+
!!result.quota !== !!observedResult.quota ||
|
|
125
|
+
!isDeepStrictEqual(result.usage || null, observedResult.usage) ||
|
|
126
|
+
(result.usageCumulative === true) !== observedResult.usageCumulative))) {
|
|
127
|
+
throw new Error('C2 projection closed runner differs from observed result')
|
|
128
|
+
}
|
|
129
|
+
if (runnerResult.sessionId != null &&
|
|
130
|
+
(typeof runnerResult.sessionId !== 'string' || !runnerResult.sessionId)) {
|
|
131
|
+
throw new TypeError('C2 projection closed session invalid')
|
|
132
|
+
}
|
|
133
|
+
if (runnerResult.sessionId && sessionId && runnerResult.sessionId !== sessionId) {
|
|
134
|
+
throw new Error('C2 projection closed session differs from events')
|
|
135
|
+
}
|
|
136
|
+
if (resumedSessionCurrent && (!runnerResult.aborted || !resumed)) {
|
|
137
|
+
throw new TypeError('C2 projection current resumed session requires abort')
|
|
138
|
+
}
|
|
139
|
+
completed = true
|
|
140
|
+
const resultText = observedResult?.text || ''
|
|
141
|
+
const payload = Object.freeze({
|
|
142
|
+
type: 'finalizeEffectCommit', receiptId, turnToken, quiet,
|
|
143
|
+
source: observedResult ? 'result' : 'close',
|
|
144
|
+
aborted: runnerResult.aborted,
|
|
145
|
+
isError: observedResult?.isError || false,
|
|
146
|
+
resumed, sessionMissing: observedResult?.sessionMissing || false,
|
|
147
|
+
freshRetried, resumedSessionCurrent,
|
|
148
|
+
transientNetwork: observedResult?.transientKind === 'network',
|
|
149
|
+
hasQuota: !!observedResult?.quota,
|
|
150
|
+
toolsCount: toolCounts.size, outputTokens,
|
|
151
|
+
resultText, bufferText, answerText,
|
|
152
|
+
...(batchReceiptIds ? { batchReceiptIds: Object.freeze([...batchReceiptIds]),
|
|
153
|
+
replyTargetReceiptId: receiptId } : {}),
|
|
154
|
+
})
|
|
155
|
+
const tools = Object.freeze([...toolCounts].map(([name, count]) => Object.freeze({ name, count })))
|
|
156
|
+
const display = Object.freeze({
|
|
157
|
+
resultText, bufferText, answerText, fullText,
|
|
158
|
+
finalAnswer: (answerText.trim() ? answerText : bufferText) || resultText,
|
|
159
|
+
silent: quiet || resultText.trim() === NO_REPLY ||
|
|
160
|
+
(!resultText.trim() && bufferText.trim() === NO_REPLY),
|
|
161
|
+
assistantEvents, tools,
|
|
162
|
+
sessionId: runnerResult.sessionId || sessionId,
|
|
163
|
+
contextTokens,
|
|
164
|
+
usage: result?.usage || null,
|
|
165
|
+
usageCumulative: result?.usageCumulative === true,
|
|
166
|
+
result: result || null,
|
|
167
|
+
})
|
|
168
|
+
return Object.freeze({ payload, display })
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Read-only, process-local text for streaming; raw content is never a
|
|
172
|
+
// reducer event or journal field.
|
|
173
|
+
const streamSnapshot = () => Object.freeze({ bufferText, assistantEvents })
|
|
174
|
+
return Object.freeze({ accept, complete, streamSnapshot })
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = { createC2TurnProjection }
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// C2 checkpoint contract. This module does not choose Engine transitions or
|
|
4
|
+
// send recovery notices. The caller must keep JSON-safe opaque state with
|
|
5
|
+
// handles only and job receipts in one committed snapshot, then acknowledge
|
|
6
|
+
// intake/effects.
|
|
7
|
+
const fs = require('node:fs')
|
|
8
|
+
const path = require('node:path')
|
|
9
|
+
const { createHash, randomUUID } = require('node:crypto')
|
|
10
|
+
const { stepId, batchStepId } = require('./engineeffectcursor')
|
|
11
|
+
|
|
12
|
+
const VERSION = 3
|
|
13
|
+
const LEGACY_VERSION = 1
|
|
14
|
+
const COMPLETED_VERSION = 2
|
|
15
|
+
const MAX_TRACKED_IDS = 100_000
|
|
16
|
+
const RECEIPT_KEYS = new Set(['id', 'chatId', 'source', 'sourceId', 'acceptedAt', 'notifiedAt', 'quiet'])
|
|
17
|
+
const COMPLETED_KEYS = new Set(['id', 'settledAt'])
|
|
18
|
+
const RESET_KEYS = new Set(['effectId', 'receiptIds', 'chatId', 'turnToken', 'generation', 'phase', 'status'])
|
|
19
|
+
const ID = /^[0-9a-f]{32}$/
|
|
20
|
+
const RESET_PHASES = new Set(['acked', 'ready'])
|
|
21
|
+
const RESET_STATUSES = new Set(['ok', 'no-reply'])
|
|
22
|
+
|
|
23
|
+
function jsonState(value) {
|
|
24
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError('invalid C2 state')
|
|
25
|
+
const stack = new Set()
|
|
26
|
+
function inspect(item) {
|
|
27
|
+
if (item === null || typeof item === 'string' || typeof item === 'boolean') return
|
|
28
|
+
if (typeof item === 'number' && Number.isFinite(item)) return
|
|
29
|
+
if (!item || typeof item !== 'object' || stack.has(item)) throw new TypeError('C2 state must be JSON-safe')
|
|
30
|
+
const prototype = Object.getPrototypeOf(item)
|
|
31
|
+
if (!Array.isArray(item) && prototype !== Object.prototype && prototype !== null) {
|
|
32
|
+
throw new TypeError('C2 state must be JSON-safe')
|
|
33
|
+
}
|
|
34
|
+
stack.add(item)
|
|
35
|
+
const keys = Reflect.ownKeys(item)
|
|
36
|
+
if (Array.isArray(item) && keys.filter(key => key !== 'length').length !== item.length) {
|
|
37
|
+
throw new TypeError('C2 state must be JSON-safe')
|
|
38
|
+
}
|
|
39
|
+
for (const key of keys) {
|
|
40
|
+
if (key === 'length' && Array.isArray(item)) continue
|
|
41
|
+
const descriptor = Object.getOwnPropertyDescriptor(item, key)
|
|
42
|
+
if (typeof key !== 'string' || key === '__proto__' || !descriptor?.enumerable || !('value' in descriptor)) {
|
|
43
|
+
throw new TypeError('C2 state must be JSON-safe')
|
|
44
|
+
}
|
|
45
|
+
inspect(descriptor.value)
|
|
46
|
+
}
|
|
47
|
+
stack.delete(item)
|
|
48
|
+
}
|
|
49
|
+
inspect(value)
|
|
50
|
+
return JSON.parse(JSON.stringify(value))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function taskId({ source, chatId, sourceId }) {
|
|
54
|
+
for (const value of [source, chatId, sourceId]) {
|
|
55
|
+
if (typeof value !== 'string' || !value) throw new TypeError('stable task ID needs source, chatId and sourceId')
|
|
56
|
+
}
|
|
57
|
+
return createHash('sha256').update('sintbot-c2-job-v1\0').update(source).update('\0')
|
|
58
|
+
.update(chatId).update('\0').update(sourceId).digest('hex').slice(0, 32)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// For sources without a platform trigger ID. Persist this value in the intake
|
|
62
|
+
// receipt before acknowledging the trigger; it cannot deduplicate a replay
|
|
63
|
+
// after a crash that precedes the first durable receipt.
|
|
64
|
+
function newSourceId() { return 'local:' + randomUUID() }
|
|
65
|
+
|
|
66
|
+
function validate(input, { sourceVersion = VERSION, strict = false } = {}) {
|
|
67
|
+
const legacy = sourceVersion === LEGACY_VERSION
|
|
68
|
+
if (!input || typeof input !== 'object' || Array.isArray(input) ||
|
|
69
|
+
(input.version !== undefined && input.version !== sourceVersion) ||
|
|
70
|
+
!Number.isSafeInteger(input.seq) || input.seq < 0 ||
|
|
71
|
+
!input.state || typeof input.state !== 'object' || Array.isArray(input.state) ||
|
|
72
|
+
!Array.isArray(input.pending) ||
|
|
73
|
+
Object.keys(input).some(key => !['version', 'seq', 'state', 'pending',
|
|
74
|
+
...(legacy ? [] : ['completed']), ...(sourceVersion === VERSION ? ['sessionResets'] : [])].includes(key)) ||
|
|
75
|
+
(legacy ? Object.hasOwn(input, 'completed') :
|
|
76
|
+
(strict && !Object.hasOwn(input, 'completed'))) ||
|
|
77
|
+
(!legacy && input.completed !== undefined && !Array.isArray(input.completed)) ||
|
|
78
|
+
(sourceVersion === VERSION && (strict && !Object.hasOwn(input, 'sessionResets') ||
|
|
79
|
+
input.sessionResets !== undefined && !Array.isArray(input.sessionResets)))) {
|
|
80
|
+
throw new TypeError('invalid C2 checkpoint shape')
|
|
81
|
+
}
|
|
82
|
+
const completed = legacy ? [] : (input.completed || [])
|
|
83
|
+
const sessionResets = sourceVersion === VERSION ? (input.sessionResets || []) : []
|
|
84
|
+
if (input.pending.length + completed.length > MAX_TRACKED_IDS) {
|
|
85
|
+
throw new RangeError('C2 completed ID capacity exceeded')
|
|
86
|
+
}
|
|
87
|
+
if (sessionResets.length > MAX_TRACKED_IDS) throw new RangeError('C2 session reset capacity exceeded')
|
|
88
|
+
const ids = new Set()
|
|
89
|
+
for (const receipt of input.pending) {
|
|
90
|
+
if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt) ||
|
|
91
|
+
Object.keys(receipt).some(key => !RECEIPT_KEYS.has(key)) ||
|
|
92
|
+
typeof receipt.id !== 'string' || !ID.test(receipt.id) ||
|
|
93
|
+
typeof receipt.chatId !== 'string' || !receipt.chatId ||
|
|
94
|
+
typeof receipt.source !== 'string' || !receipt.source ||
|
|
95
|
+
typeof receipt.sourceId !== 'string' || !receipt.sourceId ||
|
|
96
|
+
!Number.isSafeInteger(receipt.acceptedAt) || receipt.acceptedAt <= 0 ||
|
|
97
|
+
typeof receipt.quiet !== 'boolean' ||
|
|
98
|
+
(receipt.notifiedAt !== undefined && (!Number.isSafeInteger(receipt.notifiedAt) || receipt.notifiedAt <= 0)) ||
|
|
99
|
+
taskId(receipt) !== receipt.id || ids.has(receipt.id)) throw new TypeError('invalid C2 receipt')
|
|
100
|
+
ids.add(receipt.id)
|
|
101
|
+
}
|
|
102
|
+
for (const item of completed) {
|
|
103
|
+
if (!item || typeof item !== 'object' || Array.isArray(item) ||
|
|
104
|
+
Object.keys(item).length !== 2 || Object.keys(item).some(key => !COMPLETED_KEYS.has(key)) ||
|
|
105
|
+
typeof item.id !== 'string' || !ID.test(item.id) ||
|
|
106
|
+
!Number.isSafeInteger(item.settledAt) || item.settledAt <= 0 || ids.has(item.id)) {
|
|
107
|
+
throw new TypeError('invalid C2 completed ID')
|
|
108
|
+
}
|
|
109
|
+
ids.add(item.id)
|
|
110
|
+
}
|
|
111
|
+
const resetIds = new Set(), resetChats = new Set()
|
|
112
|
+
for (const item of sessionResets) {
|
|
113
|
+
if (!item || typeof item !== 'object' || Array.isArray(item) ||
|
|
114
|
+
Object.keys(item).length !== RESET_KEYS.size ||
|
|
115
|
+
Object.keys(item).some(key => !RESET_KEYS.has(key)) ||
|
|
116
|
+
typeof item.effectId !== 'string' || !ID.test(item.effectId) || resetIds.has(item.effectId) ||
|
|
117
|
+
!Array.isArray(item.receiptIds) || item.receiptIds.length < 1 || item.receiptIds.length > 1024 ||
|
|
118
|
+
item.receiptIds.some(id => typeof id !== 'string' || !ID.test(id) || !ids.has(id)) ||
|
|
119
|
+
new Set(item.receiptIds).size !== item.receiptIds.length ||
|
|
120
|
+
item.effectId !== (item.receiptIds.length === 1
|
|
121
|
+
? stepId(item.receiptIds[0], 0) : batchStepId(item.receiptIds, 0)) ||
|
|
122
|
+
typeof item.chatId !== 'string' || !item.chatId || resetChats.has(item.chatId) ||
|
|
123
|
+
!Number.isSafeInteger(item.turnToken) || item.turnToken <= 0 ||
|
|
124
|
+
typeof item.generation !== 'string' || !ID.test(item.generation) ||
|
|
125
|
+
!RESET_PHASES.has(item.phase) || !RESET_STATUSES.has(item.status)) {
|
|
126
|
+
throw new TypeError('invalid C2 session reset intent')
|
|
127
|
+
}
|
|
128
|
+
resetIds.add(item.effectId)
|
|
129
|
+
resetChats.add(item.chatId)
|
|
130
|
+
}
|
|
131
|
+
return { version: VERSION, seq: input.seq, state: jsonState(input.state),
|
|
132
|
+
pending: input.pending.map(receipt => ({ ...receipt })),
|
|
133
|
+
completed: completed.map(item => ({ id: item.id, settledAt: item.settledAt })),
|
|
134
|
+
sessionResets: sessionResets.map(item => ({ ...item })) }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function checksum(body) { return createHash('sha256').update(JSON.stringify(body)).digest('hex') }
|
|
138
|
+
|
|
139
|
+
function writeCheckpoint(file, input) {
|
|
140
|
+
const body = validate(input)
|
|
141
|
+
const previous = readCheckpoint(file, { allowMissing: true })
|
|
142
|
+
if (!previous && body.seq !== 0) throw new Error('C2 checkpoint must be initialized at sequence zero')
|
|
143
|
+
if (previous && body.seq <= previous.seq) throw new Error('C2 checkpoint sequence must advance')
|
|
144
|
+
const bytes = Buffer.from(JSON.stringify({ body, checksum: checksum(body) }) + '\n')
|
|
145
|
+
const dir = path.dirname(file)
|
|
146
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
147
|
+
const temp = file + '.' + process.pid + '.' + createHash('sha256').update(String(process.hrtime.bigint())).digest('hex').slice(0, 12) + '.tmp'
|
|
148
|
+
let fd
|
|
149
|
+
try {
|
|
150
|
+
fd = fs.openSync(temp, 'wx', 0o600)
|
|
151
|
+
fs.writeFileSync(fd, bytes)
|
|
152
|
+
fs.fsyncSync(fd)
|
|
153
|
+
fs.closeSync(fd); fd = undefined
|
|
154
|
+
fs.renameSync(temp, file)
|
|
155
|
+
// Directory fsync makes the rename durable on POSIX. Some filesystems and
|
|
156
|
+
// Windows do not support it; the data file is already synced there.
|
|
157
|
+
try {
|
|
158
|
+
const dirFd = fs.openSync(dir, 'r')
|
|
159
|
+
try { fs.fsyncSync(dirFd) } finally { fs.closeSync(dirFd) }
|
|
160
|
+
} catch {}
|
|
161
|
+
} finally {
|
|
162
|
+
if (fd !== undefined) fs.closeSync(fd)
|
|
163
|
+
try { fs.unlinkSync(temp) } catch {}
|
|
164
|
+
}
|
|
165
|
+
return body
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function readCheckpoint(file, { allowMissing = false } = {}) {
|
|
169
|
+
let bytes
|
|
170
|
+
try { bytes = fs.readFileSync(file, 'utf8') }
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (error.code === 'ENOENT' && allowMissing) return null
|
|
173
|
+
if (error.code === 'ENOENT') throw new Error('C2 checkpoint missing; recovery cannot assume an empty queue')
|
|
174
|
+
throw error
|
|
175
|
+
}
|
|
176
|
+
let envelope
|
|
177
|
+
try { envelope = JSON.parse(bytes) } catch { throw new Error('C2 checkpoint is corrupt') }
|
|
178
|
+
if (!envelope || typeof envelope !== 'object' || !envelope.body ||
|
|
179
|
+
![LEGACY_VERSION, COMPLETED_VERSION, VERSION].includes(envelope.body.version) ||
|
|
180
|
+
envelope.checksum !== checksum(envelope.body)) {
|
|
181
|
+
throw new Error('C2 checkpoint checksum or version mismatch')
|
|
182
|
+
}
|
|
183
|
+
return validate(envelope.body, { sourceVersion: envelope.body.version, strict: true })
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function outstanding(checkpoint) {
|
|
187
|
+
return validate(checkpoint).pending.filter(receipt => receipt.notifiedAt === undefined)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function markNotified(checkpoint, id, at) {
|
|
191
|
+
const body = validate(checkpoint)
|
|
192
|
+
if (!Number.isSafeInteger(at) || at <= 0) throw new TypeError('notification time required')
|
|
193
|
+
const receipt = body.pending.find(item => item.id === id)
|
|
194
|
+
if (!receipt) throw new Error('unknown C2 task receipt')
|
|
195
|
+
receipt.notifiedAt = at
|
|
196
|
+
body.seq++
|
|
197
|
+
return body
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function markDone(checkpoint, id, settledAt = Date.now()) {
|
|
201
|
+
const body = validate(checkpoint)
|
|
202
|
+
if (!Number.isSafeInteger(settledAt) || settledAt <= 0) throw new TypeError('settlement time required')
|
|
203
|
+
const index = body.pending.findIndex(item => item.id === id)
|
|
204
|
+
if (index < 0) throw new Error('unknown C2 task receipt')
|
|
205
|
+
body.pending.splice(index, 1)
|
|
206
|
+
body.completed.push({ id, settledAt })
|
|
207
|
+
body.seq++
|
|
208
|
+
return body
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = { taskId, newSourceId, writeCheckpoint, readCheckpoint, outstanding, markNotified, markDone, MAX_TRACKED_IDS }
|