sintbot 1.4.17 → 1.4.18

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 CHANGED
@@ -1,4 +1,35 @@
1
1
  [
2
+ {
3
+ "version": "1.4.18",
4
+ "date": "2026-11-12",
5
+ "name": "Browser setup and Claude idle compaction",
6
+ "items": [
7
+ {
8
+ "t": "Connect Feishu in the browser",
9
+ "d": "The first start opens a local setup page for project selection, phone confirmation, progress and resumable failures. Once configuration and journals are initialized, the page enters the console automatically. English and Chinese are supported."
10
+ },
11
+ {
12
+ "t": "Save explicit model and thinking-depth choices for new groups",
13
+ "d": "Before scanning, setup reads the selected executor's live recommended model and thinking depth and saves them for the new group. An unavailable live catalog stops setup before app creation; existing choices are preserved."
14
+ },
15
+ {
16
+ "t": "Safer cancellation and retries",
17
+ "d": "Cancellation stops at step boundaries. Retries reuse confirmed apps and groups, while configuration writes recheck current state to avoid overwriting another instance's setup."
18
+ },
19
+ {
20
+ "t": "Idle compaction for Claude groups (experimental)",
21
+ "d": "Claude Code versions advertising compact can compact an idle session after a task when context reaches half the configured compaction window. New work stops compaction first. Successful compaction and cache behavior still await real-model validation."
22
+ },
23
+ {
24
+ "t": "Immediate Claude quota updates from events",
25
+ "d": "Quota events from each run refresh the display promptly and reduce extra quota requests. Existing API and cache paths remain when events are unavailable."
26
+ },
27
+ {
28
+ "t": "Compaction exit protection and clearer setup guidance",
29
+ "d": "Unconfirmed process exit retains capacity and blocks further dispatch for that group. Configuration-file notices, language-specific guide links and startup instructions are also corrected."
30
+ }
31
+ ]
32
+ },
2
33
  {
3
34
  "version": "1.4.17",
4
35
  "date": "2026-11-12",
package/CHANGELOG.json CHANGED
@@ -1,4 +1,35 @@
1
1
  [
2
+ {
3
+ "version": "1.4.18",
4
+ "date": "2026-11-12",
5
+ "name": "浏览器接入与 Claude 空闲整理",
6
+ "items": [
7
+ {
8
+ "t": "在浏览器完成飞书接入",
9
+ "d": "首次启动会打开本机接入页;填写项目、扫码确认、查看进度与失败续做均在页面完成。配置和日志初始化完成后自动进入控制台,支持中英文。"
10
+ },
11
+ {
12
+ "t": "为新群明确保存型号与思考深度",
13
+ "d": "扫码前读取所选执行器的实时推荐型号和思考深度,并保存为新群设置;取不到实时清单时先停止接入,已有选择不会被覆盖。"
14
+ },
15
+ {
16
+ "t": "接入取消与重试更可靠",
17
+ "d": "取消会在步骤边界停止;重试复用已确认的应用和群,写配置前再次检查已有状态,避免覆盖其他实例的配置。"
18
+ },
19
+ {
20
+ "t": "Claude 群空闲整理(实验性)",
21
+ "d": "支持 compact 的 Claude Code 可在任务结束后、上下文达到自定义压缩窗口一半时整理当前会话;有新任务时先停止整理。成功压缩和缓存效果的真实模型验收仍待补充。"
22
+ },
23
+ {
24
+ "t": "Claude 额度事件即时更新",
25
+ "d": "优先读取每轮自带的额度事件,及时更新用量显示并减少额外额度查询;没有事件时保留既有接口与缓存路径。"
26
+ },
27
+ {
28
+ "t": "整理退出保护与接入说明修正",
29
+ "d": "未确认整理进程退出时保留并发名额并阻止该群继续派发;同步修正配置文件提示、指南语言链接及启动说明。"
30
+ }
31
+ ]
32
+ },
2
33
  {
3
34
  "version": "1.4.17",
4
35
  "date": "2026-11-12",
package/README.md CHANGED
@@ -45,7 +45,7 @@ curl -fsSL https://sintbot.com/install.sh | bash
45
45
 
46
46
  The script checks the environment, installs SintBot and verifies the result, without `sudo`. You can read [install.sh](./install.sh) before running it. Installation does not sign in to a model account, connect a group or start a service.
47
47
 
48
- **2. Connect your group and project.** Run `sintbot connect` and scan the QR code with Feishu under an enterprise or team identity: it creates the app, a project folder and a group, then writes the configuration. The [installation guide](https://sintbot.com/guide/install/) covers the options, the manual route and the startup steps.
48
+ **2. Connect your group and project.** Run `sintbot start --ui`; the browser opens the local setup page. Enter a project name and scan the QR code with Feishu under an enterprise or team identity: it creates the app, a project folder and a group, then writes the configuration. The [installation guide](https://sintbot.com/guide/install/) covers the options, the manual route and the startup steps.
49
49
 
50
50
  Installing with an AI assistant? Use the [AI-assisted install and first-use guide](./docs/AI-ASSISTED-INSTALL.md).
51
51
 
package/README.zh-CN.md CHANGED
@@ -45,7 +45,7 @@ curl -fsSL https://sintbot.com/install.sh | bash
45
45
 
46
46
  脚本会检查环境、安装 SintBot 并验证结果,不用 `sudo`。也可以先读 [install.sh](./install.sh)。安装本身不登录模型账号、不连接群,也不启动服务。
47
47
 
48
- **2. 接入群和项目。** 运行 `sintbot connect`,用企业/团队身份的飞书扫码:它会创建应用、项目目录和群,并写好配置。参数、手动路径和启动步骤见[安装指南](https://sintbot.com/zh/guide/install/)。
48
+ **2. 接入群和项目。** 运行 `sintbot start --ui`,浏览器会打开本机接入页;填项目名、用企业/团队身份的飞书扫码,它会创建应用、项目目录和群,写好配置并直接启动。终端里也可以用 `sintbot connect`。参数、手动路径和启动步骤见[安装指南](https://sintbot.com/zh/guide/install/)。
49
49
 
50
50
  想让本机 AI 助手帮忙安装并完成首次使用?先看 [AI 安装与首次使用说明](./docs/zh-CN/AI-ASSISTED-INSTALL.md)。
51
51
 
package/bin/sintbot-v2.js CHANGED
@@ -24,10 +24,22 @@ async function startService(rest) {
24
24
  'no-open': { type: 'boolean' }, 'ui-port': { type: 'string' },
25
25
  'ui-host': { type: 'string' }, preflight: { type: 'boolean' } } })
26
26
  const preflightOnly = values.preflight === true
27
- const { loadInstallation, assertPrivateDirectory, assertInstallationJournals, assertJournalScope } = require('../src/c2install')
27
+ const { loadInstallation, assertPrivateDirectory, assertInstallationJournals, assertJournalScope, installationStateDir } = require('../src/c2install')
28
+ // Setup mode: no configuration yet. With --ui, serve the local scan-to-connect
29
+ // page (src/setupserver.js) in this process; it writes the configuration and
30
+ // initializes the journals, then the normal start below continues.
31
+ const setupConfig = path.resolve(values.config || process.env.SBOT_CONFIG || path.join(installationStateDir(values['data-dir']), 'config.json'))
32
+ if (!fs.existsSync(setupConfig)) {
33
+ if (preflightOnly || values.ui !== true) {
34
+ throw new Error('No configuration yet. Run `sintbot start --ui` and connect Feishu in the browser (or `sintbot connect` in the terminal)')
35
+ }
36
+ await runSetupMode({ configFile: setupConfig, dataDir: values['data-dir'],
37
+ host: values['ui-host'] || '127.0.0.1', port: Number(values['ui-port'] || 4600),
38
+ open: values['no-open'] !== true })
39
+ }
28
40
  // Groups added to the configuration since the last start are registered
29
41
  // below, after the writer lock is held; existing journals stay as mapped.
30
- const installation = loadInstallation(values.config, { dataDir: values['data-dir'], allowAddedGroups: true })
42
+ const installation = loadInstallation(setupConfig, { dataDir: values['data-dir'], allowAddedGroups: true })
31
43
  const { stateDir, file, cfg, groups, routes, serviceChatId, provider } = installation
32
44
  const uiEnabled = values.ui === true || cfg.ui?.enabled === true
33
45
  const uiHost = values['ui-host'] || cfg.ui?.host || '127.0.0.1'
@@ -51,6 +63,39 @@ async function startService(rest) {
51
63
  return startMultiGroupService({ installation, preflightOnly, uiEnabled, uiHost, uiPort })
52
64
  }
53
65
 
66
+ // Serve the setup page until the configuration exists. The page is local only;
67
+ // the browser is opened once. Ctrl-C cancels the scan in progress and exits.
68
+ async function runSetupMode({ configFile, dataDir, host, port, open }) {
69
+ const { createSetupServer, probeRunners } = require('../src/setupserver')
70
+ const os = require('node:os')
71
+ if (!Number.isSafeInteger(port) || port < 1 || port > 65535) throw new Error('Console port is invalid')
72
+ const setup = createSetupServer({ configFile, dataDir, home: os.homedir(), version: require('../package.json').version,
73
+ runners: probeRunners(), host, port, log: message => process.stderr.write(`Setup · ${message}\n`) })
74
+ await setup.listen()
75
+ const url = `http://${host === '::1' ? '[::1]' : host}:${port}/`
76
+ process.stderr.write(`SintBot has no Feishu configuration yet. Open ${url} to connect by scanning.\n`)
77
+ if (open && process.platform === 'darwin') {
78
+ try { require('node:child_process').spawn('open', [url], { stdio: 'ignore', detached: true }).unref() } catch {}
79
+ }
80
+ // Ctrl-C: cancel the run (runConnect stops at its next step boundary, writing
81
+ // nothing), wait for it to end, then exit.
82
+ const onSignal = () => {
83
+ setup.session.cancel()
84
+ const ended = Promise.race([Promise.resolve(setup.session.wait()).catch(() => {}), new Promise(resolve => setTimeout(resolve, 15_000))])
85
+ ended.then(() => setup.close()).finally(() => process.exit(130))
86
+ }
87
+ process.once('SIGINT', onSignal); process.once('SIGTERM', onSignal)
88
+ try {
89
+ await setup.completed
90
+ // Let the page read the final state before the console takes the port.
91
+ await new Promise(resolve => setTimeout(resolve, 3000))
92
+ } finally {
93
+ process.off('SIGINT', onSignal); process.off('SIGTERM', onSignal)
94
+ await setup.close()
95
+ }
96
+ process.stderr.write('Setup · Feishu connected; starting the service\n')
97
+ }
98
+
54
99
  // Model and depth used to live on model cards; they now belong to each group.
55
100
  // Once every group is connected (a console upgrade can no longer roll back
56
101
  // onto this configuration), copy each card's value into the groups that have
@@ -309,6 +354,19 @@ async function startMultiGroupService({ installation, preflightOnly, uiEnabled =
309
354
  if (closeChecks[backend]?.(result) === true) closedResults.add(result)
310
355
  return result
311
356
  }
357
+ // Idle compaction shares the turn's close evidence so its permit is
358
+ // released only once the child is proven closed (or never started).
359
+ const compactSession = input => {
360
+ const started = runner.compactSession(input)
361
+ if (started === null) return null
362
+ return started.then(result => {
363
+ if (closeChecks.claude?.(result) === true) closedResults.add(result)
364
+ return result
365
+ }, error => {
366
+ if (closeErrorChecks.claude?.(error) === true) closedErrors.add(error)
367
+ throw error
368
+ })
369
+ }
312
370
  engine = new Engine({ ...cfg, groups: { [route.chatId]: route.group } }, {
313
371
  store, activity, log: message => process.stderr.write(`C2 v2 · ${message}\n`),
314
372
  durableV2Runtime: runtime, durableV2Payloads: handles,
@@ -318,6 +376,8 @@ async function startMultiGroupService({ installation, preflightOnly, uiEnabled =
318
376
  : !!error && typeof error === 'object' && closedErrors.has(error),
319
377
  durableV2AssertWriterLock: assertWriterLock, durableV2Journal: journal,
320
378
  durableV2AcquireCapacity: options => capacity.acquire(options),
379
+ durableV2CapacitySnapshot: () => capacity.snapshot(),
380
+ durableV2CompactSession: compactSession,
321
381
  // A route fault holds or recovers its group alone. Shared grants
322
382
  // are sealed only when the process writer lease itself is gone.
323
383
  durableV2SealCapacity: error => {
@@ -477,6 +537,9 @@ Usage:
477
537
  sintbot help
478
538
 
479
539
  Supports explicit Feishu groups using Codex or Claude Code. Start runs in the foreground.
540
+ start --ui with no configuration opens a local setup page that connects Feishu by scanning (the
541
+ browser form takes the same inputs as connect); the service starts once setup has written and
542
+ initialized the configuration.
480
543
  connect scans a Feishu QR code to create the app, then creates a group (or binds --chat), the project
481
544
  folder under ~/Projects, the configuration and the journals, in that order; a failed check stops before
482
545
  the folder and configuration exist. It refuses existing configuration, state or a running service;
@@ -1,4 +1,4 @@
1
- # Install SintBot 1.4.17 with an AI assistant
1
+ # Install SintBot 1.4.18 with an AI assistant
2
2
 
3
3
  English · [简体中文](./zh-CN/AI-ASSISTED-INSTALL.md)
4
4
 
@@ -7,7 +7,7 @@ Use the terminal on the Mac that will run SintBot. A cloud shell cannot install
7
7
  1. Check macOS, Node 22+, the actual Codex or Claude Code executable and login, and any existing SintBot process. Do not print credentials. Preserve existing installations, configuration and state.
8
8
  2. Run the one-line installation command in the guide; `--dry-run` is optional. Continue within the user's authorization; ask only for missing decisions. The installer does not change Node, Codex, Claude Code, shell profiles or services.
9
9
  3. The installer verifies the version and core at `~/.local/share/sintbot/bin/sintbot`. A successful package install is not a successful group test.
10
- 4. Prefer `sintbot connect`: it prints the scan link and opens a local QR page; only the user can scan and confirm on the phone, after which it creates the project folder and group, writes the configuration and initializes, so no secret passes through the assistant. It refuses when a Feishu configuration exists or the service is running; do not delete files to get past that. If the scan is not possible, prepare the configuration by the manual route in the install guide: let the user enter secrets locally or use already authorized credential references without revealing them. Use an existing authorized Feishu app only when another process is not connected to it.
10
+ 4. Prefer the browser setup: `sintbot start --ui` opens the local setup page when no configuration exists; from the terminal `sintbot connect` does the same flow, printing the scan link and opening a local QR page; only the user can scan and confirm on the phone, after which it creates the project folder and group, writes the configuration and initializes, so no secret passes through the assistant. It refuses when a Feishu configuration exists or the service is running; do not delete files to get past that. If the scan is not possible, prepare the configuration by the manual route in the install guide: let the user enter secrets locally or use already authorized credential references without revealing them. Use an existing authorized Feishu app only when another process is not connected to it.
11
11
  5. On the manual route, run `init` for fresh state only (never again after `connect` has initialized), then `start --preflight`. Refusal due to an existing journal or unresolved receipts is a stop condition: never delete files, fabricate a new authority or alter receipts to bypass it.
12
12
  6. Run the foreground service, send an authorized read-only group request and compare its response with the file. Stop cleanly and verify a restart. If no person is available, use an authorized test account to send the request; document the sender and do not call it independent human acceptance. A bot's own API message may be ignored and is not a substitute for an incoming user message.
13
13
  7. Stop temporary processes before ending the task. Report what was installed, what actually ran and any remaining setup. Do not register an automatic background service for this release.
@@ -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.17 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.
18
+ Install SintBot 1.4.18 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.17
1
+ # Install SintBot 1.4.18
2
2
 
3
3
  English · [简体中文](./zh-CN/install.md)
4
4
 
@@ -10,19 +10,33 @@ 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.17` into `~/.local/share/sintbot`, and verifies the entry, built-in core and Feishu module. Continue when you see `Installed and verified SintBot 1.4.17`. No sudo is needed.
13
+ The installer checks the environment, installs `sintbot@1.4.18` into `~/.local/share/sintbot`, and verifies the entry, built-in core and Feishu module. Continue when you see `Installed and verified SintBot 1.4.18`. 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
 
17
- ## 2. Connect by scanning (recommended)
17
+ ## 2. Connect Feishu in the browser (recommended)
18
18
 
19
19
  Prerequisite: the Feishu app on your phone is signed in with an enterprise or team identity; a personal identity without an organization cannot scan, so create a team first (free). The app created by the scan belongs to the scanner's organization; approval depends on its policies.
20
20
 
21
+ ```bash
22
+ "$HOME/.local/share/sintbot/bin/sintbot" start --ui
23
+ ```
24
+
25
+ Without a configuration file this command does not fail; it opens the local setup page (`http://127.0.0.1:4600`, reachable only from this Mac). Enter a project name (default `sintbot`, created under `~/Projects` with a `README.md`) or pick an existing folder, choose the runner if both are installed, tick the basic option if you are not an organization administrator, then click Start. Scan the QR code on the page with Feishu and confirm on the phone. It then, in order: obtains the app credentials → runs the self-check → opens the contact range and visibility and publishes a version → creates a group and adds you → creates the project folder → writes `~/.sbot/config.json` → initializes the message journals, showing each result on the page. Once the scan is confirmed, the app exists in your organization; any later failed check stops before the folder and the configuration exist and the page offers Retry (it reuses the app already created, never a second one); if the group could not be created, the page asks you to create one by hand, add the bot, paste the group ID and continue.
26
+
27
+ Before showing the QR code, setup reads the selected executor's live model catalog and displays its recommended model and thinking depth. These choices are saved explicitly for the new group. If the live list is unavailable, setup stops before creating an app; check the executor login and connection, then retry. After connecting, change the choices in the console or with `/model`. Existing group choices are preserved.
28
+
29
+ When setup completes the service starts right away and the page switches to the console a few seconds later. Keep the terminal open. In the new group, send "Read the first line of the project README and reply with only that line", then compare it with the `README.md` in the project folder.
30
+
31
+ ### Terminal route
32
+
33
+ The same flow is available without a browser; the flags map to the options on the page:
34
+
21
35
  ```bash
22
36
  "$HOME/.local/share/sintbot/bin/sintbot" connect
23
37
  ```
24
38
 
25
- The terminal prints the scan link and opens a local page with the QR code; scan it with Feishu and confirm on the phone. It then, in order: obtains the app credentials (and prints the app ID) → runs the self-check → opens the contact range and visibility and publishes a version → creates a group and adds you → creates the project folder `~/Projects/sintbot` (with a `README.md`) → writes `~/.sbot/config.json` → initializes the message journals. Every step reports its result. Once the scan is confirmed, the app exists in your organization; any later failed check or group creation stops before the folder and the configuration are created and prints the rerun command with `--app-id`.
39
+ In an interactive terminal it first asks for the project name (press Enter for the default `sintbot`), then prints the scan link and opens a local page with the QR code; the steps are the same as above, results are printed in the terminal, and a failure prints the rerun command with `--app-id`.
26
40
 
27
41
  - The credentials are stored in clear text in `~/.sbot/config.json` (file 0600, directory 0700). To switch back to environment variables, change `appId` / `appSecret` to `"env:FEISHU_APP_ID"` / `"env:FEISHU_APP_SECRET"` and export both before every start.
28
42
  - `--basic`: for a non-administrator in a company organization. It does not request `application:application:patch` and skips the range and publish steps; ask an administrator to finish those two in the Open Platform.
@@ -31,15 +45,15 @@ The terminal prints the scan link and opens a local page with the QR code; scan
31
45
  - `--workdir /absolute/path`: bind an existing directory instead of creating one; `--name` changes the default folder name; `--provider codex|claude` picks the runner (chosen automatically when only one is installed); `--no-open` prints the link without opening the page.
32
46
  - `connect` refuses to run when a Feishu configuration already exists or the service is running. The scan requests the card callbacks used by `/model`. If the card does not respond, check the callback subscription as in section 4.
33
47
 
34
- ## 3. First start
48
+ ## 3. Everyday start
35
49
 
36
- `connect` has already initialized, so start directly:
50
+ After setup, every start uses the same command; with the configuration in its default location it is the same one you used to connect:
37
51
 
38
52
  ```bash
39
- "$HOME/.local/share/sintbot/bin/sintbot" start --config "$HOME/.sbot/config.json" --ui
53
+ "$HOME/.local/share/sintbot/bin/sintbot" start --ui
40
54
  ```
41
55
 
42
- This checks the configuration and runners, connects Feishu and opens the local console. Keep the terminal open. In the group, send "Read the first line of the project README and reply with only that line", then compare it with `~/Projects/sintbot/README.md`. A passing preflight does not prove Feishu authentication or delivery.
56
+ This checks the configuration and runners, connects Feishu and opens the local console. Keep the terminal open. If you used `--config` or `--data-dir`, pass the same flags on every start. A passing preflight does not prove Feishu authentication or delivery.
43
57
 
44
58
  ## 4. Manual setup (alternative)
45
59
 
@@ -50,7 +64,7 @@ Use the manual route when the scan fails, an administrator does not approve, or
50
64
 
51
65
  #### 4.1 Create the app
52
66
 
53
- In the [Feishu Open Platform](https://open.feishu.cn/), create a custom app, enable the bot and the permissions to receive and reply to messages, choose long connection for event subscription and subscribe to message receipt. For callbacks, also choose long connection and subscribe to card action callbacks (the tap-to-pick `/model` card needs them). Publish the app, add the bot to the groups you will use, and copy each group's `oc_…` chat ID.
67
+ In the [Feishu Open Platform](https://open.feishu.cn/), create a custom app, enable the bot, grant the same permissions scan setup requests under Permissions (`im:message.group_msg`, `im:message:readonly`, `im:message:send_as_bot`, `im:resource`, `im:message.reactions:write_only`, `im:chat`, `cardkit:card:write`, `contact:contact.base:readonly`, `contact:department.base:readonly`, `contact:user.base:readonly`) and set the contact data range to all members, choose long connection for event subscription and subscribe to message receipt. For callbacks, also choose long connection and subscribe to card action callbacks (the tap-to-pick `/model` card needs them). Publish the app, add the bot to the groups you will use, and copy each group's `oc_…` chat ID.
54
68
 
55
69
  Enter the app credentials in the default macOS terminal (the secret is not echoed):
56
70
 
@@ -131,16 +145,18 @@ Any text message is a task. Replies arrive in a card that appears once the model
131
145
  | `/stop` | Stop the current task and cancel queued ones; the conversation is kept |
132
146
  | `/new` | Start a new conversation, forgetting the previous one (only while the group is idle) |
133
147
  | `/model` | Opens a tap-to-pick card: choose the backend, model and thinking depth, then confirm. You can also send `/model high`, `/model <model>` or `/model <backend>`. A model or depth change continues the conversation; a backend change starts a new one. Only while the group is idle |
134
- | `/hud` | Quota of the current model and usage of this conversation |
148
+ | `/hud` | Quota of the current model and usage of this conversation; Claude groups also show the idle compaction line and the last compaction time |
135
149
  | `/issue text` | Log feedback; review and close it in the console overview |
136
150
  | `/help` | List commands, with this group's project and model |
137
151
 
138
- Send a command as a message of its own; except for `/issue` and `/model`, do not add other text after it. If the Feishu app also subscribes to the message-recall event (the console's setup guide recommends it), recalling your own message cancels it: a queued message is dropped, and the running task stops if that message alone started it; recalling a follow-up handed to either runner cancels the whole task, including accepted input not yet read.
152
+ Send a command as a message of its own; except for `/issue` and `/model`, do not add other text after it. If the Feishu app also subscribes to the message-recall event (scan setup subscribes to it; add it on the manual path too), recalling your own message cancels it: a queued message is dropped, and the running task stops if that message alone started it; recalling a follow-up handed to either runner cancels the whole task, including accepted input not yet read.
139
153
 
140
154
  Busy follow-ups default on for both Codex and Claude Code. Set `steer: false` to queue them instead; an explicit `steer` value overrides the legacy Codex setting. Claude Code must advertise the required input capabilities, otherwise messages stay queued. Reading can wait until a long-running tool finishes. The reply card follows confirmed reading, and continuation answers are combined into one final reply. 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.
141
155
 
142
156
  When the reply card moves below a confirmed follow-up, its existing text is shown immediately and subsequent output continues from that point. Moving the card does not restart the task.
143
157
 
158
+ On Claude Code versions advertising compact, an idle group can compact its session after a normal turn once context reaches half the configured compaction window. New work stops compaction first. Compaction takes one concurrency slot and starts only when another slot remains free. A value of 0 leaves normal compaction to the executor and disables this idle step. Fresh quota events update usage immediately; while events remain fresh, supplemental quota requests are limited to once every 30 minutes. Idle compaction is experimental in this release: successful compaction and cache behavior still await real-model validation.
159
+
144
160
  ## 6. Console
145
161
 
146
162
  Model and thinking depth are chosen per group: on the group card, the Model row picks the backend and model and the next row picks the depth, from lists read live from the local Codex and Claude Code. Subscription model cards no longer carry a model or depth; a third-party endpoint keeps its model on the model card. After the upgrade, each model card's existing model and depth move into its groups with the same effective values and no session reset, after a configuration backup. Model cards distinguish requested settings from runtime reports; expand Capacity and runtime details for reference limits. Codex context and per-model compaction each offer Executor default or Custom. Numeric inputs appear only for Custom. Changes apply next turn without resetting the session; known catalog context limits are enforced. Compaction means a threshold for Codex and a window for Claude Code, and may start earlier. Explicit legacy global compaction values are converted into independent settings for existing models, preserving per-model overrides. New models use executor defaults. Reading does not rewrite the file; the converted settings are persisted on the next normal configuration save.
@@ -1,4 +1,4 @@
1
- # 用 AI 助手安装 SintBot 1.4.17
1
+ # 用 AI 助手安装 SintBot 1.4.18
2
2
 
3
3
  [English](../AI-ASSISTED-INSTALL.md) · 简体中文
4
4
 
@@ -7,7 +7,7 @@
7
7
  1. 核对 macOS、Node 22 及以上、Codex 或 Claude Code 的实际程序与登录、是否已有 SintBot 进程。不输出凭据,保留已有安装、配置和状态。
8
8
  2. 运行指南中的一行安装命令;`--dry-run` 为可选预览。用户已经授权的步骤继续执行,只问缺失的决定。安装器不修改 Node、Codex、Claude Code、终端配置或服务。
9
9
  3. 安装器会验证 `~/.local/share/sintbot/bin/sintbot` 的版本和核心加载。安装程序包成功不等于群测试成功。
10
- 4. 优先用 `sintbot connect` 接入:它打印扫码链接并打开本机二维码页面,扫码和手机确认只能由用户本人完成;之后它自己建项目目录、建群、写配置并初始化,密钥不需要经过助手。已有飞书配置或服务在跑时它会拒绝,不要删文件绕过。扫码走不通再按安装指南的手动路径准备配置:让用户在本地输入密钥,或使用已经授权的凭据引用,不显示秘密。复用授权飞书应用时,确认没有另一个进程连接该应用。
10
+ 4. 优先让用户在浏览器里接入:`sintbot start --ui` 在没有配置时打开本机接入页;助手在终端也可用 `sintbot connect`,它打印扫码链接并打开本机二维码页面,扫码和手机确认只能由用户本人完成;之后它自己建项目目录、建群、写配置并初始化,密钥不需要经过助手。已有飞书配置或服务在跑时它会拒绝,不要删文件绕过。扫码走不通再按安装指南的手动路径准备配置:让用户在本地输入密钥,或使用已经授权的凭据引用,不显示秘密。复用授权飞书应用时,确认没有另一个进程连接该应用。
11
11
  5. 手动路径下,只有全新状态才能执行 `init`(`connect` 已经初始化过的不要再执行),然后执行 `start --preflight`。若已有日志或未结收据而拒绝启动,不得删文件、新造日志权威或篡改收据来绕过。
12
12
  6. 前台运行,用已经授权的账号在群内发只读请求,把回复和文件对照,再正常停止并验证重启。没有真人协助时可用授权测试账号发送,并记录实际发送方式,不称为独立真人验收;机器人自己发的接口消息可能被忽略,不能当作用户入站消息。
13
13
  7. 本轮临时进程用完停止。说明安装了什么、实际验证了什么、还缺什么。本版不注册自动后台服务。
@@ -15,5 +15,5 @@
15
15
  ## 可复制的请求
16
16
 
17
17
  ```text
18
- 按一行安装指南,在这台 Mac 安装 SintBot 1.4.17。保留已有服务、配置、状态和 Codex 或 Claude Code 账号数据,不输出凭据。验证实际入口与二进制核心,配置飞书群及各群的执行器,只对全新状态初始化。运行前台预检和已授权的只读群测试,对照结果,再验证正常停止与重启。完成后停止临时进程,并区分程序包安装与群验收结果。
18
+ 按一行安装指南,在这台 Mac 安装 SintBot 1.4.18。保留已有服务、配置、状态和 Codex 或 Claude Code 账号数据,不输出凭据。验证实际入口与二进制核心,配置飞书群及各群的执行器,只对全新状态初始化。运行前台预检和已授权的只读群测试,对照结果,再验证正常停止与重启。完成后停止临时进程,并区分程序包安装与群验收结果。
19
19
  ```
@@ -1,4 +1,4 @@
1
- # 安装 SintBot 1.4.17
1
+ # 安装 SintBot 1.4.18
2
2
 
3
3
  [English](../install.md) · 简体中文
4
4
 
@@ -10,19 +10,33 @@
10
10
  curl -fsSL https://sintbot.com/install.sh | bash
11
11
  ```
12
12
 
13
- 安装器会自动检查环境,把 `sintbot@1.4.17` 装到 `~/.local/share/sintbot`,并验证程序入口、内置核心和飞书模块。看到 `Installed and verified SintBot 1.4.17` 就可以继续。不需要 sudo。
13
+ 安装器会自动检查环境,把 `sintbot@1.4.18` 装到 `~/.local/share/sintbot`,并验证程序入口、内置核心和飞书模块。看到 `Installed and verified SintBot 1.4.18` 就可以继续。不需要 sudo。
14
14
 
15
15
  可选预览:在命令末尾加 `-s -- --dry-run`。要自定义安装位置,设置 `SINTBOT_INSTALL_DIR` 环境变量,并同步修改后面的可执行文件路径。
16
16
 
17
- ## 2. 扫码接入(推荐)
17
+ ## 2. 在浏览器里接入飞书(推荐)
18
18
 
19
- 前提:手机飞书当前处于企业/团队身份;没有组织的个人身份扫不了,需先新建一个团队(免费)。扫码创建的应用归扫码人所在的组织,公司普通成员建了自己删不掉。
19
+ 前提:手机飞书当前处于企业/团队身份;没有组织的个人身份扫不了,需先新建一个团队(免费)。扫码创建的应用归扫码人所在的组织,审批与否取决于组织设置。
20
+
21
+ ```bash
22
+ "$HOME/.local/share/sintbot/bin/sintbot" start --ui
23
+ ```
24
+
25
+ 还没有配置文件时,这条命令不会报错,而是在本机打开接入页(`http://127.0.0.1:4600`,只有这台 Mac 能访问)。页面上填项目名(默认 `sintbot`,会在 `~/Projects` 下新建并放一份 `README.md`)或选一个已有目录,两个执行器都装了就选一个,不是组织管理员就勾上精简接入,然后点「开始扫码」。用手机飞书扫页面上的二维码并确认。接下来它依次:取得应用凭证 → 自检 → 放开通讯录范围与可用范围并发布版本 → 创建群并把你拉进去 → 新建项目目录 → 写入 `~/.sbot/config.json` → 初始化消息日志,每一步的结果都显示在页面上。扫码一旦确认,应用就已在你的组织里创建;之后任一检查失败都会在建目录、写配置之前停下,页面给出「重试」(复用已创建的应用,不会再建一个);自动建群失败时页面会让你手动建群、拉入机器人,粘贴群 ID 后点「继续」。
26
+
27
+ 显示二维码前会读取所选执行器的实时型号清单,在页面显示推荐型号和思考深度,并明确保存到新群配置。清单暂时取不到时会在创建应用之前停下;检查执行器登录和网络后重试。接入后可在控制台或通过 `/model` 调整,已有群的选择不会被覆盖。
28
+
29
+ 接入完成后服务直接接着启动,页面几秒后切到控制台。保持终端开启。在新群里发“读取项目 README 的第一行,只回复这一行”,并和项目目录里的 `README.md` 对照。
30
+
31
+ ### 终端方式
32
+
33
+ 不想开浏览器也可以在终端做同一件事,参数对应页面上的选项:
20
34
 
21
35
  ```bash
22
36
  "$HOME/.local/share/sintbot/bin/sintbot" connect
23
37
  ```
24
38
 
25
- 终端先打印扫码链接,并打开一个本机页面显示二维码;用飞书扫码、在手机上确认。随后它依次:取得应用凭证(并打印应用 ID)→ 自检 → 放开通讯录范围与可用范围并发布版本 → 创建群并把你拉进去 → 新建项目目录 `~/Projects/sintbot`(含一个 `README.md`)→ 写入 `~/.sbot/config.json` → 初始化消息日志。每一步的结果都会打印。扫码一旦确认,应用就已在你的组织里创建;之后任一检查或建群失败都会在建目录、写配置之前停下,并给出带 `--app-id` 的重跑命令。
39
+ 在交互终端里它会先问项目名(直接回车即用默认的 `sintbot`),然后打印扫码链接,并打开一个本机页面显示二维码;步骤与上面相同,结果打印在终端,失败时给出带 `--app-id` 的重跑命令。
26
40
 
27
41
  - 凭证明文写在 `~/.sbot/config.json`(文件 0600、目录 0700)。要改回环境变量,把 `appId` / `appSecret` 改成 `"env:FEISHU_APP_ID"` / `"env:FEISHU_APP_SECRET"`,并在每次启动前导出这两个变量。
28
42
  - `--basic`:公司组织里的非管理员使用。不申请 `application:application:patch`,跳过放开范围与发布;之后请管理员在开放平台补这两步。
@@ -31,15 +45,15 @@ curl -fsSL https://sintbot.com/install.sh | bash
31
45
  - `--workdir /绝对路径`:绑定一个已存在的目录,不新建;`--name` 换默认目录名;`--provider codex|claude` 指定执行器(本机只装了一个时自动选);`--no-open` 只打印链接、不打开页面。
32
46
  - 已有飞书配置或服务正在运行时,`connect` 会拒绝执行。扫码会申请模型卡所需的卡片回调。`/model` 点选卡没有反应时,按第 4 节检查回调订阅。
33
47
 
34
- ## 3. 首次启动
48
+ ## 3. 日常启动
35
49
 
36
- `connect` 已经完成初始化,直接启动:
50
+ 接入完成后以后每次启动都用同一条命令;配置在默认位置时和接入时那条一样:
37
51
 
38
52
  ```bash
39
- "$HOME/.local/share/sintbot/bin/sintbot" start --config "$HOME/.sbot/config.json" --ui
53
+ "$HOME/.local/share/sintbot/bin/sintbot" start --ui
40
54
  ```
41
55
 
42
- 它会预检配置和执行器、连接飞书并打开本机控制台。保持终端开启。在群里发“读取项目 README 的第一行,只回复这一行”,并和 `~/Projects/sintbot/README.md` 对照。预检通过不代表飞书认证或消息送达已经验证。
56
+ 它会预检配置和执行器、连接飞书并打开本机控制台。保持终端开启。用了 `--config` 或 `--data-dir` 的,启动时带同样的参数。预检通过不代表飞书认证或消息送达已经验证。
43
57
 
44
58
  ## 4. 手动接入(备选)
45
59
 
@@ -50,7 +64,7 @@ curl -fsSL https://sintbot.com/install.sh | bash
50
64
 
51
65
  #### 4.1 创建应用
52
66
 
53
- 在 [飞书开放平台](https://open.feishu.cn/) 创建企业自建应用,启用机器人、接收和回复消息的权限,事件订阅选择长连接并订阅消息接收;回调订阅同样选择长连接接收回调并订阅卡片回传交互(群里点选 `/model` 卡片要用)。发布应用后,把机器人加入要使用的群,取得每个群的 `oc_…` 群 ID。
67
+ 在 [飞书开放平台](https://open.feishu.cn/) 创建企业自建应用,启用机器人,在「权限管理」开通与扫码接入相同的权限:`im:message.group_msg`、`im:message:readonly`、`im:message:send_as_bot`、`im:resource`、`im:message.reactions:write_only`、`im:chat`、`cardkit:card:write`、`contact:contact.base:readonly`、`contact:department.base:readonly`、`contact:user.base:readonly`,并在「数据权限」把通讯录授权范围设为全部成员;事件订阅选择长连接并订阅消息接收;回调订阅同样选择长连接接收回调并订阅卡片回传交互(群里点选 `/model` 卡片要用)。发布应用后,把机器人加入要使用的群,取得每个群的 `oc_…` 群 ID。
54
68
 
55
69
  在 macOS 默认终端中输入应用凭据(密钥输入时不显示):
56
70
 
@@ -131,16 +145,18 @@ SINTBOT="$HOME/.local/share/sintbot/bin/sintbot"
131
145
  | `/stop` | 停止当前任务,连同排队的一起取消;之前的对话保留 |
132
146
  | `/new` | 忘掉之前的对话,开始新的一段(只在本群空闲时生效) |
133
147
  | `/model` | 打开点选卡,选后端、型号和思考深度后点「确认更新」;也可直接发 `/model high`、`/model 型号`、`/model 后端`。换型号或调深度接着当前对话,换后端开始新的一段对话;只在本群空闲时生效 |
134
- | `/hud` | 当前模型的额度与本段对话的用量 |
148
+ | `/hud` | 当前模型的额度与本段对话的用量;Claude 群还显示空闲整理线和上次整理时间 |
135
149
  | `/issue 一句话` | 记下问题反馈,在控制台概览里查看和标记处理 |
136
150
  | `/help` | 查看可用命令,以及本群的项目和模型 |
137
151
 
138
- 命令单独发一条;除 `/issue` 和 `/model` 外,后面不要跟别的字。如果飞书应用还订阅了消息撤回事件(控制台接入引导里建议订阅),撤回自己发的消息即取消那件事:还在排队的直接丢弃;当前任务只由这一条触发时会停下;两种执行器中,撤回已交给执行器的补话均会取消整轮,包括已受理但尚未读取的补话。
152
+ 命令单独发一条;除 `/issue` 和 `/model` 外,后面不要跟别的字。如果飞书应用还订阅了消息撤回事件(扫码接入会自动订阅,手动接入建议一并订阅),撤回自己发的消息即取消那件事:还在排队的直接丢弃;当前任务只由这一条触发时会停下;两种执行器中,撤回已交给执行器的补话均会取消整轮,包括已受理但尚未读取的补话。
139
153
 
140
154
  Codex 和 Claude Code 的忙时跟进默认开启;`steer:false` 改为排队,显式 `steer` 优先于旧 Codex 开关。Claude Code 未提供所需输入能力时保留排队。长工具调用可能延迟读取;确认读取后回复卡才跟随补话,续轮答案合并为一条最终回复。某一轮模型报错或没有结果时,只在本群提示重新发送,不会自动重试。某个群上一轮的结果无法确认时(例如回复是否送达不明),只暂停这个群,它收到新消息会回一句说明,其他群照常;不重放、不重试。
141
155
 
142
156
  回复卡移到已确认读取的补话下方时,会立即保留已有文字,后续输出继续更新;移动卡片不会重新启动任务。
143
157
 
158
+ 走 Claude Code 的群会在一轮结束、群里没人等的时候整理会话:上下文到了模型卡「压缩」阈值的一半,就对同一会话跑一次 `/compact`,群里不发消息,新消息到来会先停掉整理再开始这一轮。整理占一个并发名额,只在还有空余名额时进行;阈值为 0 时不整理。Claude 订阅额度改从每轮自带的额度事件读取,额度接口最多半小时查一次。
159
+
144
160
  ## 6. 控制台
145
161
 
146
162
  型号与思考深度按群选:在「群 · 项目」卡的「模型」行选后端和型号,下一行选深度,清单取自本机 Codex / Claude Code 的实时数据;订阅登录的模型卡上不再设型号和深度,第三方端点的型号仍在模型卡上设置。升级后,模型卡上原有的型号和深度会自动写进各群,生效值不变、会话不重置,改前备份配置。模型卡区分设置值与运行回报,容量依据可在「容量与运行信息」中查看。Codex 上下文窗口和各模型自动压缩都只提供「执行器默认 / 自定义」,仅自定义时显示数值输入;从下轮生效,保留当前会话。Codex 自定义窗口不能超过已知目录上限。压缩值对 Codex 表示阈值,对 Claude Code 表示窗口,实际可能提前触发。旧配置中的全局压缩值会转换为原有各模型的独立值,已有逐模型设置优先;新模型默认交给执行器,不继续继承旧全局值。读取不改磁盘,下一次正常保存配置时写入转换结果。
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.17
5
+ version=1.4.18
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.17",
3
+ "version": "1.4.18",
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": {
@@ -71,6 +71,7 @@
71
71
  "src/cdp.js",
72
72
  "src/config.js",
73
73
  "src/connect.js",
74
+ "src/setupserver.js",
74
75
  "src/consolesay.js",
75
76
  "src/consoletrials.js",
76
77
  "src/core/brain.js",
@@ -96,7 +96,13 @@ function normalizeEvent(ev) {
96
96
  const result = claudeNormalizeEvent(ev)
97
97
  // JSON cannot carry `undefined`; retain the frozen JS object's optional
98
98
  // property shape at the public runner boundary.
99
- if (ev.type === 'system') { result.subtype = ev.subtype; result.sessionId = ev.session_id }
99
+ if (ev.type === 'system') {
100
+ result.subtype = ev.subtype; result.sessionId = ev.session_id
101
+ // init 里 CLI 自报的可用命令:含 compact ⇒ 可以空闲整理(ABI 37 idle_compact_plan 的 canCompact);
102
+ // 2.1.311 实测在 slash_commands 里。哪版拿掉了,引擎就只在轮内压缩。
103
+ if (ev.subtype === 'init' && Array.isArray(ev.slash_commands)) result.slashCommands = ev.slash_commands.filter(c => typeof c === 'string')
104
+ // 压缩边界(compact_boundary)的 preTokens 由核心 normalize 带出,引擎据此判整理成败。
105
+ }
100
106
  else if (ev.type === 'assistant' || ev.type === 'user') {
101
107
  result.sessionId = ev.session_id
102
108
  if (ev.type === 'assistant') for (const tool of result.tools) {
@@ -158,6 +164,8 @@ function runTurn({
158
164
  softStopGraceMs = 10000,
159
165
  steer = false, onSteerReady, onSteerLanded,
160
166
  steerReceiptTimeoutMs = 10000, steerHoldMs = 20000, interruptGraceMs = 2000,
167
+ requireCloseProof = false,
168
+ closeProofGraceMs = 3000,
161
169
  closeGraceMs = 3000, // short: the grace starts AFTER the answer, so we're only waiting for
162
170
  // the OS to close a possibly-clinging pipe (MCP browser children / cmd.exe shim). Claude
163
171
  // persists its session continuously, so killing ~3s post-answer is safe → the group frees
@@ -270,6 +278,7 @@ function runTurn({
270
278
  let aborted = false
271
279
  let killTimer = null
272
280
  let closeTimer = null
281
+ let closeProofTimer = null
273
282
  let settled = false
274
283
  let childCloseObserved = false
275
284
  let acceptanceFailed = false
@@ -279,7 +288,7 @@ function runTurn({
279
288
  // Resolution is guarded (exactly once) because a close-grace timer can force the
280
289
  // turn to settle before the 'close' event fires — otherwise a lingering pipe would
281
290
  // wedge the per-group lock forever (never-hang invariant).
282
- const finish = (fn) => { if (settled) return; settled = true; followup?.dispose(); if (killTimer) clearTimeout(killTimer); if (closeTimer) clearTimeout(closeTimer); if (signal) signal.removeEventListener('abort', onAbort); fn() }
291
+ const finish = (fn) => { if (settled) return; settled = true; followup?.dispose(); if (killTimer) clearTimeout(killTimer); if (closeTimer) clearTimeout(closeTimer); if (closeProofTimer) clearTimeout(closeProofTimer); if (signal) signal.removeEventListener('abort', onAbort); fn() }
283
292
  const resolveRun = result => {
284
293
  if (childCloseObserved) closedRunnerResults.add(result)
285
294
  resolve(result)
@@ -335,6 +344,14 @@ function runTurn({
335
344
  closeTimer = setTimeout(() => {
336
345
  if (!aborted && !(lastResult && lastResult.authFailed)) log(`claude 答案已到但 ${closeGraceMs}ms 内没退出 ⇒ 强制收尾;本轮若起过后台命令(run_in_background),它会被一起收掉,下一轮可能空转一次`)
337
346
  killTree()
347
+ // Compaction shares a resumable session with the next turn. A result
348
+ // or a sent signal does not prove the process closed its files.
349
+ if (requireCloseProof && !childCloseObserved) {
350
+ closeProofTimer = setTimeout(() => finish(() => reject(Object.assign(
351
+ new Error('Claude compaction process close remains unproven'),
352
+ { code: 'CLAUDE_COMPACTION_CLOSE_UNPROVEN' }))), closeProofGraceMs)
353
+ return
354
+ }
338
355
  const complete = () => finish(() => resolveRun({ sessionId: capturedSessionId, result: lastResult, code: null, aborted }))
339
356
  if (onDurableEvent) durableWork.then(complete, failDurableEvent)
340
357
  else complete()
@@ -435,6 +452,11 @@ function runTurn({
435
452
  armCloseGrace()
436
453
  continue
437
454
  }
455
+ // 额度事件(ABI 37):订阅登录的 Claude 每次运行都带回 5 小时窗 / 7 天窗已用比例 —— 喂进额度缓存,
456
+ // 页脚与 /hud 就不必再去打那个常被限流的额度接口。第三方端点 / 自带密钥不是订阅额度,不喂。
457
+ if (raw.type === 'rate_limit_event' && isSubscription(provider)) {
458
+ try { require('./claudeusage').noteRateLimit(raw.rate_limit_info) } catch { /* 额度显示出错不能碰轮次 */ }
459
+ }
438
460
  let ev
439
461
  try { ev = normalizeEvent(raw) } catch (error) { failCore(error); return }
440
462
  if (ev.sessionId) capturedSessionId = ev.sessionId
@@ -572,4 +594,20 @@ function probeVersion(bin = 'claude') {
572
594
  })
573
595
  }
574
596
 
575
- module.exports = { runTurn, isClosedRunnerResult, isClosedRunnerError, buildEnv, normalizeEvent, probeVersion, supportsFlag, BASE_SKILL_ALLOWS }
597
+ // 这一份 provider 跑的是不是本机登录的 Claude 订阅(额度事件只对它有意义)。env 里另指了端点 / 令牌的也不算。
598
+ function isSubscription(provider = {}) {
599
+ const env = provider.env || {}
600
+ return !provider.baseUrl && !provider.apiKey && !env.ANTHROPIC_BASE_URL && !env.ANTHROPIC_AUTH_TOKEN && !env.ANTHROPIC_API_KEY
601
+ }
602
+
603
+ // 空闲整理(ABI 37,核心 idle_compact_plan 决定起不起、保留什么):对同一个会话跑一次 `/compact`。
604
+ // ⚠ 参数必须与平时轮次**完全相同**(引擎用同一组启动参数):工具定义在提示最前面,换了工具集 / MCP / 系统提示附录,
605
+ // 整段会话的缓存全部失效(旧 sbot 11-12 实测换工具集缓存写入 2.6 倍)。输入一律纯文本,不开跟进通道。
606
+ // 停:引擎的 abort signal ⇒ 与普通轮同一条两段式收尾。结果:onEvent 收到 system/compact_boundary(preTokens)与 result。
607
+ function compactSession(opts = {}) {
608
+ const { onSteerReady, onSteerLanded, steer, prompt, onDurableEvent, onTurnAccepted, keep, ...rest } = opts
609
+ if (!keep) return Promise.reject(new Error('compactSession: keep required'))
610
+ return runTurn({ ...rest, requireCloseProof: true, prompt: '/compact ' + keep })
611
+ }
612
+
613
+ module.exports = { runTurn, compactSession, isSubscription, isClosedRunnerResult, isClosedRunnerError, buildEnv, normalizeEvent, probeVersion, supportsFlag, BASE_SKILL_ALLOWS }