# ADR 0001: Peer discovery and roster model

## Status

Proposed — high priority before serious multi-agent dogfood/release.

## Context

Dogfood proved AMQ transport can support multiple peers today:

```text
sugar ↔ coffe
sugar ↔ chai
chai ↔ sugar
coffe ↔ sugar
```

`sugar` could receive from and reply to both `coffe` and `chai`. The missing part is Bridge UX/state: status badge, commands, and agent context still model one primary peer too strongly.

Users also cannot discover who is available in the AMQ root. Global root `~/.amq-bridge/mail` may contain stale or cross-project handles, so discovery must be explicit and trust-aware.

Related dogfood failures:

- identity confusion: agents forgot whether they were `sugar`, `coffe`, or `chai`
- stale identity context from old attach state
- inbox loops because old messages looked actionable forever
- weak evidence output: `sent ? -> peer`
- no read/unread, timestamps, or processed-id tracking

## Decision

### 1. Session roster is Bridge source of truth

Persist roster in Pi session custom state:

```ts
{
  version: 2,
  attached: boolean,
  self?: string,
  root?: string,
  peers: Record<string, {
    handle: string,
    attachedAt: string,
    status?: "connected" | "removed" | "stale",
    source?: "attach" | "message" | "presence"
  }>,
  primaryPeer?: string,
  updatedAt: string
}
```

Legacy `.amq-bridge/sessions/<key>/state.json` is cache/migration only, not trusted source.

### 2. Discovery is read-only and non-trusting

Discovery is CLI-first. Bridge should call AMQ public commands instead of hand-scanning mailbox internals:

- `amq who --json`
- `amq presence list --json`
- `amq receipts list --json`
- `amq monitor --peek --json`
- `amq read --json`
- `amq thread --json`
- `amq doctor --ops --json`

Mailbox file scans are fallback/debug only. This keeps AMQ plug-and-play and preserves the option to swap transport later.

Discovery never auto-connects or auto-trusts peers.

Rule:

```text
Available ≠ connected/trusted.
```

Discovery output must show:

- root
- handle
- source(s)
- freshness / stale reason
- whether connected

### 3. Commands

```text
/amq-bridge discover
/amq-bridge peers
/amq-bridge peer add <handle>
/amq-bridge peer remove <handle>
/amq-bridge peer primary <handle>
```

Compatibility:

```text
/amq-bridge attach <peer> [self]
```

means:

- set/confirm `self`
- add `<peer>` to roster
- set `<peer>` primary if no primary exists or attach explicitly replaces it

Future alias:

```text
/amq-bridge attach --add <peer>
```

must use real argv parsing; current whitespace split is insufficient.

### 4. Badge and status

Badge:

```text
[self] ↔ peer
[self] ↔ peer1, peer2
[self] ↔ peer1 +3
```

TUI styling:

- `[self]`: accent + bold
- `↔`: dim
- peers: muted

Expanded status lists full roster:

```text
You are: sugar
Primary peer: coffe
Connected peers:
  coffe · primary · active 1m ago
  chai  · connected · active 30s ago
Available agents:
  aadil · stale
Root: ~/.amq-bridge/mail
```

### 5. Send/reply targeting

Send without explicit `--to` is allowed only when:

- exactly one connected peer exists, or
- a primary peer is explicitly set

Otherwise fail closed and show choices.

Reply must target `messageId` when more than one actionable pending item exists. Reply-latest remains unsafe.

### 6. Inbox loop guard is part of roster release

Multi-peer roster cannot ship without inbox lifecycle guards:

- dedupe by message id
- read/unread state
- processed message ids
- newest-first / `--tail`
- `--unread`
- mark-read or resolve command
- one-active-message scheduler
- no repeated auto-injection of same message

### 7. Stale state guard

Context injection only uses usable v2 session state:

- version is 2
- root is absolute
- self exists
- peers object exists
- latest state is attached

Stale legacy relative roots like `.agent-mail` must not enter model context.

Detach appends `attached:false`, stops watchers, and clears identity context. Full peer-notification semantics remain future work.

## Pair review findings

Expert review agreed direction is correct, but release unsafe unless ADR gates multi-peer behind:

- evidence output (`id/from/to/kind/subject/thread/root/bodyPreview`)
- durable pending/read-unread state
- explicit reply targeting
- scheduler loop prevention
- real command parser
- stale identity guard
- discovery freshness/source rules

High-risk notes:

- `meta/config.json` can be stale; treat as advisory.
- Dynamic handle dirs may exist while config warns unknown.
- AMQ already provides discovery/ops surfaces; Bridge should wrap public CLI capabilities instead of duplicating AMQ internals.
- Global root crosses repo/trust domains.
- Discovery should not expose message bodies in model context.
- `coop init --force --agents` replaces config. **Resolved 2026-08-03:** transport initialization takes a root-scoped lock, reads existing handles through `amq who`, and passes the union of existing + requested handles; attach/connect never intentionally shrink the shared roster.

## Acceptance criteria

### Discovery

Given AMQ root contains `sugar`, `coffe`, and stale `aadil`, when user runs:

```text
/amq-bridge discover
```

Then output shows root, all handles, freshness, source, and connected/trusted status.

### Peer add

Given `[coffe] ↔ sugar`, when user runs:

```text
/amq-bridge peer add chai
```

Then status becomes:

```text
[coffe] ↔ sugar, chai
```

and `chai` is persisted in `amq-bridge-state` across `/reload`.

### Primary target

Given multiple connected peers and no primary, `/amq-bridge send hello` fails closed and lists peers.

Given primary is `sugar`, `/amq-bridge send hello` sends to `sugar` and output states `to=sugar` with id/root/thread.

### Inbox loop prevention

Given same inbound message is already processed, auto-inbox must not inject it again or trigger a new agent turn.

### Reload

Given attached roster `[sugar] ↔ coffe, chai`, after `/reload`, badge and context still show same roster.

Given detached state, after `/reload`, no AMQ identity context is injected.

## Consequences

Positive:

- users can discover available AMQ peers
- agents can maintain correct identity across reload/restart
- multi-peer status becomes visible
- loops from repeated old messages are reduced by lifecycle state

Costs:

- requires roster state model and command parser
- requires inbox lifecycle work before safe multi-peer release
- requires careful global-root security messaging

## Open questions

- Should `/amq-bridge detach` notify peers by default?
- Should detach require confirmation in TUI?
- Should discovery show stale historical message contacts by default or behind `--all`?
- What TTL defines active vs stale presence?
- Should `peer add` require peer acknowledgment later?
