Repository navigation
v0.12.0 — Slack, one agent session per thread (+ @-mention switch) - #211
Conversation
…d Lark (#19) Slack was configurable only by the owner (the global bot under Settings → Access); a regular user's "My IM bot" section offered Telegram and Lark but no Slack, so the platforms were not symmetric. A tenant now has an optional `TenantSlack` beside `TenantTelegram` / `TenantLark`, and everything the other two get, Slack gets: - the daemon runs a `slack@<tenant>` channel per configured tenant — fail-closed until a member is allowed, its rejected senders in the shared probe file, and Slack's thread-per-session and button contracts under that name; - `PUT /me/im` takes a `slack` block (both tokens checked with Slack before anything is written; a token change keeps the bound members), plus `GET /me/im/slack/user-id-candidates` (scoped to the caller's own bot) and `PUT /me/im/slack/allowed-users`; - `chat_send_file` resolves a tenant's Slack as a delivery target; - "My IM bot" gets a Slack card with the owner card's three steps. The create-app manifest route is open to every signed-in identity — it holds no secret — while the global bot's capture/allowlist stay owner-only. Co-Authored-By: firstintent <239836187+firstintent@users.noreply.github.com>
… session (#19) Owner feedback: a channel showed nothing but command echoes — every `/ccteam new …` posted an anchor and the session lived in its thread, so the session's real first message became a reply and the channel never said what any session was about. In a channel that threads sessions, a command typed at the CHANNEL level no longer opens a session in its own place. `/new …` and `/use <sid>` preset the conversation's NEXT session (validated at once; the preset is a visible channel message, so everyone sees which model each session was set up with), and the next top-level message starts — or brings over — that session in its own thread, so the thread opens with that message. `/cd` there switches the project without adopting a session. The channel menu (`/ccteam` alone) is buttons first: 📁 projects · 🧠 model · 🧵 sessions · 📊 status; 🧠 walks harness → model (the advisory model catalog) and presets with a tap. Taps on a channel-level message act at the channel level too, so 🧵 sessions presets continuing the picked one. The Slack provider stops posting an anchor for a slash command, sends a top-level `!command` and a tap on a top-level message at the channel level, and keeps one safety net: a reply the bot cannot post (not in the channel) is explained through the command's ephemeral response. The daemon reports `session_threads` to the gateway with the button capability. Telegram, Lark and web are unchanged. Co-Authored-By: firstintent <239836187+firstintent@users.noreply.github.com>
…ly when @-mentioned (#20) Telegram, Lark and Slack each got an inline (or no) group @-rule, none configurable, so two ccteam bots in one Slack channel both answered every message from a member allowed on both. Fix it once, below the providers: - Every IM credentials block (and each tenant's own bot) carries `require_mention` (default false = answer everything). - Providers only report a fact, `ChannelMessage.ambient` (a group/channel message that does not address the bot: Telegram @bot / /cmd@bot / reply, Lark mentions, Slack <@bot>); `MentionPolicy` is the single rule that decides what an ambient message does. - Provider-side early drop before any attachment download for ambient messages outside a thread; the daemon's inbound consumer is the final call, admitting an ambient Slack thread reply only when the gateway holds a session in that thread — or an addressed message already claimed it (its session may still be spawning off the loop). Decided before the security layer so chatter never spends the sender's rate-limit budget. - Telegram strips the bot's own handle from group text so `@bot /new codex` is still a command; Lark's hard-coded "group needs an @" becomes this switch (default now matches the other IMs). - REST: `require_mention` in the masked status; new `GET /me/im`; one parametrised `PUT /config/im/{platform}/require-mention` and the tenant twin. Re-saving tokens (web, tenant, `ccteam config`) keeps the switch. - SPA: one switch row on all six cards (owner + "my IM bot" x 3). Co-Authored-By: firstintent <239836187+firstintent@users.noreply.github.com>
… cut (#20) Checker review of ac14ebb flagged a possible underflow in the Telegram handle-cut arithmetic. It is unreachable (a cut is only computed when the span ends with this bot's own @username, so it is at least that long), so no code change — a regression test now pins it, and the claimed-threads comment in the inbound consumer says what a full reset can actually lose. Co-Authored-By: firstintent <239836187+firstintent@users.noreply.github.com>
Owner feedback: picking a model from the channel menu presets the next session at once, so there was no way to choose its reasoning effort from the menu (only by typing `/new <vendor> model= effort=`). After a model, the menu now offers that model's effort levels — its own declared set when it has one, else the vendor's ladder, the same rule the web composer uses (`effortRowsFor`) — with "default effort" first, which wires nothing. A harness with no effort axis (opencode) is not asked, so no menu that does nothing. Each tap lands in the same preset a typed `/new` makes; `new-effort:<effort>:<vendor>[:<model>]` leads with the effort because a model id may contain a `:` and an effort never does. Co-Authored-By: firstintent <239836187+firstintent@users.noreply.github.com>
The tenant Telegram / Lark / Slack cards only tracked "done" in local component state and used the `/me/im` status for the @-mention switch alone, so a reload rendered a configured bot as unconfigured. Worse, the allowlist editors started empty while the allowlist PUTs replace the whole list, so binding one more id after a reload dropped the ones already bound. `/me/im` (and `/config/im`) now also return the bound Telegram chat ids and Lark open_ids (not secrets, as Slack member ids already were); the cards take every step's state, token fingerprints and allowlists from the server status. Tokens are still never echoed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`make install` built the shipped `release` profile (fat LTO, one codegen unit), whose final single-threaded LTO step dominated a local rebuild. The new `local-release` profile inherits release with thin LTO and 16 codegen units: a code-only rebuild drops from ~6m to ~35s on the dev box. `release` stays the published artifact's profile; install.sh treats the new build dir as a build tree, not an install location. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tree cb1c3a1 taught install.sh that `target/local-release` (the thin-LTO profile `make install` now builds) is a build tree, not an install location, but its three copies did not follow: `ccteam update`'s `resolve_install_dir_with`, the install-channel classifier behind `ccteam status` / `doctor`, and the DSH plugin's engine installer. So `ccteam update` run from a local-release build would install back into the build tree, and status would call that binary "other" with no update hint. Both drift guards (Rust and plugin) caught it. The profile list is now one constant in ccteam-core (`install_channel::CARGO_BUILD_PROFILES`) used by both Rust readers; the plugin mirrors it and install.sh keeps the shell copy, each still held in step by its drift test. Embedded plugin tarball re-packed (two-path reproducible 4eadf708…). Co-Authored-By: firstintent <239836187+firstintent@users.noreply.github.com>
- no_silent_todo: Slack over Socket Mode (#19) delivered the last three F168 `TODO(V0.7-…)` anchors, so the "exactly three survive" check failed on a clean tree. All six are closed; assert none remain. - dsh_acp: `dsh_bin()` falls back to a copy the user's own `npx dsh` cached under `~/.npm/_npx`, so the "absent override → `dsh`" test failed on any host that has one. Pin HOME to an empty dir. Both live in `tests/*.rs`, which `make test-baseline` (= CI) skips. Co-Authored-By: firstintent <239836187+firstintent@users.noreply.github.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e97ff5eaa1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let held = match &thread_key { | ||
| Some(key) if addressed_threads.contains(key) => true, | ||
| Some((_, _, t)) => gateway.lock().await.has_current_session( |
There was a problem hiding this comment.
Expire addressed-thread claims after the spawn window
When require_mention is enabled, every addressed Slack message inserts its thread into addressed_threads, but entries are never removed except when the entire 4096-entry set is cleared. Consequently, after an addressed /stop removes the thread's current-session route—or after an addressed command such as /help creates no session—later ambient replies are still treated as held and can spawn a new session without mentioning the bot. Limit this claim to the in-flight spawn window or remove it once routing finishes.
Useful? React with 👍 / 👎.
| let ambient = group && !bot.is_some_and(|b| addresses_bot(&m, b)); | ||
| if self.mention_policy.drops_early(ambient, false) { | ||
| tracing::debug!(chat_id = %chat_id, "telegram: ambient group message dropped (require_mention)"); | ||
| continue; |
There was a problem hiding this comment.
Let bare Telegram commands bypass mention gating
With require_mention enabled in a Telegram group, a normal bare command such as /status is classified as ambient because addresses_bot recognizes a bot_command only when it has an @bot suffix. The new early-drop therefore discards these slash commands, even though commands are intended to remain available regardless of the mention switch. Treat Telegram bot_command entities as addressed, or exempt commands before applying this gate.
Useful? React with 👍 / 👎.
Re-cut of v0.12.0. The first v0.12.0 release run (37221762018) was cancelled mid-build and its tag removed, so nothing was published (no GitHub release; npm still at 0.11.2). This PR brings the follow-ups that landed since, and the release is tagged after merge.
Since
main(#210):Slack (#19)
slack@<tenant>,/me/im).One @-mention switch for every IM (#20)
require_mention(default off = answer everything). On: in a group or channel the bot answers only messages that @-mention it; DMs, slash commands and buttons are always answered; on Slack, a thread the bot already holds continues without the @.PUT /config/im/{platform}/require-mention, the tenant twin under/me/im, a newGET /me/im; a switch row on all six web cards. Re-saving tokens keeps the switch.Build / install
make installbuilds a thin-LTOlocal-releaseprofile (~6m → ~35s rebuild). Every install-dir ladder (install.sh,ccteam update, status/doctor's install channel, the DSH plugin) treats it as a build tree; embedded plugin tarball re-packed (two-path reproducible).Tests
tests/*.rschecks that drifted outside the CI baseline fixed (F168 anchors all closed;dsh_bintest isolated from the host's npx cache).Local gate: fmt clean · clippy 0 warnings (rustc 1.99.0) ·
make test-baseline2492 passed / 0 failed ·make test3218 passed, 1 failed (body_record_follows_spawn_close_and_detach, full-parallel timing only: 3/3 alone, 38/38 ×2 as a file) · ccteam-web 480 passed + the 3 knownws_*PTY tests · SPA lint + 790 · plugin 289/289.🤖 Generated with Claude Code