claude-slack-channel-bots 0.12.0 → 0.13.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/README.md +86 -24
- package/package.json +1 -1
- package/skills/EXAMPLE_CLAUDE.md +1 -1
- package/skills/debug-slack-channel-bots/SKILL.md +299 -15
- package/skills/setup-slack-channel-bots/SKILL.md +8 -3
- package/slack-app-manifest.yml +21 -0
- package/src/config.ts +130 -21
- package/src/cron-bootstrap.ts +11 -1
- package/src/cron-dispatch.ts +5 -1
- package/src/cron-log.ts +199 -29
- package/src/cron-scheduler.ts +96 -6
- package/src/cron-table-migration.ts +869 -0
- package/src/crontable.ts +3 -0
- package/src/health-check.ts +30 -2
- package/src/live-row-sequence.ts +30 -4
- package/src/permission-poller.ts +532 -183
- package/src/persona-destination-hold.ts +3 -2
- package/src/postinstall.ts +117 -41
- package/src/reload.ts +13 -4
- package/src/restart.ts +399 -36
- package/src/server.ts +71 -7
- package/src/session-manager.ts +438 -41
package/README.md
CHANGED
|
@@ -166,7 +166,8 @@ Runtime options are read from environment variables. None of them is required. S
|
|
|
166
166
|
|
|
167
167
|
| Variable | Description |
|
|
168
168
|
|---|---|
|
|
169
|
-
| `SLACK_STATE_DIR` | Override the directory where `config.json` and runtime state are stored. Defaults to `~/.claude/channels/slack`. |
|
|
169
|
+
| `SLACK_STATE_DIR` | Override the directory where `config.json` and runtime state are stored. Defaults to `~/.claude/channels/slack`. When it is set, the crontable's default moves there too (see `cron_table_path` under [Server-wide settings](#server-wide-settings)). |
|
|
170
|
+
| `XDG_CONFIG_HOME` | The standard XDG configuration directory. With `SLACK_STATE_DIR` unset and `XDG_CONFIG_HOME` an absolute path, the crontable defaults to `$XDG_CONFIG_HOME/cscb/crontab` instead of `~/.config/cscb/crontab`. An empty or relative value is ignored. |
|
|
170
171
|
| `SLACK_DRY_RUN` | Set to `1` (or `true` / `yes`) to start the server without Slack. No credentials file is read and no Slack call is made. Each persona runs with a placeholder identity (`U000DRY_<key>`), and MCP tool calls (`reply`, `react`, etc.) and server notices are logged instead of sent. Useful for integration testing. |
|
|
171
172
|
| `CSCB_LOG_MAX_BYTES` | Rotate `server.log` / `clean_restart.log` when the active file reaches this many bytes. Defaults to `10485760` (10 MiB). Values `<= 0` or non-numeric are ignored. |
|
|
172
173
|
| `CSCB_LOG_KEEP` | Number of rotated generations to retain (`server.log.1` … `server.log.N`). Defaults to `5`. Set to `0` to keep none (the log is truncated instead of rolled). Values `< 0` or non-numeric are ignored. |
|
|
@@ -370,6 +371,8 @@ To rotate a persona's tokens, run the credentials command for it again (see the
|
|
|
370
371
|
|
|
371
372
|
Never put a token in `config.json`, a ticket or a chat.
|
|
372
373
|
|
|
374
|
+
The shipped `slack-app-manifest.yml` also grants the `lists:read` and `lists:write` bot scopes, so a bot can read and write, with its own bot token, a Slack List it has been given access to, such as one list shared by all bots. CSCB itself makes no Lists call. An app created from an earlier manifest must have both scopes added and be re-installed; until then, every Lists call fails with `missing_scope`.
|
|
375
|
+
|
|
373
376
|
#### Channel delivery
|
|
374
377
|
|
|
375
378
|
This section applies in declarative mode; for fungible mode, see [Channel delivery in fungible mode](#channel-delivery-in-fungible-mode). Each channel entry's `delivery` sets which messages in that channel reach the persona:
|
|
@@ -433,7 +436,7 @@ These top-level fields apply to the whole server. A confirmed change to one take
|
|
|
433
436
|
|---|---|---|---|
|
|
434
437
|
| `bind` | string | `"127.0.0.1"` | Interface the HTTP server binds to. Use `"0.0.0.0"` to expose on all interfaces. The in-process cron scheduler delivers via `127.0.0.1`, and [`clear-latch`](#claude-slack-channel-bots-clear-latch) reaches the server there, so `bind` must include loopback (the default, or `0.0.0.0`) for scheduled fires and `clear-latch` to work. |
|
|
435
438
|
| `port` | number | `3100` | Port the HTTP server listens on. |
|
|
436
|
-
| `session_restart_delay` | number | `60` | Seconds to wait before auto-restarting a
|
|
439
|
+
| `session_restart_delay` | number | `60` | Seconds to wait before auto-restarting a session that crashed or disappeared. A session that ended cleanly (an `agent-director pause`, a `/exit`) is relaunched about 3 s after it disconnects instead, unless the persona's last launch failed or it had such a fast relaunch within the last `session_restart_delay` seconds (see "Session not restarting after crash" in [Troubleshooting](#troubleshooting)). Set to `0` to disable auto-restart, clean exits included, and the health check's reconnects (not the retries of a persona agent-director refused; see [Troubleshooting](#troubleshooting)); a persona whose session runs but can't receive messages then gets a *Not connected* or *Not receiving messages* notice at its destination (see [Troubleshooting](#troubleshooting)). At any value, a persona reported as `working` that the server can't prove idle for 10 minutes gets a *Not connected* notice. Must be non-negative. |
|
|
437
440
|
| `health_check_interval` | number | `120` | Seconds between periodic liveness polls. Set to `0` to disable; the retries of a persona agent-director refused still run. Must be non-negative. |
|
|
438
441
|
| `exit_timeout` | number | `120` | Seconds to wait for a managed Claude Code session to exit gracefully during `clean_restart` or `stop --stop-bots` before force-killing it through agent-director. |
|
|
439
442
|
| `stop_timeout` | number | `30` | Seconds to wait for the server process to exit after `SIGTERM` before escalating to `SIGKILL`. |
|
|
@@ -441,15 +444,15 @@ These top-level fields apply to the whole server. A confirmed change to one take
|
|
|
441
444
|
| `append_system_prompt_file` | string | — | Path to a file appended to every managed session's system prompt via `--append-system-prompt-file`. Missing file silently skipped. See `skills/EXAMPLE_CLAUDE.md` for a template. |
|
|
442
445
|
| `system_prompt_mode` | string | `"append"` | Controls how `append_system_prompt_file` is applied. `"append"`: the custom prompt file is appended on top of `CLAUDE.md` (default, current behavior). `"none"`: only `CLAUDE.md` is used; `append_system_prompt_file` is ignored even if set. Use `"none"` when the project's `CLAUDE.md` already contains everything the bot needs. |
|
|
443
446
|
| `cozempic_prescription` | string | `"standard"` | Cozempic cleaning intensity before resume. Valid values: `gentle`, `standard`, `aggressive`. Has no effect if cozempic is not installed. |
|
|
444
|
-
| `message_archive_db` | string | — | Path to a SQLite DB where every inbound Slack message is archived in real time. Parent directories are created if missing; schema is initialized on first open. Compatible with the `archive-messages.py` backfill script — both can write concurrently. Feature is disabled when absent. |
|
|
447
|
+
| `message_archive_db` | string | — | Path to a SQLite DB where every inbound Slack message is archived in real time. Parent directories are created if missing; schema is initialized on first open. Compatible with the `archive-messages.py` backfill script — both can write concurrently. Each channel's name is looked up through the receiving persona's Slack app, which needs the `channels:read` and `groups:read` bot scopes that the shipped `slack-app-manifest.yml` grants. An app created from an earlier manifest stores the channel ID instead, until both scopes are added and the app is re-installed; the `debug-slack-channel-bots` skill has the steps under "The message archive shows channel IDs". A DM's rows keep the DM's `D…` ID whatever the scopes, as a DM has no name; a group DM's rows keep its ID too. Feature is disabled when absent. |
|
|
445
448
|
| `claude_config_dir` | string | — | Default Claude on-disk config directory for every persona. When a persona has one (its own or this default), its session launches with `CLAUDE_CONFIG_DIR='<resolved-path>'` so the bot authenticates against a specific account. `~` is expanded and the path is resolved to absolute. A persona's own `claude_config_dir` overrides this value. When neither is set, Claude's own default applies. Must be non-empty when set. |
|
|
446
|
-
| `resume_enabled` | boolean | `true` | When `true` (default), a bot whose session died — including after a host reboot or pod resume — comes back with its prior conversation history intact instead of starting fresh. When `false`, a bot is never resumed: on startup and on runtime auto-restart it starts a new conversation on the same instance, even when a stored session exists, and its old conversation is kept as an earlier life of that instance. Set `false` as a workaround if your Claude Code version crashes with "sandbox required but unavailable" on resume (a known regression in v2.1.120). |
|
|
449
|
+
| `resume_enabled` | boolean | `true` | When `true` (default), a bot whose session died — including after a host reboot or pod resume — comes back with its prior conversation history intact instead of starting fresh; after 2 resumes in a row that end before the session starts (a transcript Claude Code can't load), it starts fresh instead (see "Session not restarting after crash" under [Troubleshooting](#troubleshooting)). When `false`, a bot is never resumed: on startup and on runtime auto-restart it starts a new conversation on the same instance, even when a stored session exists, and its old conversation is kept as an earlier life of that instance. Set `false` as a workaround if your Claude Code version crashes with "sandbox required but unavailable" on resume (a known regression in v2.1.120). |
|
|
447
450
|
| `agent_director_poll_interval_ms` | number | `1000` | Poll interval (ms) for the agent-director permission relay tick. Must be a positive integer in `[200, 3_600_000]`. Replaces the pre-rename `claude_director_poll_interval_ms` — the old name is rejected at startup. |
|
|
448
451
|
| `agent_director_call_timeout_ms` | number | `60000` | How long (ms) CSCB waits on each agent-director call it makes for its personas, in the server and in `stop --stop-bots` and `clean_restart`. Must be an integer in `[1000, 3_600_000]`; out-of-range values are rejected at load. It should be greater than the need that agent-director's timing settings give; see [Sizing the agent-director call timeout](#sizing-the-agent-director-call-timeout). |
|
|
449
452
|
| `stop_hook_bootstrap` | boolean | `true` | Default for every persona: whether the persona gets the Slack Reply Guard reminder (see [Slack Reply Guard (Stop hook)](#slack-reply-guard-stop-hook)). The value applies per persona, from the persona's first launch after the change is applied (see [Reload](#reload)). A persona's own `stop_hook_bootstrap` overrides this value. Non-boolean values are rejected at startup. |
|
|
450
|
-
| `cron_table_path` | string |
|
|
451
|
-
| `cron_log_path` | string | `<config dir>/cron.log` | Path to the `cscb_cron` log file. Defaults to `cron.log` in the directory of the loaded `config.json
|
|
452
|
-
| `cron_log_max_bytes` | number | — | Size cap in bytes for the cron log.
|
|
453
|
+
| `cron_table_path` | string | `~/.config/cscb/crontab` | Path to the crontable for the built-in cron scheduler (`cscb_cron`). When absent, it is `~/.config/cscb/crontab` (`$XDG_CONFIG_HOME/cscb/crontab` when `XDG_CONFIG_HOME` is an absolute path), outside `~/.claude`, so a bot's edit is not a write under `~/.claude`, which Claude Code asks the user to approve. With `SLACK_STATE_DIR` set, the default is `crontab` in the directory of the loaded `config.json` instead. A path you set is used as written, with `~` expanded like other path keys, even when it names `~/.config/cscb/crontab`: the server never creates its directory or moves a crontable to it. The resolved path is exported into every managed session as `CSCB_CRONTABLE_PATH` so bots can find the crontable and self-schedule (see [Scheduled Prompts](#scheduled-prompts-cscb_cron)). Must be a non-empty string when set. |
|
|
454
|
+
| `cron_log_path` | string | `<config dir>/cron.log` | Path to the `cscb_cron` log file. Defaults to `cron.log` in the directory of the loaded `config.json`, wherever the crontable is. `~` is expanded like other path keys. Must be a non-empty string when set. |
|
|
455
|
+
| `cron_log_max_bytes` | number | — | Size cap in bytes for the cron log. Once a line takes the log past the cap, the server drops its oldest whole lines until it fits. The newest line is always kept, even when it alone is larger than the cap. Nothing is rotated or archived, so dropped lines are gone. Absent means no cap: the log only grows. Must be a positive integer when set. |
|
|
453
456
|
| `ack_reaction` | string | — | Emoji name, without colons (for example `"eyes"`), of the acknowledgement reaction. When set, each persona a message is dispatched to adds the reaction under its own Slack identity once the message has reached its instance, not on receipt, so a message that reaches several personas carries one reaction per persona. A message that isn't dispatched gets no reaction: one lost because the persona's instance is down, or one the persona doesn't receive. Messages sent through `/interject` or cron aren't Slack messages and get none either. A persona's first `reply` in that conversation carrying the message's `message_id` removes that persona's reaction only; other personas' reactions stay until they reply. Absent means no acknowledgement. Must be non-empty when set. |
|
|
454
457
|
| `reply_chunk_limit` | number | `4000` | Maximum characters per posted message: the `reply` tool splits longer text into several messages. Must be a positive integer. |
|
|
455
458
|
| `reply_chunk_mode` | `"length"` \| `"newline"` | `"newline"` | How the `reply` tool splits text longer than `reply_chunk_limit`. `length`: hard split at the limit. `newline`: split at newline boundaries within the limit; a single line longer than the limit is posted whole. |
|
|
@@ -1167,6 +1170,8 @@ To install the version of CSCB sitting in your working copy (so the globally-lin
|
|
|
1167
1170
|
|
|
1168
1171
|
`bun install -g .` (and the equivalent `bun install -g <local-path>`) is broken on Bun 1.3.13 — it inserts an invalid empty-string dependency key into `~/.bun/install/global/package.json` and then any subsequent global op fails with `error: Package "@" has a dependency loop` (upstream: [oven-sh/bun#24207](https://github.com/oven-sh/bun/issues/24207)). The script uses `bun add -g file:<abs-path>` instead, and pre-emptively strips any empty-string entry a prior `bun install -g .` may have already left behind.
|
|
1169
1172
|
|
|
1173
|
+
Bun runs this install's postinstall in the worktree itself, a git checkout, so the postinstall writes the skeleton config files but leaves `~/.claude/skills/` alone: it makes no new link into the worktree, and a link already there keeps pointing where it did. While this install is in place, though, the global package `~/.bun/install/global/node_modules/claude-slack-channel-bots` resolves into your worktree, so a `debug-slack-channel-bots` link into it, as an install of the published package makes, reads the worktree's copy of the skill until you reinstall the published package ([Quick Start](#quick-start)).
|
|
1174
|
+
|
|
1170
1175
|
### Direct invocation for development
|
|
1171
1176
|
|
|
1172
1177
|
Skip the CLI and run the server directly with Bun for development or debugging:
|
|
@@ -1287,12 +1292,18 @@ The server fires scheduled prompts into personas once per minute, reading them f
|
|
|
1287
1292
|
|
|
1288
1293
|
### The crontable
|
|
1289
1294
|
|
|
1290
|
-
Schedules live in the crontable file at `cron_table_path
|
|
1295
|
+
Schedules live in the crontable file at `cron_table_path`. Without that key the crontable is at `~/.config/cscb/crontab` (`$XDG_CONFIG_HOME/cscb/crontab` when `XDG_CONFIG_HOME` is an absolute path), outside `~/.claude`, so a bot appending a schedule doesn't trigger Claude Code's approval prompt for writes under `~/.claude`. With `SLACK_STATE_DIR` set, the default is `crontab` beside `config.json` in that directory. Set `cron_table_path` in `config.json` to put it anywhere else (see [Server-wide settings](#server-wide-settings)).
|
|
1291
1296
|
|
|
1292
1297
|
```sh
|
|
1293
|
-
cat ~/.
|
|
1298
|
+
cat ~/.config/cscb/crontab
|
|
1294
1299
|
```
|
|
1295
1300
|
|
|
1301
|
+
At start the server creates the default crontable's directory. It creates the file on first boot if it is absent, with a self-documenting comment header describing the line format. See [Crontable format](#crontable-format) below for the full reference. For a `cron_table_path` you set, the directory must already exist.
|
|
1302
|
+
|
|
1303
|
+
If the server starts with the default and finds a crontable at `crontab` beside `config.json` (`~/.claude/channels/slack/crontab`), it moves that file to the default location, its lines unchanged. It leaves a symbolic link to the new file at the old path, so a resumed session whose `CSCB_CRONTABLE_PATH` still names the old path appends to the same file. A `prompts/` directory beside the old crontable moves with it and leaves a link at its old name, so relative prompt paths keep naming the same files.
|
|
1304
|
+
|
|
1305
|
+
If both paths hold a crontable, the server uses the one at the default location and logs a warning. Nothing is moved when `config.json` sets `cron_table_path`, even to `~/.config/cscb/crontab`. The `debug-slack-channel-bots` skill covers each line the move logs.
|
|
1306
|
+
|
|
1296
1307
|
### Crontable format
|
|
1297
1308
|
|
|
1298
1309
|
On first boot the server auto-creates the crontable with this self-documenting header:
|
|
@@ -1329,7 +1340,7 @@ On first boot the server auto-creates the crontable with this self-documenting h
|
|
|
1329
1340
|
# Example (every day at 09:00, run grooming-tick.md, target two personas):
|
|
1330
1341
|
# 0 9 * * * /home/horde/prompts/grooming-tick.md planner,reviewer
|
|
1331
1342
|
#
|
|
1332
|
-
# Example (every hour
|
|
1343
|
+
# Example (every hour, run standup.md, no persona list — NOT delivered yet):
|
|
1333
1344
|
# 0 * * * * /home/horde/prompts/standup.md
|
|
1334
1345
|
```
|
|
1335
1346
|
|
|
@@ -1344,7 +1355,7 @@ Rules:
|
|
|
1344
1355
|
- **Exactly 5 cron fields** (minute hour day-of-month month day-of-week). Croner's 6-field (seconds-precision) and `@macro` forms are **not** supported.
|
|
1345
1356
|
- **Name each persona by its name or its key.** A persona named twice — by name, by key, or both — receives the prompt once.
|
|
1346
1357
|
- **Use the key for a name with whitespace or a comma.** Such a name cannot be written in a crontable line.
|
|
1347
|
-
- **
|
|
1358
|
+
- **Give every line a persona list.** A line with no persona list is the all-bots form, and all-bots delivery is not built yet: the line loads, but each load logs a `warn` naming it and each fire is dropped (see [The cron log](#the-cron-log)).
|
|
1348
1359
|
- **No `*` wildcard in the persona position.** A literal `*` where a persona belongs is a parse error, not "all personas".
|
|
1349
1360
|
- **Prompt paths cannot contain spaces.** A path with spaces is unrepresentable; the extra tokens make the line a parse error and it is skipped.
|
|
1350
1361
|
- **`#` comments and blank lines are allowed** and ignored.
|
|
@@ -1360,14 +1371,14 @@ Here the 6th field (`1`) is taken as the prompt path and the real path (`~/promp
|
|
|
1360
1371
|
|
|
1361
1372
|
**Upgrading a crontable that names channels.** The server never rewrites an existing crontable, so channel-ID targets are not converted. Once your config defines personas, a Slack channel ID no longer names a bot, so edit each such line to a persona name or key; until you do, the target logs `unknown-persona`.
|
|
1362
1373
|
|
|
1363
|
-
An existing crontable also keeps its old comment header, which still tells bots to write channel IDs. Replace that comment block by hand with the header shown above.
|
|
1374
|
+
An existing crontable also keeps its old comment header, which still tells bots to write channel IDs and to leave out the target list to reach all bots. Replace that comment block by hand with the header shown above. This section is the reference for the line format, whatever a crontable's own header says.
|
|
1364
1375
|
|
|
1365
1376
|
### Path resolution
|
|
1366
1377
|
|
|
1367
1378
|
The prompt-file path resolves as follows:
|
|
1368
1379
|
|
|
1369
1380
|
- A leading `~` expands to the home directory.
|
|
1370
|
-
- A **relative** path resolves against the **crontable's own directory** — not `$HOME`. This is a deliberate divergence from system cron's convention, so you can keep a `prompts/` directory alongside the crontable and reference it as `prompts/standup.md`.
|
|
1381
|
+
- A **relative** path resolves against the **crontable's own directory** — not `$HOME`. This is a deliberate divergence from system cron's convention, so you can keep a `prompts/` directory alongside the crontable and reference it as `prompts/standup.md`. With the default location that directory is `~/.config/cscb/prompts/`.
|
|
1371
1382
|
- An **absolute** path is used as-is.
|
|
1372
1383
|
|
|
1373
1384
|
### How fires appear
|
|
@@ -1376,7 +1387,9 @@ A scheduled fire reaches only the target persona's instance, as an `/interject`
|
|
|
1376
1387
|
|
|
1377
1388
|
### The cron log
|
|
1378
1389
|
|
|
1379
|
-
Every fire outcome is recorded in the cron log at `cron_log_path` (default `<config dir>/cron.log
|
|
1390
|
+
Every fire outcome is recorded in the cron log at `cron_log_path` (default `<config dir>/cron.log`, beside `config.json` wherever the crontable is; override with the `cron_log_path` key). Each attempt writes one line per persona targeted (showing the target as first written in the crontable), plus a per-fire summary line carrying `delivered=N failed=M` counts. The log is plain text, so `grep no-session cron.log` yields readable lines.
|
|
1391
|
+
|
|
1392
|
+
By default the log only grows. To cap it, set `cron_log_max_bytes` in `config.json`: once the log passes that many bytes, its oldest whole lines are dropped, so the oldest fires disappear from it while the newest stay. There is no rotated copy; copy the log elsewhere if you need a longer history. Don't move or delete it yourself: the server keeps writing to the old file until that file passes the cap. A changed cap takes effect at the next server start.
|
|
1380
1393
|
|
|
1381
1394
|
The `outcome` field of each line is one of these classes:
|
|
1382
1395
|
|
|
@@ -1388,18 +1401,32 @@ The `outcome` field of each line is one of these classes:
|
|
|
1388
1401
|
| `prompt-missing` | The prompt file did not exist at fire time. | Create the file or correct its path in the crontable. |
|
|
1389
1402
|
| `prompt-unreadable` | The prompt file existed but could not be read (see the `errno`). | Fix file permissions or the path. |
|
|
1390
1403
|
| `prompt-oversize` | The prompt exceeds the 32KB `/interject` cap and was skipped, never truncated. | Shorten the prompt file. |
|
|
1391
|
-
| `parse-error` | The crontable line could not be parsed. | Fix the line — see
|
|
1404
|
+
| `parse-error` | The crontable line could not be parsed. | Fix the line — see [Crontable format](#crontable-format). |
|
|
1392
1405
|
| `http-error` | The localhost POST hit an unexpected HTTP status or a network failure. | Check that the server is listening on loopback (see the `bind` note below) and inspect the `errno`/`status` in the line. |
|
|
1393
|
-
| `fanout-deferred` | A line with no persona list (all-bots) was matched but not delivered. |
|
|
1406
|
+
| `fanout-deferred` | A line with no persona list (all-bots) was matched but not delivered. | Add a persona list to the line — all-bots delivery is not built yet. |
|
|
1407
|
+
|
|
1408
|
+
Each load of the crontable, at start and after each edit, writes one line with its counts. At start it is `scheduler started, N schedules loaded, M undeliverable (no persona list)`; after an edit it is `crontable reloaded, N schedules, M undeliverable (no persona list)`. `M` counts the lines that will never be delivered, and it is written even when it is 0.
|
|
1409
|
+
|
|
1410
|
+
Each load also writes one `warn` line for each line with no persona list, naming its line number in the file and its prompt path:
|
|
1411
|
+
|
|
1412
|
+
```text
|
|
1413
|
+
2026-10-09T15:12:00.002Z cscb-cron:grooming - warn line=34 prompt=/home/horde/prompts/grooming.md undeliverable: this crontable line names no personas, so it will never be delivered in this version …
|
|
1414
|
+
```
|
|
1415
|
+
|
|
1416
|
+
Add a persona list to that line. The warning comes back at every start and every edit until you do; an unchanged crontable is not reloaded, so it is not repeated each minute. To list them in the cron log at its default location:
|
|
1417
|
+
|
|
1418
|
+
```sh
|
|
1419
|
+
grep undeliverable ~/.claude/channels/slack/cron.log
|
|
1420
|
+
```
|
|
1394
1421
|
|
|
1395
1422
|
### Delivery semantics
|
|
1396
1423
|
|
|
1397
1424
|
- **No retry.** A failed fire is logged and dropped — never queued or re-sent. A persona that is not up, or has no live session, fails every fire until it is up and its session is running again; the server does not queue the missed prompts.
|
|
1398
|
-
- **Missed fires are skipped, not caught up.** While the server is down, no scheduled prompts fire, and they are not replayed on restart. The `scheduler started, N schedules loaded` line in the cron log marks when scheduling resumed, bounding the outage window.
|
|
1425
|
+
- **Missed fires are skipped, not caught up.** While the server is down, no scheduled prompts fire, and they are not replayed on restart. The `scheduler started, N schedules loaded, M undeliverable (no persona list)` line in the cron log marks when scheduling resumed, bounding the outage window.
|
|
1399
1426
|
- **Edits take effect within a minute — no restart.** The scheduler checks the crontable fresh on every tick, so an edit by hand or a line appended by a bot starts (or stops) firing within about a minute. The server is never restarted for a schedule change.
|
|
1400
1427
|
- **Deleting the crontable stops all schedules.** Nothing fires from that moment, a WARN appears in the cron log, and the server re-creates the file empty (with its header) within about two minutes — detection and re-creation happen on separate once-a-minute passes, so the re-create lands up to two tick boundaries after the deletion. Add lines back and they schedule on the next check.
|
|
1401
1428
|
- **Server-local time.** Cron expressions are evaluated in the server's local timezone.
|
|
1402
|
-
- **
|
|
1429
|
+
- **A line without a persona list is never delivered.** Each load logs a `warn` naming it and counts it undeliverable; each time it matches, it is logged `fanout-deferred` and dropped. Give the line a persona list to have it fire.
|
|
1403
1430
|
- **`bind` must include loopback.** The scheduler delivers via `127.0.0.1`, so a `bind` set to a single non-loopback interface makes every fire fail with `http-error`. Use the default `127.0.0.1` or `0.0.0.0`.
|
|
1404
1431
|
|
|
1405
1432
|
### Bot self-scheduling
|
|
@@ -1432,6 +1459,8 @@ Flow:
|
|
|
1432
1459
|
4. The operator clicks Allow / Deny in Slack, in the channel or the DM; the buttons work the same in both. CSCB resolves the click through the same persona: it calls `client.decide({ claude_instance_id, decision, request_token })` and, as that persona, updates the message to "*Permission* — Allowed" or "*Permission* — Denied by operator".
|
|
1433
1460
|
5. If a tracked prompt closes for any reason other than a Slack click, the next poller tick replaces the buttons with the verdict: "⏱ *Permission* — Timed out", "🪦 *Permission* — Session ended", "*Permission* — Allowed", "*Permission* — Denied by operator", or "*Permission* — Denied (closed)" when the reason is unknown.
|
|
1434
1461
|
|
|
1462
|
+
The prompt names the tool and shows its input: Bash's command, Edit's or Write's file path, or for any other tool the input as JSON, up to 500 characters. An input too long for Slack's 3000-character limit on a message section is cut to fit and followed by `… (truncated, <shown> of <total> chars shown)`; the rest is not shown in Slack.
|
|
1463
|
+
|
|
1435
1464
|
While a persona is not up (broken or retrying), its instance keeps running but its permission prompts are not posted and its already-posted prompts are left as they are. They appear, or get their verdict, on the first poll after the persona comes up.
|
|
1436
1465
|
|
|
1437
1466
|
If Slack refuses a post to the persona's destination, see "A permission prompt or notice doesn't arrive" under [Troubleshooting](#troubleshooting).
|
|
@@ -1662,7 +1691,7 @@ grep -E '\(key=<key>\)|persona=<key>\b' ~/.claude/channels/slack/server.log
|
|
|
1662
1691
|
|
|
1663
1692
|
While a persona is down, its Claude instance keeps running and keeps its history, but the server doesn't serve it until the persona is up.
|
|
1664
1693
|
|
|
1665
|
-
The `debug-slack-channel-bots` skill has an entry for every persona log class, every `config.json` rejection and each recovery step. It ships in the package at `skills/debug-slack-channel-bots/SKILL.md`, and postinstall links it into `~/.claude/skills/debug-slack-channel-bots`; a copied directory or file already at that path is left in place, and postinstall logs `skipped: <path> (not a link; …)` — remove it and re-run postinstall to get the link. Invoke `/debug-slack-channel-bots` from Claude Code.
|
|
1694
|
+
The `debug-slack-channel-bots` skill has an entry for every persona log class, every `config.json` rejection and each recovery step. It ships in the package at `skills/debug-slack-channel-bots/SKILL.md`, and the postinstall of an installed package (a global install) links it into `~/.claude/skills/debug-slack-channel-bots`; a copied directory or file already at that path is left in place, and postinstall logs `skipped: <path> (not a link; …)` — remove it and re-run postinstall to get the link. A postinstall run in a git checkout or worktree (including [an install from a local worktree](#installing-from-a-local-worktree)), outside a `node_modules` directory or with `CI` set leaves `~/.claude/skills/` alone and logs `skipped: <home>/.claude/skills (skill links left alone: <reason>)`. Invoke `/debug-slack-channel-bots` from Claude Code.
|
|
1666
1695
|
|
|
1667
1696
|
**A persona's instance waits at a startup prompt**
|
|
1668
1697
|
After each launch of a persona's instance (a start, a restart, a resume or a confirmed change's bring-up), the server watches it through agent-director while it starts. It does the same after a launch that timed out, once it finds that launch's instance still starting (see "A persona's instance is still starting" below). When Claude Code's folder-trust or development-channels prompt shows, the server presses Enter to accept it. It types nothing else, and nothing at all when neither prompt shows; the server runs no tmux command for this.
|
|
@@ -1832,6 +1861,39 @@ The bot is wedged in `check_permission` on a native Claude Code TUI prompt that
|
|
|
1832
1861
|
**Session not restarting after crash**
|
|
1833
1862
|
Auto-restart backs off exponentially on repeated launch failures — the delay doubles from `session_restart_delay` (default 60s) on each consecutive failure, up to a 15-minute ceiling. After 5 consecutive failures the persona hits a cap: a `SpawnCapReached` notice naming the persona is posted to the persona's destination (a channel or a DM with its contact, per its `permission_prompts` in declarative mode or its `invited.permission_prompts` in fungible mode) and automatic restarts stop. A launch that agent-director refuses is not handled by this backoff and cap: it is not counted, and the persona is retried on its own instead, even with `session_restart_delay` set to `0` (see "A persona is retried after agent-director refuses it" below).
|
|
1834
1863
|
|
|
1864
|
+
A session that ended cleanly is not treated as a crash. About 3 s after a persona's session disconnects, the server checks its row with agent-director: if the row reads `ended` (Claude Code exited through `/exit`, which is what `agent-director pause` sends), the persona is relaunched then instead of after `session_restart_delay`. Any other reading keeps the full delay: `missing` (a crash or a kill), no row, a session still running or starting, or no answer from agent-director.
|
|
1865
|
+
|
|
1866
|
+
There is no such check while the persona's last launch failed, so failed launches still back off and cap; nothing is logged for that. There is none either within `session_restart_delay` of the persona's last fast relaunch (the loop guard), so a session that exits right after each relaunch is relaunched at most once per `session_restart_delay`.
|
|
1867
|
+
|
|
1868
|
+
The check and the loop guard log these lines in `server.log`:
|
|
1869
|
+
|
|
1870
|
+
```sh
|
|
1871
|
+
grep b.4vm ~/.claude/channels/slack/server.log
|
|
1872
|
+
```
|
|
1873
|
+
|
|
1874
|
+
- `[slack] Session for persona=<key> ended cleanly (agent-director reads its row ended) — relaunching now instead of in <N>s (b.4vm)`: the clean exit is relaunched now.
|
|
1875
|
+
- `[slack] Clean-exit check for persona=<key>: <why> — the full <N>s restart delay stands (b.4vm)`: the row didn't read `ended` (`<why>` names what it read, for example `its row does not read ended (dead: the row missing)` after a crash), or the restart was stopped before the read; the restart waits the full delay.
|
|
1876
|
+
- `[slack] No clean-exit check for persona=<key> — its last clean-exit relaunch ran <S>s ago, within session_restart_delay (<base>s); the full <N>s restart delay stands (the clean-exit loop guard: at most one clean-exit relaunch per session_restart_delay) (b.4vm)`: the persona had a fast relaunch less than `session_restart_delay` ago, so the restart waits the full delay. The loop guard runs when the restart is scheduled, before the server reads why the session ended, so this line follows a crash too.
|
|
1877
|
+
|
|
1878
|
+
A resume counts only once its session starts. agent-director's resume checks only that the persona's transcript file exists, and Claude Code exits at once when the transcript holds no conversation it can load: an empty file, one that isn't JSON, or one with only metadata lines. Such a resume is a failed launch once the server reads its row `ended` or `missing`, or finds no row for it. A resume that the server's own kill of the instance ends is not counted.
|
|
1879
|
+
|
|
1880
|
+
A restart's resume that fails this way counts toward the restart limit above. One made when the persona is brought up (at a start or for a confirmed change) does not, unless a restart joins it while it is still starting.
|
|
1881
|
+
|
|
1882
|
+
After 2 such resumes in a row, the server stops resuming that transcript: the persona's next launch brings it up fresh on the same instance, without its conversation. Nothing is deleted: the transcript stays on disk, and the old conversation is kept as an earlier life of the instance.
|
|
1883
|
+
|
|
1884
|
+
Below the restart limit nothing is posted to Slack; `server.log` shows these lines:
|
|
1885
|
+
|
|
1886
|
+
```sh
|
|
1887
|
+
grep b.4q8 ~/.claude/channels/slack/server.log
|
|
1888
|
+
```
|
|
1889
|
+
|
|
1890
|
+
- `[slack] <site>: <read> for "<name>" (key=<key>): its resumed launch ended before reporting in (state=<ended|missing|no-row>) — <n> resume(s) in a row ended so; at 2 the ladder makes a reuse spawn in place of a resume (b.4q8)`: a resume ended before its session started (the persona may be named `persona=<key>` instead).
|
|
1891
|
+
- `[slack] Resumed launch failed for persona=<key>: its row ended before reporting in — counted (b.4q8)`: that resume counted as a failed launch.
|
|
1892
|
+
- `[slack] Restart for persona=<key> goes no further <before its kill|before its launch> — the persona reached the restart cap while this restart ran; nothing more is killed or launched for it (b.4q8)`, or `[slack] live-row-sequence: not launching "<name>" (key=<key>) — the persona reached the restart cap while its sequence ran; …`: that failed launch took the persona to the restart limit, so nothing more is launched for it.
|
|
1893
|
+
- `[slack] spawnForPersona: "<name>" (key=<key>): its last <n> resumes in a row ended before reporting in — not resuming its transcript again: a reuse spawn of the same id brings it up fresh, without its conversation; nothing is deleted (b.4q8)`: the persona is brought up fresh.
|
|
1894
|
+
|
|
1895
|
+
The `debug-slack-channel-bots` skill says how to check the transcript (see [A persona's resumed launch ends before it reports in](skills/debug-slack-channel-bots/SKILL.md#a-personas-resumed-launch-ends-before-it-reports-in)).
|
|
1896
|
+
|
|
1835
1897
|
The server never kills an instance it has just read as ended or missing, or found gone: such an instance needs no kill, and a kill then could end a launch someone started after that read. So a restart that finds the instance ended, missing or gone relaunches the persona with no kill, and so does one whose health check found no proof the old instance is gone (agent-director refused the reconnect, or had already finished the row or had no row for it); `server.log` shows `No kill before the launch for persona=<key>` or `No kill before the relaunch for persona=<key>` before it. The server kills the persona's old instance before a relaunch only when agent-director answers the restart's check that its own install has disappeared, which reads nothing of the instance, and then checks the result. The relaunch follows only a kill that succeeded, or one that found the instance already gone; after any other result nothing is relaunched over the old instance and nothing counts toward the restart limit: the persona is retried on its own, or held for a human (see the *Held:* entries below). `server.log` shows `Session kill for persona=<key> did not succeed (…)` for such a kill. When agent-director reports that it could not end the old worker, the persona's destination also gets a *Kill failed* notice (see "A persona posts a *Kill failed* or *Process outlived kill* notice" below).
|
|
1836
1898
|
|
|
1837
1899
|
A message sent to a persona whose instance can't take it is lost, whether it came from the persona's own channel, a shared channel or a DM: it is not delivered, saved or replayed later, and nothing else is posted where it was sent. Instead, the persona posts one lost-message notice to its destination (its destination channel, set by `permission_prompts` in declarative mode or `invited.permission_prompts` in fungible mode, or its DM with its contact). That notice is the only post; it lands in the conversation the message came from only when that conversation is the destination.
|
|
@@ -1852,7 +1914,7 @@ The notice reports the first state that applies, in the table's order.
|
|
|
1852
1914
|
| `kill failed` | The server could not end a worker of the persona, which may still be running; no restart was started. Either the persona's own old worker could not be ended and the server posted a *Kill failed* notice, or the persona is waiting on an old instance (one still running in its working directory, or its own earlier instance) that a confirmed change's teardown, a server start's clean-up or the server's later kill could not end. For such an old instance nothing is posted: the *Kill failed* text is in `startup-errors.log` (see [Startup errors](#startup-errors)). It applies until agent-director reports that worker's row ended or missing, or no longer has it. A persona that is also held for a human reports `held for a human`. | Follow the *Kill failed* notice, or for an old instance the *Kill failed* text in `startup-errors.log` (see "A persona posts a *Kill failed* or *Process outlived kill* notice" below), then resend once the persona is back. |
|
|
1853
1915
|
| `not answering` | agent-director or tmux is not answering for the persona, tmux is not available for it, or agent-director refuses its config file, and the server is retrying the persona on its own; no restart was started. It applies only while those retries are running: a persona at its restart limit reports `restart limit reached` instead, whatever is wrong with tmux or agent-director, and one whose retries have stopped reports the next state that applies. | Follow the persona's own notice (see "A persona posts a *Not answering*, *Still not answering* or *Answering again* notice", "A persona posts a *tmux unavailable* or *tmux server changed* notice" and "A persona posts an *agent-director refuses its config file* notice" below), then resend once it's back. |
|
|
1854
1916
|
| `starting` | The persona's session is starting but has not come up yet, so no restart was started: a launch of it is running, or the server checked with agent-director once, when it found the message lost, and the session was still starting. A check that fails never reports `starting`. | Resend once the persona is up. |
|
|
1855
|
-
| `restarting` | A restart of the persona's instance was already under way, or the server is replacing the persona's old instance, still running, on the same instance: with a new conversation, for a relaunch that can't resume it because `resume_enabled` is false or its working directory or config directory changed; or keeping its conversation, for a relaunch after its session was found gone while agent-director still reads it as running. It is also reported while the server is ending an old instance the persona waits on (see "A persona waits on an old instance in its working directory" below), even while the persona's own row reads `pending`. A message lost while the old instance is being replaced or ended starts no restart of its own. | Resend the message (or ask the sender to) once the persona is back. |
|
|
1917
|
+
| `restarting` | A restart of the persona's instance was already under way, or the server is replacing the persona's old instance, still running, on the same instance: with a new conversation, for a relaunch that can't resume it because `resume_enabled` is false or its working directory or config directory changed; or keeping its conversation, for a relaunch after its session was found gone while agent-director still reads it as running. It is also reported while the server is ending an old instance the persona waits on (see "A persona waits on an old instance in its working directory" below), even while the persona's own row reads `pending`. A message lost while the old instance is being replaced or ended starts no restart of its own. A restart after a clean exit, such as `agent-director pause`, relaunches the persona about 3 s after its session disconnected; one after a crash waits `session_restart_delay` first (see above). | Resend the message (or ask the sender to) once the persona is back. |
|
|
1856
1918
|
| `auto-restart disabled` | `session_restart_delay` is `0`, so no restart was started. A persona the server is already retrying on its own (see "A persona is retried after agent-director refuses it" below) comes back when a retry succeeds; otherwise a server restart recovers it. | If the persona isn't being retried, restart the server; resend once it's back. |
|
|
1857
1919
|
| `restart limit reached` | The persona is capped; automatic restarts are suspended. It takes precedence over `not answering`: a capped persona reports it even while tmux or agent-director is failing for it, unless a restart is already under way, auto-restart is disabled, or its session is starting. | Restart the server to clear the limit, then resend. |
|
|
1858
1920
|
| `starting now` | The lost message triggered a fast restart (the backoff delay is clamped down to 5 seconds, never raised). A persona waiting on an old instance in its working directory comes up only once that instance has ended (see "A persona waits on an old instance in its working directory" below). | Resend in a moment, once the persona is back. |
|
|
@@ -1925,7 +1987,7 @@ Only that proof, that the bot's session is gone, leads the server to end a sessi
|
|
|
1925
1987
|
|
|
1926
1988
|
A closely related symptom is a bot that still *looks* connected but silently drops every inbound message — its underlying message stream went away without the connection registering as closed. The same health-check path recovers this on the same two-consecutive-tick cadence, so no inbound message or server restart is needed; with `session_restart_delay` set to `0` the persona's destination gets a *Not receiving messages* notice instead. A message for such a persona that arrives before recovery lands is lost, and the persona's destination gets a lost-message notice as described under "Session not restarting after crash" above.
|
|
1927
1989
|
|
|
1928
|
-
To verify recovery in the field, tail `server.log` for a stranded persona and confirm a tick-driven recovery lands — look for a `[slack] Scheduling restart for persona=<key> in <N>s (backoff)` line and a `[slack] Session alive but disconnected — reconnecting MCP for persona=<key>` line naming that persona's key. When the bot is mid-turn, with its busy spinner or an API retry message on screen, that line is followed by `[slack] reconnectSession: persona=<key> is working — deferring /mcp reconnect to a later tick (b.9a7/b.rmy)`, and a later tick reconnects it. A bot reported `working` whose screen sits idle and whose transcript ends with a finished reply logs `[slack] reconnectSession: persona=<key> is working; its pane shows an idle screen (no busy indicator, no prompt) and its transcript ends with a completed turn, both unchanged for <N>s of the 60s needed — deferring /mcp reconnect to a later tick, which reads them again (b.f2b)` instead, and then, once both have stayed the same for a minute, `[slack] reconnectSession: persona=<key> reads working, but its pane has shown the same idle screen (no busy indicator, no prompt) and its transcript has ended with a completed turn, both unchanged, for <N>s — treating the row as stale and reconnecting (b.f2b)`. An idle screen whose transcript doesn't end with a finished reply, or can't be read, logs `[slack] reconnectSession: persona=<key> is working and its pane shows an idle screen, but <why> — no idle evidence; deferring /mcp reconnect to a later tick (b.f2b/b.rmy)` and is not reconnected. A bot killed mid-turn logs `[slack] reconnectSession: persona=<key> is working but agent-director's read-pane found no pane of its launch: <error> — not deferring; sweeping and escalating (escalate-dead); the restart path's re-probe decides (read-pane class=<CLASS>; b.d61, b.jg5 SRJ-603)` instead, then `[slack] escalate-dead: persona=<key> verdict=working-tmux-gone — agent-director's read-pane found no pane of the row's launch (GONE) on its working row, triggering internal findMissing reconciliation (the row may stay live for further ticks, each escalate-dead tick sweeping again; ~/startup/find-missing-loop.sh is belt-and-braces)`. The server then checks the row again, and that check decides: once agent-director reads the row dead it logs `[slack] Session reads dead after escalate-dead reconciliation — relaunching in this restart run for persona=<key> (b.d61)` and `[slack] Relaunching session for persona=<key> cwd="<path>"` right after; while agent-director still reads the row live it logs `[slack] Session still reads live after escalate-dead — no relaunch in this restart run for persona=<key>; the row may stay live for further ticks, each escalate-dead tick sweeping again`, relaunches nothing, and the row is swept again at later health checks until it reads dead (see "A persona posts a *Slow recovery* notice" below). A **capped** persona is exempt: the health-check skips it entirely, including reconnects, so a capped persona still requires a server restart (see above).
|
|
1990
|
+
To verify recovery in the field, tail `server.log` for a stranded persona and confirm a tick-driven recovery lands — look for a `[slack] Scheduling restart for persona=<key> in <N>s (backoff)` line; about 3 s later, `[slack] Clean-exit check for persona=<key>: its row does not read ended (live) — the full <N>s restart delay stands (b.4vm)`, since the session still runs and the reconnect waits the full delay (see "Session not restarting after crash" above); and then a `[slack] Session alive but disconnected — reconnecting MCP for persona=<key>` line naming that persona's key. When the bot is mid-turn, with its busy spinner or an API retry message on screen, that line is followed by `[slack] reconnectSession: persona=<key> is working — deferring /mcp reconnect to a later tick (b.9a7/b.rmy)`, and a later tick reconnects it. A bot reported `working` whose screen sits idle and whose transcript ends with a finished reply logs `[slack] reconnectSession: persona=<key> is working; its pane shows an idle screen (no busy indicator, no prompt) and its transcript ends with a completed turn, both unchanged for <N>s of the 60s needed — deferring /mcp reconnect to a later tick, which reads them again (b.f2b)` instead, and then, once both have stayed the same for a minute, `[slack] reconnectSession: persona=<key> reads working, but its pane has shown the same idle screen (no busy indicator, no prompt) and its transcript has ended with a completed turn, both unchanged, for <N>s — treating the row as stale and reconnecting (b.f2b)`. An idle screen whose transcript doesn't end with a finished reply, or can't be read, logs `[slack] reconnectSession: persona=<key> is working and its pane shows an idle screen, but <why> — no idle evidence; deferring /mcp reconnect to a later tick (b.f2b/b.rmy)` and is not reconnected. A bot killed mid-turn logs `[slack] reconnectSession: persona=<key> is working but agent-director's read-pane found no pane of its launch: <error> — not deferring; sweeping and escalating (escalate-dead); the restart path's re-probe decides (read-pane class=<CLASS>; b.d61, b.jg5 SRJ-603)` instead, then `[slack] escalate-dead: persona=<key> verdict=working-tmux-gone — agent-director's read-pane found no pane of the row's launch (GONE) on its working row, triggering internal findMissing reconciliation (the row may stay live for further ticks, each escalate-dead tick sweeping again; ~/startup/find-missing-loop.sh is belt-and-braces)`. The server then checks the row again, and that check decides: once agent-director reads the row dead it logs `[slack] Session reads dead after escalate-dead reconciliation — relaunching in this restart run for persona=<key> (b.d61)` and `[slack] Relaunching session for persona=<key> cwd="<path>"` right after; while agent-director still reads the row live it logs `[slack] Session still reads live after escalate-dead — no relaunch in this restart run for persona=<key>; the row may stay live for further ticks, each escalate-dead tick sweeping again`, relaunches nothing, and the row is swept again at later health checks until it reads dead (see "A persona posts a *Slow recovery* notice" below). A **capped** persona is exempt: the health-check skips it entirely, including reconnects, so a capped persona still requires a server restart (see above).
|
|
1929
1991
|
|
|
1930
1992
|
At a server start, a persona whose instance agent-director reports as `working` is reconnected once its turn ends, and it doesn't hold up the other personas or the health check. `server.log` shows `[slack] startupSessionManager: "<name>" (key=<key>) is waiting for its working row to settle — the start pass goes on without it; its launch stays in flight in the background (b.f2b)`, then, when the wait ends, `[slack] startupSessionManager: background launch for "<name>" (key=<key>) settled: <outcome> (b.f2b)`. `reconnected` means the reconnect was typed into the session; `not-reconnected` means the session was left running without it, and the line just before says what happens next. An idle screen and a transcript ending with a finished reply (or with a turn you interrupted), both unchanged for a minute, end the wait with a reconnect, and a prompt or dialog on screen for a minute raises a *Waiting on a prompt* notice. Otherwise the wait gives up after 10 minutes; if agent-director still reports the persona as `working` then, its destination gets a *Not connected* notice. While agent-director can't report the persona's state, the wait keeps waiting and posts nothing; if it still can't after 10 minutes, the instance is left running for the health check (with `session_restart_delay` set to `0`, a *Not connected* notice reports it), with no `spawn-failed` entry and nothing counted toward the restart limit. If agent-director no longer has a row for the persona, it is relaunched. Removing the persona with a confirmed change ends its wait at once, with nothing typed.
|
|
1931
1993
|
|
|
@@ -2317,7 +2379,7 @@ Fix the cause first: for `UNAVAILABLE`, confirm agent-director is installed and
|
|
|
2317
2379
|
**Bots come back with no memory of the prior conversation after a reboot**
|
|
2318
2380
|
With `resume_enabled: true`, a bot whose host rebooted (or pod resumed) should return with its conversation history. The agent-director version is not the cause: the server starts only on agent-director Phase 1, since the startup gate refuses any binary below it (`ad-system-install-too-old` or `ad-below-phase1-floor`, see [Startup errors](#startup-errors)). `bun run install-check` passes a binary below Phase 1 only with its note that the server will not start on it (see [Checking your agent-director install](#checking-your-agent-director-install)). A bot that comes back amnesiac on a running server started a new conversation for one of the reasons below, which the log names.
|
|
2319
2381
|
|
|
2320
|
-
A bot also starts a new conversation, by design, when its session no longer matches the applied configuration. A config edit takes effect only once it is applied (see [Reload](#reload)); a restart or reboot alone runs the last-applied record. When a change to a persona's `working_directory` is applied, the persona is torn down and brought up fresh, once its old instance has ended (see "A persona waits on an old instance in its working directory" above). When a change to a persona's effective `claude_config_dir` (its own or the top-level default) is applied, the bot starts a new conversation the next time it would be resumed (see [When next-launch and server-wide changes take effect](#when-next-launch-and-server-wide-changes-take-effect)); a bot that keeps running keeps its old config directory until then. A bot whose instance no longer matches its working directory or config directory, or whose `resume_enabled` is false, starts a new conversation on the same instance: the old conversation stays in its directory as an earlier life of that instance, and nothing is deleted. A bot that is still running is first ended, with each kill checked, before the new conversation starts; one that is still starting is never typed into: it is ended with a checked kill, and the new conversation starts only once agent-director's grace period has passed since its launch start (see "A persona's instance is still starting" above). A persona retired by a confirmed change (removed and added again with the same key, renamed back, or brought up from its new entry after a `credentials_file` path or `working_directory` change) starts a new conversation on the same instance, restarts included, and its old conversation is never resumed. So does a persona whose instance a server start found while the persona was not in the applied configuration (for example, after `config.json.last-applied` was deleted) and that is added back later: that start recorded its key as retired. After upgrading from an earlier release, each bot also starts fresh once: a bot instance from before the upgrade is never resumed (see [Step 10: Start the new CSCB](#step-10-start-the-new-cscb)). The log names the reason: search `server.log` for `sweeping row`, `pre-persona row`, `replacing the row`, `not resuming; replacing its row` or `
|
|
2382
|
+
A bot also starts a new conversation, by design, when its session no longer matches the applied configuration. A config edit takes effect only once it is applied (see [Reload](#reload)); a restart or reboot alone runs the last-applied record. When a change to a persona's `working_directory` is applied, the persona is torn down and brought up fresh, once its old instance has ended (see "A persona waits on an old instance in its working directory" above). When a change to a persona's effective `claude_config_dir` (its own or the top-level default) is applied, the bot starts a new conversation the next time it would be resumed (see [When next-launch and server-wide changes take effect](#when-next-launch-and-server-wide-changes-take-effect)); a bot that keeps running keeps its old config directory until then. A bot whose instance no longer matches its working directory or config directory, or whose `resume_enabled` is false, starts a new conversation on the same instance: the old conversation stays in its directory as an earlier life of that instance, and nothing is deleted. A bot that is still running is first ended, with each kill checked, before the new conversation starts; one that is still starting is never typed into: it is ended with a checked kill, and the new conversation starts only once agent-director's grace period has passed since its launch start (see "A persona's instance is still starting" above). A persona retired by a confirmed change (removed and added again with the same key, renamed back, or brought up from its new entry after a `credentials_file` path or `working_directory` change) starts a new conversation on the same instance, restarts included, and its old conversation is never resumed. So does a persona whose instance a server start found while the persona was not in the applied configuration (for example, after `config.json.last-applied` was deleted) and that is added back later: that start recorded its key as retired. After upgrading from an earlier release, each bot also starts fresh once: a bot instance from before the upgrade is never resumed (see [Step 10: Start the new CSCB](#step-10-start-the-new-cscb)). A bot whose last 2 resumes ended before its session started also starts fresh: its transcript holds no conversation Claude Code can load (see "Session not restarting after crash" above). The log names the reason: search `server.log` for `sweeping row`, `pre-persona row`, `replacing the row`, `not resuming; replacing its row`, `key is retired` or `not resuming its transcript again`.
|
|
2321
2383
|
|
|
2322
2384
|
**Session crashes on resume with "sandbox required but unavailable"**
|
|
2323
2385
|
This is a known regression in certain Claude Code releases (e.g. v2.1.120) where `--resume` triggers a sandbox check that fails in headless environments. Set `resume_enabled: false` in `config.json`, apply the change and restart the server (`resume_enabled`, like every server-wide setting but `allow_invited_channels`, takes effect at the next start; see [Reload](#reload)) to disable `--resume` entirely — the bot will always start a fresh Claude session instead of resuming a prior conversation, both on startup and on runtime auto-restart:
|
|
@@ -2337,7 +2399,7 @@ The server daemon writes runtime output to `~/.claude/channels/slack/server.log`
|
|
|
2337
2399
|
|
|
2338
2400
|
`stop --stop-bots` and `clean_restart` append their bot-teardown lines to `server.log` while the server is stopped (see [What the command prints when a bot can't be stopped](#what-the-command-prints-when-a-bot-cant-be-stopped)); those lines take the same timestamped form and are rotated the same way.
|
|
2339
2401
|
|
|
2340
|
-
Note this covers only `server.log` / `clean_restart.log`. `startup-errors.log` and `permission-trail.jsonl` are separate append-only files with their own retention (see below).
|
|
2402
|
+
Note this covers only `server.log` / `clean_restart.log`. `startup-errors.log` and `permission-trail.jsonl` are separate append-only files with their own retention (see below). The cron log is never rotated; `cron_log_max_bytes` caps it instead (see [The cron log](#the-cron-log)).
|
|
2341
2403
|
|
|
2342
2404
|
---
|
|
2343
2405
|
|
|
@@ -2756,7 +2818,7 @@ This major version accepts only the persona configuration format. These are the
|
|
|
2756
2818
|
2. **Pick persona names whose keys don't start with one another.** The configuration check rejects such a pair. If your bots were named `horde`, `horde_admin`, …, don't name a persona `horde` beside `horde_admin`: give the shorter name a suffix, such as `horde_main` (see [Persona name and key](#persona-name-and-key)).
|
|
2757
2819
|
3. **Set the reply settings in `config.json`.** Nothing else carries an acknowledgement reaction over: to keep one, set `ack_reaction` as a top-level setting. If you had changed how replies are split, set `reply_chunk_limit` and `reply_chunk_mode` there too. See [Server-wide settings](#server-wide-settings).
|
|
2758
2820
|
4. **Move the tokens into credentials files.** Tokens come only from each persona's credentials file. Create one [credentials file](#credentials-files) per persona, then remove any token environment variables you exported for the previous version.
|
|
2759
|
-
5. **Give each persona its own Slack app.** Your existing app can serve one persona; create another app for each additional persona.
|
|
2821
|
+
5. **Give each persona its own Slack app.** Your existing app can serve one persona; create another app for each additional persona. Add the `im:write`, `channels:read` and `groups:read` bot scopes to the existing app and re-install it; the `debug-slack-channel-bots` skill has the steps under "A persona can't open a DM" and "The message archive shows channel IDs".
|
|
2760
2822
|
6. **Decide who can reach each persona.** In declarative mode, the mode an upgrade starts in, who can reach a persona is decided only by its `channels`, each channel's `delivery` and its `dm.enabled` switch (see [Channel delivery](#channel-delivery) and [Direct messages](#direct-messages-dmenabled)). Fungible mode changes that (see [Who can reach a persona in fungible mode](#who-can-reach-a-persona-in-fungible-mode)).
|
|
2761
2823
|
7. **Rewrite crontable lines to name personas.** A crontable target that names a channel matches no persona, and the line is logged `unknown-persona` each time it fires. Rewrite each target as a persona's name or key (see [Scheduled Prompts](#scheduled-prompts-cscb_cron)).
|
|
2762
2824
|
8. **Update `/interject` callers to send `persona`.** A request without `persona` is refused with 400, and a successful response holds only `ok` and `persona`. Change every script that calls `/interject`, including host crontab `curl` lines, to name a persona by name or key (see [Interject](#interject)).
|
package/package.json
CHANGED
package/skills/EXAMPLE_CLAUDE.md
CHANGED
|
@@ -13,7 +13,7 @@ You are one persona: a Slack bot with its own name and avatar. People reach you
|
|
|
13
13
|
- Your persona key is in the `CSCB_PERSONA` environment variable. Use it to name yourself, for example when you schedule a prompt for yourself.
|
|
14
14
|
- A message without a `via` attribute is a scheduled prompt or an injected message. It needs no reply unless it asks for one.
|
|
15
15
|
- A Slack message's `via` attribute says how it reached you: `dm` (a direct message), `mention` (you were @mentioned), `broadcast` (`@here` or `@channel`), `receive_all_shared` (every message in a channel other personas also read in full) or `receive_all` (every message in a channel only you read in full).
|
|
16
|
-
- To schedule a prompt for yourself, append one line to the crontable named by `CSCB_CRONTABLE_PATH`, for example `0 9 * * 1-5 prompts/standup.md $CSCB_PERSONA`.
|
|
16
|
+
- To schedule a prompt for yourself, append one line to the crontable named by `CSCB_CRONTABLE_PATH`, for example `echo "0 9 * * 1-5 prompts/standup.md $CSCB_PERSONA" >> "$CSCB_CRONTABLE_PATH"`. Only append with `>>`: never rewrite, reorder or delete other lines, and never save the whole file from an editor.
|
|
17
17
|
|
|
18
18
|
# Role
|
|
19
19
|
<!-- Replace this section with what your personas do. This file is appended to every persona's
|