Skip to content

Command system phases 2+3: authority, busy policy, native registration - #629

Merged
jamiepine merged 6 commits into
mainfrom
jamiepine/command-access
Aug 10, 2026
Merged

Command system phases 2+3: authority, busy policy, native registration#629
jamiepine merged 6 commits into
mainfrom
jamiepine/command-access

Conversation

@jamiepine

Copy link
Copy Markdown
Member

Phases 2 and 3 of the command system design (docs/design-docs/slash-commands.md). Stacks on the phase 1 registry from #626.

Phase 2 — access and busy policy

Dispatch moves out of the channel actor into the inbound router, before the message queue — where the matched binding already lives. Control commands now execute on a control plane (src/commands/control.rs) without creating an inbound message, which makes them busy-immune by construction: /quiet lands mid-turn instead of sitting behind an in-flight LLM turn. Live state is shared through two cells on ChannelState (turn-active flag, response mode) reachable via ChannelControlHandle, so mode changes apply immediately and the router can answer "busy?" without entering the queue.

Authority is opt-in per scope:

[[bindings]]
agent_id = "orion"
channel = "discord"
guild_id = 123456
authority = ["91827364"]   # may run /quiet, /active, /mention-only here

Resolution is binding list first, then an adapter-instance authority default, open when neither is set — existing configs behave exactly as before. /help and /status are always available, denials name the commands the sender can run, and /help output is filtered to what the caller can actually use. Agent commands rewrite to MessageContent::Command; queued-behind-a-turn commands get an ephemeral acknowledgment, and BusyPolicy::Reject refuses with a pointer to /stop (no command in the current set uses Reject yet — it's wired for the phase 4 set).

Fixes along the way: the coalesce flush no longer runs a full batched turn before a control command is handled; /status@someotherbot in a Telegram group is no longer answered (the parser now checks the addressed bot is us); ephemeral replies render in the portal via SSE instead of being silently dropped.

Phase 3 — native registration

All generated from the registry through platform-neutral specs in src/commands/native.rs:

  • Discord — application commands with diff-only sync on ready: fetch, compare, skip when identical, bulk overwrite when not. Guild-scoped when bindings declare guilds, global otherwise, cap-aware with one log line counting anything dropped. Slash-command interactions defer (ephemeral for Control), and replies resolve the deferral via an interaction-token map; expired tokens fall back to normal channel sends.
  • Telegram — bot menu registered via setMyCommands at adapter start. Hyphenated names mangle to underscores (/mention_only) and the parser folds them back on resolve.
  • Slack — one /spacebot umbrella with subcommands over the existing Socket Mode connection. The config-driven alias system is gone: SlackCommandConfig, its loader, fingerprint segments, and the never-read slack_command_agent_id metadata are deleted (the old agent_id mapping never routed anything — routing was already binding-based).

A parity test walks command × platform, so a command available on a surface but missing from its native listing fails the build instead of silently disappearing.

Docs: authority rows in the config reference, Slack slash-command setup section, applications.commands added to the Discord invite scopes.

Testing

New unit tests for authority resolution (wildcard, scope precedence, empty-list semantics, denial text), parser addressing, and the native spec generators including Telegram naming limits and the parity walk. just gate-pr green: fmt, clippy -Dwarnings, 954 lib tests, integration targets compile, typegen regenerated.

Not in this PR: the phase 4 command set (/stop, /retry, /memory, /model, ...), GET /api/commands + POST /api/channels/:id/command for the portal palette, and Mattermost native ephemeral posts (router replies degrade to regular messages there).

Phase 2 — dispatch moves to the inbound router, before the channel queue.
Control commands execute on a control plane (no inbound message, busy-immune,
work mid-turn via shared response-mode/turn-active cells on ChannelState).
Binding-level authority lists (+ adapter-instance defaults) gate state-mutating
commands, with /help and /status always available and denials naming what the
sender can run. Agent commands rewrite to MessageContent::Command; queued
commands are acknowledged, Reject-policy commands refuse with a /stop pointer.
Also fixes the coalesce-flush ordering so control commands don't wait out a
batched LLM turn, and the @botName strip now checks it's actually our bot.

Phase 3 — native registration generated from the registry. Discord: application
commands with diff-only sync (guild-scoped from bindings, global otherwise,
cap-aware), interaction handling with ephemeral deferral for control replies.
Telegram: setMyCommands menu with hyphen->underscore mangling that folds back
on parse. Slack: one /spacebot umbrella with subcommands; the config-driven
command alias system (SlackCommandConfig, slack_command_agent_id) is gone.
Ephemeral replies now render in portal (SSE) instead of vanishing.

Parity test walks command x platform so a command can't silently miss a
surface.
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 25e860d7-d15a-4224-bb81-2dd94f8305a7

📥 Commits

Reviewing files that changed from the base of the PR and between f6626f4 and 29b510b.

📒 Files selected for processing (4)
  • src/agent/channel.rs
  • src/conversation/channel_settings.rs
  • src/conversation/portal.rs
  • src/conversation/settings.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/conversation/channel_settings.rs
  • src/conversation/portal.rs
  • src/agent/channel.rs

Walkthrough

This change adds authority-gated commands, shared inbound dispatch, live response controls, native command registration, authority configuration, binding management, and autonomy and wake API schemas.

Changes

Command access and native command flow

Layer / File(s) Summary
Command metadata and authority resolution
src/commands.rs, src/commands/registry.rs, src/commands/access.rs
Commands now define access, busy, and platform availability. Authority resolution supports binding and adapter defaults, matching rules, filtered help, and denial responses.
Inbound control and live state
src/commands/dispatch.rs, src/commands/control.rs, src/agent/channel.rs, src/agent/process_control.rs, src/conversation/*
Slash commands are dispatched before normal message processing. Control commands update persisted and live response modes. Turn activity uses shared atomic state with RAII cleanup.
Native adapter integration
src/commands/native.rs, src/messaging/discord.rs, src/messaging/slack.rs, src/messaging/telegram.rs
Adapters generate or register native commands from the shared registry. Discord synchronizes application commands, Slack parses structured subcommands, and Telegram registers its command menu.

Configuration, binding, and API contracts

Layer / File(s) Summary
Authority configuration and binding management
src/config/*, src/api/bindings.rs, src/cli/binding.rs, interface/src/api/schema.d.ts
Authority lists load from TOML, propagate through resolved configuration, support API and CLI updates, and reload through shared defaults. Binding schemas preserve omitted and explicit empty values.
Autonomy and wake API schemas
interface/src/api/schema.d.ts
The API schema adds autonomy ceiling and wake operations, related request and response types, and internal-server-error responses for goal operations.
Setup documentation and fixtures
docs/content/docs/(configuration)/config.mdx, docs/content/docs/(messaging)/*, src/config.rs
Documentation describes authority lists, supported binding platforms, Discord application-command scopes, and optional Slack slash-command setup. Fixtures include authority fields and binding matching coverage.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the command-system phases and the primary changes: authority, busy policy, and native registration.
Description check ✅ Passed The description directly explains the authority, busy-policy, dispatch, native-registration, documentation, and testing changes in the pull request.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jamiepine/command-access

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jamiepine
jamiepine marked this pull request as ready for review August 9, 2026 09:36

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
docs/content/docs/(configuration)/config.mdx (1)

657-700: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document authority settings for every supported adapter.

AdapterAuthorityDefaults::from_config also reads authority lists for Telegram instances, Twitch, Signal, Mattermost, and Email. Add these fields to their root and named-instance tables. Operators otherwise cannot discover all supported authority configuration.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/content/docs/`(configuration)/config.mdx around lines 657 - 700, Extend
the configuration documentation to include the authority setting for every
supported adapter handled by AdapterAuthorityDefaults::from_config. Add the
appropriate authority rows to the root and named-instance tables for Telegram,
Twitch, Signal, Mattermost, and Email, matching the existing defaults and
descriptions used by the implementation.
src/main.rs (1)

1317-1334: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Resolve binding scope for preassigned portal messages.

PortalSendRequest sets InboundMessage.agent_id, and Binding::matches_route can route a portal message by matching that agent ID. Because dispatch already bypasses resolve_agent_for_message and matched_binding_authority when agent_id is present, binding settings and authority are skipped. Compute binding_settings and binding_authority even when message.agent_id is already set, using the same route match.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main.rs` around lines 1317 - 1334, Update the agent-selection branch
around message.agent_id so preassigned portal messages also resolve their
matching binding metadata. When agent_id is already present, load the current
bindings and use the same route-matching logic to populate binding_settings and
binding_authority; retain the existing resolved-agent behavior and fallback
resolution for messages without an agent_id.
🧹 Nitpick comments (1)
src/commands/native.rs (1)

107-111: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse ArgSpec::hint() for the choice branch.

ArgSpec::hint() already formats Choice as [a|b] (src/commands/registry.rs lines 142-148). The explicit if let ArgSpec::Choice(...) branch duplicates that formatting. A future change to hint() will silently diverge from the Telegram menu.

♻️ Proposed simplification
             let mut description = def.description.to_string();
-            if let ArgSpec::Choice(options) = def.args {
-                description.push_str(&format!(" [{}]", options.join("|")));
-            } else if let Some(hint) = def.args.hint() {
+            if let Some(hint) = def.args.hint() {
                 description.push_str(&format!(" {hint}"));
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/native.rs` around lines 107 - 111, In the command description
construction, remove the explicit ArgSpec::Choice branch and rely on
def.args.hint() for all argument types, including choices. Preserve the existing
formatting produced by ArgSpec::hint() so choice options remain displayed as
[a|b] consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/content/docs/`(configuration)/config.mdx:
- Line 800: Update the authority configuration row in the table to include the
missing [] default cell before its description, preserving the existing type and
description columns.

In `@src/commands/access.rs`:
- Around line 101-105: Update the binding selection logic in the access command
to distinguish an omitted binding_authority from an explicitly empty list,
ensuring the empty list is preserved and does not fall through to
adapter_default. Keep adapter fallback only for the omitted-list case, and add a
regression test covering an explicitly empty binding_authority with a restricted
adapter default.

In `@src/commands/control.rs`:
- Around line 170-192: In src/commands/control.rs lines 170-192, update the
PortalConversationStore flow so a get failure does not become default settings
followed by an upsert; instead return a failure reply or use an atomic
response-mode-only update. Apply the same safeguard in src/commands/control.rs
lines 196-220 for ChannelSettingsStore: preserve existing settings through an
atomic response-mode-only update or return a failure reply, and do not report
success after persistence fails.

In `@src/commands/dispatch.rs`:
- Around line 115-118: Serialize SetResponseMode execution per conversation
instead of spawning each control command independently in the dispatch path.
Update the task flow around plane.execute and send_ephemeral to enqueue these
state-mutating commands in a per-conversation FIFO executor, while preserving
existing handling for other commands and response delivery.

In `@src/commands/native.rs`:
- Around line 112-116: Update the minimum description check in the command
description handling to use character count rather than String::len() bytes.
Keep the existing fallback to /{command}, and align the guard with the
chars().count() unit used by the test.

In `@src/messaging/discord.rs`:
- Around line 1143-1170: Update sync_application_commands to iterate all
configured guild IDs rather than only ready_guilds, using each ID to fetch and
register commands independently. Preserve the existing in-sync fast path, but
log fetch, registration, and non-member/guild-create errors with the guild ID
while continuing to process the remaining configured guilds instead of silently
skipping them.
- Around line 1009-1064: Add Discord-specific validation in
build_discord_create_commands before constructing commands passed to
set_commands or set_global_commands. Validate spec.name, spec.description,
option hints, and each choice against Discord’s field limits, excluding or
handling invalid entries so one invalid spec cannot fail the batch while
preserving the dropped-count behavior. Add a Discord limits test modeled on
telegram_menu_entries_satisfy_platform_limits.

In `@src/messaging/slack.rs`:
- Around line 487-517: Filter the resolved command in the Slack subcommand
handling flow so commands with `def.availability.slack` set to false are treated
as unresolved. Preserve `REGISTRY.resolve(&subcommand)` alias and
case-insensitive matching, but only dispatch when the resolved definition is
available on `Surface::Slack`; otherwise continue through the existing
usage/listing response path.

---

Outside diff comments:
In `@docs/content/docs/`(configuration)/config.mdx:
- Around line 657-700: Extend the configuration documentation to include the
authority setting for every supported adapter handled by
AdapterAuthorityDefaults::from_config. Add the appropriate authority rows to the
root and named-instance tables for Telegram, Twitch, Signal, Mattermost, and
Email, matching the existing defaults and descriptions used by the
implementation.

In `@src/main.rs`:
- Around line 1317-1334: Update the agent-selection branch around
message.agent_id so preassigned portal messages also resolve their matching
binding metadata. When agent_id is already present, load the current bindings
and use the same route-matching logic to populate binding_settings and
binding_authority; retain the existing resolved-agent behavior and fallback
resolution for messages without an agent_id.

---

Nitpick comments:
In `@src/commands/native.rs`:
- Around line 107-111: In the command description construction, remove the
explicit ArgSpec::Choice branch and rely on def.args.hint() for all argument
types, including choices. Preserve the existing formatting produced by
ArgSpec::hint() so choice options remain displayed as [a|b] consistently.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bd67cf43-72f5-40f0-a9cf-ec7d75631f89

📥 Commits

Reviewing files that changed from the base of the PR and between 04e048e and 30b6cfe.

📒 Files selected for processing (27)
  • docs/content/docs/(configuration)/config.mdx
  • docs/content/docs/(messaging)/discord-setup.mdx
  • docs/content/docs/(messaging)/slack-setup.mdx
  • interface/src/api/schema.d.ts
  • src/agent/channel.rs
  • src/agent/process_control.rs
  • src/api/bindings.rs
  • src/api/messaging.rs
  • src/cli/binding.rs
  • src/commands.rs
  • src/commands/access.rs
  • src/commands/control.rs
  • src/commands/dispatch.rs
  • src/commands/native.rs
  • src/commands/registry.rs
  • src/config.rs
  • src/config/load.rs
  • src/config/toml_schema.rs
  • src/config/types.rs
  • src/config/watcher.rs
  • src/conversation/settings.rs
  • src/main.rs
  • src/messaging/discord.rs
  • src/messaging/email.rs
  • src/messaging/slack.rs
  • src/messaging/telegram.rs
  • tests/context_dump.rs
💤 Files with no reviewable changes (1)
  • src/api/messaging.rs

Comment thread docs/content/docs/(configuration)/config.mdx Outdated
Comment thread src/commands/access.rs Outdated
Comment thread src/commands/control.rs Outdated
Comment thread src/commands/dispatch.rs Outdated
Comment thread src/commands/native.rs
Comment thread src/messaging/discord.rs
Comment thread src/messaging/discord.rs Outdated
Comment thread src/messaging/slack.rs
…d native registration

- binding authority is now Option: omitted defers to the adapter default,
  explicit [] opens the scope even when the adapter default is restricted
- response-mode changes fail loudly: a settings read/write error no longer
  upserts defaults or reports a false confirmation, and the live cell only
  updates after a successful persist
- SetResponseMode executes inline in the router so mode changes apply in
  delivery order; read-only control commands stay on spawned tasks
- discord: validate command names/choices and clamp descriptions before bulk
  registration so one bad spec can't fail the batch; sync every configured
  guild instead of only READY payload guilds, logging per-guild failures
- slack: /spacebot subcommands only resolve commands available on slack
- telegram: description minimum uses char count; choice hints reuse
  ArgSpec::hint()
- portal sends with a preassigned agent_id resolve binding settings/authority
- docs: authority rows for telegram instances, twitch, signal, mattermost,
  email; bindings table gets its missing default column

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/api/bindings.rs (1)

57-58: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve explicit empty authority lists in API requests.

Vec<String> with #[serde(default)] makes an omitted value and authority: [] identical. The create and update writers then omit the TOML key for both cases. A client cannot create a binding-level open override when an adapter default restricts access.

  • src/api/bindings.rs#L57-L58: Use an optional authority field so omitted and explicit empty values remain distinct.
  • src/api/bindings.rs#L169-L170: Preserve the same distinction for update requests.
  • src/api/bindings.rs#L502-L508: Write authority = [] when the request contains Some(vec![]).
  • src/api/bindings.rs#L888-L897: Remove the key only when authority is omitted according to the update contract.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/bindings.rs` around lines 57 - 58, Preserve omitted versus explicitly
empty authority values across the binding API. In src/api/bindings.rs lines
57-58 and 169-170, change the create and update request authority fields to
optional values without collapsing explicit empty arrays. In lines 502-508,
write authority = [] for Some(vec![]), while omitting the key only for None;
apply the same omission rule in the update writer at lines 888-897, using the
relevant create/update request and writer symbols.
src/messaging/discord.rs (1)

1212-1218: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Register guild commands before removing global commands.

set_global_commands(http, Vec::new()) runs before the guild fetch and registration loop. If a guild registration then fails, that guild can have neither its former global commands nor its new guild commands until a later Ready event. Sync all configured guilds first. Clear global commands only after every target guild is confirmed in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/messaging/discord.rs` around lines 1212 - 1218, Reorder the command
synchronization flow around ApplicationCommand::set_global_commands: complete
the guild fetch and registration loop for every configured guild first, and only
clear non-empty global commands after all target guilds have been confirmed in
sync. Preserve the existing warning behavior for failures while ensuring a guild
registration failure does not leave that guild without either command set.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/content/docs/`(configuration)/config.mdx:
- Line 710: Update every named-instance authority row in
docs/content/docs/(configuration)/config.mdx at lines 710, 734, 781, 808, and
836 to document that an empty authority list means open access by adding “Empty
= open” to each description.

In `@src/commands/control.rs`:
- Around line 222-244: Update persist_response_mode and the underlying
ChannelSettingsStore/PortalConversationStore write paths to prevent
read-modify-write races when changing response_mode. Use an existing
per-conversation lock, optimistic revision check, or atomic response_mode-only
update, and ensure concurrent settings writers cannot overwrite fields changed
after the initial read.

In `@src/messaging/discord.rs`:
- Around line 1017-1051: Rename the newly introduced abbreviated variables to
descriptive names: in src/messaging/discord.rs lines 1017-1051, rename the
discord_spec_violation parameter spec; in src/messaging/discord.rs lines
1077-1094, rename the spec closure and collection variables; and in
src/messaging/slack.rs lines 428-430, rename the def closure variable. Use names
such as command_spec and command_definition consistently at each affected site.

---

Outside diff comments:
In `@src/api/bindings.rs`:
- Around line 57-58: Preserve omitted versus explicitly empty authority values
across the binding API. In src/api/bindings.rs lines 57-58 and 169-170, change
the create and update request authority fields to optional values without
collapsing explicit empty arrays. In lines 502-508, write authority = [] for
Some(vec![]), while omitting the key only for None; apply the same omission rule
in the update writer at lines 888-897, using the relevant create/update request
and writer symbols.

In `@src/messaging/discord.rs`:
- Around line 1212-1218: Reorder the command synchronization flow around
ApplicationCommand::set_global_commands: complete the guild fetch and
registration loop for every configured guild first, and only clear non-empty
global commands after all target guilds have been confirmed in sync. Preserve
the existing warning behavior for failures while ensuring a guild registration
failure does not leave that guild without either command set.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 35e32062-09d5-4cf9-92fa-fca32ac11738

📥 Commits

Reviewing files that changed from the base of the PR and between 30b6cfe and e189497.

📒 Files selected for processing (13)
  • docs/content/docs/(configuration)/config.mdx
  • interface/src/api/schema.d.ts
  • src/api/bindings.rs
  • src/commands/access.rs
  • src/commands/control.rs
  • src/commands/dispatch.rs
  • src/commands/native.rs
  • src/config.rs
  • src/config/toml_schema.rs
  • src/config/types.rs
  • src/main.rs
  • src/messaging/discord.rs
  • src/messaging/slack.rs
🚧 Files skipped from review as they are similar to previous changes (8)
  • interface/src/api/schema.d.ts
  • src/commands/dispatch.rs
  • src/config/types.rs
  • src/config.rs
  • src/main.rs
  • src/config/toml_schema.rs
  • src/commands/access.rs
  • src/commands/native.rs

Comment thread docs/content/docs/(configuration)/config.mdx Outdated
Comment thread src/commands/control.rs
Comment thread src/messaging/discord.rs
Comment on lines +1017 to +1051
fn discord_spec_violation(spec: &crate::commands::native::NativeCommandSpec) -> Option<String> {
use crate::commands::native::NativeArg;

// Registry names are ASCII; Discord additionally allows lowercase
// unicode letters, which no command uses.
let name_valid = !spec.name.is_empty()
&& spec.name.chars().count() <= DISCORD_NAME_MAX
&& spec
.name
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_');
if !name_valid {
return Some(format!(
"name must be 1-{DISCORD_NAME_MAX} lowercase [a-z0-9_-] characters"
));
}
if let Some(NativeArg::Choice { options }) = &spec.arg {
if options.len() > DISCORD_CHOICE_CAP {
return Some(format!(
"{} choices exceed discord's cap of {DISCORD_CHOICE_CAP}",
options.len()
));
}
// A truncated choice value would dispatch different args than the
// user picked, so over-long choices invalidate the command instead.
if options
.iter()
.any(|option| option.is_empty() || option.chars().count() > DISCORD_CHOICE_VALUE_MAX)
{
return Some(format!(
"choice values must be 1-{DISCORD_CHOICE_VALUE_MAX} characters"
));
}
}
None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use descriptive variable names.

Replace newly introduced spec and def variables with names such as command_spec and command_definition.

  • src/messaging/discord.rs#L1017-L1051: rename the spec parameter in discord_spec_violation.
  • src/messaging/discord.rs#L1077-L1094: rename the spec closure and collection variables.
  • src/messaging/slack.rs#L428-L430: rename the def closure variable.

As per coding guidelines, “Don't abbreviate variable names. Use queue not q, message not msg, channel not ch.”

📍 Affects 2 files
  • src/messaging/discord.rs#L1017-L1051 (this comment)
  • src/messaging/discord.rs#L1077-L1094
  • src/messaging/slack.rs#L428-L430
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/messaging/discord.rs` around lines 1017 - 1051, Rename the newly
introduced abbreviated variables to descriptive names: in
src/messaging/discord.rs lines 1017-1051, rename the discord_spec_violation
parameter spec; in src/messaging/discord.rs lines 1077-1094, rename the spec
closure and collection variables; and in src/messaging/slack.rs lines 428-430,
rename the def closure variable. Use names such as command_spec and
command_definition consistently at each affected site.

Source: Coding guidelines

…w-feedback

# Conflicts:
#	src/agent/channel.rs
#	tests/context_dump.rs
…feedback

- response_mode persists via a single json_set statement in both stores, so
  a mode change can't race concurrent whole-row settings writers and restore
  stale fields
- bindings API create/update requests take Option<Vec> for authority: omitted
  writes no TOML key, [] writes an explicit empty list that opens the scope
- discord: stale global commands are cleared only after every configured
  guild is confirmed in sync, so a failed guild registration keeps the
  globals as a fallback until the next ready re-sync
- docs: named-instance authority rows note that empty means open

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/commands/control.rs (1)

210-236: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Guard response_mode updates from whole-settings writes.

persist_response_mode now patches only response_mode, but /channels/{channel_id}/settings PUT reads the full channel settings, then ChannelSettingsStore::upsert writes the entire ConversationSettings value. A concurrent settings update that completes after this patch can overwrite the new mode. Use atomic JSON updates for the full-settings flow too, or make mode the final write after settings changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/control.rs` around lines 210 - 236, Update the full-settings PUT
flow around ChannelSettingsStore::upsert so it cannot overwrite a concurrently
persisted response mode. Replace the whole-settings write with an atomic
JSON/settings update that preserves the current response_mode, or perform the
response_mode patch after the full settings write; ensure persist_response_mode
remains protected from later stale upserts.
🧹 Nitpick comments (1)
src/conversation/channel_settings.rs (1)

45-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Both ChannelSettingsStore::set_response_mode and PortalConversationStore::set_response_mode implement the identical block that serializes ResponseMode to its JSON string form (serde_json::to_value(mode).as_str().to_string(), with the same error message). Extract this into one shared helper, for example a method on ResponseMode in src/conversation/settings.rs, so the two stores stay in sync if the serialization ever changes.

  • src/conversation/channel_settings.rs#L45-L77: replace the inline serialization block with a call to the shared helper.
  • src/conversation/portal.rs#L190-L221: replace the inline serialization block with a call to the same shared helper.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/conversation/channel_settings.rs` around lines 45 - 77, Extract the
duplicated ResponseMode serialization logic into one shared helper, preferably
on ResponseMode in src/conversation/settings.rs, preserving the existing error
behavior and returned string. Replace the inline serde_json conversion in
ChannelSettingsStore::set_response_mode at
src/conversation/channel_settings.rs:45-77 and
PortalConversationStore::set_response_mode at src/conversation/portal.rs:190-221
with calls to that helper so both stores remain consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/commands/control.rs`:
- Around line 210-236: Update the full-settings PUT flow around
ChannelSettingsStore::upsert so it cannot overwrite a concurrently persisted
response mode. Replace the whole-settings write with an atomic JSON/settings
update that preserves the current response_mode, or perform the response_mode
patch after the full settings write; ensure persist_response_mode remains
protected from later stale upserts.

---

Nitpick comments:
In `@src/conversation/channel_settings.rs`:
- Around line 45-77: Extract the duplicated ResponseMode serialization logic
into one shared helper, preferably on ResponseMode in
src/conversation/settings.rs, preserving the existing error behavior and
returned string. Replace the inline serde_json conversion in
ChannelSettingsStore::set_response_mode at
src/conversation/channel_settings.rs:45-77 and
PortalConversationStore::set_response_mode at src/conversation/portal.rs:190-221
with calls to that helper so both stores remain consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ef3562ef-6805-4005-906a-9d7aaed9055d

📥 Commits

Reviewing files that changed from the base of the PR and between e189497 and f6626f4.

📒 Files selected for processing (14)
  • docs/content/docs/(configuration)/config.mdx
  • interface/src/api/schema.d.ts
  • src/agent/channel.rs
  • src/api/bindings.rs
  • src/commands/control.rs
  • src/config/load.rs
  • src/config/toml_schema.rs
  • src/config/types.rs
  • src/config/watcher.rs
  • src/conversation/channel_settings.rs
  • src/conversation/portal.rs
  • src/main.rs
  • src/messaging/discord.rs
  • tests/context_dump.rs
🚧 Files skipped from review as they are similar to previous changes (8)
  • tests/context_dump.rs
  • src/config/types.rs
  • src/api/bindings.rs
  • src/agent/channel.rs
  • src/config/load.rs
  • src/messaging/discord.rs
  • src/main.rs
  • docs/content/docs/(configuration)/config.mdx

… setter

- the channel's in-queue SetResponseMode arm still did a spawned
  read-modify-write with a default fallback; it now uses
  ChannelSettingsStore::set_response_mode, so a stale upsert can't restore
  old fields over a concurrent settings write or router-side mode change
- ResponseMode::as_setting_str replaces the duplicated serde string
  conversion in both stores
@jamiepine
jamiepine merged commit 0bc28ab into main Aug 10, 2026
5 checks passed
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