sintbot 1.0.15 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.json +118 -0
- package/CHANGELOG.json +118 -0
- package/README.md +20 -156
- package/README.zh-CN.md +20 -164
- package/THIRD-PARTY-NOTICES.md +394 -0
- package/bin/sintbot-v2.js +363 -0
- package/docs/AI-ASSISTED-INSTALL.md +19 -0
- package/docs/install.md +93 -0
- package/docs/zh-CN/AI-ASSISTED-INSTALL.md +19 -0
- package/docs/zh-CN/install.md +93 -0
- package/install.sh +84 -0
- package/package.json +74 -34
- package/src/agent/claudecode.js +149 -108
- package/src/agent/codexapp.js +458 -0
- package/src/agent/codexcode.js +210 -107
- package/src/agent/errtag.js +34 -0
- package/src/agent/privatecore.js +790 -0
- package/src/agent/privatecore.wasm +0 -0
- package/src/agent/runner.js +10 -8
- package/src/agent/winexec.js +35 -5
- package/src/c2install.js +117 -0
- package/src/config.js +2 -1
- package/src/core/c2cardeffectbridge.js +51 -0
- package/src/core/c2cardterminalpath.js +216 -0
- package/src/core/c2durableingress.js +148 -0
- package/src/core/c2durablejournal.js +1560 -0
- package/src/core/c2durableruntime.js +2361 -0
- package/src/core/c2eventidentity.js +90 -0
- package/src/core/c2globalfault.js +401 -0
- package/src/core/c2localeffectexecutor.js +193 -0
- package/src/core/c2payloadhandles.js +333 -0
- package/src/core/c2persistrelation.js +179 -0
- package/src/core/c2persiststate.js +342 -0
- package/src/core/c2platformrecoverysender.js +76 -0
- package/src/core/c2recoverygate.js +385 -0
- package/src/core/c2runnerinput.js +63 -0
- package/src/core/c2servicestartgate.js +103 -0
- package/src/core/c2sessionbridge.js +72 -0
- package/src/core/c2sourcequarantine.js +30 -0
- package/src/core/c2startuprecovery.js +105 -0
- package/src/core/c2successorbridge.js +151 -0
- package/src/core/c2terminaleffectbridge.js +154 -0
- package/src/core/c2terminalprune.js +108 -0
- package/src/core/c2turnprojection.js +177 -0
- package/src/core/enginecheckpoint.js +211 -0
- package/src/core/engineeffectcursor.js +394 -0
- package/src/core/enginestepcoordinator.js +183 -0
- package/src/core/store.js +52 -8
- package/src/core/v2engine.js +1469 -0
- package/src/platform/feishu.js +324 -67
- package/src/plugins.js +1 -9
- package/assets/design-styles/acid-pop.md +0 -155
- package/assets/design-styles/archive-index.md +0 -161
- package/assets/design-styles/art-deco.md +0 -164
- package/assets/design-styles/cinematic-noir.md +0 -178
- package/assets/design-styles/editorial.md +0 -121
- package/assets/design-styles/extra/README.md +0 -23
- package/assets/design-styles/extra/constructivism.md +0 -146
- package/assets/design-styles/extra/neo-brutalism.md +0 -175
- package/assets/design-styles/extra/new-wave.md +0 -166
- package/assets/design-styles/extra/nordic-material.md +0 -162
- package/assets/design-styles/extra/paris-couture.md +0 -162
- package/assets/design-styles/extra/tropical-modern.md +0 -168
- package/assets/design-styles/extra/ulm-functional.md +0 -182
- package/assets/design-styles/extra/wa-shikimen.md +0 -160
- package/assets/design-styles/extra/y2k-chrome.md +0 -172
- package/assets/design-styles/geometric-pop.md +0 -122
- package/assets/design-styles/hand-drawn.md +0 -117
- package/assets/design-styles/islamic-geometry.md +0 -155
- package/assets/design-styles/liquid-glass.md +0 -183
- package/assets/design-styles/neo-chinese.md +0 -136
- package/assets/design-styles/precision-mono.md +0 -156
- package/assets/design-styles/quiet-luxury.md +0 -163
- package/assets/design-styles/riso-print.md +0 -116
- package/assets/design-styles/soft-friendly.md +0 -174
- package/assets/design-styles/swiss-grid.md +0 -156
- package/assets/design-styles/tech-dark.md +0 -182
- package/assets/favicon.svg +0 -6
- package/assets/logo.svg +0 -24
- package/bin/sbot.js +0 -4711
- package/docs/slack-app-manifest.yaml +0 -104
- package/sbot.config.example.json +0 -235
- package/src/agent/askonce.js +0 -76
- package/src/agent/claudemodels.js +0 -60
- package/src/agent/claudestatus.js +0 -74
- package/src/agent/claudeusage.js +0 -200
- package/src/agent/codexstatus.js +0 -181
- package/src/agent/imagen.js +0 -458
- package/src/agent/quotaerr.js +0 -44
- package/src/agent/transienterr.js +0 -32
- package/src/cdp.js +0 -254
- package/src/clilang.js +0 -31
- package/src/core/cron.js +0 -182
- package/src/core/cronfire.js +0 -120
- package/src/core/cronrun.js +0 -116
- package/src/core/engine.js +0 -2762
- package/src/core/interfaces.js +0 -114
- package/src/core/issues.js +0 -185
- package/src/core/ledger.js +0 -30
- package/src/core/opsalert.js +0 -214
- package/src/core/pending.js +0 -123
- package/src/core/proxy.js +0 -424
- package/src/core/quotaalert.js +0 -79
- package/src/core/quotaerr.js +0 -55
- package/src/core/scheduler.js +0 -59
- package/src/design.js +0 -5161
- package/src/gitrepo.js +0 -150
- package/src/health.js +0 -353
- package/src/installdeps.js +0 -72
- package/src/leftover.js +0 -174
- package/src/platform/boardnotice.js +0 -77
- package/src/platform/boardops.js +0 -36
- package/src/platform/boardwatch.js +0 -447
- package/src/platform/cli.js +0 -45
- package/src/platform/slack.js +0 -1289
- package/src/platform/takeovercard.js +0 -37
- package/src/platform/wecom.js +0 -938
- package/src/platform/wecomcli.js +0 -128
- package/src/platform/wecomgate.js +0 -108
- package/src/platform/wecomname.js +0 -43
- package/src/platform/wecompoll.js +0 -241
- package/src/preview.js +0 -581
- package/src/projectmemory.js +0 -175
- package/src/service.js +0 -317
- package/src/ui/auth.js +0 -31
- package/src/ui/browser.html +0 -392
- package/src/ui/browserroutes.js +0 -234
- package/src/ui/console.html +0 -5494
- package/src/ui/dashboard.js +0 -168
- package/src/ui/feishulogin.js +0 -136
- package/src/ui/server.js +0 -2314
- package/src/ui/sessions.js +0 -78
- package/src/ui/takeover.js +0 -108
- package/src/updater.js +0 -243
- package/src/upgrade.js +0 -35
- package/src/usage.js +0 -144
package/install.sh
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SintBot 1.3.0: isolated macOS package install. Does not change services/config.
|
|
3
|
+
set -u
|
|
4
|
+
VERSION=1.3.0
|
|
5
|
+
PREFIX="${HOME}/.local/share/sintbot/releases/${VERSION}"
|
|
6
|
+
PACKAGE="sintbot@${VERSION}"
|
|
7
|
+
DRY=0
|
|
8
|
+
STAGE=''
|
|
9
|
+
LOCK=''
|
|
10
|
+
fail() { printf '%s\n' "SintBot: $*" >&2; exit 1; }
|
|
11
|
+
cleanup() {
|
|
12
|
+
if [ -n "$STAGE" ] && [ -d "$STAGE" ]; then rm -rf -- "$STAGE"; fi
|
|
13
|
+
if [ -n "$LOCK" ] && [ -d "$LOCK" ]; then rmdir -- "$LOCK" 2>/dev/null || true; fi
|
|
14
|
+
}
|
|
15
|
+
trap cleanup EXIT
|
|
16
|
+
trap 'exit 130' INT
|
|
17
|
+
trap 'exit 143' TERM
|
|
18
|
+
while [ "$#" -gt 0 ]; do
|
|
19
|
+
case "$1" in
|
|
20
|
+
--dry-run) DRY=1; shift ;;
|
|
21
|
+
--prefix|--package)
|
|
22
|
+
[ "$#" -ge 2 ] || fail "$1 needs a value"
|
|
23
|
+
if [ "$1" = --prefix ]; then PREFIX="$2"; else PACKAGE="$2"; fi
|
|
24
|
+
shift 2 ;;
|
|
25
|
+
--help|-h)
|
|
26
|
+
printf '%s\n' 'SintBot 1.3.0 — macOS / one Feishu group / Codex' \
|
|
27
|
+
'Usage: bash install.sh [--dry-run] [--prefix /new/absolute/directory]' \
|
|
28
|
+
' [--package /absolute/sintbot-1.3.0.tgz]' \
|
|
29
|
+
'Installs to a new private prefix. Does not replace an existing global install,' \
|
|
30
|
+
'change configuration, authenticate Codex, start a bot or install a service.'
|
|
31
|
+
exit 0 ;;
|
|
32
|
+
*) fail "Unknown argument: $1" ;;
|
|
33
|
+
esac
|
|
34
|
+
done
|
|
35
|
+
[ "$(uname -s)" = Darwin ] || fail 'This release supports macOS only.'
|
|
36
|
+
command -v node >/dev/null 2>&1 || fail 'Install Node.js 22 or newer, then run this installer again.'
|
|
37
|
+
command -v npm >/dev/null 2>&1 || fail 'npm is required.'
|
|
38
|
+
node -e 'if (Number(process.versions.node.split(".")[0]) < 22) process.exit(1)' || fail 'Node.js 22 or newer is required.'
|
|
39
|
+
case "$PREFIX" in /*) ;; *) fail '--prefix must be an absolute path.' ;; esac
|
|
40
|
+
PREFIX="$(node -e 'process.stdout.write(require("node:path").resolve(process.argv[1]))' "$PREFIX")" || fail 'Invalid prefix.'
|
|
41
|
+
[ "$PREFIX" != / ] || fail 'The filesystem root is not an installation prefix.'
|
|
42
|
+
[ ! -e "$PREFIX" ] && [ ! -L "$PREFIX" ] || fail 'The target already exists; use a new prefix. Existing installations are never overwritten.'
|
|
43
|
+
if [ "$PACKAGE" != "sintbot@${VERSION}" ]; then
|
|
44
|
+
case "$PACKAGE" in /*.tgz) ;; *) fail '--package must name an absolute local .tgz file.' ;; esac
|
|
45
|
+
[ -f "$PACKAGE" ] || fail 'The local package does not exist.'
|
|
46
|
+
fi
|
|
47
|
+
printf '%s\n' "SintBot ${VERSION}: isolated install into ${PREFIX}" 'Scope: macOS, one Feishu group, Codex messages; foreground operation.'
|
|
48
|
+
if [ "$DRY" = 1 ]; then
|
|
49
|
+
printf '%s\n' "Would install ${PACKAGE}, verify version/binary/platform loading, then show setup instructions." 'Preview complete. No changes were made.'
|
|
50
|
+
exit 0
|
|
51
|
+
fi
|
|
52
|
+
PARENT="$(dirname "$PREFIX")"
|
|
53
|
+
mkdir -p -- "$PARENT" || fail 'Cannot create the installation parent.'
|
|
54
|
+
LOCK="${PREFIX}.install-lock"
|
|
55
|
+
if ! mkdir -- "$LOCK" 2>/dev/null; then LOCK=''; fail 'Another install or a stale install lock exists; inspect it before retrying.'; fi
|
|
56
|
+
[ ! -e "$PREFIX" ] && [ ! -L "$PREFIX" ] || fail 'The target appeared during installation; refusing to overwrite it.'
|
|
57
|
+
STAGE="$(mktemp -d "${PARENT}/.sintbot-${VERSION}.XXXXXX")" || fail 'Cannot create staging directory.'
|
|
58
|
+
if ! npm install --registry=https://registry.npmjs.org --global --prefix "$STAGE" --omit=dev --ignore-scripts --no-audit --no-fund "$PACKAGE"; then
|
|
59
|
+
fail 'Package installation failed; the previous installation was not changed.'
|
|
60
|
+
fi
|
|
61
|
+
if ! node - "$STAGE/lib/node_modules/sintbot" "$VERSION" <<'NODE'
|
|
62
|
+
const path = require('node:path')
|
|
63
|
+
const root = process.argv[2]
|
|
64
|
+
const pkg = require(path.join(root, 'package.json'))
|
|
65
|
+
if (pkg.name !== 'sintbot' || pkg.version !== process.argv[3]) throw new Error('Package version mismatch')
|
|
66
|
+
if (pkg.bin?.sintbot !== 'bin/sintbot-v2.js') throw new Error('Package does not use the supported 1.3.0 entry')
|
|
67
|
+
require(path.join(root, 'src/agent/privatecore.js'))
|
|
68
|
+
require(path.join(root, 'src/platform/feishu.js'))
|
|
69
|
+
NODE
|
|
70
|
+
then fail 'Installed package failed version, entry or runtime verification.'; fi
|
|
71
|
+
ACTUAL="$("$STAGE/bin/sintbot" --version)" || fail 'Installed executable did not start.'
|
|
72
|
+
[ "$ACTUAL" = "$VERSION" ] || fail 'Installed executable reported an unexpected version.'
|
|
73
|
+
[ ! -e "$PREFIX" ] && [ ! -L "$PREFIX" ] || fail 'The target appeared during installation; refusing to overwrite it.'
|
|
74
|
+
if ! mv -- "$STAGE" "$PREFIX"; then fail 'Cannot finalize the isolated install.'; fi
|
|
75
|
+
STAGE=''
|
|
76
|
+
FINAL_VERSION="$("$PREFIX/bin/sintbot" --version)" || fail 'Final executable failed after relocation; inspect the new prefix before retrying.'
|
|
77
|
+
[ "$FINAL_VERSION" = "$VERSION" ] || fail 'Final executable version mismatch.'
|
|
78
|
+
printf '\n%s\n' "Installed and verified SintBot ${VERSION}. Existing services and configuration were not changed." \
|
|
79
|
+
'Finish the one-group configuration and first run using:' \
|
|
80
|
+
'https://github.com/sintlab/sintbot/blob/v1.3.0/docs/install.md' \
|
|
81
|
+
'Use this exact executable for the new installation:'
|
|
82
|
+
printf ' %q\n' "$PREFIX/bin/sintbot"
|
|
83
|
+
printf '%s\n' 'An existing multi-group installation must stay on its current version.' \
|
|
84
|
+
'Do not delete old state or receipts to make initialization succeed.'
|
package/package.json
CHANGED
|
@@ -1,52 +1,93 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sintbot",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "A built-in agent core for one Feishu group on macOS, powered by Codex.",
|
|
5
6
|
"bin": {
|
|
6
|
-
"sbot": "bin/
|
|
7
|
-
"sintbot": "bin/
|
|
7
|
+
"sbot": "bin/sintbot-v2.js",
|
|
8
|
+
"sintbot": "bin/sintbot-v2.js"
|
|
8
9
|
},
|
|
10
|
+
"type": "commonjs",
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=22"
|
|
13
|
+
},
|
|
14
|
+
"os": [
|
|
15
|
+
"darwin"
|
|
16
|
+
],
|
|
9
17
|
"files": [
|
|
10
|
-
"bin/",
|
|
11
|
-
"src/",
|
|
12
|
-
"assets/",
|
|
13
|
-
"CHANGELOG.json",
|
|
14
18
|
"CHANGELOG.en.json",
|
|
15
|
-
"
|
|
19
|
+
"CHANGELOG.json",
|
|
20
|
+
"LICENSE",
|
|
16
21
|
"README.md",
|
|
17
22
|
"README.zh-CN.md",
|
|
18
|
-
"LICENSE",
|
|
19
23
|
"THIRD-PARTY-NOTICES.md",
|
|
20
|
-
"
|
|
24
|
+
"bin/sintbot-v2.js",
|
|
25
|
+
"docs/AI-ASSISTED-INSTALL.md",
|
|
26
|
+
"docs/install.md",
|
|
27
|
+
"docs/zh-CN/AI-ASSISTED-INSTALL.md",
|
|
28
|
+
"docs/zh-CN/install.md",
|
|
29
|
+
"install.sh",
|
|
30
|
+
"src/agent/claudecode.js",
|
|
31
|
+
"src/agent/codexapp.js",
|
|
32
|
+
"src/agent/codexcode.js",
|
|
33
|
+
"src/agent/errtag.js",
|
|
34
|
+
"src/agent/privatecore.js",
|
|
35
|
+
"src/agent/privatecore.wasm",
|
|
36
|
+
"src/agent/runner.js",
|
|
37
|
+
"src/agent/visionproxy.js",
|
|
38
|
+
"src/agent/winexec.js",
|
|
39
|
+
"src/c2install.js",
|
|
40
|
+
"src/config.js",
|
|
41
|
+
"src/core/c2cardeffectbridge.js",
|
|
42
|
+
"src/core/c2cardterminalpath.js",
|
|
43
|
+
"src/core/c2durableingress.js",
|
|
44
|
+
"src/core/c2durablejournal.js",
|
|
45
|
+
"src/core/c2durableruntime.js",
|
|
46
|
+
"src/core/c2eventidentity.js",
|
|
47
|
+
"src/core/c2globalfault.js",
|
|
48
|
+
"src/core/c2localeffectexecutor.js",
|
|
49
|
+
"src/core/c2payloadhandles.js",
|
|
50
|
+
"src/core/c2persistrelation.js",
|
|
51
|
+
"src/core/c2persiststate.js",
|
|
52
|
+
"src/core/c2platformrecoverysender.js",
|
|
53
|
+
"src/core/c2recoverygate.js",
|
|
54
|
+
"src/core/c2runnerinput.js",
|
|
55
|
+
"src/core/c2servicestartgate.js",
|
|
56
|
+
"src/core/c2sessionbridge.js",
|
|
57
|
+
"src/core/c2sourcequarantine.js",
|
|
58
|
+
"src/core/c2startuprecovery.js",
|
|
59
|
+
"src/core/c2successorbridge.js",
|
|
60
|
+
"src/core/c2terminaleffectbridge.js",
|
|
61
|
+
"src/core/c2terminalprune.js",
|
|
62
|
+
"src/core/c2turnprojection.js",
|
|
63
|
+
"src/core/datadir.js",
|
|
64
|
+
"src/core/enginecheckpoint.js",
|
|
65
|
+
"src/core/engineeffectcursor.js",
|
|
66
|
+
"src/core/enginestepcoordinator.js",
|
|
67
|
+
"src/core/instancelock.js",
|
|
68
|
+
"src/core/mdtable.js",
|
|
69
|
+
"src/core/redact.js",
|
|
70
|
+
"src/core/store.js",
|
|
71
|
+
"src/core/time.js",
|
|
72
|
+
"src/core/v2engine.js",
|
|
73
|
+
"src/core/verify.js",
|
|
74
|
+
"src/platform/board.js",
|
|
75
|
+
"src/platform/feishu.js",
|
|
76
|
+
"src/platform/multi.js",
|
|
77
|
+
"src/platform/wswatchdog.js",
|
|
78
|
+
"src/plugins.js",
|
|
79
|
+
"src/projectsroot.js",
|
|
80
|
+
"src/setupmodels.js"
|
|
21
81
|
],
|
|
22
|
-
"type": "commonjs",
|
|
23
|
-
"engines": {
|
|
24
|
-
"node": ">=22"
|
|
25
|
-
},
|
|
26
82
|
"scripts": {
|
|
27
|
-
"start": "node bin/
|
|
28
|
-
"test": "node --test",
|
|
29
|
-
"prepublishOnly": "npm test",
|
|
30
|
-
"test:redact": "node --test tests/unit/redact.test.js",
|
|
31
|
-
"test:store": "node --test tests/unit/store.test.js",
|
|
32
|
-
"test:config": "node --test tests/unit/config.test.js",
|
|
33
|
-
"test:engine": "node --test tests/unit/engine.test.js",
|
|
34
|
-
"test:agent": "node --test tests/unit/agent.test.js",
|
|
35
|
-
"test:health": "node --test tests/unit/health.test.js",
|
|
36
|
-
"test:visionproxy": "node --test tests/unit/visionproxy.test.js"
|
|
83
|
+
"start": "node bin/sintbot-v2.js start"
|
|
37
84
|
},
|
|
38
85
|
"keywords": [
|
|
39
|
-
"claude-code",
|
|
40
86
|
"codex",
|
|
41
87
|
"feishu",
|
|
42
|
-
"slack",
|
|
43
|
-
"wecom",
|
|
44
88
|
"group-chat",
|
|
45
89
|
"chatops",
|
|
46
|
-
"
|
|
47
|
-
"deepseek",
|
|
48
|
-
"self-hosted",
|
|
49
|
-
"daemon"
|
|
90
|
+
"self-hosted"
|
|
50
91
|
],
|
|
51
92
|
"author": "WaterTian",
|
|
52
93
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -59,7 +100,6 @@
|
|
|
59
100
|
"url": "https://github.com/sintlab/sintbot/issues"
|
|
60
101
|
},
|
|
61
102
|
"dependencies": {
|
|
62
|
-
"@larksuiteoapi/node-sdk": "^1.76.0"
|
|
63
|
-
"ws": "^8.21.3"
|
|
103
|
+
"@larksuiteoapi/node-sdk": "^1.76.0"
|
|
64
104
|
}
|
|
65
105
|
}
|
package/src/agent/claudecode.js
CHANGED
|
@@ -14,17 +14,21 @@
|
|
|
14
14
|
// See DESIGN.md §1 (process model) / §4.1 (permissions).
|
|
15
15
|
|
|
16
16
|
const { spawn } = require('node:child_process')
|
|
17
|
+
const { StringDecoder } = require('node:string_decoder')
|
|
17
18
|
const { ensureProxy, proxyBaseFor } = require('./visionproxy')
|
|
18
19
|
|
|
19
20
|
// Always-on 群项目基础能力: auto-allowed regardless of preset (even read-only) so the
|
|
20
21
|
// self-knowledge manifest's advertised base skills actually run. Both CLIs fence to the
|
|
21
22
|
// group's own chat ($SBOT_CHAT_ID); feishu also fences paths to cwd. Additive; deny wins.
|
|
22
|
-
const
|
|
23
|
+
const { claudeBaseSkillAllows, claudeBuildArgs, claudePluginAllowRules, claudeNormalizeEvent } = require('./privatecore')
|
|
24
|
+
const { coreFailedResult } = require('./errtag')
|
|
25
|
+
const BASE_SKILL_ALLOWS = claudeBaseSkillAllows()
|
|
23
26
|
// Windows executable resolution (npm .cmd shim vs native .exe) is shared with the
|
|
24
27
|
// codex runner — see winexec.js for the why (ENOENT on .cmd, never the POSIX shim).
|
|
25
28
|
const { IS_WIN, resolveSpawn } = require('./winexec')
|
|
26
|
-
const {
|
|
27
|
-
const
|
|
29
|
+
const { tagError } = require('./errtag') // 出错原话 → quota / sessionMissing / transient 之一(优先级只在那一处定),与 codex runner 共用
|
|
30
|
+
const closedRunnerResults = new WeakSet()
|
|
31
|
+
const isClosedRunnerResult = result => !!result && typeof result === 'object' && closedRunnerResults.has(result)
|
|
28
32
|
|
|
29
33
|
// Build the child env from a resolved provider. `baseUrlOverride` (when given)
|
|
30
34
|
// replaces provider.baseUrl for ANTHROPIC_BASE_URL — used to point `claude` at
|
|
@@ -66,72 +70,26 @@ function buildEnv(provider = {}, baseUrlOverride, extraEnv) {
|
|
|
66
70
|
|
|
67
71
|
// Map a raw `claude` stream-json line to a normalized AgentEvent (interfaces.js).
|
|
68
72
|
function normalizeEvent(ev) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
const pre = Number(meta.pre_tokens != null ? meta.pre_tokens : meta.preTokens)
|
|
79
|
-
const post = Number(meta.post_tokens != null ? meta.post_tokens : meta.postTokens)
|
|
80
|
-
if (pre > 0) r.preTokens = pre
|
|
81
|
-
if (post > 0) r.postTokens = post
|
|
82
|
-
}
|
|
83
|
-
return r
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
case 'assistant': {
|
|
87
|
-
const blocks = (ev.message && ev.message.content) || []
|
|
88
|
-
return {
|
|
89
|
-
type: 'assistant',
|
|
90
|
-
text: blocks.filter((b) => b.type === 'text').map((b) => b.text).join(''),
|
|
91
|
-
thinking: blocks.filter((b) => b.type === 'thinking').map((b) => b.thinking).join(''),
|
|
92
|
-
tools: blocks.filter((b) => b.type === 'tool_use').map((b) => ({ name: b.name, input: b.input })),
|
|
93
|
-
sessionId: ev.session_id,
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
case 'user': {
|
|
98
|
-
const blocks = (ev.message && ev.message.content) || []
|
|
99
|
-
return {
|
|
100
|
-
type: 'tool_result',
|
|
101
|
-
results: blocks.filter((b) => b.type === 'tool_result'),
|
|
102
|
-
sessionId: ev.session_id,
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
case 'result': {
|
|
107
|
-
// ⚠ 出错时 CLI 的原因多半**不在 result 里**而在 errors[](error_during_execution / error_max_turns 的真实
|
|
108
|
-
// 形状,2026-09-08 审查从 2.1.260 二进制核对);API 过载(529)那种则是 subtype success + is_error +
|
|
109
|
-
// result 带「API Error: 529 …」。两种都得让账本与瞬时错判据看见 —— 只看 result,前一种原因永远是空。
|
|
110
|
-
const errs = ev.is_error && Array.isArray(ev.errors)
|
|
111
|
-
? ev.errors.map((x) => (typeof x === 'string' ? x : (x && x.message) || JSON.stringify(x))).filter(Boolean)
|
|
112
|
-
: []
|
|
113
|
-
const text = ev.result != null && ev.result !== '' ? ev.result : (errs.length ? errs.join('; ') : ev.result)
|
|
114
|
-
const r = {
|
|
115
|
-
type: 'result',
|
|
116
|
-
subtype: ev.subtype,
|
|
117
|
-
text,
|
|
118
|
-
isError: ev.is_error,
|
|
119
|
-
usage: ev.usage,
|
|
120
|
-
sessionId: ev.session_id,
|
|
121
|
-
}
|
|
122
|
-
// 瞬时错(服务端过载 / 限流)给群面一句人话;text 仍是原话,留给账本(判据在 transienterr,两 runner 共用)
|
|
123
|
-
if (ev.is_error) { const t = classifyTransient(text); if (t) r.transient = t }
|
|
124
|
-
// 额度 / 账号类(用完了 · 账号被停 · 登录态刷新失败)标 quota:引擎据此自动切到备用模型(判据在 agent/quotaerr,两 runner 共用)
|
|
125
|
-
if (ev.is_error) { const q = classifyQuota(text); if (q) r.quota = q }
|
|
126
|
-
// 续接的会话在 CLI 那边已经没了(典型:Claude Code 默认清掉 30 天没动过的会话记录)。引擎据此换新会话重做,
|
|
127
|
-
// 否则这个群之后每条消息都带着同一个死 id 去续接、条条报错,直到有人 /new。
|
|
128
|
-
if (ev.is_error && /No conversation found with session ID/i.test(String(text || ''))) r.sessionGone = true
|
|
129
|
-
return r
|
|
73
|
+
const result = claudeNormalizeEvent(ev)
|
|
74
|
+
// JSON cannot carry `undefined`; retain the frozen JS object's optional
|
|
75
|
+
// property shape at the public runner boundary.
|
|
76
|
+
if (ev.type === 'system') { result.subtype = ev.subtype; result.sessionId = ev.session_id }
|
|
77
|
+
else if (ev.type === 'assistant' || ev.type === 'user') {
|
|
78
|
+
result.sessionId = ev.session_id
|
|
79
|
+
if (ev.type === 'assistant') for (const tool of result.tools) {
|
|
80
|
+
if (!Object.hasOwn(tool, 'name')) tool.name = undefined
|
|
81
|
+
if (!Object.hasOwn(tool, 'input')) tool.input = undefined
|
|
130
82
|
}
|
|
131
|
-
|
|
132
|
-
default:
|
|
133
|
-
return { type: ev.type, raw: ev }
|
|
134
83
|
}
|
|
84
|
+
else if (ev.type === 'result') {
|
|
85
|
+
result.subtype = ev.subtype
|
|
86
|
+
result.isError = ev.is_error
|
|
87
|
+
result.usage = ev.usage
|
|
88
|
+
result.sessionId = ev.session_id
|
|
89
|
+
if (!Object.hasOwn(result, 'text')) result.text = undefined
|
|
90
|
+
if (ev.is_error) tagError(result, result.text)
|
|
91
|
+
}
|
|
92
|
+
return result
|
|
135
93
|
}
|
|
136
94
|
|
|
137
95
|
// key 型模型认证失败时造的那条出错 result(形状同 normalizeEvent 的 result)。quota 直接标 account:
|
|
@@ -168,6 +126,8 @@ function runTurn({
|
|
|
168
126
|
extraEnv,
|
|
169
127
|
appendSystemPrompt,
|
|
170
128
|
onEvent,
|
|
129
|
+
onDurableEvent,
|
|
130
|
+
onTurnAccepted,
|
|
171
131
|
signal,
|
|
172
132
|
log = () => {}, // daemon logger — 与 codex runner 同一个口子(engine 两边都传 this.log)
|
|
173
133
|
spawnFn = spawn, // 可注入,供单测驱动 spawn/解析环路(与 codexcode 同一形状)
|
|
@@ -180,6 +140,7 @@ function runTurn({
|
|
|
180
140
|
} = {}) {
|
|
181
141
|
return new Promise((resolve, reject) => {
|
|
182
142
|
if (!prompt) return reject(new Error('runTurn: prompt required'))
|
|
143
|
+
if (onDurableEvent !== undefined && typeof onDurableEvent !== 'function') return reject(new TypeError('onDurableEvent must be a function'))
|
|
183
144
|
|
|
184
145
|
// CLI 能力探测必须先于 spawn(见 supportsFlag)。结果按 bin 缓存 ⇒ 只有进程内第一轮真探一次
|
|
185
146
|
// (`--help` 毫秒级);不需要压缩的群连探都不探。
|
|
@@ -213,19 +174,13 @@ function runTurn({
|
|
|
213
174
|
// to start) can resolve a base-URL override first. Closes over resolve/reject.
|
|
214
175
|
function launch(baseUrlOverride) {
|
|
215
176
|
// Prompt travels via stdin (see spawn below), so it's absent from argv here.
|
|
216
|
-
const args = ['-p', '--output-format', 'stream-json', '--verbose']
|
|
217
|
-
const m = model || provider.model
|
|
218
|
-
if (m) args.push('--model', m)
|
|
219
|
-
if (sessionId) args.push('--resume', sessionId)
|
|
220
|
-
if (permissionMode && permissionMode !== 'default') args.push('--permission-mode', permissionMode)
|
|
221
177
|
// 长会话上下文上限:超过即让 CLI 原地压缩(--autocompact)。不传 ⇒ CLI 只在模型窗口边缘
|
|
222
178
|
// 才压 —— 多天群会话实测涨到 96 万 token 都没压过一次。
|
|
223
179
|
// ⚠ 旗子先经能力探测(见 supportsFlag):老 CLI 不认它,传了**每轮必崩**。探不出来也不传 ——
|
|
224
180
|
// 降级(压缩不生效)好过整群跑不动。但**降级必须出声**:静默失效正是最难查的那类(教训 26),
|
|
225
181
|
// 所以这里打日志、doctor 另有一行如实标。
|
|
226
|
-
if (autocompactTokens > 0) {
|
|
227
|
-
if (
|
|
228
|
-
else if (onEvent) {
|
|
182
|
+
if (autocompactTokens > 0 && autocompactSupported !== true) {
|
|
183
|
+
if (onEvent) {
|
|
229
184
|
try {
|
|
230
185
|
onEvent({ type: 'log', message: autocompactSupported === false
|
|
231
186
|
? 'claude CLI 不认 --autocompact(版本偏低)⇒ 长会话自动压缩已停用;升级 claude 即可恢复'
|
|
@@ -240,34 +195,23 @@ function runTurn({
|
|
|
240
195
|
// merged here without touching the group's saved allow list; deny still wins.
|
|
241
196
|
// Groups without a plugins config keep the traditional self-loading — zero regression.
|
|
242
197
|
// Config travels as a file, not inline JSON argv (Windows cmd.exe shim mangles quotes).
|
|
198
|
+
let mcpConfigPath, pluginAllows = []
|
|
243
199
|
if (plugins && workdir) {
|
|
244
|
-
const { buildMcpConfig,
|
|
200
|
+
const { buildMcpConfig, writeMcpConfigFile } = require('../plugins')
|
|
245
201
|
// 传 log:跳过死 MCP 的那一行要走 daemon 带时间戳的 logger(v0.11.61 口径),不能退成裸 stderr
|
|
246
202
|
const mcpCfg = buildMcpConfig(workdir, plugins.mcp || [], undefined, { log })
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (extra.length) allowedTools = [...(allowedTools || []), ...extra]
|
|
203
|
+
mcpConfigPath = writeMcpConfigFile(workdir, mcpCfg)
|
|
204
|
+
pluginAllows = claudePluginAllowRules(Object.keys(mcpCfg.mcpServers || {}), plugins.skills || [])
|
|
250
205
|
}
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
// hang. Enforced by the CLI harness, so it's identical across model backends.
|
|
258
|
-
if (allowedTools && allowedTools.length) args.push('--allowedTools', allowedTools.join(','))
|
|
259
|
-
if (disallowedTools && disallowedTools.length) args.push('--disallowedTools', disallowedTools.join(','))
|
|
260
|
-
// Extra system prompt (engine injects the NO_REPLY/sender-tag contract).
|
|
261
|
-
// Travels as argv: keep callers' text ASCII-safe so
|
|
262
|
-
// the Windows cmd.exe shim path can't mangle it (user prompt stays on stdin).
|
|
263
|
-
if (appendSystemPrompt) args.push('--append-system-prompt', appendSystemPrompt)
|
|
264
|
-
// Directory scope: cwd = the project's own workdir, and the ONLY --add-dir is that
|
|
265
|
-
// same workdir — never an external path. So file tools (Read/Edit/Write/Grep/Glob)
|
|
266
|
-
// stay confined to the project directory. (Bash isn't OS-jailed by the CLI — its
|
|
267
|
-
// reach is bounded by the per-project allow list, not by a directory sandbox.)
|
|
268
|
-
if (workdir) args.push('--add-dir', workdir)
|
|
206
|
+
// The binary owns the permission, tool allow/deny and argument policy.
|
|
207
|
+
// Node only resolves capability and writes the project-scoped MCP file.
|
|
208
|
+
const args = claudeBuildArgs({ model, providerModel: provider.model, sessionId,
|
|
209
|
+
permissionMode, autocompactTokens, autocompactSupported,
|
|
210
|
+
mcpConfigPath, allowedTools, pluginAllows, disallowedTools,
|
|
211
|
+
appendSystemPrompt, workdir })
|
|
269
212
|
|
|
270
|
-
const { file, spawnArgs } = resolveSpawn(bin, args)
|
|
213
|
+
const { file, spawnArgs, fallback } = resolveSpawn(bin, args)
|
|
214
|
+
if (fallback) log(`配置的 agent 路径 '${bin}' 不存在,本轮改用 ${fallback} —— 请把配置改回正确路径`) // 兜底只救急,配置仍是错的
|
|
271
215
|
const child = spawnFn(file, spawnArgs, {
|
|
272
216
|
env: buildEnv(provider, baseUrlOverride, extraEnv),
|
|
273
217
|
cwd: workdir || process.cwd(),
|
|
@@ -293,14 +237,50 @@ function runTurn({
|
|
|
293
237
|
let killTimer = null
|
|
294
238
|
let closeTimer = null
|
|
295
239
|
let settled = false
|
|
240
|
+
let childCloseObserved = false
|
|
241
|
+
let acceptanceFailed = false
|
|
242
|
+
let turnAccepted = false
|
|
243
|
+
let durableWork = Promise.resolve()
|
|
244
|
+
let durableFailed = false
|
|
296
245
|
// Resolution is guarded (exactly once) because a close-grace timer can force the
|
|
297
246
|
// turn to settle before the 'close' event fires — otherwise a lingering pipe would
|
|
298
247
|
// wedge the per-group lock forever (never-hang invariant).
|
|
299
248
|
const finish = (fn) => { if (settled) return; settled = true; if (killTimer) clearTimeout(killTimer); if (closeTimer) clearTimeout(closeTimer); if (signal) signal.removeEventListener('abort', onAbort); fn() }
|
|
249
|
+
const resolveRun = result => {
|
|
250
|
+
if (childCloseObserved) closedRunnerResults.add(result)
|
|
251
|
+
resolve(result)
|
|
252
|
+
}
|
|
300
253
|
const killTree = () => {
|
|
301
254
|
if (IS_WIN && child.pid) { try { spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore', windowsHide: true }) } catch {}; return }
|
|
302
255
|
try { child.kill('SIGKILL') } catch { /* already gone */ }
|
|
303
256
|
}
|
|
257
|
+
// The durable v2 dispatch ACK is a separate, synchronous boundary. Renderer
|
|
258
|
+
// callbacks intentionally swallow errors; this callback must never do so.
|
|
259
|
+
// No normalized event is delivered before the child's init identifies the
|
|
260
|
+
// accepted turn. CLI tools may already have executed before observation.
|
|
261
|
+
const failTurnAcceptance = (error) => {
|
|
262
|
+
acceptanceFailed = true
|
|
263
|
+
buf = ''
|
|
264
|
+
killTree()
|
|
265
|
+
finish(() => reject(error))
|
|
266
|
+
}
|
|
267
|
+
const failDurableEvent = (error) => {
|
|
268
|
+
if (durableFailed || settled) return
|
|
269
|
+
durableFailed = true
|
|
270
|
+
buf = ''
|
|
271
|
+
killTree()
|
|
272
|
+
finish(() => reject(error))
|
|
273
|
+
}
|
|
274
|
+
const persistEvent = async (event) => {
|
|
275
|
+
if (!onDurableEvent || !event) return
|
|
276
|
+
let timer
|
|
277
|
+
try {
|
|
278
|
+
await Promise.race([
|
|
279
|
+
Promise.resolve().then(() => onDurableEvent(event)),
|
|
280
|
+
new Promise((_, fail) => { timer = setTimeout(() => fail(new Error('Claude durable event hook timed out')), 10000) }),
|
|
281
|
+
])
|
|
282
|
+
} finally { clearTimeout(timer) }
|
|
283
|
+
}
|
|
304
284
|
// Force-resolve if 'close' doesn't arrive within closeGraceMs of the trigger (the
|
|
305
285
|
// final result, or an abort). On Windows the cmd.exe shim / spawned MCP servers can
|
|
306
286
|
// hold the stdout pipe open long past claude's own exit, so 'close' may lag or never
|
|
@@ -321,7 +301,9 @@ function runTurn({
|
|
|
321
301
|
closeTimer = setTimeout(() => {
|
|
322
302
|
if (!aborted && !(lastResult && lastResult.authFailed)) log(`claude 答案已到但 ${closeGraceMs}ms 内没退出 ⇒ 强制收尾;本轮若起过后台命令(run_in_background),它会被一起收掉,下一轮可能空转一次`)
|
|
323
303
|
killTree()
|
|
324
|
-
finish(() =>
|
|
304
|
+
const complete = () => finish(() => resolveRun({ sessionId: capturedSessionId, result: lastResult, code: null, aborted }))
|
|
305
|
+
if (onDurableEvent) durableWork.then(complete, failDurableEvent)
|
|
306
|
+
else complete()
|
|
325
307
|
}, closeGraceMs)
|
|
326
308
|
}
|
|
327
309
|
const onAbort = () => {
|
|
@@ -341,8 +323,26 @@ function runTurn({
|
|
|
341
323
|
else signal.addEventListener('abort', onAbort, { once: true })
|
|
342
324
|
}
|
|
343
325
|
|
|
344
|
-
|
|
345
|
-
|
|
326
|
+
// 这段回调不在任何 Promise 链里,抛错会直接带走整个进程 ⇒ 核心失败只结束当前轮。
|
|
327
|
+
// 已经拿到结果时保留结果(答案已交付,只差收尾)。原因只进日志,不带事件原文。
|
|
328
|
+
let coreFailed = false
|
|
329
|
+
const failCore = (error) => {
|
|
330
|
+
if (onDurableEvent) { failDurableEvent(error); return }
|
|
331
|
+
coreFailed = true
|
|
332
|
+
buf = ''
|
|
333
|
+
log(`binary core failed in claude stream: ${error && error.message}`)
|
|
334
|
+
if (lastResult) return
|
|
335
|
+
lastResult = coreFailedResult(capturedSessionId)
|
|
336
|
+
if (onEvent) { try { onEvent(lastResult) } catch { /* renderer must not kill the turn */ } }
|
|
337
|
+
killTree()
|
|
338
|
+
armCloseGrace()
|
|
339
|
+
}
|
|
340
|
+
const stdoutDecoder = new StringDecoder('utf8') // 一个汉字可能被拆在两个分块里,逐块 toString 会变成乱码
|
|
341
|
+
const processChunk = async (chunk) => {
|
|
342
|
+
// Legacy callers may still receive a late buffered line after close;
|
|
343
|
+
// only the v2 gate forbids post-settlement delivery.
|
|
344
|
+
if (coreFailed || acceptanceFailed || durableFailed || ((onTurnAccepted || onDurableEvent) && settled)) return
|
|
345
|
+
buf += stdoutDecoder.write(chunk)
|
|
346
346
|
let nl
|
|
347
347
|
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
348
348
|
const line = buf.slice(0, nl).trim()
|
|
@@ -350,18 +350,43 @@ function runTurn({
|
|
|
350
350
|
if (!line) continue
|
|
351
351
|
let raw
|
|
352
352
|
try { raw = JSON.parse(line) } catch { continue } // skip non-JSON noise
|
|
353
|
+
if (onTurnAccepted && !turnAccepted) {
|
|
354
|
+
if (raw.type !== 'system' || raw.subtype !== 'init' || typeof raw.session_id !== 'string' || !raw.session_id) {
|
|
355
|
+
failTurnAcceptance(new Error('Claude turn emitted an event before start confirmation'))
|
|
356
|
+
return
|
|
357
|
+
}
|
|
358
|
+
let accepted
|
|
359
|
+
try { accepted = onTurnAccepted({ channel: 'claude-code', sessionId: raw.session_id }) } catch (error) {
|
|
360
|
+
failTurnAcceptance(error)
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
let asyncAccepted = false
|
|
364
|
+
try { asyncAccepted = !!accepted && typeof accepted.then === 'function' } catch (error) {
|
|
365
|
+
failTurnAcceptance(error)
|
|
366
|
+
return
|
|
367
|
+
}
|
|
368
|
+
if (asyncAccepted) {
|
|
369
|
+
// An async write cannot guard following lines in this same chunk.
|
|
370
|
+
Promise.resolve(accepted).catch(() => {})
|
|
371
|
+
failTurnAcceptance(new Error('Claude turn start confirmation must be synchronous'))
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
turnAccepted = true
|
|
375
|
+
}
|
|
353
376
|
// key 错快速失败(1.0.3):claude 对 401 / 403 默认重试约 3 分钟才放弃,第一次重试时(实测 0.8 秒)就会先报一条
|
|
354
377
|
// `system/api_retry` 带 error_status。只对 API key 型模型(有 baseUrl)当场收尾 —— 订阅登录的 401 可能是
|
|
355
378
|
// Claude Code 自己在刷新登录,让它重试;5xx / 529 这类过载照旧重试。造一条出错 result 交给上层:账号类失败
|
|
356
379
|
// 那套(备用模型 / 运维告警 / 群里人话)和重试耗尽时一样走,只是不用等。
|
|
357
380
|
if (raw.type === 'system' && raw.subtype === 'api_retry' && provider.baseUrl && (raw.error_status === 401 || raw.error_status === 403) && !lastResult && !aborted) {
|
|
358
381
|
lastResult = authFailedResult(raw.error_status, raw.error, capturedSessionId)
|
|
382
|
+
if (onDurableEvent) await persistEvent(lastResult)
|
|
359
383
|
if (onEvent) { try { onEvent(lastResult) } catch { /* renderer must not kill the turn */ } }
|
|
360
384
|
killTree()
|
|
361
385
|
armCloseGrace()
|
|
362
386
|
continue
|
|
363
387
|
}
|
|
364
|
-
|
|
388
|
+
let ev
|
|
389
|
+
try { ev = normalizeEvent(raw) } catch (error) { failCore(error); return }
|
|
365
390
|
if (ev.sessionId) capturedSessionId = ev.sessionId
|
|
366
391
|
if (ev.type === 'assistant' && raw.message && raw.message.usage) {
|
|
367
392
|
lastMsgUsage = raw.message.usage
|
|
@@ -374,27 +399,43 @@ function runTurn({
|
|
|
374
399
|
// message.usage 里(2.1.222 真机核对),兜底走 usage.iterations 末元素(同一形状)。
|
|
375
400
|
const last = lastMsgUsage || (ev.usage && Array.isArray(ev.usage.iterations) ? ev.usage.iterations[ev.usage.iterations.length - 1] : null)
|
|
376
401
|
if (last) ev.contextTokens = (last.input_tokens || 0) + (last.cache_read_input_tokens || 0) + (last.cache_creation_input_tokens || 0)
|
|
377
|
-
lastResult = ev; armCloseGrace() // answer in → bound the wait for 'close'
|
|
378
402
|
}
|
|
403
|
+
if (onDurableEvent) await persistEvent(ev)
|
|
404
|
+
if (ev.type === 'result') { lastResult = ev; armCloseGrace() } // answer in → bound the wait for 'close'
|
|
379
405
|
if (onEvent) { try { onEvent(ev) } catch { /* renderer must not kill the turn */ } }
|
|
380
406
|
}
|
|
407
|
+
}
|
|
408
|
+
child.stdout.on('data', (chunk) => {
|
|
409
|
+
if (!onDurableEvent) { processChunk(chunk).catch(failDurableEvent); return }
|
|
410
|
+
child.stdout.pause?.()
|
|
411
|
+
durableWork = durableWork.then(() => processChunk(chunk)).catch(failDurableEvent).finally(() => {
|
|
412
|
+
if (!durableFailed && !settled) child.stdout.resume?.()
|
|
413
|
+
})
|
|
381
414
|
})
|
|
382
415
|
|
|
383
416
|
child.stderr.on('data', (c) => { stderr += c.toString('utf8') })
|
|
384
417
|
|
|
385
418
|
child.on('error', (err) => finish(() => {
|
|
419
|
+
// 配置路径与各候选位置都没有:群里只说人话(这条 message 会原样进群卡与账本),路径与产品名只进日志。
|
|
420
|
+
// 英文原话带安装路径,不能进群。code 给引擎认(发运维告警),不按文案判。
|
|
386
421
|
if (err.code === 'ENOENT') {
|
|
387
|
-
|
|
422
|
+
log(`cannot find agent binary '${bin}' (and no fallback copy on PATH / ~/.local/bin / nvm) — is Claude Code installed?`)
|
|
423
|
+
return reject(Object.assign(new Error('模型程序在这台机器上找不到了,需要管理员处理'), { code: 'AGENT_BIN_MISSING' }))
|
|
388
424
|
}
|
|
389
425
|
reject(err)
|
|
390
426
|
}))
|
|
391
427
|
|
|
392
|
-
|
|
428
|
+
const completeClose = (code) => finish(() => {
|
|
393
429
|
if (code !== 0 && !lastResult && !aborted) {
|
|
394
430
|
return reject(new Error(`claude exited ${code}: ${stderr.trim().slice(0, 800) || '(no stderr)'}`))
|
|
395
431
|
}
|
|
396
|
-
|
|
397
|
-
})
|
|
432
|
+
resolveRun({ sessionId: capturedSessionId, result: lastResult, code, aborted })
|
|
433
|
+
})
|
|
434
|
+
child.on('close', (code) => {
|
|
435
|
+
childCloseObserved = true
|
|
436
|
+
if (onDurableEvent) durableWork.then(() => { if (!durableFailed) completeClose(code) })
|
|
437
|
+
else completeClose(code)
|
|
438
|
+
})
|
|
398
439
|
}
|
|
399
440
|
})
|
|
400
441
|
}
|
|
@@ -459,4 +500,4 @@ function probeVersion(bin = 'claude') {
|
|
|
459
500
|
})
|
|
460
501
|
}
|
|
461
502
|
|
|
462
|
-
module.exports = { runTurn, buildEnv, normalizeEvent, probeVersion, supportsFlag, BASE_SKILL_ALLOWS }
|
|
503
|
+
module.exports = { runTurn, isClosedRunnerResult, buildEnv, normalizeEvent, probeVersion, supportsFlag, BASE_SKILL_ALLOWS }
|