bots-garden/sidekickspublic Fork
⑂ v1.0.0
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/sidekicks.git
git clone ssh://git@rickub.com/bots-garden/sidekicks.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP
🛟 Updated. 1d19377 · on v1.0.0 · k33g · 3d ago
0006-channels.md · 62 lines · 6.0 KBmarkdown
Blame HistoryOpen raw

ADR 0006 — Channels, routing between agents, history

Context

Milestone M2 kept one conversation per agent, in memory. Milestone M3 adds what the brief asks for: channels the user creates, several agents in one channel, @agent and /agent:command, and a history that survives a restart. ACP is one client talking with one agent: everything multi-agent is Sidekicks' job. The agents are small local models, two at most in memory on the reference machine.

Decisions

1. Channels and direct conversations are one thing

internal/channels replaces internal/conversations. A direct conversation (clicking an agent) is a channel with one member and no name, flagged direct with the agent's name; the Channels panel lists it under "Direct messages". The same code streams, queues, asks permissions and stores both. Mentions are plain text in a direct conversation: its agent always answers.

2. Who answers

  • /agent:command arguments goes to that agent, sent as /command arguments.
  • Otherwise the agents mentioned with @name answer, one after the other, in the order of the mentions. A mentioned agent that is not a member is invited first, and the channel says so.
  • Otherwise the last agent who spoke in the channel answers; with a single member, that member.
  • Otherwise nobody: the channel says to mention an agent.

Answering in parallel was rejected: two local models generating at once on a laptop are both slower, and the second answer could not take the first into account. One agent answers one channel at a time: a message for an agent busy elsewhere waits (the channel shows whose turn it is).

3. What each agent reads

Each member has its own ACP session for the channel and remembers the id of the last channel message it has seen. Before its turn, an agent receives the finished user and agent messages it has not seen — what the user said to others, what other agents answered — then the user's text:

[Messages in #general you have not seen yet]
user: @alice explain goroutines
alice: Goroutines are light threads…
[End of the messages]

The user now writes to you (answer directly, without your name in front):
@bob what do you think?

With nothing unseen, the text is sent alone. At most the last 20 messages are re-sent, each cut at 1500 characters: small context windows. A slash command is sent alone, without this context, or the agent would not recognise it. The last sentence of the prompt was added after testing: without it, Bob imitated the transcript and answered "Bob: 4217".

4. After a restart

The ACP session id is stored with the membership. An agent that announces loadSession gets session/load; its replayed history is ignored (the interface already shows it from the store). Otherwise — mm has no session/load — a new session is opened and is "fresh": its first prompt carries the last messages of the channel, its own answers included. The user sees no difference and no "agent restarted" notice any more (ADR 0004's notice is gone). The same happens when an agent was stopped to make room for another.

5. The store

Pebble v2 in internal/store, one database per configuration file under <user config dir>/sidekicks/data/<hash of the path>/, with the keys of ADR 0003 plus direct/<agent> and value/<key> (the last commands each agent announced, so that / can suggest them before the agent starts). Ids sort by time (milliseconds, counter, random). Messages are written when they are finished; a streaming answer is not. After Close, operations return ErrClosed instead of letting Pebble panic: an answer can end while Sidekicks quits. A database that cannot be opened (another Sidekicks uses it) is replaced by one in memory, and the interface says the history is unavailable.

6. The interface

ChannelService replaces ChatService. Events are keyed by channel; channels:changed carries the whole list and commands:changed an agent's commands. The composer opens a popup after @ (agents, filtered on name and title, letters in order) and after / at the start of a message (the members' commands; /agent:command with several members). A streaming answer shows a spinner with what it is doing — thinking, using a tool, writing — and a line "… is getting ready" covers the time before its first part (agent starting, waiting for its turn). A channel is loaded from Go when first shown, then kept up to date by events only. Unread answers are counted in memory.

There are no Sidekicks commands typed in the composer in M3: inviting, removing, renaming and deleting are buttons, so / only ever means an agent's command.

Consequences

  • Deleting a channel deletes its history, after an explicit confirmation.
  • The first / in a conversation starts the members that never announced their commands and opens their session (ChannelService.Prepare): mm announces them right after session/new without loading its model (measured: about 0.1 s). The session is kept for the agent's first answer and marked fresh, so that answer is still told the channel's past. Doing it when a channel is merely opened was rejected: a started agent takes one of the two slots and could stop an idle one.
  • Messages written by a model are re-sent to other models: a long channel costs context, which the 20-message limit bounds.
  • An answer interrupted by quitting is lost (it was never finished, so never stored).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# ADR 0006 — Channels, routing between agents, history

- **Status**: accepted
- **Date**: 2026-10-29
- **Issues**: [#20](../../.tickets/issues/0020-persister-channels-et-messages-avec-pebble.yaml), [#21](../../.tickets/issues/0021-panel-channels-creer-renommer-supprimer.yaml), [#22](../../.tickets/issues/0022-invoquer-un-agent-avec-et-autocompletion.yaml), [#23](../../.tickets/issues/0023-router-les-messages-entre-plusieurs-agents-d-un.yaml), [#24](../../.tickets/issues/0024-commandes-slash-exposees-par-les-agents.yaml), [#25](../../.tickets/issues/0025-reprendre-les-conversations-apres-redemarrage.yaml)
- **Builds on**: [ADR 0003](0003-architecture.md) (packages, keys, events) and [ADR 0004](0004-conversations.md) (the conversation model of M2)

## Context

Milestone M2 kept one conversation per agent, in memory. Milestone M3 adds what the brief asks for: channels the user creates, several agents in one channel, `@agent` and `/agent:command`, and a history that survives a restart. ACP is one client talking with one agent: everything multi-agent is Sidekicks' job. The agents are small local models, two at most in memory on the reference machine.

## Decisions

### 1. Channels and direct conversations are one thing

`internal/channels` replaces `internal/conversations`. A direct conversation (clicking an agent) is a channel with one member and no name, flagged `direct` with the agent's name; the Channels panel lists it under "Direct messages". The same code streams, queues, asks permissions and stores both. Mentions are plain text in a direct conversation: its agent always answers.

### 2. Who answers

- `/agent:command arguments` goes to that agent, sent as `/command arguments`.
- Otherwise the agents mentioned with `@name` answer, **one after the other**, in the order of the mentions. A mentioned agent that is not a member is invited first, and the channel says so.
- Otherwise the **last agent who spoke** in the channel answers; with a single member, that member.
- Otherwise nobody: the channel says to mention an agent.

Answering in parallel was rejected: two local models generating at once on a laptop are both slower, and the second answer could not take the first into account. One agent answers **one channel at a time**: a message for an agent busy elsewhere waits (the channel shows whose turn it is).

### 3. What each agent reads

Each member has its own ACP session for the channel and remembers the id of the last channel message it has seen. Before its turn, an agent receives the finished user and agent messages it has not seen — what the user said to others, what other agents answered — then the user's text:

```text
[Messages in #general you have not seen yet]
user: @alice explain goroutines
alice: Goroutines are light threads…
[End of the messages]

The user now writes to you (answer directly, without your name in front):
@bob what do you think?
```

With nothing unseen, the text is sent alone. At most the last 20 messages are re-sent, each cut at 1500 characters: small context windows. A slash command is sent alone, without this context, or the agent would not recognise it. The last sentence of the prompt was added after testing: without it, Bob imitated the transcript and answered "Bob: 4217".

### 4. After a restart

The ACP session id is stored with the membership. An agent that announces `loadSession` gets `session/load`; its replayed history is ignored (the interface already shows it from the store). Otherwise — mm has no `session/load` — a new session is opened and is "fresh": its first prompt carries the last messages of the channel, its own answers included. The user sees no difference and no "agent restarted" notice any more (ADR 0004's notice is gone). The same happens when an agent was stopped to make room for another.

### 5. The store

Pebble v2 in `internal/store`, one database per configuration file under `<user config dir>/sidekicks/data/<hash of the path>/`, with the keys of ADR 0003 plus `direct/<agent>` and `value/<key>` (the last commands each agent announced, so that `/` can suggest them before the agent starts). Ids sort by time (milliseconds, counter, random). Messages are written when they are finished; a streaming answer is not. After `Close`, operations return `ErrClosed` instead of letting Pebble panic: an answer can end while Sidekicks quits. A database that cannot be opened (another Sidekicks uses it) is replaced by one in memory, and the interface says the history is unavailable.

### 6. The interface

`ChannelService` replaces `ChatService`. Events are keyed by channel; `channels:changed` carries the whole list and `commands:changed` an agent's commands. The composer opens a popup after `@` (agents, filtered on name and title, letters in order) and after `/` at the start of a message (the members' commands; `/agent:command` with several members). A streaming answer shows a spinner with what it is doing — thinking, using a tool, writing — and a line "… is getting ready" covers the time before its first part (agent starting, waiting for its turn). A channel is loaded from Go when first shown, then kept up to date by events only. Unread answers are counted in memory.

There are no Sidekicks commands typed in the composer in M3: inviting, removing, renaming and deleting are buttons, so `/` only ever means an agent's command.

## Consequences

- Deleting a channel deletes its history, after an explicit confirmation.
- The first `/` in a conversation starts the members that never announced their commands and opens their session (`ChannelService.Prepare`): mm announces them right after `session/new` without loading its model (measured: about 0.1 s). The session is kept for the agent's first answer and marked fresh, so that answer is still told the channel's past. Doing it when a channel is merely opened was rejected: a started agent takes one of the two slots and could stop an idle one.
- Messages written by a model are re-sent to other models: a long channel costs context, which the 20-message limit bounds.
- An answer interrupted by quitting is lost (it was never finished, so never stored).