Skip to content

v0.12.0 — Slack, one agent session per thread (+ @-mention switch) - #211

Merged
firstintent merged 10 commits into
mainfrom
dev
Oct 5, 2026
Merged

firstintent merged 10 commits into
mainfrom
dev

Conversation

@firstintent

Copy link
Copy Markdown
Owner

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)

  • A regular user can run their own Slack bot, like Telegram and Lark (slack@<tenant>, /me/im).
  • Choose first, then talk: at a channel's top level, commands preset the next session and the next message starts it in its own thread. 🧠 walks harness → model → reasoning effort (the model's own levels, else the harness's; skipped where there is none).

One @-mention switch for every IM (#20)

  • Telegram / Lark / Slack, the owner's bots and each user's own, carry 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 @.
  • REST PUT /config/im/{platform}/require-mention, the tenant twin under /me/im, a new GET /me/im; a switch row on all six web cards. Re-saving tokens keeps the switch.
  • The tenant cards show what is saved after a reload (bound ids no longer dropped on the next save).

Build / install

  • make install builds a thin-LTO local-release profile (~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

  • Two tests/*.rs checks that drifted outside the CI baseline fixed (F168 anchors all closed; dsh_bin test isolated from the host's npx cache).

Local gate: fmt clean · clippy 0 warnings (rustc 1.99.0) · make test-baseline 2492 passed / 0 failed · make test 3218 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 known ws_* PTY tests · SPA lint + 790 · plugin 289/289.

🤖 Generated with Claude Code

firstintent and others added 10 commits October 5, 2026 02:10
…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>
#19, #20)

Co-Authored-By: firstintent <239836187+firstintent@users.noreply.github.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-05T16:12:52.566323Z e97ff5e PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@firstintent
firstintent merged commit 47d0fd6 into main Oct 5, 2026
3 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +1513 to +1515
let held = match &thread_key {
Some(key) if addressed_threads.contains(key) => true,
Some((_, _, t)) => gateway.lock().await.has_current_session(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +798 to +801
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant