sintbot 1.0.15 → 1.3.0

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