Home channel, plus /sethome, /whoami and /pause - #635
Conversation
Adds the instance's default outbound destination — the one conversation the agent can reach when nothing is in scope. - SettingsStore accessors with an explicit/implicit flag. adopt_home_channel is the invariant point: an implicit home never overwrites a claimed one. - resolve_home_target() implements the wake-target > home > nothing order. Unresolvable means silence, never a recently-seen channel. - defaults.home_channel seeds an instance that ships pre-configured, rejected at load if it isn't adapter:target. The database owns it from then on. - set_home_channel tool on user channels, resolving the chat it was called from, plus /sethome over the same handler. Authority-gated, validated at set time so an unreachable home fails while the user is still looking. - /status reports the resolved home and marks an adopted one.
- The first user conversation to complete a turn adopts the home when nothing has claimed it, and says so in that chat — the destination is never a default the user finds out about by receiving something. - Autonomy status carries the resolved home; the dial card shows where proactive messages land, or that findings are only recorded. - DELETE /agents/autonomy/home gives it up. There is no set-from-here counterpart: a home is claimed from the chat that should receive it.
/whoami answers what the sender may do here rather than who they are — the authority split is otherwise invisible until someone hits a denial. /pause holds off on starting new work and survives restart, so an emergency stop isn't undone by a bounce. It gates three entry points: inbound messages in the router, autonomy runs, and cron fires. Commands dispatch before the router gate, so /pause off and /status still land while paused. A cron fire skipped during a pause is dropped after its cursor advances rather than banked into a burst at resume. Pause is per-agent, following the settings store it lives in. Control actions now receive their validated argument string, which /pause is the first to need.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (3)
WalkthroughThe change adds persistent home-channel routing, pause controls, autonomy transcript continuity, authority-aware commands and tools, API status management, and interface controls. It also adds empty-instance autonomy guidance and home-channel configuration. ChangesHome channel and autonomy
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (1)
src/settings/store.rs (1)
196-202: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse full names for closure bindings.
Rename
tandvtotargetandvaluein the new home and pause accessors. These names violate the Rust path rule for descriptive variable names.As per coding guidelines:
src/**/*.rssays, “Don't abbreviate variable names. Usequeuenotq,messagenotmsg,channelnotch. Common abbreviations likeconfigare fine.”Also applies to: 215-216
🤖 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/settings/store.rs` around lines 196 - 202, Rename the closure bindings in the new home and pause accessors from abbreviated names to descriptive names: use target instead of t and value instead of v. Update all references within those closures while preserving the existing filtering and explicit-value matching behavior.Source: Coding guidelines
🤖 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/design-docs/autonomy.md`:
- Around line 310-311: Update the earlier enrichment pattern at the
`set_outcome` reference to use `autonomy_complete`, matching the completion flow
described near “Calls autonomy_complete.” Keep the surrounding enrichment
sequence unchanged.
- Around line 274-282: Update the earlier cortex wake-context description and
the implementation note around the run-summary handling to remove separate
injection of recent run summaries from the runs table. Keep the transcript,
including persisted assistant-turn summaries, as the sole continuity delivery
path while retaining the run store only for indexing and provenance.
In `@docs/design-docs/home-channel.md`:
- Around line 124-127: Update the wake-target resolution logic in the target
parser around parse_delivery_target and SettingsStore::home_channel() to
distinguish an absent wake target from an explicitly provided but invalid one.
Preserve home-channel fallback only when no target is supplied; treat an
unparseable explicit target as unset for delivery and route it through the
record/silence branch without sending to home.
In `@src/agent/channel.rs`:
- Around line 2308-2309: Verify whether REGISTRY.parse and parse_addressed can
produce different results for the same input, especially around addressing
requirements. If they can, update the handle_message control-command branch
around handle_control_command to check authority with the parsed command
definition using the same context.allows(...) gate as dispatch_inbound before
executing the control action; preserve existing handling for authorized
commands.
- Around line 2622-2653: Update handle_message_batch’s non-suppressed completion
path, alongside its message_count increment and check_memory_persistence call,
to await claim_home_channel_if_unset. Preserve the existing suppression behavior
and ensure batched turns adopt the home channel after their first completed
turn.
In `@src/commands/control.rs`:
- Around line 354-364: The home-channel check in the control flow around
conversation_broadcast_target and SettingsStore::adopt_home_channel is racy
because the read and writes are separate. Move the empty-home validation and
both home-value updates into one serialized storage transaction, ensuring
exactly one concurrent claimant succeeds and subsequent claimants return without
announcing adoption; add a concurrency test covering two simultaneous adoption
attempts.
In `@src/settings/store.rs`:
- Around line 194-210: Update home-channel and pause-state accessors and
mutators, including home_channel(), pause_reason(), adopt_home_channel(),
set_home_channel(), clear_home_channel(), and set_paused(), to distinguish
NotFound from other storage errors and propagate or explicitly log non-NotFound
failures instead of defaulting silently. Perform each coupled state transition
in a single redb write transaction so concurrent claims are serialized and
home/pause fields remain consistent, preserving explicit home-channel values and
fail-safe pause behavior. Add concurrency and storage-failure-injection tests
covering these transitions.
In `@src/tools/set_home_channel.rs`:
- Around line 78-86: Enforce sender authority in the set-home-channel tool flow
before invoking set_home_channel: propagate the verified authority through tool
registration or the tool state used by call, and either omit registration for
unauthorized turns or return a structured authorization error. Ensure
non-authority requests never reach crate::commands::control::set_home_channel.
---
Nitpick comments:
In `@src/settings/store.rs`:
- Around line 196-202: Rename the closure bindings in the new home and pause
accessors from abbreviated names to descriptive names: use target instead of t
and value instead of v. Update all references within those closures while
preserving the existing filtering and explicit-value matching behavior.
🪄 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: e2095eb1-ab29-443f-98ab-410562605a45
📒 Files selected for processing (26)
docs/design-docs/autonomy.mddocs/design-docs/home-channel.mdinterface/src/api/client.tsinterface/src/api/schema.d.tsinterface/src/components/autonomy/AutonomyDialCard.tsxinterface/src/routes/AgentAutonomy.tsxprompts/en/tools/set_home_channel_description.md.j2src/agent/autonomy.rssrc/agent/channel.rssrc/api/autonomy.rssrc/api/server.rssrc/commands/control.rssrc/commands/dispatch.rssrc/commands/registry.rssrc/config/load.rssrc/config/toml_schema.rssrc/config/types.rssrc/cron/scheduler.rssrc/lib.rssrc/main.rssrc/messaging/target.rssrc/prompts/text.rssrc/settings.rssrc/settings/store.rssrc/tools.rssrc/tools/set_home_channel.rs
| let settings = deps.runtime_config.settings.load().as_ref().clone()?; | ||
| if settings.home_channel().is_some() { | ||
| return None; | ||
| } | ||
|
|
||
| let target = conversation_broadcast_target(deps, conversation_id, is_portal) | ||
| .await | ||
| .ok()? | ||
| .to_string(); | ||
|
|
||
| match settings.adopt_home_channel(&target) { |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Make first-run home adoption atomic.
Two completed user turns can both pass the check on Line 355. SettingsStore::adopt_home_channel also performs a separate read before its writes. The later write can replace the earlier adopted target, and both conversations can announce that they became the home channel.
Perform the empty-home check and both home values writes in one serialized storage transaction. Add a concurrent adoption test that permits exactly one successful claimant.
🤖 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 354 - 364, The home-channel check in
the control flow around conversation_broadcast_target and
SettingsStore::adopt_home_channel is racy because the read and writes are
separate. Move the empty-home validation and both home-value updates into one
serialized storage transaction, ensuring exactly one concurrent claimant
succeeds and subsequent claimants return without announcing adoption; add a
concurrency test covering two simultaneous adoption attempts.
| async fn call(&self, _args: Self::Args) -> Result<Self::Output, Self::Error> { | ||
| let message = crate::commands::control::set_home_channel( | ||
| &self.deps, | ||
| &self.conversation_id, | ||
| self.is_portal, | ||
| ) | ||
| .await; | ||
|
|
||
| Ok(SetHomeChannelOutput { message }) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Enforce authority before changing the home channel.
This tool has no authority check. It bypasses the authority restriction on /sethome. A non-authority sender can use prompt injection to cause the model to set that sender's conversation as the instance home channel.
Pass verified sender authority into the tool registration or tool state. Do not register the tool for non-authority turns, or return a structured authorization error before calling set_home_channel.
🤖 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/tools/set_home_channel.rs` around lines 78 - 86, Enforce sender authority
in the set-home-channel tool flow before invoking set_home_channel: propagate
the verified authority through tool registration or the tool state used by call,
and either omit registration for unauthorized turns or return a structured
authorization error. Ensure non-authority requests never reach
crate::commands::control::set_home_channel.
An empty instance surveyed nothing, concluded "nothing new here", and exited — and since nothing changed, every later wake reached the same conclusion. The survey already knows when it came back empty, so the briefing branches on it rather than leaving the agent to notice: read what is here, record it, check `spacebot_docs` for capability gaps, and work out the one question worth asking. Recording is instructed at every level and framed as a licence rather than a quota, so a run that learned nothing records nothing instead of manufacturing an observation to fill the space. `spacebot_docs` was registered only on the branch and cortex tool servers. The autonomy channel takes direct mode and does not branch, so it could not reach the docs it is now told to consult.
…omic Review findings from #635. The channel re-parses raw text with `REGISTRY.parse`, which the router had already declined via `parse_addressed`. Those diverge: with a bot username configured, `/sethome@otherbot` is not a command to the router but resolves in the channel — running an Authority command with no check. Authority now resolves once in the router, where the binding config lives, and rides the message as metadata. Absent means none, so a path that never passed the router cannot inherit one. Both paths gate through `access_allows`. The set_home_channel tool had the same hole from the other side: no check at all, so a non-authority sender could talk the model into pointing the instance's outreach at their own conversation. It is now registered only on turns driven by a sender holding authority. An explicit wake delivery_target that no longer parses is recorded rather than redirected home. The wake named somewhere that is not home, and a rename must not turn that into a send to the destination it declined. Settings state that is only meaningful together now moves in one redb transaction, and adoption does its empty check inside the write, so two first-run claimants cannot both win. Reads distinguish a missing key from an unreadable store: an unreadable pause reports paused, since resuming is one command away but running through an emergency stop is not recoverable. Home adoption also fires from the coalesced-batch path, which a fresh instance's first turns often take.
…nel' into jamiepine/commands-and-home-channel
There was a problem hiding this comment.
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 (1)
src/agent/autonomy.rs (1)
557-586: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftApply visibility filtering before the 200-task limit.
TaskStore::listreturns at most 200 tasks beforetask_visible_to_agentfilters them. If the first 200 tasks belong to other agents,has_tasksbecomes false even when this agent has visible tasks later. The new empty-instance branch can then give cold-start guidance and cause the agent to skip assigned work. Push the visibility predicate into the query or paginate until a visible task is found, and add a multi-agent regression test with more than 200 tasks.🤖 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/agent/autonomy.rs` around lines 557 - 586, The render_task_state flow currently applies task_visible_to_agent after TaskStore::list truncates results to 200, so visible tasks beyond that limit are missed. Move the visibility predicate into the task-store query when supported, or paginate through results until all relevant visible tasks are found, while preserving the existing section/status behavior; add a regression test covering more than 200 tasks across multiple agents.
🤖 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 `@prompts/en/autonomy_channel.md.j2`:
- Around line 60-71: Update the empty-instance instructions and the following
global memory guidance so they do not direct observe runs to call memory_save.
Gate both recording instructions behind the existing write-permission condition,
preserving read-only behavior for observe runs; do not treat memory_save as an
exception unless that contract is explicitly established and tested.
In `@src/agent/autonomy.rs`:
- Around line 526-529: Update the instance_is_empty predicate in the autonomy
run setup to classify a run as non-empty whenever it has tasks, active goals,
wake_events, or active_workers. Preserve the empty classification only when all
four autonomy signals are absent.
In `@src/commands/dispatch.rs`:
- Around line 89-108: Move the authority resolution and AUTHORITY_METADATA_KEY
stamping out of the post-content branch and before the MessageContent match in
the inbound dispatch flow. Ensure every non-system message, including
MessageContent::Media captions, receives the computed authority metadata before
any content-type return, while preserving the existing AccessContext inputs and
authority calculation.
---
Outside diff comments:
In `@src/agent/autonomy.rs`:
- Around line 557-586: The render_task_state flow currently applies
task_visible_to_agent after TaskStore::list truncates results to 200, so visible
tasks beyond that limit are missed. Move the visibility predicate into the
task-store query when supported, or paginate through results until all relevant
visible tasks are found, while preserving the existing section/status behavior;
add a regression test covering more than 200 tasks across multiple agents.
🪄 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: 951d56cf-3938-4ad0-a9f1-b2ae2cb2b406
📒 Files selected for processing (12)
docs/design-docs/autonomy.mdprompts/en/autonomy_channel.md.j2src/agent/autonomy.rssrc/agent/channel.rssrc/commands.rssrc/commands/access.rssrc/commands/dispatch.rssrc/messaging/target.rssrc/prompts/engine.rssrc/settings/store.rssrc/tools.rstests/context_dump.rs
🚧 Files skipped from review as they are similar to previous changes (5)
- src/agent/channel.rs
- src/messaging/target.rs
- src/tools.rs
- src/settings/store.rs
- docs/design-docs/autonomy.md
| {% if instance_is_empty %} | ||
| There are no tasks and no goals. That is not a reason to do nothing — on an instance this empty, learning about {{ agent_name }}'s user and this system is the most valuable work available, and it is what makes every later run better. This run: | ||
| - **Look at what is actually here.** The workspace, registered projects, files the user has already put in place. A cloned repository is a statement of intent — read it and work out what they are trying to do. | ||
| - **Record what you learn** as memories, following the rule below. | ||
| - **Find the gaps.** Use `spacebot_docs` to check which capabilities fit what this user appears to be doing and have not been set up yet. | ||
| - **Work out the one question worth asking.** If there is a single thing the user could tell you that would unlock the most, identify it and put it in your summary. One answered question is worth more than a long survey of an empty system. | ||
|
|
||
| Do not invent tasks to look busy. Proposing work nobody asked for is worse than proposing nothing. | ||
| {% endif %} | ||
|
|
||
| Record what you notice as you go, with `memory_save`, rather than saving it all up for the end — a run can be cut short by its timeout and lose everything it was holding. Save what would genuinely be useful to a future run or to the user: what this system is for, how they work, what they care about, what is broken. This is a licence, not a quota. A run that learned nothing records nothing, and that is a perfectly good run — do not restate what this briefing already told you, and do not record your own activity as though it were a discovery. | ||
|
|
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Keep observe runs read-only.
The observe rules at Line 45 prohibit all mutations, but the empty-instance block and the global guidance tell every level to record findings with memory_save. This can persist data during an observe run. Gate both instructions to levels that permit writes, or explicitly define memory_save as an observe exception and test that 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 `@prompts/en/autonomy_channel.md.j2` around lines 60 - 71, Update the
empty-instance instructions and the following global memory guidance so they do
not direct observe runs to call memory_save. Gate both recording instructions
behind the existing write-permission condition, preserving read-only behavior
for observe runs; do not treat memory_save as an exception unless that contract
is explicitly established and tested.
…ld start Second round of review findings from #635. Authority was stamped after the content match, which returns early for media. The channel builds its command text from captions and registers authority-gated tools per turn, so an authority sender lost both on any turn carrying a file. Resolution and stamping now happen ahead of the match, in one function the test drives directly. A run woken by a wake event, or with workers still running, is no longer classified as an empty instance. It has a reason to exist and a bounded turn to spend on it; cold-start discovery would spend that on the workspace instead. The observe rules read as forbidding memory writes while the guidance below them asked for memory writes. Recording is the point of the level — observe accumulates memories and says nothing — so the exception is now explicit rather than the instruction being gated.
…nel' into jamiepine/commands-and-home-channel
Builds out the home channel — the instance's default outbound destination, the one conversation the agent can reach when nothing is in scope — and adds the first commands that need it.
Phases 1–3 of
docs/design-docs/home-channel.md. Phase 4 (autonomous outreach) is deliberately not here; see below.Home channel
WakeDef.delivery_targethas been validated, persisted, and read by nothing since it landed, because there was no sensible default behind it. This gives it one.SettingsStoreaccessors carrying an explicit/implicit flag.adopt_home_channelis the single invariant point: an implicit home never overwrites a claimed one, so first run wins until a principal sets it deliberately.resolve_home_target()implements wake target → home → nothing. The third branch is load-bearing: unresolvable means silence, never "the most recent channel we saw," which is how an agent posts a private observation into a group.defaults.home_channelseeds an instance that ships pre-configured, rejected at load if it isn'tadapter:target. The database owns it from then on, so a reload never clobbers a runtime change.set_home_channeltool on user channels resolving the chat it was called from, with/sethomeas a second entry point over the same handler. Authority-gated, and validated at set time so a home the agent can't reach fails while the user is still looking at the reply.DELETE /agents/autonomy/homegives it up. There's no set-from-here counterpart on purpose — a home is claimed from the chat that should receive it, so the only thing a settings screen can offer is giving it up.Commands
/whoami— answers what you may do here, not who you are. The authority split is otherwise invisible until someone hits a denial./pause [reason | off]— holds off on starting new work and survives restart, so an emergency stop isn't undone by a bounce. Gates three entry points: inbound messages in the router, autonomy runs, and cron fires.Two details worth knowing about
/pause:Commands dispatch before the router gate, so
/pause offand/statusstill land while paused — the stop can always be undone from the chat that set it.A cron fire skipped during a pause is dropped after its cursor has already advanced, rather than banked:
Pause is per-agent, following the settings store it lives in. On a single-agent instance that's global; on a multi-agent one, pausing in one agent's chat won't quiet the others.
Control actions now receive their validated argument string —
/pauseis the first to need one.Not in this PR
resolve_home_target()takes a wake target and is tested, but nothing passes one yet: the only caller that would is the autonomy send path, which is phase 4. Wiring sending without phase 4's level gating would let anobserve-level instance start talking, which breaks mute-by-default. The autonomy channel still drains and drops its responses (src/agent/autonomy.rs).Testing
cargo test --lib— 1110 passing. New coverage:resolve_home_targetacross all three branches, including an unparseable wake target falling through to home/statusrendering for unset / adopted / explicit homes, and the pause line leading the block/whoamidescribing one boundary from both sidescargo clippy --lib --all-targetsclean,bunx tsc --noEmitclean.