sintbot 1.0.16 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.json +99 -0
- package/CHANGELOG.json +99 -0
- package/README.md +20 -158
- package/README.zh-CN.md +20 -166
- package/THIRD-PARTY-NOTICES.md +394 -0
- package/bin/sintbot-v2.js +363 -0
- package/docs/AI-ASSISTED-INSTALL.md +11 -35
- package/docs/install.md +61 -167
- package/docs/zh-CN/AI-ASSISTED-INSTALL.md +11 -35
- package/docs/zh-CN/install.md +61 -165
- package/install.sh +84 -0
- package/package.json +73 -37
- package/src/agent/claudecode.js +149 -108
- package/src/agent/codexapp.js +458 -0
- package/src/agent/codexcode.js +210 -107
- package/src/agent/errtag.js +34 -0
- package/src/agent/privatecore.js +790 -0
- package/src/agent/privatecore.wasm +0 -0
- package/src/agent/runner.js +10 -8
- package/src/agent/winexec.js +35 -5
- package/src/c2install.js +117 -0
- package/src/config.js +2 -1
- package/src/core/c2cardeffectbridge.js +51 -0
- package/src/core/c2cardterminalpath.js +216 -0
- package/src/core/c2durableingress.js +148 -0
- package/src/core/c2durablejournal.js +1560 -0
- package/src/core/c2durableruntime.js +2361 -0
- package/src/core/c2eventidentity.js +90 -0
- package/src/core/c2globalfault.js +401 -0
- package/src/core/c2localeffectexecutor.js +193 -0
- package/src/core/c2payloadhandles.js +333 -0
- package/src/core/c2persistrelation.js +179 -0
- package/src/core/c2persiststate.js +342 -0
- package/src/core/c2platformrecoverysender.js +76 -0
- package/src/core/c2recoverygate.js +385 -0
- package/src/core/c2runnerinput.js +63 -0
- package/src/core/c2servicestartgate.js +103 -0
- package/src/core/c2sessionbridge.js +72 -0
- package/src/core/c2sourcequarantine.js +30 -0
- package/src/core/c2startuprecovery.js +105 -0
- package/src/core/c2successorbridge.js +151 -0
- package/src/core/c2terminaleffectbridge.js +154 -0
- package/src/core/c2terminalprune.js +108 -0
- package/src/core/c2turnprojection.js +177 -0
- package/src/core/enginecheckpoint.js +211 -0
- package/src/core/engineeffectcursor.js +394 -0
- package/src/core/enginestepcoordinator.js +183 -0
- package/src/core/store.js +52 -8
- package/src/core/v2engine.js +1469 -0
- package/src/platform/feishu.js +324 -67
- package/src/plugins.js +1 -9
- package/assets/design-styles/acid-pop.md +0 -155
- package/assets/design-styles/archive-index.md +0 -161
- package/assets/design-styles/art-deco.md +0 -164
- package/assets/design-styles/cinematic-noir.md +0 -178
- package/assets/design-styles/editorial.md +0 -121
- package/assets/design-styles/extra/README.md +0 -23
- package/assets/design-styles/extra/constructivism.md +0 -146
- package/assets/design-styles/extra/neo-brutalism.md +0 -175
- package/assets/design-styles/extra/new-wave.md +0 -166
- package/assets/design-styles/extra/nordic-material.md +0 -162
- package/assets/design-styles/extra/paris-couture.md +0 -162
- package/assets/design-styles/extra/tropical-modern.md +0 -168
- package/assets/design-styles/extra/ulm-functional.md +0 -182
- package/assets/design-styles/extra/wa-shikimen.md +0 -160
- package/assets/design-styles/extra/y2k-chrome.md +0 -172
- package/assets/design-styles/geometric-pop.md +0 -122
- package/assets/design-styles/hand-drawn.md +0 -117
- package/assets/design-styles/islamic-geometry.md +0 -155
- package/assets/design-styles/liquid-glass.md +0 -183
- package/assets/design-styles/neo-chinese.md +0 -136
- package/assets/design-styles/precision-mono.md +0 -156
- package/assets/design-styles/quiet-luxury.md +0 -163
- package/assets/design-styles/riso-print.md +0 -116
- package/assets/design-styles/soft-friendly.md +0 -174
- package/assets/design-styles/swiss-grid.md +0 -156
- package/assets/design-styles/tech-dark.md +0 -182
- package/assets/favicon.svg +0 -6
- package/assets/logo.svg +0 -24
- package/bin/sbot.js +0 -4716
- package/docs/slack-app-manifest.yaml +0 -104
- package/sbot.config.example.json +0 -235
- package/src/agent/askonce.js +0 -76
- package/src/agent/claudemodels.js +0 -60
- package/src/agent/claudestatus.js +0 -74
- package/src/agent/claudeusage.js +0 -200
- package/src/agent/codexstatus.js +0 -181
- package/src/agent/imagen.js +0 -458
- package/src/agent/quotaerr.js +0 -44
- package/src/agent/transienterr.js +0 -32
- package/src/cdp.js +0 -254
- package/src/cli-secret.js +0 -36
- package/src/clilang.js +0 -31
- package/src/core/cron.js +0 -182
- package/src/core/cronfire.js +0 -120
- package/src/core/cronrun.js +0 -116
- package/src/core/engine.js +0 -2758
- package/src/core/interfaces.js +0 -114
- package/src/core/issues.js +0 -185
- package/src/core/ledger.js +0 -30
- package/src/core/opsalert.js +0 -214
- package/src/core/pending.js +0 -123
- package/src/core/proxy.js +0 -424
- package/src/core/quotaalert.js +0 -79
- package/src/core/quotaerr.js +0 -55
- package/src/core/scheduler.js +0 -59
- package/src/design.js +0 -5161
- package/src/gitrepo.js +0 -150
- package/src/health.js +0 -353
- package/src/installdeps.js +0 -72
- package/src/leftover.js +0 -174
- package/src/platform/boardnotice.js +0 -77
- package/src/platform/boardops.js +0 -36
- package/src/platform/boardwatch.js +0 -447
- package/src/platform/cli.js +0 -45
- package/src/platform/slack.js +0 -1289
- package/src/platform/takeovercard.js +0 -37
- package/src/platform/wecom.js +0 -938
- package/src/platform/wecomcli.js +0 -128
- package/src/platform/wecomgate.js +0 -108
- package/src/platform/wecomname.js +0 -43
- package/src/platform/wecompoll.js +0 -241
- package/src/preview.js +0 -581
- package/src/projectmemory.js +0 -175
- package/src/service.js +0 -317
- package/src/ui/auth.js +0 -31
- package/src/ui/browser.html +0 -392
- package/src/ui/browserroutes.js +0 -234
- package/src/ui/console.html +0 -5526
- package/src/ui/dashboard.js +0 -168
- package/src/ui/feishulogin.js +0 -136
- package/src/ui/server.js +0 -2314
- package/src/ui/sessions.js +0 -78
- package/src/ui/takeover.js +0 -108
- package/src/updater.js +0 -243
- package/src/upgrade.js +0 -35
- package/src/usage.js +0 -144
|
@@ -0,0 +1,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 }
|