STATUS: shipped at 9d192bf. Kept in the repo as historical context
for why the surface looks the way it does. Current behavior is
authoritative in help.go / lalia protocol.
A plan to replace the current tunnel/room split and turn-enforced two-party channel with a unified channels model: one mailbox per peer-pair, one mailbox per room, four verbs, no turn FSM, no sids in the CLI.
Three friction points observed in live use:
- Turn FSM blocks the most common case. Reviewer wants to fire
a follow-up ("also, saw this") before the reviewee has responded.
Current tunnel rejects the second send with
not_your_turn. send/awaitnames don't map to English intent. Coordinator agents parse instructions like "publish an update to codex" and "ask copilot about X" into the same verb. The command name should carry the intent so the LLM doesn't guess wrong.- Multi-session per peer-pair is unused overhead. Agents are episodic; they read one inbox per invocation. Two concurrent sids with the same peer either go unused or become stale-tunnel noise. Rooms already work with a single-handle-per-channel model and that works fine.
- Fire-and-forget
send(async, non-blocking, no turn flip). - Blocking consume with optional timeout, for both P2P and rooms.
- Single
askhelper for the one-shot question-expects-answer case. - Non-destructive inspect (
peek) on both P2P and rooms. - One persistent channel per
(self, peer); no sid in the CLI. - Verb naming maps cleanly to English intent.
- Git-backed transcript preserved (this is non-negotiable).
- No change to Ed25519 signing, registry, or workspace git layout beyond the filesystem path rename below.
- No new transport beyond P2P + rooms. (Multicast, federation, cross-machine sync remain out of scope.)
- No built-in "negotiate" or "conversation" primitive. Multi-round
exchange is the agent's responsibility, composed from
askortell+readloops.
# Peer-to-peer
lalia tell <peer> "msg" async, returns immediately
lalia ask <peer> "msg" [--timeout N] tell + block for next inbound from peer
lalia read <peer> [--timeout N] block until next inbound (0 = non-blocking)
lalia peek <peer> non-destructive inspect of pending
# Room
lalia room_create <name> [--desc s]
lalia join <room>
lalia leave <room>
lalia rooms
lalia participants <room>
lalia post <room> "msg" async broadcast
lalia read <room> [--timeout N] consume pending (0 = non-blocking)
lalia peek <room> non-destructive inspect
# Housekeeping (unchanged in shape, may change args)
lalia register | agents | renew | history <peer|room> | stop | protocol
Seven communication verbs total: tell, ask, post, read,
peek, join, leave. Plus room/registration management.
| Human phrasing | Command |
|---|---|
| "tell / notify / inform / publish an update to X" | tell X "..." |
| "ask / check with / query X" | ask X "..." |
| "negotiate / discuss / coordinate with X" | loop of ask / tell + read |
| "post / announce / share with the room" | post #room "..." |
| "check if there are any messages" | peek X or read X --timeout 0 |
| "wait for a message" | read X --timeout 300 (or whatever) |
A channel is a persistent, git-backed, ordered message log with a mailbox per recipient. Two kinds:
- P2P channel: keyed by the unordered pair
{a, b}. Created implicitly on firsttellorask. Never explicitly opened; closed only by the last message decaying in the git log or by explicit agent deregistration. - Room channel: keyed by room name. Created by
room_create. Members managed byjoin/leave.
Append message to the channel. Deliver into each recipient's mailbox (for P2P: the peer's mailbox; for room: every member except the sender). Commit to git. Return. Never blocks. No turn. Identical semantics for both channel kinds; different verbs because the user intent (directed vs broadcast) is different.
Consume and remove the oldest unread message from the caller's
mailbox on the named channel. With --timeout N > 0, block up to
N seconds waiting for a message. With --timeout 0, return
immediately with either a message or an empty result. Messages
remain in the git transcript; only the mailbox entry is consumed.
Pure client-side composition: tell then read with a timeout,
on the same channel. No new wire op. Returns the peer's reply or
timeout. Equivalent to the current send behavior, minus the
turn flip.
Return all pending messages in the caller's mailbox without consuming. Non-blocking. Works for both P2P and rooms (the latter is already shipped).
P2P mailboxes: unbounded by default (the common case is low volume between two agents). If a peer is offline for a long time, the mailbox grows; when the peer registers and reads, they see everything. No drop policy.
Room mailboxes: keep current bounded-with-drop-oldest + overflow
notice behavior (shipped in feat/rooms). Rationale: N-party
broadcast has realistic flood scenarios (8 members all posting);
P2P does not.
Additive. Existing Request.Op values remain; new ones added:
tell— args:{from, peer, body}— non-blocking, returns after commit.read— args:{from, peer_or_room, kind: "peer"|"room", timeout}— blocking consume.peek— args:{from, peer_or_room, kind}— non-blocking inspect.
No struct-shape changes to Request/Response (respects the
protocol.go lock from workstream F).
Old ops (tunnel, send, await, await-any, close,
session) become either aliases to the new ops or deprecated:
send→ alias totell(deprecation warning on stderr).await→ alias toreadwith default timeout.await-any→ implemented as a server-side loop over all channels the caller participates in; no semantic change.tunnel <peer>→ no-op that prints a deprecation notice. Channel creation is implicit on firsttell.close <sid>→ no direct equivalent. A channel is "closed" by the fact that no further messages flow. If you want a visible marker in the git log,tell <peer> "closing"is sufficient. Deprecate with a notice.session→ replaced bypeek+ a newlalia channelslisting.
Exit code 4 not_your_turn is removed from the code path. Keep
it reserved in protocol.go for one release so old clients see a
stable enum, then delete in the version after.
Current layout:
~/.lalia/workspace/
tunnels/<sid>/SESSION.md
tunnels/<sid>/MSG-000001.md
...
rooms/<name>/ROOM.md
rooms/<name>/MEMBERS.md
rooms/<name>/000001-<from>.md
New layout:
~/.lalia/workspace/
peers/<a>--<b>/000001-<from>.md # <a>, <b> sorted alphabetically
peers/<a>--<b>/000002-<from>.md
...
rooms/<name>/ROOM.md # unchanged
rooms/<name>/MEMBERS.md # unchanged
rooms/<name>/000001-<from>.md # unchanged
peers/<a>--<b>/ uses -- as pair separator. Alphabetical order
ensures (alice, bob) and (bob, alice) map to the same directory.
Existing tunnels/<sid>/ directories are left in place as read-only
history. No migration script; old transcripts are immutable and
just stop getting new writes.
- Workstream D (Resumable blocking): kill. Without the FSM
there is no waiter state to lose on timeout.
readsimply returns empty and the caller callsreadagain. Noresumecommand needed. Ifclaude-codeis assigned D in the current batch (BACKLOG.mdas of7bccb07), reassign them before they start.
- Implicit channel creation.
tell Xon a peer you have never talked to creates the channel. Is that desirable, or do we want an explicitlalia open <peer>gate? Recommendation: keep implicit. Matches room-implicit-first-post and removes one command from the surface. Reject withCodeNotFoundifXis not a registered agent. - Non-member reads from a room. Current behavior (feat/rooms):
non-members get
room not foundonread/peek. Preserve. readdefault timeout. Currentawaitdefaults to 300s. Propose: keep 300s forreadwhen--timeoutomitted; explicit--timeout 0for non-blocking. This makes the common case ergonomic.asktimeout. Sinceask = tell + read, what's the timeout? Propose:--timeoutonaskapplies only to thereadhalf;tellportion is effectively instant (daemon-local commit).- Renames in
client.goand elsewhere. Significant surface touch. Can be done mechanically. One agent, one branch; no parallel work here. - Lease on
tell/read/peek. All authenticated ops must renew lease (already true viadispatchpre-switch). Verifypeekis signed — it should be, it's caller-identifying.
Single workstream, single agent. Touches protocol.go, state.go,
tunnel.go (or replace it), rooms.go (unify read), client.go,
main.go, help.go, writer.go (path change), plus tests. Not
parallelizable against identity (A), keychain (E), or structured
errors (F) — heavy overlap with state.go and client.go.
Recommended sequencing:
- Finish current batch (A/D/E) per
BACKLOG.md. Kill D (resumable blocking) and reassign that agent or leave them idle pending this workstream. - Land structured errors (F) solo after A and E merge. Structured errors rewrites error shapes across every handler; doing it before this redesign means redoing the error work for removed ops.
- Land this workstream (call it G. Channels redesign) solo
after F. Single agent, branch
feat/channels. - After G merges, retire
tunnels/directory and tunnel-era commands from docs. Release as v0.2.
Alternative sequencing: land G before F. Argument: F touches
every handler; if G removes handlers (tunnel, close, session)
F has less surface. Counter-argument: F is the structured-errors
refactor the team already agreed on; doing G first means two
protocol churns in a row. Recommendation: G before F, do the
structural redesign while the protocol is still fluid, then F
formalizes errors over the new surface.
Must-have, before merge:
- P2P:
tellreturns immediately; peerreadsees it. - P2P: consecutive
tells from the same sender arrive in order on onepeek, consumable in order on successivereads. - P2P:
askroundtrip; timeout returns empty. - P2P:
peeknon-destructive. - P2P: channel persists across daemon restart (git replay via existing write queue).
- P2P:
tellto unregistered peer returnsCodeNotFound. - Rooms:
readwith--timeout 0matches currentinboxbehavior (non-blocking consume, clears mailbox). - Rooms:
readwith--timeout Nblocks and returns on post. - Deprecation:
sendstill works, prints deprecation notice on stderr. - Deprecation:
awaitsame. - Storage:
peers/<a>--<b>/path format;(alice, bob)and(bob, alice)write to the same directory.
Nice-to-have:
- Fuzz test: 100 interleaved
tells from both sides; transcript matches send order; each side reads each other's messages exactly once. - Stress: 1k messages in a channel, daemon restart mid-flow, replay completes.
Do we bump to v0.2 on this merge? The command surface change
is user-visible. lalia tunnel going away (even as a deprecated
no-op) is a breaking ergonomic change. Either:
- (a) Ship as v0.2 with a CHANGELOG entry. Deprecation warnings for a release, then removal in v0.3.
- (b) Ship as v0.1.1 since there are no external users yet. Announce internally.
Recommendation: (b). Pre-1.0 and no external users; we shouldn't accumulate deprecation debt for hypothetical future users.