claude-slack-channel-bots 0.16.0 → 0.17.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 +3 -1
- package/docs/agent-director.md +1 -1
- package/docs/bots.md +13 -9
- package/docs/channel-modes.md +1 -1
- package/docs/configuration.md +5 -15
- package/docs/migration.md +41 -26
- package/docs/reload.md +5 -3
- package/docs/scheduled-prompts.md +6 -8
- package/docs/server-settings.md +18 -10
- package/docs/troubleshooting.md +9 -6
- package/package.json +2 -1
- package/skills/debug-slack-channel-bots/SKILL.md +268 -143
- package/src/ack-tracker.ts +68 -1
- package/src/ad-settings.ts +25 -5
- package/src/ad-version-gate.ts +35 -4
- package/src/agent-director-errors.ts +2 -0
- package/src/agent-director-startup.ts +34 -0
- package/src/agent-director-template.ts +8 -29
- package/src/bot-system-prompt.ts +53 -0
- package/src/codex-config-keys.ts +307 -0
- package/src/config.ts +84 -2
- package/src/cron-dispatch.ts +36 -198
- package/src/cron-log.ts +4 -2
- package/src/delivery-ad-scope.ts +15 -0
- package/src/delivery-constants.ts +13 -0
- package/src/delivery-decision.ts +6 -3
- package/src/delivery-engine.ts +514 -0
- package/src/delivery-intake.ts +146 -0
- package/src/delivery-lifecycle.ts +100 -0
- package/src/delivery-notices.ts +179 -0
- package/src/delivery-policy.ts +113 -0
- package/src/delivery-queue.ts +451 -0
- package/src/delivery-receipt.ts +195 -0
- package/src/delivery-screen.ts +169 -0
- package/src/health-check.ts +26 -47
- package/src/interject.ts +10 -88
- package/src/jsonl-persistence-check.ts +1 -0
- package/src/lib.ts +1 -15
- package/src/live-row-sequence.ts +28 -0
- package/src/lost-message.ts +1 -87
- package/src/outage-state.ts +23 -3
- package/src/pending-row.ts +6 -0
- package/src/permission-poller.ts +5 -0
- package/src/persona-diagnostics.ts +31 -0
- package/src/persona-episodes.ts +7 -4
- package/src/persona-identity.ts +8 -1
- package/src/persona-lifecycle.ts +1 -1
- package/src/persona-routing.ts +88 -525
- package/src/registry.ts +19 -35
- package/src/reload-plan.ts +7 -4
- package/src/reload-preview-clauses.ts +5 -0
- package/src/reload.ts +8 -0
- package/src/restart.ts +11 -19
- package/src/server.ts +241 -60
- package/src/session-manager.ts +378 -74
- package/src/slow-recovery.ts +6 -6
- package/src/tag-description.ts +10 -0
- package/src/tag-wrapper.ts +100 -0
- package/src/test-mcp-drop.ts +32 -0
- package/src/test-timer-overrides.ts +134 -0
- package/src/unavailable-retry.ts +16 -15
- package/stop-hooks/slack-reply-guard.sh +42 -8
- package/src/inbound-dedupe.ts +0 -105
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ A single HTTP MCP server that runs several independent Claude Code bots, called
|
|
|
47
47
|
## Prerequisites
|
|
48
48
|
|
|
49
49
|
- [Bun](https://bun.sh) `>= 1.0.21` (agent-director minimum)
|
|
50
|
-
- [Claude Code](https://claude.ai/code) 2.1.
|
|
50
|
+
- [Claude Code](https://claude.ai/code) 2.1.280 or later for the workers, installed and authenticated: CSCB's supported minimum and the version the fleet runs. No version pin or channels permission is required. A Claude Code too old for exec-form hooks (older than 2.1.139) has its hooks ignored, so every launch stays `pending`. The `claude` on the workers' `PATH` must be Claude Code's native binary or a wrapper that `exec`s it: a launcher or shim that runs Claude Code as a child process makes agent-director ignore every hook (`ad.hook.ignored` with the reason `pid_mismatch`), so every launch stays `pending`.
|
|
51
51
|
- [`agent-director`](https://github.com/gabemahoney/agent-director) 0.12.0 or later, **installed system-wide** as a prerequisite, like `git` or `docker`. The npm `agent-director` package CSCB depends on is the client only: it finds the system-installed binary at startup and refuses to start when the binary is missing, too old, or unreachable. This release pins the client to `agent-director@0.12.0` and requires the CLI at 0.12.0 or later, installed on the host together with it, and the two are rolled back together; CSCB changes no agent-director code (see "Switching over to agent-director Phase 1" in [Migration](docs/migration.md#migration)). Every agent on the host, with every long-running agent-director process (`agent-director serve` included), is stopped before either binary change (the Phase 1 install, and the rollback's restore of the previous binary) and started again after it. The startup gate refuses a binary below the client's own minimum (`min_binary_version` in the client's `dist/version-floor.json`) or below 0.12.0, and names the required version (see [Startup errors](docs/troubleshooting.md#startup-errors)). To check the install before you start, run `bun run install-check` (see [Checking your agent-director install](docs/agent-director.md#checking-your-agent-director-install)).
|
|
52
52
|
- [tmux](https://github.com/tmux/tmux) 3.2 or later, with `remain-on-exit` off. agent-director runs it; CSCB starts no tmux process. agent-director does not depend on tmux's `base-index` or `pane-base-index`, so their values don't matter.
|
|
53
53
|
- Slack workspace admin access (to create and configure one Slack app per persona)
|
|
@@ -111,6 +111,8 @@ Replace the channel ID and paths with your own. [Configuration](docs/configurati
|
|
|
111
111
|
|
|
112
112
|
Talk to a persona in its channels or, when its `dm.enabled` is `true`, by direct message. When its Claude instance needs a tool approved, an **Allow** / **Deny** prompt is posted to its destination, its `permission_prompts` (see [Permission Relay](docs/bots.md#permission-relay)).
|
|
113
113
|
|
|
114
|
+
CSCB durably queues accepted messages and types them into the persona’s terminal. Delivery is ordered and at-least-once: the next item waits for transcript confirmation of the previous one, and an unconfirmed item may appear again. The bot answers with `reply`; see [How a persona receives messages](docs/bots.md#how-a-persona-receives-messages).
|
|
115
|
+
|
|
114
116
|
### Changing the configuration
|
|
115
117
|
|
|
116
118
|
On a running server, saving `config.json` changes nothing yet. Within about 5 seconds the server writes a preview of the change to `config.json.pending`. Read it, then confirm it by renaming it:
|
package/docs/agent-director.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# agent-director
|
|
2
2
|
|
|
3
|
-
CSCB requires agent-director CLI 0.12.0 or later and pins its TypeScript client to `agent-director@0.12.0`. Workers require Claude Code 2.1.
|
|
3
|
+
CSCB requires agent-director CLI 0.12.0 or later and pins its TypeScript client to `agent-director@0.12.0`. Workers require Claude Code 2.1.280 or later. Upgrade the CLI with its released installer before upgrading CSCB; see [Migration](migration.md#migration).
|
|
4
4
|
|
|
5
5
|
CSCB runs every bot through [agent-director](https://github.com/gabemahoney/agent-director), installed system-wide. This page covers checking that install, the `install-cscb` skill that walks you through a failed check, and the agent-director settings CSCB reads.
|
|
6
6
|
|
package/docs/bots.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# What a bot can do
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Tools, receiving messages, injecting a message, permission prompts and the Slack Reply Guard. See also [Scheduled Prompts](scheduled-prompts.md).
|
|
4
4
|
|
|
5
5
|
## Tools
|
|
6
6
|
|
|
@@ -32,9 +32,9 @@ On a host whose Claude Code permission settings don't allow the server's tools a
|
|
|
32
32
|
|
|
33
33
|
## Interject
|
|
34
34
|
|
|
35
|
-
POST to `/interject` to
|
|
35
|
+
POST to `/interject` to store a message for a configured persona from localhost. The message is queued only for the named persona, including while it is down. Each request is an independent queued item, even with identical sender and text; requests are never merged or deduplicated. Only requests from `127.0.0.1` or `::1` are accepted — external callers are rejected with 403. The bot sees it as described in [How a persona receives messages](#how-a-persona-receives-messages); the Slack Reply Guard does not remind it to reply.
|
|
36
36
|
|
|
37
|
-
An injected message carries no Slack conversation
|
|
37
|
+
An injected message carries no Slack conversation; say in its text where to post any Slack reply.
|
|
38
38
|
|
|
39
39
|
### Request
|
|
40
40
|
|
|
@@ -52,7 +52,7 @@ curl -X POST http://localhost:<port>/interject \
|
|
|
52
52
|
|
|
53
53
|
### Response
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
After durable storage, returns HTTP 200. This acknowledges storage, not delivery:
|
|
56
56
|
|
|
57
57
|
```json
|
|
58
58
|
{ "ok": true, "persona": "planner" }
|
|
@@ -69,7 +69,7 @@ On success, returns HTTP 200:
|
|
|
69
69
|
| 404 | No persona in the applied configuration has that name or key. |
|
|
70
70
|
| 405 | Must use POST method. |
|
|
71
71
|
| 413 | Request body exceeds 32KB. |
|
|
72
|
-
|
|
|
72
|
+
| 500 | The message could not be stored (`Message not stored`). |
|
|
73
73
|
|
|
74
74
|
### Example: crontab reminder
|
|
75
75
|
|
|
@@ -116,13 +116,15 @@ The template also pre-allows each bot to read its own persistent-memory director
|
|
|
116
116
|
|
|
117
117
|
## How a persona receives messages
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
CSCB stores accepted messages, then types their text into the bot’s terminal in a `<channel source="slack-channel-router" …>` tag. Every Slack message carries its sender’s ID; use the shared prompt file to say whose instructions to follow. The bot answers through `reply`. A message from Slack carries a `via` attribute that says how it reached the persona. An injected message (a scheduled prompt or an `/interject` message) carries no `via`. A received item is durably stored; delivered means its complete text is confirmed in the bot’s transcript. Delivery is at-least-once: an unconfirmed item is offered again, so the bot may see it more than once. Each persona uses strict FIFO across Slack, `/interject` and scheduled prompts, with one item in flight; the next waits until the previous item is confirmed delivered. There is no age cutoff. Messages wait while MCP is disconnected, trigger the configured reconnect, and are delivered once connected again. Which channels a persona serves, and at which delivery, depends on the channel mode (see [Channel modes](channel-modes.md#channel-modes)).
|
|
120
120
|
|
|
121
121
|
A message from Slack carries these tag attributes:
|
|
122
122
|
|
|
123
123
|
| Attribute | Value |
|
|
124
124
|
|---|---|
|
|
125
125
|
| `chat_id` | The channel ID, or the DM conversation ID (`D…`) for a direct message. |
|
|
126
|
+
| `channel_name` | Channel name without `#`; `dm` for a DM; channel ID if the name cannot be resolved. |
|
|
127
|
+
| `part` | `k/n` for part k of n when a long message is split. |
|
|
126
128
|
| `message_id`, `ts` | The message's Slack timestamp, for example `1789936743.069939`. |
|
|
127
129
|
| `user` | The author's Slack display name, falling back to real name, then Slack username, then user ID. For a post by a bot or integration without a user: the post's username, else its bot profile name, else its bot ID. |
|
|
128
130
|
| `user_id` | The author's Slack user ID. |
|
|
@@ -140,7 +142,7 @@ Each way a message reaches a persona:
|
|
|
140
142
|
| `@here` / `@channel` broadcast | A message that uses `@here` or `@channel`. In declarative mode: in a channel listed in the persona's `channels`, with either `delivery`. In fungible mode: in a public or private channel its app is in that is not externally shared, at either channel delivery. `@everyone` and user-group mentions are not broadcasts. | Slack attributes | `broadcast` | Yes, broadcast wording |
|
|
141
143
|
| Every message, shared channel | A message that neither @mentions the persona nor broadcasts. In declarative mode: in a channel listed in its `channels` with `delivery: all`, when at least one other persona in the applied configuration also has `delivery: all` for that channel, whether or not that persona is up. In fungible mode: in a public or private channel its app is in that is not externally shared, where its channel delivery is `all` (a stored choice, after the loop guard; see [Channel delivery in fungible mode](channel-modes.md#channel-delivery-in-fungible-mode)), when at least one other persona in the applied configuration has a channel delivery of `all` for that channel by the same rule, whether or not that persona's app remains in the channel. | Slack attributes | `receive_all_shared` | Yes, shared-channel wording |
|
|
142
144
|
| Every message, this persona alone | A message that neither @mentions the persona nor broadcasts. In declarative mode: in a channel listed in its `channels` with `delivery: all`, when no other persona in the applied configuration has `delivery: all` for that channel. In fungible mode: in a public or private channel its app is in that is not externally shared, where its channel delivery is `all`, when no other persona in the applied configuration has a channel delivery of `all` for that channel. | Slack attributes | `receive_all` | Yes, only-you wording |
|
|
143
|
-
| Scheduled prompt | A crontable line naming the persona fires, and the scheduler
|
|
145
|
+
| Scheduled prompt | A crontable line naming the persona fires, and the scheduler queues it for that persona only (see [Scheduled Prompts](scheduled-prompts.md#scheduled-prompts-cscb_cron)). | Only `user` = `cscb-cron:<prompt file name without its extension>` (`cscb-cron:standup` for `standup.md`) and `ts` = the fire time in seconds, with six decimals. No `chat_id`, `message_id` or `via`. | none | Never |
|
|
144
146
|
| `/interject` message | A localhost POST to `/interject` addressed by `persona` (see [Interject](#interject)); it reaches only that persona. | Only `user` = the request's `sender` (default `interject`) and `ts`, as for a scheduled prompt. No `chat_id`, `message_id` or `via`. | none | Never |
|
|
145
147
|
|
|
146
148
|
A "Yes" in the Reminder column means the [Slack Reply Guard](#slack-reply-guard-stop-hook) reminds the persona once if it ends its turn without replying, wherever the reminder is on for that persona. Each `via` has its own reminder wording, stating how the message arrived; that section lists them.
|
|
@@ -150,7 +152,7 @@ A "Yes" in the Reminder column means the [Slack Reply Guard](#slack-reply-guard-
|
|
|
150
152
|
- **Every author counts.** Posts by people, bots, integrations and other personas all arrive by these rules. Each persona posts as its own Slack app, so another persona's post reaches it like anyone else's.
|
|
151
153
|
- **Anyone in the workspace can reach a persona,** in both modes: in declarative mode in its listed channels, by each channel's `delivery`; in fungible mode in every channel its app is invited to that it serves, by its channel delivery there; and in both modes by DM when its `dm.enabled` is `true`. There is no approval step. In fungible mode an invite changes who that is (see [Who can reach a persona in fungible mode](channel-modes.md#who-can-reach-a-persona-in-fungible-mode)).
|
|
152
154
|
- **A persona never receives its own posts.**
|
|
153
|
-
- **
|
|
155
|
+
- **Slack redeliveries add no second queue item, including across server restarts.** Each accepted Slack message keeps its key for at least 24 hours after intake; the paired channel @mention adds no second item.
|
|
154
156
|
- **In fungible mode, an @mention is delivered from its `message` event.** Of the two events Slack sends for a channel @mention, the `app_mention` event decides nothing and the `message` event carrying the same mention decides, so each @mention is delivered once, whichever event arrives first. An edit that adds a mention to an earlier message sends no new `message` event, so in fungible mode it is not delivered.
|
|
155
157
|
- **A message without `via` is an injected prompt.** Its `user` label is free-form, so an `/interject` sender label, even one starting with `cscb-cron:`, cannot make it look like a Slack message.
|
|
156
158
|
- **When the reminder is off.** A persona gets no reminder when its effective `stop_hook_bootstrap` was `false` at its last launch, when it has no `claude_config_dir` (its own or the top-level one), when that directory resolves to `~/.claude`, or when `jq` is not installed. See [Slack Reply Guard](#slack-reply-guard-stop-hook).
|
|
@@ -176,6 +178,8 @@ This turn started from a Slack direct message and you haven't replied. If you me
|
|
|
176
178
|
|
|
177
179
|
When several Slack messages started the turn, the last one picks the wording. A `reply` call after the earliest of them counts as a reply. The persona then continues once: it can reply, or end the turn without replying. That continuation carries `stop_hook_active=true`, which the guard always lets through, so the reminder appears at most once per turn and never loops.
|
|
178
180
|
|
|
181
|
+
CSCB types the tag, including when a message arrives mid-turn or is pasted into the composer. The guard recognises these forms in the transcript.
|
|
182
|
+
|
|
179
183
|
**Only messages from Slack trigger it.** The guard recognises a message from Slack by its `via` attribute; any `via` value counts. Injected prompts (scheduled prompts and `/interject` messages) carry no `via`, so they never trigger the reminder: no Slack conversation is waiting on them. An injected message that arrives right after a Slack message does not cancel the reminder for the Slack message. [How a persona receives messages](#how-a-persona-receives-messages) lists every kind of message and its `via`.
|
|
180
184
|
|
|
181
185
|
### Turning it on or off
|
|
@@ -190,4 +194,4 @@ The guard only recognises a reply made with `mcp__slack-channel-router__reply`.
|
|
|
190
194
|
|
|
191
195
|
### When it stays silent
|
|
192
196
|
|
|
193
|
-
The guard is fail-open: when anything is missing or goes wrong, the turn ends normally with no reminder. Besides the opt-out, it stays silent for a persona with no `claude_config_dir`, one whose directory resolves to your own `~/.claude` (the server never edits `~/.claude/settings.json`), any persona while `jq` is not installed (installing it later is enough), and when the message's `<channel>` tag, which
|
|
197
|
+
The guard is fail-open: when anything is missing or goes wrong, the turn ends normally with no reminder. Besides the opt-out, it stays silent for a persona with no `claude_config_dir`, one whose directory resolves to your own `~/.claude` (the server never edits `~/.claude/settings.json`), any persona while `jq` is not installed (installing it later is enough), and when the message's `<channel>` tag, which CSCB types, isn't in the shape the guard expects. After every CSCB or Claude Code upgrade, check the guard end to end with the steps in the debugging skill's ["Checking the guard after an upgrade"](../skills/debug-slack-channel-bots/SKILL.md#checking-the-guard-after-an-upgrade). How the server installs the hook and the per-persona record it reads are in ["How the guard is installed and what it reads"](../skills/debug-slack-channel-bots/SKILL.md#how-the-guard-is-installed-and-what-it-reads).
|
package/docs/channel-modes.md
CHANGED
|
@@ -68,7 +68,7 @@ In fungible mode, a persona's channel delivery for a channel sets which messages
|
|
|
68
68
|
|
|
69
69
|
## Permission prompts in fungible mode
|
|
70
70
|
|
|
71
|
-
In fungible mode, a persona's destination is its `invited.permission_prompts`, `"dm"` when it is absent. Its permission prompts and its server notices, such as
|
|
71
|
+
In fungible mode, a persona's destination is its `invited.permission_prompts`, `"dm"` when it is absent. Its permission prompts and its server notices, such as *Message not saved* notices and restart-limit warnings, are posted there as the persona. See the `invited.permission_prompts` row in [Persona fields](configuration.md#persona-fields).
|
|
72
72
|
|
|
73
73
|
- **A channel ID:** any channel; it needs no listing and no DMs. Invite the persona's app to it, or Slack refuses the posts (see "A permission prompt or notice doesn't arrive" in [Troubleshooting](troubleshooting.md#troubleshooting)). Everyone in that channel sees the prompts and notices.
|
|
74
74
|
- **`"dm"`, written or by default:** a DM from the persona's app to its `dm.contact`. It needs `dm.enabled: true` and a `dm.contact`.
|
package/docs/configuration.md
CHANGED
|
@@ -31,19 +31,14 @@ A complete `config.json` in declarative mode (`allow_invited_channels` absent),
|
|
|
31
31
|
"name": "planner",
|
|
32
32
|
"credentials_file": "~/.config/claude-slack-channel-bots/planner-credentials.json",
|
|
33
33
|
"working_directory": "~/projects/alpha",
|
|
34
|
-
"channels": [
|
|
35
|
-
{ "id": "C0123456789", "delivery": "all" },
|
|
36
|
-
{ "id": "C0555555555", "delivery": "all" }
|
|
37
|
-
],
|
|
34
|
+
"channels": [{ "id": "C0123456789", "delivery": "all" }, { "id": "C0555555555", "delivery": "all" }],
|
|
38
35
|
"permission_prompts": "C0123456789"
|
|
39
36
|
},
|
|
40
37
|
{
|
|
41
38
|
"name": "reviewer",
|
|
42
39
|
"credentials_file": "~/.config/claude-slack-channel-bots/reviewer-credentials.json",
|
|
43
40
|
"working_directory": "~/projects/beta",
|
|
44
|
-
"channels": [
|
|
45
|
-
{ "id": "C0555555555", "delivery": "mentions" }
|
|
46
|
-
],
|
|
41
|
+
"channels": [{ "id": "C0555555555", "delivery": "mentions" }],
|
|
47
42
|
"dm": { "enabled": true, "contact": "U0123456789" },
|
|
48
43
|
"permission_prompts": "dm",
|
|
49
44
|
"claude_config_dir": "~/.claude-reviewer",
|
|
@@ -61,8 +56,7 @@ A complete `config.json` in declarative mode (`allow_invited_channels` absent),
|
|
|
61
56
|
"session_restart_delay": 60,
|
|
62
57
|
"claude_config_dir": "~/.claude-bots",
|
|
63
58
|
"ack_reaction": "eyes",
|
|
64
|
-
"reply_chunk_limit": 3000,
|
|
65
|
-
"reply_chunk_mode": "newline"
|
|
59
|
+
"reply_chunk_limit": 3000, "reply_chunk_mode": "newline"
|
|
66
60
|
}
|
|
67
61
|
```
|
|
68
62
|
|
|
@@ -144,7 +138,6 @@ Each persona's Slack tokens live in its own credentials file, and `config.json`
|
|
|
144
138
|
Write the file with `claude-slack-channel-bots credentials <persona>` in your own terminal, once the persona is declared in `config.json`: the [credentials command](../skills/setup-slack-channel-bots/SKILL.md#credentials-command) of the setup wizard, `setup-slack-channel-bots`. It checks both tokens with Slack and writes the file, mode 0600, only when both pass; see [`claude-slack-channel-bots credentials`](cli.md#claude-slack-channel-bots-credentials).
|
|
145
139
|
|
|
146
140
|
To rotate a persona's tokens, run the credentials command for it again (see the wizard's [Rotate a persona's tokens](../skills/setup-slack-channel-bots/SKILL.md#rotate-a-personas-tokens)), then confirm the pending change (see [Confirming a change](reload.md#confirming-a-change)). Only that persona reconnects, and no restart is needed.
|
|
147
|
-
|
|
148
141
|
Never put a token in `config.json`, a ticket or a chat.
|
|
149
142
|
|
|
150
143
|
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`.
|
|
@@ -166,8 +159,6 @@ Any number of personas may list the same channel, each with its own `delivery`.
|
|
|
166
159
|
- **Off:** DMs to the persona's app are not delivered. The server logs one `persona-dm-dropped` line naming the persona and `dm.enabled`. The persona never opens, reads or posts in a DM.
|
|
167
160
|
- **Group DMs** (DMs with more than one person) are never delivered to any persona, whatever `dm.enabled` says.
|
|
168
161
|
|
|
169
|
-
In declarative mode, a persona with no `channels` must have `dm.enabled` set to `true`.
|
|
170
|
-
|
|
171
162
|
`dm.contact` is the person who receives the persona's prompts and notices when its destination is `"dm"`: in declarative mode when `permission_prompts` is `"dm"`, and in fungible mode when `invited.permission_prompts` is `"dm"` or unset. In either mode a `"dm"` destination needs `dm.enabled: true` and a `dm.contact`. `dm.contact` gates nothing else: it doesn't limit who can DM the persona.
|
|
172
163
|
|
|
173
164
|
Starting a DM with a user (a `reply` to a user ID, or a persona whose destination is `"dm"`) needs the `im:write` bot scope, which the shipped `slack-app-manifest.yml` grants. A `"dm"` destination always opens its DM through Slack before it first posts there, even when the DM already exists. For an app created from an earlier manifest, a `"dm"` persona's prompts and notices are not posted, and a `reply` to a user ID fails with `missing_scope`, until the scope is added and the app is re-installed.
|
|
@@ -176,12 +167,12 @@ Receiving DMs and replying in an existing DM (a `D…` ID) work without the scop
|
|
|
176
167
|
|
|
177
168
|
### Permission prompts
|
|
178
169
|
|
|
179
|
-
This section applies in declarative mode, where `permission_prompts` is required; for fungible mode, see [Permission prompts in fungible mode](channel-modes.md#permission-prompts-in-fungible-mode). It is the persona's destination: its permission prompts and its server notices, such as
|
|
170
|
+
This section applies in declarative mode, where `permission_prompts` is required; for fungible mode, see [Permission prompts in fungible mode](channel-modes.md#permission-prompts-in-fungible-mode). It is the persona's destination: its permission prompts and its server notices, such as *Message not saved* notices and restart-limit warnings, are posted there as the persona.
|
|
180
171
|
|
|
181
172
|
- **A channel ID:** one of the persona's own `channels`. Everyone in that channel sees the prompts and notices.
|
|
182
173
|
- **`"dm"`:** a DM from the persona's app to its `dm.contact`. It needs `dm.enabled: true` and a `dm.contact`.
|
|
183
174
|
|
|
184
|
-
|
|
175
|
+
Choose a destination with [channel delivery](#channel-delivery) in mind. If two personas each receive every message in the other’s destination and storing fails, the notices stop after one *Message not saved* each per episode. Further failures are counted in `server.log` (see [Troubleshooting](troubleshooting.md#troubleshooting)).
|
|
185
176
|
|
|
186
177
|
[Permission Relay](bots.md#permission-relay) covers how prompts are delivered and answered, and [Troubleshooting](troubleshooting.md#troubleshooting) what happens when Slack refuses a post to the destination.
|
|
187
178
|
|
|
@@ -194,7 +185,6 @@ Give each persona a destination that no other persona receives every message in,
|
|
|
194
185
|
```sh
|
|
195
186
|
CLAUDE_CONFIG_DIR=~/.claude-reviewer claude auth login --claudeai
|
|
196
187
|
```
|
|
197
|
-
|
|
198
188
|
Use `--console` instead of `--claudeai` for a Console account. When neither the persona nor the top level sets one, Claude's own default applies.
|
|
199
189
|
|
|
200
190
|
- A directory that doesn't exist yet is fine when its parent exists.
|
package/docs/migration.md
CHANGED
|
@@ -55,7 +55,7 @@ Do this beforehand, with the old CSCB running and still installed as the global
|
|
|
55
55
|
2. **agent-director's version.** Run `agent-director version` in the bot server's launcher environment as the workers' user. Record its version and resolved path. Stage the released 0.12.0 installer and both binaries, and keep the prior install and flags for restoration. A host already on 0.12.0 or later needs no directory migration; verify its `config_dir` and client compatibility before restarting callers.
|
|
56
56
|
3. **The tmux socket (operator action).** Pin the tmux socket for the bot server's launcher, the host's sweep schedule and the workers. The sweep schedule is whatever runs `agent-director find-missing` on a schedule on your host: a cron entry, a systemd timer or a loop script. Run `tmux display-message -p '#{socket_path}'` from the bot server's launcher, the sweep schedule's environment and a worker's. Confirm that all three print the same path, that it is the pinned path (a `TMUX_TMPDIR` that names a missing path falls back silently to `/tmp`), and that the three share one HOME. A host with no sweep schedule checks the other two, and runs the one step 11 adds in the same environment. Record the result in the switch-over log.
|
|
57
57
|
4. **tmux.** Confirm tmux 3.2 or later, with `remain-on-exit` off.
|
|
58
|
-
5. **Claude Code.** Run `claude --version` in the bot server's launcher environment (the same user and `PATH` the server launches workers with), and confirm that the workers' Claude Code is 2.1.
|
|
58
|
+
5. **Claude Code.** Run `claude --version` in the bot server's launcher environment (the same user and `PATH` the server launches workers with), and confirm that the workers' Claude Code is 2.1.280 or later: CSCB's supported minimum and the version the fleet runs. On a Claude Code too old for exec-form hooks (older than 2.1.139), each hook prints nothing, agent-director records `ad.hook.ignored` with the reason `no_exec_form`, and every launch stays `pending`. If it is older than 2.1.280, stop here, before anything goes down. In the same environment, run `command -v claude` and confirm that it resolves to Claude Code's native binary or to a wrapper that `exec`s it. A launcher or shim that runs Claude Code as a child process makes agent-director ignore every hook, recording `ad.hook.ignored` with the reason `pid_mismatch`, and every launch stays `pending`; replace such a launcher now.
|
|
59
59
|
6. **agent-director's timing settings.** Before the AD directory move, read all nine keys of the `[tmux]` table of the existing installation’s `~/.agent-director/config.toml` (an explicit pre-upgrade path exception). If AD is already upgraded, use `~/.config/agent-director/config.toml` instead. A missing file, a missing key or `0` means the default (see [agent-director's timing settings](agent-director.md#agent-directors-timing-settings)). Also read that file’s `[pause] timeout_seconds` (30 s when the file or the key is missing). Record the effective values in the switch-over log. Confirm that the three windows, `pending_grace_seconds`, `stopping_window_seconds` and `starting_session_seconds`, are the values you intend and each is at or above its minimum: `starting_session_seconds` 60, `stopping_window_seconds` 30, and `pending_grace_seconds` the larger of 30 and ⌈(`create_timeout_ms` + `pipe_close_wait_ms`) / 1000⌉ + 20. A `pending_grace_seconds` above 540 s shortens agent-director's SessionStart wait to 540 s. In the same file, read `[defaults] expire_retention_days`: Phase 1 accepts 1 to 106751, and `0` or a missing key gives 31 days. A negative value or one above 106751 makes Phase 1 refuse its whole configuration with `ErrConfigMalformed`, from the install on: every store-backed verb, `serve` and the hooks. Correct such a value now, and record the effective value in the switch-over log.
|
|
60
60
|
7. **The call timeout.** Always computing from this host's values, confirm that the `agent_director_call_timeout_ms` the persona configuration will carry (staged below; `60000` when it leaves the setting out) exceeds the need: the largest ceiling among the verbs CSCB calls, plus the 15 s margin (15000 ms). Record the computed need in the switch-over log. With Q, A, C, W and E the host's `query_timeout_ms`, `action_timeout_ms`, `create_timeout_ms`, `pipe_close_wait_ms` and `kill_exit_wait_ms`, and B its `sweep_budget_seconds` in ms, the ceilings, in ms, are:
|
|
61
61
|
- `kill`: the larger of 2Q + 2A + E + 4W and 3Q + 2A + 5W;
|
|
@@ -67,8 +67,8 @@ Do this beforehand, with the old CSCB running and still installed as the global
|
|
|
67
67
|
|
|
68
68
|
`expire` is not among them: CSCB never calls it. [Sizing the agent-director call timeout](agent-director.md#sizing-the-agent-director-call-timeout) works through examples.
|
|
69
69
|
8. **Leftover sessions.** As the workers' user, on the pinned socket, compare `tmux ls` with `agent-director list`, and record in the switch-over log every `slack_bot_` session that no row names.
|
|
70
|
-
9. **Stage the new release without installing it.** Choose its exact version, record it in the switch-over log and confirm that it is available to install. Write the persona configuration, with `agent_director_call_timeout_ms`, in a separate file, never `config.json`, which the old CSCB reads. Prepare the Slack apps. Nothing is installed over the global package yet, so step 3's `stop --stop-bots` is the old version's own and reads the old, pre-persona `config.json` from the old state directory, `~/.claude/channels/slack`, where the old package keeps it. In the staged file, name each persona's `credentials_file` in that directory too, never under `~/.config/claude-slack-channel-bots` (step 7 says why). Steps 1, 2, 3, 5 and 6 of [Upgrading to personas](#upgrading-to-personas) belong here.
|
|
71
|
-
10. **Copies for rollback.**
|
|
70
|
+
9. **Stage the new release without installing it.** Choose its exact version, record it in the switch-over log and confirm that it is available to install. On an existing-persona host (0.11.0 or later), retain the current persona configuration and credentials files, persona keys, `working_directory`, `claude_config_dir`, `resume_enabled` and state paths; check the existing call timeout against item 7. Nothing is installed over the global package yet, so step 3 uses the old package's own stop command against its current files. The rest of this item applies only to a pre-persona host (0.10.x). Write the persona configuration, with `agent_director_call_timeout_ms`, in a separate file, never `config.json`, which the old CSCB reads. Prepare the Slack apps. Nothing is installed over the global package yet, so step 3's `stop --stop-bots` is the old version's own and reads the old, pre-persona `config.json` from the old state directory, `~/.claude/channels/slack`, where the old package keeps it. In the staged file, name each persona's `credentials_file` in that directory too, never under `~/.config/claude-slack-channel-bots` (step 7 says why). Steps 1, 2, 3, 5 and 6 of [Upgrading to personas](#upgrading-to-personas) belong here.
|
|
71
|
+
10. **Copies for rollback.** On an existing-persona host (0.11.0 or later), back up the current persona `config.json`, credentials files, crontable, `/interject` callers and state files, including `config.json.last-applied` and `retired-keys.json` when present, outside the state directory. Record their current paths for restoration. On a pre-persona host (0.10.x), keep copies of the pre-persona `config.json`, the crontable, the `/interject` callers (the host crontab's `curl` lines included), `access.json` and the Slack token environment variables the old CSCB uses. The old CSCB keeps `config.json`, the crontable and `access.json` in the old state directory, `~/.claude/channels/slack`: keep the copies outside it, because the first start in step 10 moves that directory. Record the old CSCB's exact version in the switch-over log beside them.
|
|
72
72
|
11. **The orchestrator prompt (operator action).** The shared orchestrator prompt's "ship now" wording may go out any time before the switch-over. The shared orchestrator prompt is the system prompt your orchestrator bots load, for example the file `append_system_prompt_file` names.
|
|
73
73
|
|
|
74
74
|
#### Step 2: Disable CSCB's autostart
|
|
@@ -83,7 +83,7 @@ With the old CSCB still installed, run its own stop:
|
|
|
83
83
|
claude-slack-channel-bots stop --stop-bots
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
On an existing-persona host (0.11.0 or later), this is the old package's own stop command, reading its current persona configuration and state paths. On a pre-persona host (0.10.x), this is the old version's own `stop --stop-bots`, which reads the old, pre-persona `config.json` from the old state directory, `~/.claude/channels/slack`, where the old package keeps it, since nothing new is installed before step 7. An operator arriving from a startup refusal has first reinstalled the previous CSCB (see [Arrived here from a startup refusal?](#arrived-here-from-a-startup-refusal)), so this holds for them too. From here until step 10, no bot server runs.
|
|
87
87
|
|
|
88
88
|
#### Step 4: Wait for the old rows to end
|
|
89
89
|
|
|
@@ -125,41 +125,53 @@ A leftover that cannot be ended this way means no Phase 1 install: take step 8's
|
|
|
125
125
|
```sh
|
|
126
126
|
bun install -g claude-slack-channel-bots@<the version recorded in step 1>
|
|
127
127
|
```
|
|
128
|
-
2.
|
|
129
|
-
3.
|
|
130
|
-
4.
|
|
128
|
+
2. On an existing-persona host (0.11.0 or later), retain its current persona configuration and paths. On a pre-persona host (0.10.x), put the persona configuration in place as `config.json`, in the old state directory, `~/.claude/channels/slack`, which the new CLI keeps using until the new CSCB's first start.
|
|
129
|
+
3. On an existing-persona host, retain its credentials files and paths. On a pre-persona host (0.10.x), write each persona's credentials file with the new CLI (see [`claude-slack-channel-bots credentials`](cli.md#claude-slack-channel-bots-credentials)), into the same directory: each `credentials_file` in the staged configuration names a path there.
|
|
130
|
+
4. On an existing-persona host, retain the current crontable and `/interject` callers. On a pre-persona host (0.10.x), rewrite crontable targets and `/interject` callers to name personas, in the crontable in the same directory.
|
|
131
131
|
5. Run the new install check (see [Checking your agent-director install](agent-director.md#checking-your-agent-director-install)). An older CLI is refused by the 0.12.0 client; do not start CSCB until step 8 installs the required CLI.
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
On a pre-persona host (0.10.x), steps 4, 7 and 8 of [Upgrading to personas](#upgrading-to-personas) belong here. From this step the new CSCB is deployed but not started.
|
|
134
134
|
|
|
135
|
-
**
|
|
135
|
+
For a host whose state has not yet moved to the current directory, **steps 7 and 8, the call-timeout edit included, must not create `~/.config/claude-slack-channel-bots`.** The credentials command makes the directory of a `credentials_file` that is missing, so a path under `~/.config/claude-slack-channel-bots` creates it. The new CSCB's first start moves the old state directory to `~/.config/claude-slack-channel-bots` in one rename when that does not exist; when it exists, the start merges the old directory into it entry by entry, and a file that differs in the two is a conflict that leaves the old directory in place.
|
|
136
136
|
|
|
137
137
|
#### Step 8: Install agent-director Phase 1
|
|
138
138
|
|
|
139
139
|
1. **The go-ahead (operator action).** Confirm that the go-ahead for this host still stands, and record it in the switch-over log.
|
|
140
140
|
|
|
141
|
-
**No go.** If there is no go-ahead, there is no Phase 1 install. Reinstall the previous CSCB, the version step 1 recorded,
|
|
141
|
+
**No go.** If there is no go-ahead, there is no Phase 1 install. Reinstall the previous CSCB, the version step 1 recorded, and restore its version-appropriate files at the paths saved in step 1. On an existing-persona host (0.11.0 or later), restore the persona configuration, credentials files, crontable, `/interject` callers and saved state, including `config.json.last-applied` and `retired-keys.json` when backed up. On a pre-persona host (0.10.x), restore the `config.json`, crontable, `/interject` callers, `access.json` and Slack token environment variables saved in step 1. Start it on the previous compatible CLI and re-enable its autostart (operator action). Every other agent this step already stopped is started again on the previous compatible CLI through its launcher (operator action). The runbook stops there.
|
|
142
142
|
2. **Stop every other agent (operator action).** Otherwise, before the install, disable everything that starts agents and stop every other agent on the host: orchestrators' workers, hand-started sessions and sessions with an `agent-director serve`. Stop every long-running AD process and every caller, including TypeScript clients, before the installer runs. Use the still-installed CLI's `kill` and `find-missing` to reconcile stopped agents, using step 6's version-specific identifiers and flags. Confirm with `agent-director list` that all rows are non-live (`ended` or `missing`), with a read-only `tmux ls` that no agent session is left, and with process checks that all workers, callers, serves and launchers are stopped. Record it in the switch-over log. An unresolved live row or a process that cannot be stopped means "no go": do not run the installer, delete rows or edit SQL to fake a terminal state. No agent may run across the upgrade; the installer enforces this with `ErrAgentsLive`.
|
|
143
143
|
3. **Back up the store and locate the original install.** Run from a shell agent-director did not start, in the directory the original installer ran in (or the same repository). Before the move, the default store is `~/.agent-director/state.db`. For an external `[store] db_path`, save `state.db`, `state.db-wal` and `state.db-shm` while all callers are stopped; the installer migrates an external store in place and rollback needs that copy. Keep the original installer flags and registrations in the switch-over log.
|
|
144
144
|
4. **Install released agent-director 0.12.0 with its own installer.** Follow [agent-director's installer instructions](https://github.com/gabemahoney/agent-director/blob/v0.12.0/skills/install-agent-director/SKILL.md). Do not copy binaries by hand or create templates in the new directory first. The installer moves `~/.agent-director` to `~/.config/agent-director`, keeps the original as `~/.agent-director.pre-move-<timestamp>`, migrates the store to schema 9, and migrates config and templates. Preserve that kept directory until rollback is no longer wanted. Fix any manual PATH, hook or MCP entries the installer reports, including entries under other Claude config directories; confirm `agent-director version` reports 0.12.0 or later and the new `config_dir`.
|
|
145
145
|
5. **Write the `[tmux]` values.** Right after the install, before the restarts that follow and so before the new CSCB starts, write any non-default `[tmux]` values you want into `~/.config/agent-director/config.toml`. A long-running agent-director process reads the file only when it starts.
|
|
146
146
|
6. **Restart callers (operator action).** Restart every `agent-director serve` process and every caller, including TypeScript clients. Compare process start times to confirm that none is older than the install. Record the version and that result in the switch-over log.
|
|
147
|
-
7. **Check the settings again.** Read the nine timing settings and `[pause] timeout_seconds` again as in step 1, and record the effective values. Confirm them and the call timeout as in step 1; if the need has grown, raise `agent_director_call_timeout_ms` in `config.json` before step 10, in the
|
|
147
|
+
7. **Check the settings again.** Read the nine timing settings and `[pause] timeout_seconds` again as in step 1, and record the effective values. Confirm them and the call timeout as in step 1; if the need has grown, raise `agent_director_call_timeout_ms` in `config.json` before step 10, in the host's current configuration at its existing path (step 7's warning applies if the state directory has not yet moved). Confirm that `agent-director list` answers without `ErrConfigMalformed`.
|
|
148
148
|
|
|
149
149
|
#### Step 9: Start the other agents again
|
|
150
150
|
|
|
151
|
-
Every other agent on the host is started again
|
|
151
|
+
Every other agent on the host is started again through its launcher (operator action), which also starts its `serve` processes on the Phase 1 binary.
|
|
152
152
|
|
|
153
153
|
#### Step 10: Start the new CSCB
|
|
154
154
|
|
|
155
155
|
1. **The orchestrator prompt's worker cleanup (operator action).** Before the new CSCB starts, change worker cleanup in the shared orchestrator prompt, and in its source copy if you keep one, from row-delete cleanup to "kill, then leave the row". This change goes out in the same deploy as agent-director Phase 1. Kill-then-leave works on 0.10.0 too, so a rollback does not revert it.
|
|
156
156
|
2. **Start the new CSCB**, then re-enable the host's autostart for CSCB (operator action):
|
|
157
|
+
|
|
158
|
+
On a host already running personas (0.11.0 or later), check each persona before starting:
|
|
159
|
+
```sh
|
|
160
|
+
agent-director get --instance-id cscb_<key>
|
|
161
|
+
```
|
|
162
|
+
Record the row's state, labels and `session_id`. Keep the persona's key, `working_directory` and `claude_config_dir` unchanged, and confirm on the pinned tmux socket that no leftover session has the persona's session name. Record the results in the switch-over log.
|
|
163
|
+
|
|
164
|
+
Start the server:
|
|
157
165
|
```sh
|
|
158
166
|
claude-slack-channel-bots start
|
|
159
167
|
```
|
|
160
|
-
|
|
168
|
+
Every earlier-release bot starts a fresh conversation once, including bots on a host already running personas (0.11.0 or later). Memory files are kept; the old conversation context is not retained. The replacement row carries `tag_prompt=1`. A row without the marker is never resumed or typed into. A live row read `working` or `check_permission` is left alone until eligible; one read `waiting`, `pending` or `ask_user` is replaced through the checked recovery path.
|
|
161
169
|
|
|
162
|
-
|
|
170
|
+
On a host switching over from a pre-persona release (0.10.x), the first start has no last-applied record yet, so it checks `config.json`, records it and applies it; after that, edits wait until you apply them (see [Reload](reload.md#reload)). Each persona starts fresh once during this upgrade. Pre-persona rows are kept and never resumed.
|
|
171
|
+
|
|
172
|
+
Check each replacement with the `agent-director get` command above: it must carry the marker and a new `session_id`. Record the result in the switch-over log. Later restarts resume the marked replacement when `resume_enabled` is enabled, the row is `ended` or `missing`, its current `session_id` is non-empty and `transcript_status: present`. For that later resume, check `server.log` for `spawnForPersona: attempting resume for …`. agent-director 0.12.0 resumes only the current session: it no longer falls back to an older session's transcript when the current one is missing.
|
|
173
|
+
|
|
174
|
+
A host already using the current state directory keeps that path; with `SLACK_STATE_DIR` set, nothing moves. On a pre-persona host (0.10.x) still using the old default path, this start also moves the old state directory: `~/.claude/channels/slack` is renamed to `~/.config/claude-slack-channel-bots`, a symbolic link to it is left at the old name, and the server logs one `[slack] state-migrate: moved the state directory from …` line to `~/.config/claude-slack-channel-bots/server.log`. Edit `config.json` and the other state files through `~/.config/claude-slack-channel-bots` after it. An existing-persona host still using an earlier default state directory follows the state-directory move described in [Migration](#migration); keep its configuration and credentials paths intact.
|
|
163
175
|
3. **The post-install check (operator action).** Once every agent has been started again, confirm that `agent-director list --state pending` shows a `launch_started_at` on every row. Record the result in the switch-over log. A row without one is a human's to look at, and the persona whose row it is is held (see "A persona posts a *Held: launch start not recorded* notice" and "How a hold ends" in [Troubleshooting](troubleshooting.md#troubleshooting)).
|
|
164
176
|
|
|
165
177
|
#### Step 11: Schedule the daily expire
|
|
@@ -227,17 +239,17 @@ As the workers' user, on the socket switch-over step 1 pinned, run a read-only `
|
|
|
227
239
|
- **A leftover whose row is live:** end it with `agent-director kill --instance-id cscb_<key>`, and check its result; on an error, don't delete or respawn. When that `kill` is refused with "not this launch's session", handle it as "Operator actions" describes.
|
|
228
240
|
- **A finished row's own leftover session:** handle it as "Operator actions" describes.
|
|
229
241
|
- **An operator action refused inside the stopping window or the starting-session bound:** retry it once the longer of the two has passed. Read all nine timing settings again, as in switch-over step 1, for the two windows' values, and record them in the switch-over log. A plain `kill` never refuses for that reason.
|
|
230
|
-
- **A leftover with no row:** handle it as in [switch-over step 5](#step-5-check-for-leftover-sessions), against the previous CSCB's session names (`slack_bot_<name>_<channel>` or `slack_bot_<channel ID>`) instead of the new personas'.
|
|
242
|
+
- **A leftover with no row:** handle it as in [switch-over step 5](#step-5-check-for-leftover-sessions), against the previous CSCB's session names: `slack_bot_<key>` for an existing-persona release (0.11.0 or later); for a pre-persona release (0.10.x), the previous CSCB's session names (`slack_bot_<name>_<channel>` or `slack_bot_<channel ID>`) instead of the new personas'.
|
|
231
243
|
|
|
232
|
-
Check each result, then run `agent-director find-missing` and switch-over step 5's gone check again. Don't go on while a leftover remains
|
|
244
|
+
Check each result, then run `agent-director find-missing` and switch-over step 5's gone check again. Don't go on while a leftover remains. When rolling back to a pre-persona release (0.10.x), the previous CSCB's first start deletes every row without a `channel` label. This row-deletion warning does not apply when restoring an existing-persona release (0.11.0 or later).
|
|
233
245
|
|
|
234
246
|
#### Step 6: Stop every agent and restore the previous agent-director
|
|
235
247
|
|
|
236
248
|
1. **Stop every other agent (operator action).** Disable everything that starts agents. Before the previous binary is restored, stop every other agent on the host and every long-running agent-director process: orchestrators' workers, hand-started sessions and sessions with an `agent-director serve`. A `serve` runs inside its Claude session, so that session is stopped too. Stop every caller, including TypeScript clients, as well. Confirm as in [switch-over step 8](#step-8-install-agent-director-phase-1): `agent-director list` shows each stopped agent's row `ended` or `missing`, and a read-only `tmux ls` shows no agent session left. Record it in the switch-over log.
|
|
237
249
|
|
|
238
|
-
The restore does not begin while any agent runs. Every row must be non-live and every worker, session, caller, serve and launcher stopped before restoration. An unresolved row means no go; do not delete rows or edit SQL to fake a terminal state. If one cannot be stopped, the rollback stops as step 4 says, and every agent this step stopped is started again on Phase 1
|
|
250
|
+
The restore does not begin while any agent runs. Every row must be non-live and every worker, session, caller, serve and launcher stopped before restoration. An unresolved row means no go; do not delete rows or edit SQL to fake a terminal state. If one cannot be stopped, the rollback stops as step 4 says, and every agent this step stopped is started again on Phase 1 (operator action).
|
|
239
251
|
2. **Restore the previous agent-director** using [the released 0.12.0 way-back procedure](https://github.com/gabemahoney/agent-director/blob/v0.12.0/docs/operator-actions.md#going-back-to-the-previous-release). Set the new directory aside and restore the kept `~/.agent-director.pre-move-*` directory to `~/.agent-director`; restore any external store from its pre-upgrade copy. Restore registrations, hooks and paths with the previous release's own installer and its original flags, following that procedure. Schema 9 has no downgrade recipe: a binary swap or SQL column removal cannot roll it back. Data written since the upgrade is lost. Restart only callers compatible with the restored CLI.
|
|
240
|
-
3. **Start the other agents again (operator action).** Every other agent on the host is started again
|
|
252
|
+
3. **Start the other agents again (operator action).** Every other agent on the host is started again on the previous binary, which also starts its `serve` processes on it.
|
|
241
253
|
|
|
242
254
|
#### Step 7: Revert the orchestrator prompt's hold wording
|
|
243
255
|
|
|
@@ -245,13 +257,13 @@ Revert the shared orchestrator prompt's "hold until after" wording that switch-o
|
|
|
245
257
|
|
|
246
258
|
#### Step 8: Reinstall the previous CSCB
|
|
247
259
|
|
|
248
|
-
1. Move aside `config.json.last-applied` and `retired-keys.json` (see [Files beside the config file](reload.md#files-beside-the-config-file)), which are in
|
|
260
|
+
1. Move aside `config.json.last-applied` and `retired-keys.json` from the upgraded run (see [Files beside the config file](reload.md#files-beside-the-config-file)), which are in the host's current state directory after the new CSCB's first start. For rollback to an existing-persona release (0.11.0 or later), restore their switch-over step 1 copies along with the other saved state in item 3; do not discard the previous release's applied configuration or retired-key state.
|
|
249
261
|
2. Reinstall the previous CSCB, the version switch-over step 1 recorded, or else the version the host ran before, without starting it:
|
|
250
262
|
```sh
|
|
251
263
|
bun install -g claude-slack-channel-bots@<the previous version>
|
|
252
264
|
```
|
|
253
|
-
3.
|
|
254
|
-
4. Where switch-over step 1's copy of a file is missing, the operator rebuilds the pre-persona file by hand from the persona configuration, reversing the manual conversion in [Upgrading to personas](#upgrading-to-personas). No tooling does this: CSCB ships no conversion tooling in either direction.
|
|
265
|
+
3. For rollback to an existing-persona release (0.11.0 or later), restore the saved persona configuration, credentials files, crontable, `/interject` callers and state files, including the last-applied and retired-key records when backed up, at the paths recorded in switch-over step 1. Keep the persona keys, `working_directory`, `claude_config_dir` and `resume_enabled` unchanged. For rollback to a pre-persona release (0.10.x), put back what switch-over step 1 saved: the pre-persona `config.json`, the crontable, the `/interject` callers (the host crontab's `curl` lines included), `access.json` and the Slack token environment variables. Put `config.json`, the crontable and `access.json` back in the old state directory, by its old name, `~/.claude/channels/slack`: after the new CSCB's first start that name is a symbolic link to `~/.config/claude-slack-channel-bots`, and the previous CSCB reads its files through the link. Leave the link in place, and do not make a directory at the old name.
|
|
266
|
+
4. The following manual conversion applies only when rolling back to a pre-persona release (0.10.x). Where switch-over step 1's copy of a file is missing, the operator rebuilds the pre-persona file by hand from the persona configuration, reversing the manual conversion in [Upgrading to personas](#upgrading-to-personas). No tooling does this: CSCB ships no conversion tooling in either direction.
|
|
255
267
|
|
|
256
268
|
#### Step 9: Start the previous CSCB
|
|
257
269
|
|
|
@@ -263,6 +275,8 @@ claude-slack-channel-bots start
|
|
|
263
275
|
|
|
264
276
|
### Upgrading to personas
|
|
265
277
|
|
|
278
|
+
After upgrading, every bot starts one fresh conversation; memory files are kept. Remove the Claude Code version pin after the upgrade.
|
|
279
|
+
|
|
266
280
|
This major version accepts only the persona configuration format. These are the configuration steps of an upgrade from an earlier major version. Do them inside the switch-over runbook, not on their own: steps 1, 2, 3, 5 and 6 (the persona file, names, reply settings, Slack apps and who can reach each persona) at [step 1 of the switch-over](#step-1-check-the-host-and-stage-the-release), and steps 4, 7 and 8 (credentials files, crontable and `/interject` callers) at [step 7 of the switch-over](#step-7-install-the-new-cscb-without-starting-it). At switch-over step 1, these steps are written into the separate staged file, not `config.json`; step 7 puts that file in place as `config.json`. Until the new CSCB's first start, the new CLI keeps using the old state directory, `~/.claude/channels/slack`, so the persona `config.json`, the credentials files and the crontable go there; the first start moves that directory to `~/.config/claude-slack-channel-bots`.
|
|
267
281
|
|
|
268
282
|
1. **Rewrite `config.json` by hand.** A configuration from an earlier major version stops the server at start, with an error that names the offending setting and says the configuration must be converted to personas. Nothing is converted automatically and the file is not changed. Write a `personas` list as described in [Personas (config.json)](configuration.md#personas-configjson); the server-wide settings keep their names. The `debug-slack-channel-bots` skill covers this error under "Pre-persona configuration".
|
|
@@ -276,12 +290,13 @@ This major version accepts only the persona configuration format. These are the
|
|
|
276
290
|
|
|
277
291
|
### Downgrading to an earlier release
|
|
278
292
|
|
|
279
|
-
|
|
293
|
+
Earlier releases ignore this release’s queue and can resume a bot without their required channels launch, leaving it unable to receive messages. Follow all four phases in order, including when the configuration already fits the target release.
|
|
280
294
|
|
|
281
|
-
1. **
|
|
282
|
-
2. **
|
|
283
|
-
|
|
284
|
-
|
|
295
|
+
1. **Stop sending and empty the queues.** Wait until `server.log` shows every received item delivered. Otherwise explicitly accept that undelivered messages are lost when using the earlier release.
|
|
296
|
+
2. **Disable resume on the current release.** Set `resume_enabled` to `false` in `config.json`, [confirm the pending change](reload.md#confirming-a-change), and verify `config.json.last-applied` holds `resume_enabled: false`. Keep the current server running until this is applied.
|
|
297
|
+
If the target release does not support these settings, first give every persona a valid declarative section (`permission_prompts`, and `channels` unless `dm.enabled` is `true`; see [Load-time rules](configuration.md#load-time-rules)), remove `allow_invited_channels` and every persona’s `invited`, confirm, and ensure the last-applied record holds neither. These precautions do not replace any phase.
|
|
298
|
+
3. **Stop with the bots, then install and pin the earlier build.** Run `claude-slack-channel-bots stop --stop-bots` and verify it succeeds, install the earlier build (for example `bun install -g claude-slack-channel-bots@<version>`), then restore its required Claude Code version pin before starting it.
|
|
299
|
+
4. **Start the earlier release fresh with channels, then restore resume.** Run `claude-slack-channel-bots start`; verify every bot starts a new conversation with that release’s channels launch and no configuration load error. Only then set `resume_enabled` back to its previous value and confirm the change. The earlier build ignores `channel-delivery.json`; leave it in place.
|
|
285
300
|
|
|
286
301
|
---
|
|
287
302
|
|
package/docs/reload.md
CHANGED
|
@@ -14,6 +14,8 @@ These files sit in the same directory as `config.json` (`~/.config/claude-slack-
|
|
|
14
14
|
| `retired-keys.json` | The retired-key record: the persona keys the server holds as retired. A confirmed change records the key of each removed persona (a renamed persona's old key included) and of each persona whose `name`, `credentials_file` or `working_directory` changed, before `config.json.last-applied` is rewritten. A server start also records, before its clean-up of old instances ends any of them, the key of every agent-director instance labelled with a persona that is not in the applied configuration (for example, a persona that `config.json` no longer names, at a start after `config.json.last-applied` was deleted; see [Start rules](#start-rules)), so a persona added back later with that key starts fresh. If that write fails, the start logs it in `server.log`, holds those keys as retired until the server stops, and records them again at the next start. Only the server writes it, never the CLI. It is kept across restarts and is absent until a key is first recorded. A key's entry is removed on its own once the persona's new session is running. If it can't be read, the server doesn't start; moving it aside is the fix (see `retired-keys-unreadable` under [Startup errors](troubleshooting.md#startup-errors)). |
|
|
15
15
|
| `channel-delivery.json` | The stored-choice file: each persona's stored channel-delivery choices (see [Channel delivery in fungible mode](channel-modes.md#channel-delivery-in-fungible-mode)), by persona key and channel, with each persona's `name`, `credentials_file` and `working_directory`. It holds no token and no message text. Only the server reads and writes it, never the CLI. It is absent until the first accepted `set_channel_delivery` call, so it is never created in declarative mode. Never edit it by hand. The server reads it once at each start. A persona's choices are dropped when a confirmed change retires its key (the persona is removed, or its `name`, `credentials_file` or `working_directory` changes), and at a start when its key is not an applied persona, its `name`, `credentials_file` or `working_directory` differs from the one stored, or `retired-keys.json` holds its key as retired and the persona's new session has not yet begun; each drop logs a `[slack] channel-delivery:` line. If the file can't be read, parsed or validated at a start, the server leaves it in place, logs one [`channel-delivery-unreadable`](../skills/debug-slack-channel-bots/SKILL.md#channel-delivery-unreadable) line, and starts and runs every persona, but for that run no stored choice applies: in fungible mode every channel is at `mentions`, `set_channel_delivery` is refused, and nothing is written to the file or dropped from it. To fix it, move the file aside, then restart the server; this discards every stored choice. If you restore that same file in place instead of moving it aside, a persona removed and added back with the same `name`, `credentials_file` and `working_directory` while it was unreadable gets its earlier choices again. |
|
|
16
16
|
|
|
17
|
+
| `queue/<key>.jsonl` | The persona’s durable queue, holding message text until delivery. Removing a persona retains its queue file; delivery resumes only when the same key returns. |
|
|
18
|
+
|
|
17
19
|
## Start rules
|
|
18
20
|
|
|
19
21
|
| At start | What happens |
|
|
@@ -60,7 +62,7 @@ claude-slack-channel-bots: pending configuration change (written by the server)
|
|
|
60
62
|
fingerprint: sha256:<64 hex digits>
|
|
61
63
|
|
|
62
64
|
A configuration change is pending; nothing has been applied. personas: 1 added, 1 removed, 0 destructively modified, 1 modified in place, 1 with changed credentials; server-wide settings: 2 changed.
|
|
63
|
-
DESTRUCTIVE: persona "scribe" (key=scribe) is removed: the persona will be retired: its session stopped and never resumed.
|
|
65
|
+
DESTRUCTIVE: persona "scribe" (key=scribe) is removed: the persona will be retired: its session stopped and never resumed; 2 open queued items kept, delivered only if a persona with the same key is added again.
|
|
64
66
|
persona "helper" (key=helper) is added but cannot come up: working directory does not exist.
|
|
65
67
|
persona "planner" (key=planner): channels changed: applied in place immediately, instance kept.
|
|
66
68
|
persona "reviewer" (key=reviewer): credentials file "/home/operator/.config/claude-slack-channel-bots/reviewer-credentials.json" changed: a new connection opens, then the old one closes, instance kept.
|
|
@@ -156,7 +158,7 @@ To make them take effect sooner, wait for the `reload-applied` line, then run `c
|
|
|
156
158
|
|
|
157
159
|
## Destructive changes
|
|
158
160
|
|
|
159
|
-
`DESTRUCTIVE:` lines in the preview name the personas the change retires when you confirm it (see the table above). A retired persona's session is stopped and never resumed. Read the preview before you rename it. There is no undo. A persona removed and added again with the same key, or renamed and then renamed back, always starts a new conversation, even with a server restart in between: its old conversation is never resumed. A persona brought up from its new entry after a `credentials_file` path or `working_directory` change starts fresh the same way.
|
|
161
|
+
`DESTRUCTIVE:` lines in the preview name the personas the change retires when you confirm it (see the table above). A retired persona's session is stopped and never resumed. Its queue is retained: add a persona with the same key to deliver its queued messages. Read the preview before you rename it. There is no undo. A persona removed and added again with the same key, or renamed and then renamed back, always starts a new conversation, even with a server restart in between: its old conversation is never resumed. A persona brought up from its new entry after a `credentials_file` path or `working_directory` change starts fresh the same way.
|
|
160
162
|
|
|
161
163
|
CSCB never deletes an agent-director row, and it checks the result of every kill it makes. A teardown ends the old session, checks the result, and keeps the row; a removed persona's row stays until agent-director's `expire` removes it, and is never resumed. A kill agent-director can't carry out right now is tried up to 3 times, 2 s apart. When the kill still fails (for example, the worker outlived each of its kills), `server.log` shows the teardown's line for it, which ends `— the row is kept; nothing latches and no retry timer is armed …`. The persona's new session is then not started over the old one: the bring-up after a `credentials_file` path or `working_directory` change ends the old session first and starts the new conversation only once that succeeds. A renamed persona's new name is brought up only once the old instance has ended, and so is any other persona that shares the old instance's working directory; until then a Claude Code session started from that directory is refused (see "A persona waits on an old instance in its working directory" in [Troubleshooting](troubleshooting.md#troubleshooting)). The old conversation is never resumed either way.
|
|
162
164
|
|
|
@@ -170,7 +172,7 @@ To fix it, wait for the server to write `config.json.pending` again (within abou
|
|
|
170
172
|
|
|
171
173
|
## Size limit
|
|
172
174
|
|
|
173
|
-
`config.json`, the files beside it and each credentials file are read only up to 64 KiB; a larger file is treated as unreadable. The retired-key record (`retired-keys.json`) and the stored-choice file (`channel-delivery.json`) have no size cap: only the server writes them. If a very large change makes `config.json.pending` itself larger, its rename is refused as stale, so split the change into smaller edits.
|
|
175
|
+
`config.json`, the files beside it and each credentials file are read only up to 64 KiB; a larger file is treated as unreadable. The retired-key record (`retired-keys.json`) and the stored-choice file (`channel-delivery.json`) have no size cap: only the server writes them. The queue is also read in full with no size cap (`queue/<key>.jsonl`). If a very large change makes `config.json.pending` itself larger, its rename is refused as stale, so split the change into smaller edits.
|
|
174
176
|
|
|
175
177
|
## No reload command
|
|
176
178
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Scheduled Prompts (cscb_cron)
|
|
2
2
|
|
|
3
|
-
The server fires scheduled prompts into personas once per minute, reading them from a crontable. Each fire is
|
|
3
|
+
The server fires scheduled prompts into personas once per minute, reading them from a crontable. Each fire is stored in the target persona’s queue as its own injected message.
|
|
4
4
|
|
|
5
5
|
## The crontable
|
|
6
6
|
|
|
@@ -91,11 +91,11 @@ The prompt-file path resolves as follows:
|
|
|
91
91
|
|
|
92
92
|
## How fires appear
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
The server directly queues a scheduled fire only for the target persona, as an injected message with no Slack conversation attached. Its `sender` label is `cscb-cron:<prompt-file-basename>` — for a prompt file `standup.md` the sender is `cscb-cron:standup`. This distinguishes a cron tick from a human and from peer-bot traffic. The [Slack Reply Guard](bots.md#slack-reply-guard-stop-hook) never reminds a bot to reply to a scheduled prompt: like every injected message, a fire carries no `via`. Because a fire carries no channel, a prompt file that wants a Slack reply must say where to post it. Each schedule queues its own message independently, so when several schedules match the same minute for the same persona each one is its own queued item.
|
|
95
95
|
|
|
96
96
|
## The cron log
|
|
97
97
|
|
|
98
|
-
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 `
|
|
98
|
+
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 `queued=N failed=M` counts. The log is plain text, so `grep queue-failed cron.log` yields readable lines.
|
|
99
99
|
|
|
100
100
|
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.
|
|
101
101
|
|
|
@@ -103,14 +103,13 @@ The `outcome` field of each line is one of these classes:
|
|
|
103
103
|
|
|
104
104
|
| Outcome | What happened | What to do |
|
|
105
105
|
|---|---|---|
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
106
|
+
| `queued` | The prompt was durably stored for the target persona. | Nothing — storage succeeded; this is not a delivery confirmation. |
|
|
107
|
+
| `queue-failed` | The prompt could not be stored. | Check storage availability and permissions in the server log. |
|
|
108
108
|
| `unknown-persona` | No persona in the applied configuration has that name or key. | Correct the target in the crontable. |
|
|
109
109
|
| `prompt-missing` | The prompt file did not exist at fire time. | Create the file or correct its path in the crontable. |
|
|
110
110
|
| `prompt-unreadable` | The prompt file existed but could not be read (see the `errno`). | Fix file permissions or the path. |
|
|
111
111
|
| `prompt-oversize` | The prompt exceeds the 32KB `/interject` cap and was skipped, never truncated. | Shorten the prompt file. |
|
|
112
112
|
| `parse-error` | The crontable line could not be parsed. | Fix the line — see [Crontable format](#crontable-format). |
|
|
113
|
-
| `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. |
|
|
114
113
|
| `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. |
|
|
115
114
|
|
|
116
115
|
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.
|
|
@@ -129,13 +128,12 @@ grep undeliverable ~/.config/claude-slack-channel-bots/cron.log
|
|
|
129
128
|
|
|
130
129
|
## Delivery semantics
|
|
131
130
|
|
|
132
|
-
- **
|
|
131
|
+
- **Accepted fires stay queued.** A configured persona can accept prompts while it is down or disconnected. Every accepted fire is an independent item, never merged or dropped, and delivered in FIFO order across all sources, carrying its fire timestamp as `ts`; a failed store is logged as `queue-failed` and is not an accepted item.
|
|
133
132
|
- **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.
|
|
134
133
|
- **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.
|
|
135
134
|
- **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.
|
|
136
135
|
- **Server-local time.** Cron expressions are evaluated in the server's local timezone.
|
|
137
136
|
- **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.
|
|
138
|
-
- **`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`.
|
|
139
137
|
|
|
140
138
|
## Bot self-scheduling
|
|
141
139
|
|