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,458 @@
1
+ // Codex 群默认走 App Server;显式关闭 codexSteer 时走 exec,Claude 群不受影响。
2
+ // 每轮独立起进程并等它干净退出,确保会话 rollout 落盘后才允许下一轮续接。
3
+
4
+ const { spawn } = require('node:child_process')
5
+ const closedRunnerResults = new WeakSet()
6
+ const isClosedRunnerResult = result => !!result && typeof result === 'object' && closedRunnerResults.has(result)
7
+ const { IS_WIN, resolveSpawn } = require('./winexec')
8
+ const { normalizeEvent, isEffortRejection } = require('./codexcode')
9
+ const { coreFailedResult, isCoreFailure } = require('./errtag')
10
+ const { HUMAN_BY_KIND, appErrorOverride, appNormalizeItem, appStreamTail, appNoticeAction, appDeltaNext, appLandedAction, appCompletionAction, appCompletionNoticeAction, appUsageFromTotal, appBuildRequest } = require('./privatecore')
11
+ const { version } = require('../../package.json')
12
+ const { StringDecoder } = require('node:string_decoder')
13
+
14
+ // 按 codex app-server 的协议定义(0.166 generate-json-schema)给出的拒绝回复;其余方法回 JSON-RPC 错误。
15
+ const SERVER_REQUEST_REPLIES = {
16
+ 'item/commandExecution/requestApproval': { decision: 'decline' },
17
+ 'item/fileChange/requestApproval': { decision: 'decline' },
18
+ 'mcpServer/elicitation/request': { action: 'decline' },
19
+ 'item/tool/requestUserInput': { answers: {} },
20
+ }
21
+
22
+ // App Server 报的是整条 thread 的累计量;交给引擎按已有基线扣成本轮用量。
23
+ function usageFromTotal(total) {
24
+ return appUsageFromTotal(total)
25
+ }
26
+
27
+ function failedTurnEvent(error = {}) {
28
+ const result = normalizeEvent({ type: 'turn.failed', error })
29
+ const info = error.codexErrorInfo
30
+ if (!result || !info || typeof info !== 'object') return result
31
+ result.codexErrorInfo = info
32
+ const kind = ['httpConnectionFailed', 'responseStreamConnectionFailed', 'responseStreamDisconnected', 'responseTooManyFailedAttempts']
33
+ .find((key) => Object.hasOwn(info, key))
34
+ const status = kind && Number(info[kind] && info[kind].httpStatusCode)
35
+ const override = appErrorOverride(status, !!kind, !!result.quota, !!result.sessionMissing, !!result.transient)
36
+ // 结构化的 401 是账号证据:与共享错误判定同一优先级(账号 > 瞬时错)。
37
+ if (override === 'account') {
38
+ result.quota = { kind: 'account', window: null }; delete result.transient; delete result.transientKind
39
+ } else if (override) {
40
+ // 原话没有命中时使用结构化兜底;群面人话仍由 JS 映射。
41
+ result.transientKind = override
42
+ result.transient = HUMAN_BY_KIND[result.transientKind]
43
+ }
44
+ return result
45
+ }
46
+
47
+ // 保持现有引擎的事件形状,卡片、步数和压缩提醒才可沿用同一套逻辑。
48
+ function itemEvent(method, item) {
49
+ const result = appNormalizeItem(method, item)
50
+ if (result === null) return null
51
+ // Preserve missing JS properties and the original item identity at the
52
+ // public event boundary; JSON cannot encode undefined or object references.
53
+ if (method !== 'item/completed' && item.type === 'commandExecution') result.tools[0].input.command = item.command
54
+ else if (method !== 'item/completed' && item.type === 'fileChange') (item.changes || []).forEach((change, i) => { result.tools[i].input.path = change.path })
55
+ else if (method !== 'item/completed' && (item.type === 'mcpToolCall' || item.type === 'webSearch')) result.tools[0].input = item
56
+ return result
57
+ }
58
+
59
+ // App Server 的请求和通知共用一条 stdout 流;单轮独立进程使关闭成为续接边界。
60
+ function runTurn(opts = {}) {
61
+ const {
62
+ bin = 'codex', prompt, provider = {}, model, workdir, sessionId,
63
+ permissionMode, autocompactTokens, appendSystemPrompt, codexApp, codexProjectDocFallback, extraEnv,
64
+ onEvent, onDurableEvent, onSteerReady, onSteerLanded, onTurnAccepted, signal, log = () => {},
65
+ softStopGraceMs = 10000, spawnFn = spawn,
66
+ } = opts
67
+ if (!prompt) return Promise.reject(new Error('runTurn: prompt required'))
68
+ if (onDurableEvent !== undefined && typeof onDurableEvent !== 'function') return Promise.reject(new TypeError('onDurableEvent must be a function'))
69
+
70
+ return new Promise((resolve, reject) => {
71
+ const env = { ...process.env, ...(extraEnv || {}) }
72
+ delete env.CLAUDECODE
73
+ delete env.OPENAI_API_KEY // 只使用机器现有的 ChatGPT 订阅登录态,与 codex exec 一致。
74
+ const args = ['app-server', '-c', 'approval_policy=never']
75
+ if (autocompactTokens > 0) args.push('-c', `model_auto_compact_token_limit=${autocompactTokens}`)
76
+ const { file, spawnArgs, fallback } = resolveSpawn(bin, args)
77
+ if (fallback) log(`配置的 agent 路径 '${bin}' 不存在,本轮改用 ${fallback}`)
78
+ let child
79
+ try {
80
+ child = spawnFn(file, spawnArgs, { env, cwd: workdir || process.cwd(), stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true })
81
+ } catch (e) { reject(e); return }
82
+
83
+ let capturedSessionId = sessionId || null
84
+ let activeTurnId = null
85
+ let lastAgentText = ''
86
+ let lastResult = null
87
+ let totalUsage = null
88
+ let contextTokens = 0
89
+ let contextWindowTokens = 0
90
+ let contextCompacted = false
91
+ let stderr = ''
92
+ let buf = ''
93
+ let nextId = 1
94
+ let aborted = false
95
+ let childCloseObserved = false
96
+ let settled = false
97
+ let completed = false
98
+ let effort = provider.reasoningEffort || ''
99
+ let retriedEffort = false
100
+ let pendingTurnStart = false
101
+ let acceptedTurnId = null
102
+ const deferredTurnNotices = []
103
+ let deferredInit = null
104
+ const earlyCompleted = new Map()
105
+ let graceTimer = null
106
+ let durableWork = Promise.resolve()
107
+ let streamWork = Promise.resolve()
108
+ let durableFailed = false
109
+ const requests = new Map()
110
+ const landed = new Set() // 重复通知只确认一次,避免一个补充被重复记进账本。
111
+ const messageDeltas = new Map()
112
+ const pendingUsage = new Map()
113
+ const legacyEmit = (ev) => { if (ev && onEvent) { try { onEvent(ev) } catch {} } }
114
+ const emit = (ev) => {
115
+ if (!ev) return
116
+ if (!onDurableEvent) { legacyEmit(ev); return }
117
+ durableWork = durableWork.then(async () => {
118
+ let timer
119
+ try {
120
+ await Promise.race([
121
+ Promise.resolve().then(() => onDurableEvent(ev)),
122
+ new Promise((_, fail) => { timer = setTimeout(() => fail(new Error('Codex app-server durable event hook timed out')), 10000) }),
123
+ ])
124
+ } finally { clearTimeout(timer) }
125
+ if (!durableFailed && !settled) legacyEmit(ev)
126
+ })
127
+ durableWork.catch(failDurableEvent)
128
+ }
129
+ const recordUsage = (usage = {}) => {
130
+ totalUsage = usageFromTotal(usage.total)
131
+ if (Number.isFinite(usage.last && usage.last.inputTokens) && usage.last.inputTokens > 0) contextTokens = usage.last.inputTokens
132
+ if (Number.isFinite(usage.modelContextWindow) && usage.modelContextWindow > 0) contextWindowTokens = usage.modelContextWindow
133
+ }
134
+ const write = (obj) => {
135
+ if (child.stdin.destroyed) throw new Error('codex app-server stdin closed')
136
+ child.stdin.write(JSON.stringify(obj) + '\n')
137
+ }
138
+ const request = (method, params) => new Promise((ok, fail) => {
139
+ const id = nextId++
140
+ requests.set(id, { ok, fail })
141
+ try { write({ id, method, params }) } catch (e) { requests.delete(id); fail(e) }
142
+ })
143
+ const deactivate = () => { activeTurnId = null } // 完成/中止后立即拒绝晚到的补充。
144
+ const control = {
145
+ async steer({ msgId, text }) {
146
+ const turnId = activeTurnId
147
+ if (!turnId || completed || aborted) return false
148
+ try {
149
+ await request('turn/steer', {
150
+ threadId: capturedSessionId, expectedTurnId: turnId,
151
+ clientUserMessageId: msgId || undefined,
152
+ input: [{ type: 'text', text }],
153
+ })
154
+ return true
155
+ } catch (e) {
156
+ log(`codex steer rejected: ${String(e && e.message || e).slice(0, 250)}`)
157
+ return false
158
+ }
159
+ },
160
+ get turnId() { return activeTurnId },
161
+ }
162
+ const failPending = (error) => {
163
+ for (const p of requests.values()) p.fail(error)
164
+ requests.clear()
165
+ }
166
+ const settle = (fn) => {
167
+ if (settled) return
168
+ settled = true
169
+ deactivate()
170
+ if (graceTimer) clearTimeout(graceTimer)
171
+ if (signal) signal.removeEventListener('abort', onAbort)
172
+ failPending(new Error('codex app-server closed'))
173
+ fn()
174
+ }
175
+ const resolveRun = result => {
176
+ if (childCloseObserved) closedRunnerResults.add(result)
177
+ resolve(result)
178
+ }
179
+ const killChild = () => {
180
+ if (IS_WIN && child.pid) {
181
+ try { spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore', windowsHide: true }) } catch {}
182
+ }
183
+ try { child.kill('SIGTERM') } catch {}
184
+ // 和 exec runner 一样有硬杀兜底,避免子进程忽略 TERM 后拖住关闭。
185
+ const hardKill = setTimeout(() => { try { child.kill('SIGKILL') } catch {} }, softStopGraceMs)
186
+ hardKill.unref?.()
187
+ }
188
+ const failDurableEvent = (error) => {
189
+ if (durableFailed || settled) return
190
+ durableFailed = true
191
+ completed = true
192
+ deactivate()
193
+ killChild()
194
+ settle(() => reject(error))
195
+ }
196
+ const onAbort = () => {
197
+ aborted = true
198
+ deactivate()
199
+ killChild()
200
+ graceTimer = setTimeout(() => {
201
+ const complete = () => settle(() => resolveRun({ sessionId: capturedSessionId, result: lastResult, code: null, aborted: true }))
202
+ if (onDurableEvent) streamWork.then(() => durableWork).then(complete, failDurableEvent)
203
+ else complete()
204
+ }, softStopGraceMs)
205
+ }
206
+ if (signal) {
207
+ if (signal.aborted) onAbort()
208
+ else signal.addEventListener('abort', onAbort, { once: true })
209
+ }
210
+ child.stdin.on('error', () => {})
211
+
212
+ const start = async () => {
213
+ await request('initialize', { clientInfo: { name: 'sintbot', title: 'SintBot', version } })
214
+ write({ method: 'initialized', params: {} })
215
+ const threadRequest = appBuildRequest({ phase: 'thread', workdir: workdir || process.cwd(),
216
+ model, providerModel: provider.model, permissionMode, autocompactTokens,
217
+ codexProjectDocFallback, codexApp, appendSystemPrompt, sessionId })
218
+ const thread = await request(threadRequest.method, threadRequest.params)
219
+ capturedSessionId = thread && thread.thread && thread.thread.id || capturedSessionId
220
+ if (!capturedSessionId) throw new Error('codex app-server returned no thread id')
221
+ if (Array.isArray(thread.instructionSources)) {
222
+ const sources = thread.instructionSources
223
+ if (sources.length) log(`codex project instructions (${capturedSessionId}): ${sources.join(', ')}`)
224
+ else if (!sessionId) log(`codex project instructions (${capturedSessionId}): none loaded`)
225
+ }
226
+ const initEvent = { type: 'system', subtype: 'init', sessionId: capturedSessionId }
227
+ if (onTurnAccepted) deferredInit = initEvent
228
+ else { emit(initEvent); if (onDurableEvent) await durableWork }
229
+ await startModelTurn()
230
+ }
231
+ const startModelTurn = async () => {
232
+ const turnRequest = appBuildRequest({ phase: 'turn', sessionId: capturedSessionId,
233
+ isResume: !!sessionId, prompt, appendSystemPrompt, codexApp,
234
+ model, providerModel: provider.model, effort, workdir: workdir || process.cwd(), permissionMode })
235
+ pendingTurnStart = true
236
+ let turn
237
+ try { turn = await request(turnRequest.method, turnRequest.params) }
238
+ finally { pendingTurnStart = false }
239
+ activeTurnId = turn && turn.turn && turn.turn.id
240
+ if (!activeTurnId) throw new Error('codex app-server returned no turn id')
241
+ if (onTurnAccepted) {
242
+ try {
243
+ const accepted = onTurnAccepted({ channel: 'codex-app-server', sessionId: capturedSessionId, turnId: activeTurnId })
244
+ if (accepted && typeof accepted.then === 'function') throw new Error('onTurnAccepted must persist synchronously')
245
+ acceptedTurnId = activeTurnId
246
+ } catch (error) {
247
+ aborted = true
248
+ completed = true
249
+ deactivate()
250
+ killChild()
251
+ settle(() => reject(error))
252
+ return
253
+ }
254
+ if (deferredInit) { emit(deferredInit); deferredInit = null; if (onDurableEvent) await durableWork }
255
+ for (const notice of deferredTurnNotices.splice(0)) {
256
+ handleLine(notice)
257
+ if (onDurableEvent) await durableWork
258
+ }
259
+ }
260
+ if (pendingUsage.has(activeTurnId)) recordUsage(pendingUsage.get(activeTurnId))
261
+ pendingUsage.clear()
262
+ const done = earlyCompleted.get(activeTurnId)
263
+ earlyCompleted.clear()
264
+ if (done) {
265
+ completedTurn(done)
266
+ if (onDurableEvent) await durableWork
267
+ return
268
+ }
269
+ if (onSteerReady) { try { onSteerReady(control) } catch {} }
270
+ }
271
+ const completedTurn = (turn) => {
272
+ const matchesTurn = !!turn && turn.id === activeTurnId
273
+ if (!matchesTurn || completed) return
274
+ const failed = turn.status === 'failed' && effort && !retriedEffort ? failedTurnEvent(turn.error || {}) : null
275
+ const action = appCompletionAction(turn.status, matchesTurn, completed, !!effort, retriedEffort || !!onTurnAccepted,
276
+ !!failed && isEffortRejection(failed))
277
+ if (action === 0) return
278
+ deactivate()
279
+ if (action === 1) {
280
+ retriedEffort = true
281
+ const rejectedEffort = effort
282
+ effort = ''
283
+ log(`codex 推理强度 '${rejectedEffort}' 不被当前模型/套餐接受 — 已回退模型默认重试一轮`)
284
+ startModelTurn().catch((e) => fatal(e))
285
+ return
286
+ }
287
+ completed = true
288
+ if (action === 2) lastResult = failed || failedTurnEvent(turn.error || {})
289
+ else if (action === 3) { aborted = true; lastResult = null }
290
+ else lastResult = { type: 'result', subtype: 'success', text: lastAgentText, usage: totalUsage, usageCumulative: !!totalUsage,
291
+ contextTokens, contextWindowTokens, contextCompacted }
292
+ if (lastResult && retriedEffort) lastResult.effortFellBack = provider.reasoningEffort || ''
293
+ if (lastResult) emit(lastResult)
294
+ // 让服务端自行落盘退出;收到完成通知就强杀会破坏下一轮 resume。
295
+ try { child.stdin.end() } catch {}
296
+ }
297
+ const fatal = (e) => {
298
+ if (completed || aborted || settled) return
299
+ completed = true
300
+ deactivate()
301
+ const message = String(e && e.message || e)
302
+ if (isCoreFailure(e)) log(`binary core failed in codex app-server stream: ${message}`)
303
+ try { lastResult = isCoreFailure(e) ? coreFailedResult(sessionId || null) : normalizeEvent({ type: 'error', message }) }
304
+ catch (error) { log(`binary core failed on codex app-server error: ${error && error.message}`); lastResult = coreFailedResult(sessionId || null) }
305
+ // 新会话传输失败时不能保存 thread id,否则下一轮可能续接不存在的 rollout。
306
+ if (!sessionId) capturedSessionId = null
307
+ emit(lastResult)
308
+ try { child.stdin.end() } catch {}
309
+ }
310
+ const stdoutDecoder = new StringDecoder('utf8') // 一个汉字可能被拆在两个分块里,逐块 toString 会变成乱码
311
+ // 这段回调不在任何 Promise 链里,抛错会直接带走整个进程 ⇒ 核心失败只结束当前轮。
312
+ // 已完成的轮 fatal 会直接返回,结果不受影响。
313
+ const handleLine = (raw) => {
314
+ // 对方主动发来的请求同时带 id 和 method:id 是对方的号段,可能与我们的请求相撞,绝不能当响应处理;
315
+ // 不回应它就会一直等,这一轮挂到超时。无人值守,按协议一律拒绝。
316
+ if (raw.id != null && raw.method) {
317
+ const reply = SERVER_REQUEST_REPLIES[raw.method]
318
+ log(`codex server request declined: ${raw.method}`)
319
+ try { write(reply ? { id: raw.id, result: reply } : { id: raw.id, error: { code: -32601, message: `unsupported in headless mode: ${raw.method}` } }) } catch {}
320
+ return
321
+ }
322
+ if (raw.id != null) {
323
+ const p = requests.get(raw.id)
324
+ if (p) { requests.delete(raw.id); raw.error ? p.fail(new Error(raw.error.message || 'Codex RPC error')) : p.ok(raw.result) }
325
+ return
326
+ }
327
+ if (onTurnAccepted && pendingTurnStart && !acceptedTurnId) {
328
+ deferredTurnNotices.push(raw)
329
+ return
330
+ }
331
+ const params = raw.params || {}
332
+ const action = appNoticeAction(raw.method, {
333
+ threadMatches: params.threadId === capturedSessionId,
334
+ turnMatches: params.turnId === activeTurnId,
335
+ activeTurn: !!activeTurnId,
336
+ completed, codexApp,
337
+ hasTurnId: !!params.turnId,
338
+ hasItemId: !!params.itemId,
339
+ })
340
+ if (action === 1) recordUsage(params.tokenUsage)
341
+ else if (action === 2) pendingUsage.set(params.turnId, params.tokenUsage)
342
+ else if (action === 3) {
343
+ const delta = String(params.delta || '')
344
+ const next = appDeltaNext(messageDeltas.get(params.itemId) || '', delta)
345
+ if (next !== null) {
346
+ messageDeltas.set(params.itemId, next)
347
+ emit({ type: 'assistant', text: delta, contextTokens })
348
+ }
349
+ } else if (action === 4) {
350
+ const item = params.item
351
+ if (appLandedAction(item && item.type, raw.method === 'item/completed', !!(item && item.clientId), !!(item && landed.has(item.clientId)))) {
352
+ landed.add(item.clientId)
353
+ if (onSteerLanded) { try { onSteerLanded(item.clientId) } catch {} }
354
+ }
355
+ let ev = itemEvent(raw.method, item)
356
+ if (ev && ev.type === 'system' && ev.subtype === 'compact') {
357
+ if (contextTokens > 0) ev.preTokens = contextTokens
358
+ contextCompacted = true
359
+ contextTokens = 0 // 压缩后未收到新用量前,旧数不能冒充当前上下文。
360
+ }
361
+ if (ev && ev.type === 'assistant' && ev.text && item.type === 'agentMessage') {
362
+ lastAgentText = ev.text
363
+ const streamed = messageDeltas.get(item.id) || ''
364
+ if (streamed) {
365
+ const tail = appStreamTail(ev.text, streamed)
366
+ if (tail !== null) ev = { ...ev, text: tail }
367
+ else {
368
+ // An authoritative completion can differ from streamed fragments; avoid duplicating the answer.
369
+ log(`codex message delta mismatch (${capturedSessionId}, ${item.id})`)
370
+ ev = null
371
+ }
372
+ }
373
+ messageDeltas.delete(item.id)
374
+ }
375
+ emit(ev)
376
+ } else if (action === 5) {
377
+ const turnId = params.turn && params.turn.id
378
+ const decision = appCompletionNoticeAction({
379
+ threadMatches: params.threadId === capturedSessionId,
380
+ hasTurnId: !!turnId,
381
+ activeTurn: !!activeTurnId,
382
+ activeTurnMatches: turnId === activeTurnId,
383
+ pendingStart: pendingTurnStart,
384
+ completed,
385
+ })
386
+ if (decision === 1) completedTurn(params.turn)
387
+ else if (decision === 2) earlyCompleted.set(turnId, params.turn)
388
+ }
389
+ }
390
+ let coreFailed = false
391
+ const processChunk = async (chunk) => {
392
+ if (coreFailed || durableFailed || (onDurableEvent && settled)) return
393
+ buf += stdoutDecoder.write(chunk)
394
+ let nl
395
+ while ((nl = buf.indexOf('\n')) >= 0) {
396
+ const line = buf.slice(0, nl).trim(); buf = buf.slice(nl + 1)
397
+ if (!line) continue
398
+ let raw; try { raw = JSON.parse(line) } catch { continue }
399
+ try {
400
+ handleLine(raw)
401
+ if (onDurableEvent) {
402
+ // An RPC response resumes startModelTurn in a microtask; let it
403
+ // enqueue the deferred init event before reading another line.
404
+ await Promise.resolve()
405
+ await durableWork
406
+ }
407
+ } catch (error) {
408
+ if (onDurableEvent) { failDurableEvent(error); return }
409
+ coreFailed = true
410
+ buf = ''
411
+ fatal(error)
412
+ return
413
+ }
414
+ }
415
+ }
416
+ child.stdout.on('data', (chunk) => {
417
+ if (!onDurableEvent) { processChunk(chunk).catch(fatal); return }
418
+ child.stdout.pause?.()
419
+ streamWork = streamWork.then(() => processChunk(chunk)).catch(failDurableEvent).finally(() => {
420
+ if (!durableFailed && !settled) child.stdout.resume?.()
421
+ })
422
+ })
423
+ child.stderr.on('data', (c) => { stderr = (stderr + c.toString('utf8')).slice(-2000) })
424
+ child.on('error', (e) => {
425
+ if (e.code === 'ENOENT') e = Object.assign(new Error('模型程序在这台机器上找不到了,需要管理员处理'), { code: 'AGENT_BIN_MISSING' })
426
+ settle(() => reject(e))
427
+ })
428
+ const completeClose = (code) => settle(() => {
429
+ if (onDurableEvent && !completed && !aborted) {
430
+ reject(new Error(`codex app-server exited ${code} before a durable result`))
431
+ return
432
+ }
433
+ if (!completed && !aborted) {
434
+ try { lastResult = normalizeEvent({ type: 'error', message: `codex app-server exited ${code}: ${stderr.trim().slice(-800) || '(no stderr)'}` }) }
435
+ catch (error) { log(`binary core failed on codex app-server exit: ${error && error.message}`); lastResult = coreFailedResult(sessionId || null) }
436
+ if (!sessionId) capturedSessionId = null
437
+ }
438
+ resolveRun({ sessionId: capturedSessionId, result: lastResult, code, aborted })
439
+ })
440
+ child.on('close', (code) => {
441
+ childCloseObserved = true
442
+ if (onDurableEvent) streamWork.then(() => durableWork).then(() => { if (!durableFailed) completeClose(code) }, failDurableEvent)
443
+ else completeClose(code)
444
+ })
445
+ start().catch((error) => {
446
+ if (onDurableEvent) { failDurableEvent(error); return }
447
+ if (onTurnAccepted && !acceptedTurnId && !settled) {
448
+ aborted = true
449
+ completed = true
450
+ deactivate()
451
+ killChild()
452
+ settle(() => reject(error))
453
+ } else fatal(error)
454
+ })
455
+ })
456
+ }
457
+
458
+ module.exports = { runTurn, isClosedRunnerResult, usageFromTotal, itemEvent, failedTurnEvent }