sintbot 1.4.0 → 1.4.2
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 +38 -0
- package/CHANGELOG.json +38 -0
- package/bin/sintbot-v2.js +94 -16
- package/docs/AI-ASSISTED-INSTALL.md +2 -2
- package/docs/install.md +13 -5
- package/docs/zh-CN/AI-ASSISTED-INSTALL.md +2 -2
- package/docs/zh-CN/install.md +13 -5
- package/install.sh +1 -1
- package/package.json +5 -1
- package/src/agent/claudecode.js +7 -2
- package/src/agent/codexapp.js +7 -2
- package/src/agent/codexcode.js +8 -3
- package/src/c2configcontrol.js +13 -3
- package/src/c2console.js +15 -7
- package/src/c2incident.js +125 -0
- package/src/c2install.js +1 -1
- package/src/c2multigroupservice.js +157 -26
- package/src/c2updater.js +361 -0
- package/src/c2upgradeswitch.js +169 -0
- package/src/core/c2sourcequarantine.js +22 -3
- package/src/core/v2engine.js +23 -1
- package/src/ui/console.html +41 -18
- package/src/updater.js +243 -0
package/CHANGELOG.en.json
CHANGED
|
@@ -1,4 +1,42 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.4.2",
|
|
4
|
+
"date": "2026-11-09",
|
|
5
|
+
"name": "Check and install new versions from the console",
|
|
6
|
+
"items": [
|
|
7
|
+
{
|
|
8
|
+
"t": "Version checks in the console",
|
|
9
|
+
"d": "Software updates in Settings checks for stable releases at startup and daily and shows release notes when a new version is available."
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"t": "One-click upgrade for background runs",
|
|
13
|
+
"d": "When a launchd agent runs SintBot, Upgrade now waits until every group is idle and settled, installs the new version into its own folder and switches to it; if it does not connect every group within 90 seconds, the previous version is restored. New messages wait about a minute during the switch and are asked to be resent."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"t": "Steps for foreground runs",
|
|
17
|
+
"d": "A foreground run shows how to stop, reinstall and start. Upgrades are not announced in groups."
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"version": "1.4.1",
|
|
23
|
+
"date": "2026-11-09",
|
|
24
|
+
"name": "A failing group stops only itself",
|
|
25
|
+
"items": [
|
|
26
|
+
{
|
|
27
|
+
"t": "One group's fault leaves the others working",
|
|
28
|
+
"d": "When a group's last task has an outcome that cannot be confirmed (for example a reply whose delivery is unknown, or a runner that exited without a result), only that group is paused; nothing is replayed or retried and the other groups keep working. A paused group answers new messages with a short notice."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"t": "One group no longer blocks startup",
|
|
32
|
+
"d": "If a group's records cannot be recovered, the other groups still start and the console marks that group Needs attention. The startup preflight fails and lists such groups."
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"t": "Incident seal command",
|
|
36
|
+
"d": "Restart first; for a group that is still paused, stop the service and run sintbot incident seal --group <group ID>. The old records are kept unchanged, the group reopens with new records, and messages left unfinished before the seal are not processed again."
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
},
|
|
2
40
|
{
|
|
3
41
|
"version": "1.4.0",
|
|
4
42
|
"date": "2026-11-08",
|
package/CHANGELOG.json
CHANGED
|
@@ -1,4 +1,42 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.4.2",
|
|
4
|
+
"date": "2026-11-09",
|
|
5
|
+
"name": "在控制台查看和安装新版本",
|
|
6
|
+
"items": [
|
|
7
|
+
{
|
|
8
|
+
"t": "控制台检查新版本",
|
|
9
|
+
"d": "设置页的软件更新启动时和每天各检查一次正式版,有新版本时提示并附发布说明。"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"t": "后台运行时一键升级",
|
|
13
|
+
"d": "由 launchd 后台任务运行时,点「现在升级」会等所有群空闲、记录结清后,把新版本装到单独的目录再切换;新版本 90 秒内没连上全部群会自动换回旧版。切换期间约一分钟不处理新消息,会提示稍后重发。"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"t": "前台运行给出升级步骤",
|
|
17
|
+
"d": "前台运行时卡片说明如何停止、重新安装并启动。升级不在群里播报。"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"version": "1.4.1",
|
|
23
|
+
"date": "2026-11-09",
|
|
24
|
+
"name": "一个群出问题,只停这个群",
|
|
25
|
+
"items": [
|
|
26
|
+
{
|
|
27
|
+
"t": "一个群出问题,其他群照常",
|
|
28
|
+
"d": "某个群上一个任务的结果无法确认时(例如回复是否送达不明,或执行器退出时没有结果),只暂停这个群,不重放、不重试,其他群继续处理。暂停的群收到新消息会回一句说明。"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"t": "启动时不再被一个群拖住",
|
|
32
|
+
"d": "某个群的记录恢复不了时,其他群照常启动,控制台把这个群标为「需要处理」。启动预检遇到这样的群会直接失败并列出原因。"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"t": "新增封存命令",
|
|
36
|
+
"d": "先重启服务;仍然暂停的群,在服务停止后运行 sintbot incident seal --group 群 ID。原记录原样封存,这个群换一份新记录重新开放,封存前未完成的消息不会再处理。"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
},
|
|
2
40
|
{
|
|
3
41
|
"version": "1.4.0",
|
|
4
42
|
"date": "2026-11-08",
|
package/bin/sintbot-v2.js
CHANGED
|
@@ -112,6 +112,7 @@ async function startMultiGroupService({ installation, preflightOnly, uiEnabled =
|
|
|
112
112
|
let service
|
|
113
113
|
let ui
|
|
114
114
|
let configControl
|
|
115
|
+
let updater
|
|
115
116
|
let liveTransport
|
|
116
117
|
let onSigint, onSigterm
|
|
117
118
|
try {
|
|
@@ -122,11 +123,18 @@ async function startMultiGroupService({ installation, preflightOnly, uiEnabled =
|
|
|
122
123
|
for (const name of ['pending-turns.json', 'engine-receipts.json']) {
|
|
123
124
|
if (fs.existsSync(path.join(stateDir, name))) throw new Error('Legacy pending authority needs explicit migration')
|
|
124
125
|
}
|
|
126
|
+
// A journal that cannot be opened or audited holds only its own group.
|
|
125
127
|
const journals = new Map()
|
|
128
|
+
const holds = new Map()
|
|
126
129
|
for (const route of routes) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
try {
|
|
131
|
+
const journal = C2DurableJournal.open(route.file, validators)
|
|
132
|
+
assertJournalScope(journal, route.chatId, { requireSettled: preflightOnly || route.paused })
|
|
133
|
+
journals.set(route.chatId, journal)
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (!assertWriterLock()) throw error
|
|
136
|
+
holds.set(route.chatId, error?.message || String(error))
|
|
137
|
+
}
|
|
130
138
|
}
|
|
131
139
|
// All group Engines must share this exact object: separate Store caches
|
|
132
140
|
// would overwrite sibling sessions when they save the same state file.
|
|
@@ -136,6 +144,13 @@ async function startMultiGroupService({ installation, preflightOnly, uiEnabled =
|
|
|
136
144
|
'codex-exec': require('../src/agent/codexcode').isClosedRunnerResult,
|
|
137
145
|
'codex-app': require('../src/agent/codexapp').isClosedRunnerResult,
|
|
138
146
|
}
|
|
147
|
+
// A runner that failed from its own exit event has closed; this frees its
|
|
148
|
+
// model permit but never proves the turn's outcome.
|
|
149
|
+
const closeErrorChecks = {
|
|
150
|
+
claude: require('../src/agent/claudecode').isClosedRunnerError,
|
|
151
|
+
'codex-exec': require('../src/agent/codexcode').isClosedRunnerError,
|
|
152
|
+
'codex-app': require('../src/agent/codexapp').isClosedRunnerError,
|
|
153
|
+
}
|
|
139
154
|
const limits = { maxEventCount: 100_000, maxJournalBytes: 64 * 1024 * 1024,
|
|
140
155
|
maxCompletionEvents: 1_000, maxCompletionBytes: 8 * 1024 * 1024 }
|
|
141
156
|
let finishing = false
|
|
@@ -143,12 +158,15 @@ async function startMultiGroupService({ installation, preflightOnly, uiEnabled =
|
|
|
143
158
|
if (finishing) return
|
|
144
159
|
finishing = true
|
|
145
160
|
ui?.close()
|
|
161
|
+
updater?.close()
|
|
146
162
|
lock.release()
|
|
147
163
|
process.stderr.write(`C2 multi-group · ${result.reason}; platform stopped=${result.platformStopped}; runner drained=${result.drained}\n`,
|
|
148
164
|
() => process.exit(result.code))
|
|
149
165
|
}
|
|
150
166
|
service = createC2MultiGroupService({ routes, assertWriterLock, quarantine: combinedQuarantine,
|
|
151
|
-
closeCapacity: () => capacity.close(),
|
|
167
|
+
holds, closeCapacity: () => capacity.close(),
|
|
168
|
+
// Faults in a shared resource stop every group; others hold one group.
|
|
169
|
+
sharedFault: () => capacity.snapshot().closed ? 'model concurrency closed' : null,
|
|
152
170
|
log: message => process.stderr.write(`C2 multi-group · ${message}\n`),
|
|
153
171
|
onFatal: (error, result) => {
|
|
154
172
|
process.stderr.write(`C2 multi-group failure: ${error?.message || String(error)}\n`)
|
|
@@ -199,10 +217,15 @@ async function startMultiGroupService({ installation, preflightOnly, uiEnabled =
|
|
|
199
217
|
verifyFile: path.join(stateDir, 'verify.json'), assertWriterLock,
|
|
200
218
|
resolveTurn: identity => resolveC2LocalEffectTurn(engine, identity) })
|
|
201
219
|
const closedResults = new WeakSet()
|
|
220
|
+
const closedErrors = new WeakSet()
|
|
202
221
|
const startTurn = async input => {
|
|
203
222
|
const backend = routeBackend(input.provider?.runner, input.codexApp,
|
|
204
223
|
input.codexSteer, input.codexProjectDocFallback)
|
|
205
|
-
|
|
224
|
+
let result
|
|
225
|
+
try { result = await runner.runTurn(input) } catch (error) {
|
|
226
|
+
if (closeErrorChecks[backend]?.(error) === true) closedErrors.add(error)
|
|
227
|
+
throw error
|
|
228
|
+
}
|
|
206
229
|
if (closeChecks[backend]?.(result) === true) closedResults.add(result)
|
|
207
230
|
return result
|
|
208
231
|
}
|
|
@@ -210,14 +233,15 @@ async function startMultiGroupService({ installation, preflightOnly, uiEnabled =
|
|
|
210
233
|
store, log: message => process.stderr.write(`C2 v2 · ${message}\n`),
|
|
211
234
|
durableV2Runtime: runtime, durableV2Payloads: handles,
|
|
212
235
|
durableV2StartTurn: startTurn, durableV2ApplyLocalEffect: applyLocalEffect,
|
|
213
|
-
durableV2VerifyRunnerClosed: ({ result }) =>
|
|
214
|
-
typeof result === 'object' && closedResults.has(result)
|
|
236
|
+
durableV2VerifyRunnerClosed: ({ result, error }) => result
|
|
237
|
+
? typeof result === 'object' && closedResults.has(result)
|
|
238
|
+
: !!error && typeof error === 'object' && closedErrors.has(error),
|
|
215
239
|
durableV2AssertWriterLock: assertWriterLock, durableV2Journal: journal,
|
|
216
240
|
durableV2AcquireCapacity: options => capacity.acquire(options),
|
|
217
|
-
// A
|
|
218
|
-
//
|
|
241
|
+
// A route fault holds or recovers its group alone. Shared grants
|
|
242
|
+
// are sealed only when the process writer lease itself is gone.
|
|
219
243
|
durableV2SealCapacity: error => {
|
|
220
|
-
if (
|
|
244
|
+
if (assertWriterLock() !== true) capacity.close(error)
|
|
221
245
|
},
|
|
222
246
|
durableV2SwitchProvider: (chatId, name) => {
|
|
223
247
|
if (chatId !== route.chatId) throw new Error('Model switch targets another group')
|
|
@@ -231,9 +255,26 @@ async function startMultiGroupService({ installation, preflightOnly, uiEnabled =
|
|
|
231
255
|
} })
|
|
232
256
|
// One configuration writer for the console and the chat /model command.
|
|
233
257
|
configControl = require('../src/c2configcontrol').createC2ConfigControl({
|
|
234
|
-
installation, getBindings: () => service.bindings,
|
|
235
|
-
|
|
258
|
+
installation, getBindings: () => service.bindings, getHeld: () => service.held,
|
|
259
|
+
assertWriterLock, store,
|
|
260
|
+
activateRoute: route => service.addRoute(route), isFrozen: () => service.inMaintenance,
|
|
236
261
|
log: message => process.stderr.write(`C2 multi-group · ${message}\n`) })
|
|
262
|
+
if (!preflightOnly) {
|
|
263
|
+
// Console software updates. Only a launchd job that starts this exact
|
|
264
|
+
// entry may upgrade, and only while every group is idle and settled.
|
|
265
|
+
const c2updates = require('../src/c2updater')
|
|
266
|
+
const pkg = require('../package.json')
|
|
267
|
+
const upgradeGate = c2updates.createC2UpgradeGate({ isRunning: () => running,
|
|
268
|
+
service: () => service, capacity, assertJournalScope })
|
|
269
|
+
updater = c2updates.createC2Updater({ currentVersion: pkg.version,
|
|
270
|
+
runMode: c2updates.detectRunMode({ privateBuild: pkg.private === true, entry: __filename }),
|
|
271
|
+
stateFile: path.join(stateDir, 'update.json'), dataDir: stateDir, configPath: cfg._path,
|
|
272
|
+
gate: upgradeGate, beginMaintenance: () => service.beginMaintenance(),
|
|
273
|
+
endMaintenance: () => service.endMaintenance(), expectedGroups: () => service.bindings.size,
|
|
274
|
+
registries: c2updates.registriesFromEnv(),
|
|
275
|
+
fetchImpl: require('../src/core/proxy').getProxyFetch() || globalThis.fetch,
|
|
276
|
+
autoCheck: uiEnabled, log: message => process.stderr.write(`C2 update · ${message}\n`) })
|
|
277
|
+
}
|
|
237
278
|
const stop = reason => { void service.stop(reason, 0).then(finish) }
|
|
238
279
|
onSigint = () => stop('SIGINT')
|
|
239
280
|
onSigterm = () => stop('SIGTERM')
|
|
@@ -243,9 +284,9 @@ async function startMultiGroupService({ installation, preflightOnly, uiEnabled =
|
|
|
243
284
|
ui = require('../src/c2console').createC2Console({
|
|
244
285
|
configPath: cfg._path, stateDir, host: uiHost,
|
|
245
286
|
getConfig: () => cfg, getBindings: () => service.bindings,
|
|
246
|
-
getCapacity: () => capacity.snapshot(),
|
|
287
|
+
getHeld: () => service.held, getCapacity: () => capacity.snapshot(),
|
|
247
288
|
getConnHealth: () => liveTransport?.platform.connHealth?.() || {},
|
|
248
|
-
assertWriterLock, ...configControl,
|
|
289
|
+
assertWriterLock, ...configControl, updater,
|
|
249
290
|
log: message => process.stderr.write(`Console · ${message}\n`) })
|
|
250
291
|
await new Promise((resolve, reject) => {
|
|
251
292
|
ui.server.once('error', reject)
|
|
@@ -261,6 +302,13 @@ async function startMultiGroupService({ installation, preflightOnly, uiEnabled =
|
|
|
261
302
|
process.on('exit', lock.release)
|
|
262
303
|
running = true
|
|
263
304
|
process.stderr.write(`SintBot Feishu ingress connected for ${started.groups} groups\n`)
|
|
305
|
+
for (const item of started.held || []) {
|
|
306
|
+
process.stderr.write(`SintBot group ${item.chatId} is held: ${item.reason}\n`)
|
|
307
|
+
}
|
|
308
|
+
// A console upgrade that switched to this version is done once every
|
|
309
|
+
// group is connected; then the daily version check starts.
|
|
310
|
+
if (!started.held?.length) updater?.confirmStarted()
|
|
311
|
+
void updater?.start()
|
|
264
312
|
} catch (error) {
|
|
265
313
|
ui?.close()
|
|
266
314
|
const stopped = await service?.stop('startup-failed', 1)
|
|
@@ -271,6 +319,7 @@ async function startMultiGroupService({ installation, preflightOnly, uiEnabled =
|
|
|
271
319
|
throw error
|
|
272
320
|
} finally {
|
|
273
321
|
if (!running) {
|
|
322
|
+
updater?.close()
|
|
274
323
|
if (onSigint) process.off('SIGINT', onSigint)
|
|
275
324
|
if (onSigterm) process.off('SIGTERM', onSigterm)
|
|
276
325
|
capacity.close(); lock.release()
|
|
@@ -303,6 +352,7 @@ const HELP = `SintBot macOS Feishu multi-group candidate
|
|
|
303
352
|
Usage:
|
|
304
353
|
sintbot init [--config <absolute path>] [--data-dir <absolute path>]
|
|
305
354
|
sintbot start [--config <absolute path>] [--data-dir <absolute path>] [--preflight] [--ui] [--ui-host <address>] [--ui-port <port>] [--no-open]
|
|
355
|
+
sintbot incident seal --group <oc_…> [--config <absolute path>] [--data-dir <absolute path>] [--reason <text>]
|
|
306
356
|
sintbot version
|
|
307
357
|
sintbot help
|
|
308
358
|
|
|
@@ -311,7 +361,10 @@ Initialization is for a new installation only; existing journals are never reset
|
|
|
311
361
|
Console shows live groups, binds groups and switches models or projects of idle groups;
|
|
312
362
|
in a group, /model lists or switches models. Unsupported actions stay disabled.
|
|
313
363
|
Canvas, scheduled tasks, terminal chat and other platforms are unavailable.
|
|
314
|
-
|
|
364
|
+
A group whose last result cannot be proven is held alone; the other groups keep serving.
|
|
365
|
+
Restart first: a journal that can recover reopens on its own. If the group is still held,
|
|
366
|
+
stop the service, run incident seal for that group, then start again.
|
|
367
|
+
Sealing keeps the old journal unchanged; unknown effects are never replayed.
|
|
315
368
|
`
|
|
316
369
|
|
|
317
370
|
async function main(argv = process.argv.slice(2)) {
|
|
@@ -326,10 +379,35 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
326
379
|
process.stdout.write(HELP)
|
|
327
380
|
return
|
|
328
381
|
}
|
|
329
|
-
if (
|
|
382
|
+
if (sub === 'upgrade-switch') {
|
|
383
|
+
// Internal: started detached by a console upgrade; never run it by hand.
|
|
384
|
+
const { values } = parseArgs({ args: rest, allowPositionals: false, options: { state: { type: 'string' } } })
|
|
385
|
+
if (!values.state || !path.isAbsolute(values.state)) throw new Error('upgrade-switch needs --state <absolute path>')
|
|
386
|
+
const outcome = await require('../src/c2upgradeswitch').runUpgradeSwitch({ stateFile: values.state })
|
|
387
|
+
process.exitCode = outcome === 'done' ? 0 : 1
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
if (!['init', 'start', 'incident'].includes(sub)) {
|
|
330
391
|
throw new Error(`${sub} is unavailable in this release; use sintbot help`)
|
|
331
392
|
}
|
|
332
393
|
if (process.platform !== 'darwin') throw new Error('this release supports macOS only')
|
|
394
|
+
if (sub === 'incident') {
|
|
395
|
+
const [action, ...args] = rest
|
|
396
|
+
if (rest.some(arg => arg === '--help' || arg === '-h')) { process.stdout.write(HELP); return }
|
|
397
|
+
if (action !== 'seal') throw new Error('Usage: sintbot incident seal --group <oc_…> [--config <path>] [--data-dir <path>]')
|
|
398
|
+
const { values } = parseArgs({ args, allowPositionals: false, options: {
|
|
399
|
+
config: { type: 'string' }, 'data-dir': { type: 'string' },
|
|
400
|
+
group: { type: 'string' }, reason: { type: 'string' } } })
|
|
401
|
+
const installation = require('../src/c2install').loadInstallation(values.config,
|
|
402
|
+
{ dataDir: values['data-dir'], allowAddedGroups: true })
|
|
403
|
+
const result = require('../src/c2incident').sealC2GroupIncident({ installation,
|
|
404
|
+
chatId: values.group, ...(values.reason !== undefined ? { reason: values.reason } : {}),
|
|
405
|
+
version: require('../package.json').version })
|
|
406
|
+
process.stdout.write(`Sealed the group's previous journal unchanged in ${result.incident}.\n` +
|
|
407
|
+
`${result.refusedSources} unsettled message(s) will not be processed again. ` +
|
|
408
|
+
'Start the service to reopen the group with a new journal.\n')
|
|
409
|
+
return
|
|
410
|
+
}
|
|
333
411
|
if (sub === 'init') {
|
|
334
412
|
const { values } = parseArgs({ args: rest, allowPositionals: false,
|
|
335
413
|
options: { config: { type: 'string' }, 'data-dir': { type: 'string' } } })
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Install SintBot 1.4.
|
|
1
|
+
# Install SintBot 1.4.2 with an AI assistant
|
|
2
2
|
|
|
3
3
|
English · [简体中文](./zh-CN/AI-ASSISTED-INSTALL.md)
|
|
4
4
|
|
|
@@ -15,5 +15,5 @@ Use the terminal on the Mac that will run SintBot. A cloud shell cannot install
|
|
|
15
15
|
## Copyable request
|
|
16
16
|
|
|
17
17
|
```text
|
|
18
|
-
Install SintBot 1.4.
|
|
18
|
+
Install SintBot 1.4.2 on this Mac using the one-line installation guide. Preserve existing services, configuration, state and Codex or Claude Code account data. Do not expose credentials. Verify the actual installed executable and binary core, configure the Feishu groups with their runners, and initialize only clean new state. Run the foreground preflight and an authorized read-only group test, check the result, then verify clean stop/restart. Stop temporary processes when finished, and distinguish package installation from successful group acceptance.
|
|
19
19
|
```
|
package/docs/install.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Install SintBot 1.4.
|
|
1
|
+
# Install SintBot 1.4.2
|
|
2
2
|
|
|
3
3
|
English · [简体中文](./zh-CN/install.md)
|
|
4
4
|
|
|
@@ -10,7 +10,7 @@ You need a Mac, Node.js 22 or newer, and projects stored on that Mac. Install an
|
|
|
10
10
|
curl -fsSL https://sintbot.com/install.sh | bash
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
The installer checks the environment, installs `sintbot@1.4.
|
|
13
|
+
The installer checks the environment, installs `sintbot@1.4.2` into `~/.local/share/sintbot`, and verifies the entry, built-in core and Feishu module. Continue when you see `Installed and verified SintBot 1.4.2`. No sudo is needed.
|
|
14
14
|
|
|
15
15
|
To preview, append `-s -- --dry-run`. To choose another location, set `SINTBOT_INSTALL_DIR` and adjust the executable paths below.
|
|
16
16
|
|
|
@@ -98,11 +98,13 @@ Any text message is a task. Replies arrive in a card that appears once the model
|
|
|
98
98
|
| `/hud` | Quota of the current model and usage of this conversation |
|
|
99
99
|
| `/issue text` | Log feedback; review and close it in the console overview |
|
|
100
100
|
|
|
101
|
-
Messages sent while a task runs are added to that task in Codex groups (`codexSteer: true`) and queued in Claude Code groups. If a turn returns a model error or no result, only that group is asked to resend; there is no automatic retry and other groups
|
|
101
|
+
Messages sent while a task runs are added to that task in Codex groups (`codexSteer: true`) and queued in Claude Code groups. If a turn returns a model error or no result, only that group is asked to resend; there is no automatic retry. If a group's last result cannot be confirmed, such as a reply whose delivery is unknown, only that group is paused and answers new messages with a short notice; other groups keep working. Nothing is replayed or retried.
|
|
102
102
|
|
|
103
103
|
## 6. Console
|
|
104
104
|
|
|
105
|
-
With `--ui`, open `http://127.0.0.1:4600` on this Mac. The console shows each group, stops tasks, binds new groups, and switches the model or project folder of an idle group. For LAN access, set a console password in its settings first, then enable LAN access.
|
|
105
|
+
With `--ui`, open `http://127.0.0.1:4600` on this Mac. The console shows each group (a paused group shows Needs attention), stops tasks, binds new groups, and switches the model or project folder of an idle group. For LAN access, set a console password in its settings first, then enable LAN access.
|
|
106
|
+
|
|
107
|
+
Settings → Software updates checks for new releases. In the foreground setup above it shows the upgrade steps: press Ctrl-C and wait for exit, rerun the one-line installer, then start again with the same command. If your own launchd agent starts SintBot, Upgrade now waits until every group is idle and settled, installs the new version into `~/.local/share/sintbot/releases/<version>` and switches the agent to it; if the new version does not connect every group within 90 seconds, the previous one is restored. Upgrades are not announced in groups.
|
|
106
108
|
|
|
107
109
|
You can also add a group without the console: add it to the configuration, stop the service and run `start` again; the new group is registered automatically. Bound groups cannot be removed or renamed by editing the configuration.
|
|
108
110
|
|
|
@@ -114,6 +116,12 @@ Press Ctrl-C and wait for exit. With the Feishu variables set, run:
|
|
|
114
116
|
"$HOME/.local/share/sintbot/bin/sintbot" start --config "$HOME/.sbot/config.json" --ui
|
|
115
117
|
```
|
|
116
118
|
|
|
117
|
-
If
|
|
119
|
+
If a group stays paused, first restart: a group whose records can recover reopens on its own and is told once to resend. If it is still paused, check what actually happened in that group, then stop the service and seal the group's incident:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
"$HOME/.local/share/sintbot/bin/sintbot" incident seal --config "$HOME/.sbot/config.json" --group oc_PAUSED_GROUP_ID
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Sealing keeps the old journal unchanged under `incidents/` in the data directory and gives the group a new journal; messages left unfinished before the seal are never processed again. Start the service again to reopen the group. A preflight fails while any group would stay paused. Never delete journals or resend to get around a paused group. Run only one connected process per Feishu app. This release does not register a background service.
|
|
118
126
|
|
|
119
127
|
[Illustrated website guide](https://sintbot.com/guide/install/) · [AI-assisted installation](./AI-ASSISTED-INSTALL.md)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# 用 AI 助手安装 SintBot 1.4.
|
|
1
|
+
# 用 AI 助手安装 SintBot 1.4.2
|
|
2
2
|
|
|
3
3
|
[English](../AI-ASSISTED-INSTALL.md) · 简体中文
|
|
4
4
|
|
|
@@ -15,5 +15,5 @@
|
|
|
15
15
|
## 可复制的请求
|
|
16
16
|
|
|
17
17
|
```text
|
|
18
|
-
按一行安装指南,在这台 Mac 安装 SintBot 1.4.
|
|
18
|
+
按一行安装指南,在这台 Mac 安装 SintBot 1.4.2。保留已有服务、配置、状态和 Codex 或 Claude Code 账号数据,不输出凭据。验证实际入口与二进制核心,配置飞书群及各群的执行器,只对全新状态初始化。运行前台预检和已授权的只读群测试,对照结果,再验证正常停止与重启。完成后停止临时进程,并区分程序包安装与群验收结果。
|
|
19
19
|
```
|
package/docs/zh-CN/install.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# 安装 SintBot 1.4.
|
|
1
|
+
# 安装 SintBot 1.4.2
|
|
2
2
|
|
|
3
3
|
[English](../install.md) · 简体中文
|
|
4
4
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
curl -fsSL https://sintbot.com/install.sh | bash
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
安装器会自动检查环境,把 `sintbot@1.4.
|
|
13
|
+
安装器会自动检查环境,把 `sintbot@1.4.2` 装到 `~/.local/share/sintbot`,并验证程序入口、内置核心和飞书模块。看到 `Installed and verified SintBot 1.4.2` 就可以继续。不需要 sudo。
|
|
14
14
|
|
|
15
15
|
可选预览:在命令末尾加 `-s -- --dry-run`。要自定义安装位置,设置 `SINTBOT_INSTALL_DIR` 环境变量,并同步修改后面的可执行文件路径。
|
|
16
16
|
|
|
@@ -98,11 +98,13 @@ SINTBOT="$HOME/.local/share/sintbot/bin/sintbot"
|
|
|
98
98
|
| `/hud` | 当前模型的额度与本段对话的用量 |
|
|
99
99
|
| `/issue 一句话` | 记下问题反馈,在控制台概览里查看和标记处理 |
|
|
100
100
|
|
|
101
|
-
任务进行中再发的消息:Codex 群(`codexSteer: true`)会补充进当前任务,Claude Code
|
|
101
|
+
任务进行中再发的消息:Codex 群(`codexSteer: true`)会补充进当前任务,Claude Code 群会排队,等这一轮结束再处理。某一轮模型报错或没有结果时,只在本群提示重新发送,不会自动重试。某个群上一轮的结果无法确认时(例如回复是否送达不明),只暂停这个群,它收到新消息会回一句说明,其他群照常;不重放、不重试。
|
|
102
102
|
|
|
103
103
|
## 6. 控制台
|
|
104
104
|
|
|
105
|
-
带 `--ui` 启动后,在本机浏览器打开 `http://127.0.0.1:4600
|
|
105
|
+
带 `--ui` 启动后,在本机浏览器打开 `http://127.0.0.1:4600`。控制台可以查看各群状态(暂停的群显示「需要处理」)、停止任务、绑定新群、给空闲的群切换模型或项目目录。要从局域网访问,先在控制台的设置里设访问密码,再开启局域网访问。
|
|
106
|
+
|
|
107
|
+
「设置 → 软件更新」会检查新版本。按上面前台方式运行时,卡片给出升级步骤:按 Ctrl-C 等待退出,重新运行一行安装命令,再用原来的命令启动。如果用你自己的 launchd 后台任务启动 SintBot,「现在升级」会等所有群空闲、记录结清后,把新版本装到 `~/.local/share/sintbot/releases/<版本>` 并切换过去;新版本 90 秒内没连上全部群会自动换回旧版。升级不在群里播报。
|
|
106
108
|
|
|
107
109
|
不用控制台也能加群:在配置里加上新群,停止后重新 `start` 即可,新群会自动登记。已绑定的群不能通过改配置删除或改名。
|
|
108
110
|
|
|
@@ -114,6 +116,12 @@ SINTBOT="$HOME/.local/share/sintbot/bin/sintbot"
|
|
|
114
116
|
"$HOME/.local/share/sintbot/bin/sintbot" start --config "$HOME/.sbot/config.json" --ui
|
|
115
117
|
```
|
|
116
118
|
|
|
117
|
-
|
|
119
|
+
某个群一直暂停时,先重启:记录能恢复的群会自己重新开放,并提示一次重新发送。重启后仍暂停,先核对这个群实际发生了什么,再停止服务,封存这个群的事故记录:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
"$HOME/.local/share/sintbot/bin/sintbot" incident seal --config "$HOME/.sbot/config.json" --group oc_暂停的群ID
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
封存会把原日志原样留在数据目录的 `incidents/` 下,并给这个群换一份新日志;封存前没处理完的消息不会再处理。再次启动服务,这个群就重新开放。有群会保持暂停时,预检会失败。不要通过删除日志或重复发送来绕过暂停。一个飞书应用同一时间只运行一个连接进程。本版不自动注册后台服务。
|
|
118
126
|
|
|
119
127
|
[带图示的官网指南](https://sintbot.com/zh/guide/install/) · [AI 辅助安装](./AI-ASSISTED-INSTALL.md)
|
package/install.sh
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# The function is parsed in full before installation starts, including when piped.
|
|
3
3
|
sintbot_install() (
|
|
4
4
|
set -eu
|
|
5
|
-
version=1.4.
|
|
5
|
+
version=1.4.2
|
|
6
6
|
prefix="${SINTBOT_INSTALL_DIR:-$HOME/.local/share/sintbot}"
|
|
7
7
|
fail() { printf 'SintBot: %s\n' "$*" >&2; exit 1; }
|
|
8
8
|
case "${1:-}" in
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sintbot",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Drive your Mac projects from Feishu groups with Codex or Claude Code, using a built-in agent core.",
|
|
6
6
|
"bin": {
|
|
@@ -58,10 +58,13 @@
|
|
|
58
58
|
"src/agent/winexec.js",
|
|
59
59
|
"src/c2configcontrol.js",
|
|
60
60
|
"src/c2console.js",
|
|
61
|
+
"src/c2incident.js",
|
|
61
62
|
"src/c2install.js",
|
|
62
63
|
"src/c2migration.js",
|
|
63
64
|
"src/c2multigroupcapacity.js",
|
|
64
65
|
"src/c2multigroupservice.js",
|
|
66
|
+
"src/c2updater.js",
|
|
67
|
+
"src/c2upgradeswitch.js",
|
|
65
68
|
"src/cdp.js",
|
|
66
69
|
"src/config.js",
|
|
67
70
|
"src/core/c2cardeffectbridge.js",
|
|
@@ -128,6 +131,7 @@
|
|
|
128
131
|
"src/ui/serverbase.js",
|
|
129
132
|
"src/ui/sessions.js",
|
|
130
133
|
"src/ui/takeover.js",
|
|
134
|
+
"src/updater.js",
|
|
131
135
|
"src/usage.js"
|
|
132
136
|
],
|
|
133
137
|
"scripts": {
|
package/src/agent/claudecode.js
CHANGED
|
@@ -29,6 +29,11 @@ const { IS_WIN, resolveSpawn } = require('./winexec')
|
|
|
29
29
|
const { tagError } = require('./errtag') // 出错原话 → quota / sessionMissing / transient 之一(优先级只在那一处定),与 codex runner 共用
|
|
30
30
|
const closedRunnerResults = new WeakSet()
|
|
31
31
|
const isClosedRunnerResult = result => !!result && typeof result === 'object' && closedRunnerResults.has(result)
|
|
32
|
+
// A rejection raised from the child's own 'close' event: the process has
|
|
33
|
+
// exited even though the turn has no result. It never proves the turn's outcome.
|
|
34
|
+
const closedRunnerErrors = new WeakSet()
|
|
35
|
+
const isClosedRunnerError = error => !!error && typeof error === 'object' && closedRunnerErrors.has(error)
|
|
36
|
+
const closedError = error => { closedRunnerErrors.add(error); return error }
|
|
32
37
|
const PRE_INIT_HOOK_EVENTS = new Set(['hook_started', 'hook_response'])
|
|
33
38
|
const DURABLE_EVENT_TYPES = new Set(['system', 'assistant', 'user', 'result', 'tool_result'])
|
|
34
39
|
// Tool output is completion evidence only, never answer text.
|
|
@@ -451,7 +456,7 @@ function runTurn({
|
|
|
451
456
|
|
|
452
457
|
const completeClose = (code) => finish(() => {
|
|
453
458
|
if (code !== 0 && !lastResult && !aborted) {
|
|
454
|
-
return reject(new Error(`claude exited ${code}: ${stderr.trim().slice(0, 800) || '(no stderr)'}`))
|
|
459
|
+
return reject(closedError(new Error(`claude exited ${code}: ${stderr.trim().slice(0, 800) || '(no stderr)'}`)))
|
|
455
460
|
}
|
|
456
461
|
resolveRun({ sessionId: capturedSessionId, result: lastResult, code, aborted })
|
|
457
462
|
})
|
|
@@ -524,4 +529,4 @@ function probeVersion(bin = 'claude') {
|
|
|
524
529
|
})
|
|
525
530
|
}
|
|
526
531
|
|
|
527
|
-
module.exports = { runTurn, isClosedRunnerResult, buildEnv, normalizeEvent, probeVersion, supportsFlag, BASE_SKILL_ALLOWS }
|
|
532
|
+
module.exports = { runTurn, isClosedRunnerResult, isClosedRunnerError, buildEnv, normalizeEvent, probeVersion, supportsFlag, BASE_SKILL_ALLOWS }
|
package/src/agent/codexapp.js
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
const { spawn } = require('node:child_process')
|
|
5
5
|
const closedRunnerResults = new WeakSet()
|
|
6
6
|
const isClosedRunnerResult = result => !!result && typeof result === 'object' && closedRunnerResults.has(result)
|
|
7
|
+
// A rejection raised from the child's own 'close' event: the process has
|
|
8
|
+
// exited even though the turn has no result. It never proves the turn's outcome.
|
|
9
|
+
const closedRunnerErrors = new WeakSet()
|
|
10
|
+
const isClosedRunnerError = error => !!error && typeof error === 'object' && closedRunnerErrors.has(error)
|
|
11
|
+
const closedError = error => { closedRunnerErrors.add(error); return error }
|
|
7
12
|
const { IS_WIN, resolveSpawn } = require('./winexec')
|
|
8
13
|
const { normalizeEvent, isEffortRejection } = require('./codexcode')
|
|
9
14
|
const { coreFailedResult, isCoreFailure } = require('./errtag')
|
|
@@ -439,7 +444,7 @@ function runTurn(opts = {}) {
|
|
|
439
444
|
})
|
|
440
445
|
const completeClose = (code) => settle(() => {
|
|
441
446
|
if (onDurableEvent && !completed && !aborted) {
|
|
442
|
-
reject(new Error(`codex app-server exited ${code} before a durable result`))
|
|
447
|
+
reject(closedError(new Error(`codex app-server exited ${code} before a durable result`)))
|
|
443
448
|
return
|
|
444
449
|
}
|
|
445
450
|
if (!completed && !aborted) {
|
|
@@ -467,4 +472,4 @@ function runTurn(opts = {}) {
|
|
|
467
472
|
})
|
|
468
473
|
}
|
|
469
474
|
|
|
470
|
-
module.exports = { runTurn, isClosedRunnerResult, usageFromTotal, itemEvent, failedTurnEvent }
|
|
475
|
+
module.exports = { runTurn, isClosedRunnerResult, isClosedRunnerError, usageFromTotal, itemEvent, failedTurnEvent }
|
package/src/agent/codexcode.js
CHANGED
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
const { spawn } = require('node:child_process')
|
|
14
14
|
const closedRunnerResults = new WeakSet()
|
|
15
15
|
const isClosedRunnerResult = result => !!result && typeof result === 'object' && closedRunnerResults.has(result)
|
|
16
|
+
// A rejection raised from the child's own 'close' event: the process has
|
|
17
|
+
// exited even though the turn has no result. It never proves the turn's outcome.
|
|
18
|
+
const closedRunnerErrors = new WeakSet()
|
|
19
|
+
const isClosedRunnerError = error => !!error && typeof error === 'object' && closedRunnerErrors.has(error)
|
|
20
|
+
const closedError = error => { closedRunnerErrors.add(error); return error }
|
|
16
21
|
const { StringDecoder } = require('node:string_decoder')
|
|
17
22
|
const { IS_WIN, resolveSpawn } = require('./winexec')
|
|
18
23
|
const { tagError } = require('./errtag') // 出错原话 → quota / sessionMissing / transient 之一(优先级只在那一处定),与 claude runner 共用
|
|
@@ -398,11 +403,11 @@ function runOnce(opts, effort) {
|
|
|
398
403
|
|
|
399
404
|
const completeClose = (code) => {
|
|
400
405
|
if (onDurableEvent && code !== 0 && !lastResult && !aborted) {
|
|
401
|
-
done(() => reject(new Error(`codex exited ${code} before a durable result`)))
|
|
406
|
+
done(() => reject(closedError(new Error(`codex exited ${code} before a durable result`))))
|
|
402
407
|
return
|
|
403
408
|
}
|
|
404
409
|
if (durableStart && !acceptedStart && !aborted && !acceptanceFailed) {
|
|
405
|
-
done(() => reject(new Error('Codex exited before turn.started acceptance')))
|
|
410
|
+
done(() => reject(closedError(new Error('Codex exited before turn.started acceptance'))))
|
|
406
411
|
return
|
|
407
412
|
}
|
|
408
413
|
// Watch the answer→close (rollout-flush) lag. Healthy = ~2–3s (real-machine verified),
|
|
@@ -450,4 +455,4 @@ function probeVersion(bin = 'codex') {
|
|
|
450
455
|
})
|
|
451
456
|
}
|
|
452
457
|
|
|
453
|
-
module.exports = { runTurn, runOnce, isClosedRunnerResult, probeVersion, normalizeEvent, parseCodexError, isEffortRejection, sandboxMode, buildArgs, buildPrompt }
|
|
458
|
+
module.exports = { runTurn, runOnce, isClosedRunnerResult, isClosedRunnerError, probeVersion, normalizeEvent, parseCodexError, isEffortRejection, sandboxMode, buildArgs, buildPrompt }
|
package/src/c2configcontrol.js
CHANGED
|
@@ -17,10 +17,11 @@ const RESET_KEYS = ['provider', 'model', 'workdir', 'codexApp']
|
|
|
17
17
|
// model, project or backend clears its session before the new configuration
|
|
18
18
|
// is written, so no later turn can resume a session from another backend.
|
|
19
19
|
// It never moves a journal, settles a receipt or reconnects the platform.
|
|
20
|
-
function createC2ConfigControl({ installation, getBindings,
|
|
21
|
-
store = null, activateRoute = null, probeRunner = assertRunnerExecutable,
|
|
22
|
-
log = () => {} } = {}) {
|
|
20
|
+
function createC2ConfigControl({ installation, getBindings, getHeld = () => new Map(),
|
|
21
|
+
assertWriterLock, store = null, activateRoute = null, probeRunner = assertRunnerExecutable,
|
|
22
|
+
isFrozen = () => false, log = () => {} } = {}) {
|
|
23
23
|
if (!installation?.cfg?._path || typeof getBindings !== 'function' ||
|
|
24
|
+
typeof getHeld !== 'function' || typeof isFrozen !== 'function' ||
|
|
24
25
|
typeof assertWriterLock !== 'function' ||
|
|
25
26
|
store !== null && typeof store.resetGroupSession !== 'function' ||
|
|
26
27
|
activateRoute !== null && typeof activateRoute !== 'function' ||
|
|
@@ -46,6 +47,7 @@ function createC2ConfigControl({ installation, getBindings, assertWriterLock,
|
|
|
46
47
|
function validateConfig(raw) {
|
|
47
48
|
prepared = null
|
|
48
49
|
held()
|
|
50
|
+
if (isFrozen()) throw new Error('SintBot is upgrading; settings cannot change until it restarts')
|
|
49
51
|
const next = loadInstallationFromRaw(raw, cfg._path, { dataDir: stateDir,
|
|
50
52
|
allowAddedGroups: !!activateRoute && !!store })
|
|
51
53
|
const bindings = getBindings()
|
|
@@ -72,11 +74,19 @@ function createC2ConfigControl({ installation, getBindings, assertWriterLock,
|
|
|
72
74
|
throw new Error('Changing global concurrency requires a service restart')
|
|
73
75
|
}
|
|
74
76
|
const resets = [], activations = [], changed = []
|
|
77
|
+
const heldGroups = getHeld()
|
|
75
78
|
for (const route of routes) {
|
|
76
79
|
const target = next.routes.find(item => item.chatId === route.chatId)
|
|
77
80
|
if (route.group.platform !== target.group.platform) {
|
|
78
81
|
throw new Error('Moving a group to another platform requires an explicit state migration')
|
|
79
82
|
}
|
|
83
|
+
if (heldGroups.has(route.chatId)) {
|
|
84
|
+
// Edits elsewhere proceed; the held group keeps its exact settings.
|
|
85
|
+
if (!isDeepStrictEqual(route.group, target.group)) {
|
|
86
|
+
throw new Error('This group is held after an unresolved fault; seal its incident and restart before changing it')
|
|
87
|
+
}
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
80
90
|
if (!isDeepStrictEqual(route.group, target.group)) changed.push(route.chatId)
|
|
81
91
|
if (RESET_KEYS.some(key => !isDeepStrictEqual(route.group[key], target.group[key]))) {
|
|
82
92
|
if (!store) throw new Error('Changing model or project requires session reset authority')
|