You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#469 shipped the floor: a worktree JSONL room, a device room, peer_message, /tell, /peek, a presence note when the live set changes, and a one-shot collision gate that tells the model to speak before mutating a shared tree.
That is presence plus a mailbox. It is not a conversation. Two sessions in the same folder can post past each other, the model is still told it can target "all" (it cannot), injected peer lines look like the human spoke, and the TUI has no /tell or /peek at all.
This issue is the speech half of #469. Do not reopen device-wide model "all" — that was retired because it flooded every folder on the box.
What is already true (so we do not rebuild it)
Wire: append-only JSONL rooms in src/presence_chan.zig. Worktree room is a room (every live session on that tree hears every line). Device room is folder-scoped (deviceHears). to is addressing metadata, not an inbox.
Model tool: peer_message in src/peer_channel.zig, wired through src/schema.zig / src/agent_tools.zig. One-way. Tool result only confirms the post.
User REPL: /tell <session|all> <text>, /peek <session> in src/commands_misc.zig.
The presence note (peerNoteIfChanged) still says peer_message to "all" or a name… reaches every session. That is false for the model.
A model that follows the note will get a tool error on the first attempt. That is the cheapest regression and the first slice.
2. One-way firehose, no conversation
There is no reply thread, no ack, no "did they hear." The sender's tool result is "posted." Delivery happens later, at the receiver's next step boundary. The sender never learns that.
/peek is user-only. The model cannot ask what a peer is doing; it only gets a presence list when the set of live sessions changes, not when a peer starts a long bash or finishes a turn.
3. Injected as role: user
Inbound peer lines are stuffed into history as a user message ([peer message from X]: …). Compaction, cache, and /peek all treat that as the human. /peek already special-cases the [peer message prefix to skip it — that is a smell, not a protocol.
4. Room vs DM is easy to confuse
Worktree room: everyone on the tree hears every line; to is rendered, not enforced.
Device room: to is enforced (deviceHears).
Same tool, two topologies. Nothing in the prompt explains the difference in one sentence the model will keep.
5. TUI is missing the verbs
TUI/catalog.zig has no /tell or /peek. A TUI session can be told (delivery still runs) but the human cannot speak or inspect from the pager.
6. Targeting is a substring
Session-name / pid match. Ambiguous names error. No stable id the model can hold across turns. A rename or a second graff in the same folder breaks the address.
7. No structured claims
The collision gate asks the model to talk, then hopes free text is enough. There is no locking src/foo.zig the gate can parse, so two models can both "announce" and both edit.
8. Backlog is a wall
backlog_tail_max = 10. REPL /peek shows the last 2 user lines. No threads, no fork, no "since I last looked."
What "better" means (acceptance, not vibes)
A pair of sessions on the same worktree should be able to:
See who else is live, and a one-line "what they last did" without the human /peeking.
Say something to a named peer (or the room) and get a tool result that distinguishes posted from heard at the receiver's next step.
Hear inbound speech as a first-class channel event, not a fake user utterance. /peek and compaction must not treat it as the human.
Do that from the TUI with the same /tell / /peek the line REPL already has.
Not be lied to by the schema or the presence note.
Out of scope for this issue: mid-turn preemption (#430), a full Slack-like pane, device-wide model broadcast.
Proposed slices (ship in this order)
Each slice should land with tests that zig build test actually reaches (src/main.zig and/or TUI/root.zig import). Suite count goes up. Hand-written files stay ≤ 600 LOC.
Slice A — tell the truth (small, do first)
Drop "all" from peer_message schema. Document worktree-room vs device-room in one sentence.
Rewrite peerNoteIfChanged so it matches handleMessage. Name the live peers. Do not mention "all".
Keep user /tell all (human broadcast is still valid).
Slice B — TUI /tell and /peek
Same verbs as src/commands_misc.zig, wired through TUI/catalog.zig + the TUI command path.
Drive with TUI/sim.zigTerm (no PTY, no Ghostty window). Assert the catalog lists them and a /tell from the pager appends a room line the other session would hear.
Slice C — typed channel events
Stop injecting peer speech as role: user.
Engine event + a history representation that is not the human (dedicated prefix/role the compact/peek paths already understand).
Update /peek to show peer lines as peer lines, instead of skipping the prefix as if it were noise.
Slice D — peer_peek (or a richer presence note)
Model-callable: live peers + last user prompt + last tool name + last peer line they sent.
Reuse summarizeTranscript / the same fields /peek already walks. Do not invent a second transcript parser.
Presence note can stay as the "set changed" ping; peer_peek is "what are they doing right now."
Slice E — ack / thread id
Optional in_reply_to on peer_message.
Sender tool result: posted vs delivered (delivered = receiver crossed a step boundary and deliverInbound ran).
Enough to stop "I told them, why are they still editing."
Why this exists
#469 shipped the floor: a worktree JSONL room, a device room,
peer_message,/tell,/peek, a presence note when the live set changes, and a one-shot collision gate that tells the model to speak before mutating a shared tree.That is presence plus a mailbox. It is not a conversation. Two sessions in the same folder can post past each other, the model is still told it can target
"all"(it cannot), injected peer lines look like the human spoke, and the TUI has no/tellor/peekat all.This issue is the speech half of #469. Do not reopen device-wide model
"all"— that was retired because it flooded every folder on the box.What is already true (so we do not rebuild it)
src/presence_chan.zig. Worktree room is a room (every live session on that tree hears every line). Device room is folder-scoped (deviceHears).tois addressing metadata, not an inbox.peer_messageinsrc/peer_channel.zig, wired throughsrc/schema.zig/src/agent_tools.zig. One-way. Tool result only confirms the post./tell <session|all> <text>,/peek <session>insrc/commands_misc.zig.deliverInboundat every step boundary ofagent.runTurn(src/agent.zig). Mid-stream interrupt is Input inversion (Phase 1b): approvals, pickers, mid-turn asks, Esc ownership, readline queries #430 and stays a sibling, not a prerequisite.src/presence.zigbefore a mutating tool on a shared tree; the model is told topeer_messagefirst.src/peer_channel.zigunit tests +scripts/test-pty-peer-channel.py.What is wrong today
1. The docs lie
tool_schemastill documents"all"as a validto.handleMessagerejects"all"(error.BroadcastRetired).peerNoteIfChanged) still sayspeer_message to "all" or a name… reaches every session. That is false for the model.A model that follows the note will get a tool error on the first attempt. That is the cheapest regression and the first slice.
2. One-way firehose, no conversation
There is no reply thread, no ack, no "did they hear." The sender's tool result is "posted." Delivery happens later, at the receiver's next step boundary. The sender never learns that.
/peekis user-only. The model cannot ask what a peer is doing; it only gets a presence list when the set of live sessions changes, not when a peer starts a long bash or finishes a turn.3. Injected as
role: userInbound peer lines are stuffed into history as a user message (
[peer message from X]: …). Compaction, cache, and/peekall treat that as the human./peekalready special-cases the[peer messageprefix to skip it — that is a smell, not a protocol.4. Room vs DM is easy to confuse
Worktree room: everyone on the tree hears every line;
tois rendered, not enforced.Device room:
tois enforced (deviceHears).Same tool, two topologies. Nothing in the prompt explains the difference in one sentence the model will keep.
5. TUI is missing the verbs
TUI/catalog.zighas no/tellor/peek. A TUI session can be told (delivery still runs) but the human cannot speak or inspect from the pager.6. Targeting is a substring
Session-name / pid match. Ambiguous names error. No stable id the model can hold across turns. A rename or a second
graffin the same folder breaks the address.7. No structured claims
The collision gate asks the model to talk, then hopes free text is enough. There is no
locking src/foo.zigthe gate can parse, so two models can both "announce" and both edit.8. Backlog is a wall
backlog_tail_max = 10. REPL/peekshows the last 2 user lines. No threads, no fork, no "since I last looked."What "better" means (acceptance, not vibes)
A pair of sessions on the same worktree should be able to:
/peeking./peekand compaction must not treat it as the human./tell//peekthe line REPL already has.Out of scope for this issue: mid-turn preemption (#430), a full Slack-like pane, device-wide model broadcast.
Proposed slices (ship in this order)
Each slice should land with tests that
zig build testactually reaches (src/main.zigand/orTUI/root.zigimport). Suite count goes up. Hand-written files stay ≤ 600 LOC.Slice A — tell the truth (small, do first)
"all"frompeer_messageschema. Document worktree-room vs device-room in one sentence.peerNoteIfChangedso it matcheshandleMessage. Name the live peers. Do not mention"all".#469in user-visible strings ([#469 presence], "issue Session presence + inter-session channel: co-resident graffs on one device talk to each other (cloud relay later) #469"). The issue number is for us, not the model./tell all(human broadcast is still valid).Slice B — TUI
/telland/peeksrc/commands_misc.zig, wired throughTUI/catalog.zig+ the TUI command path.TUI/sim.zigTerm(no PTY, no Ghostty window). Assert the catalog lists them and a/tellfrom the pager appends a room line the other session would hear.Slice C — typed channel events
role: user./peekto show peer lines as peer lines, instead of skipping the prefix as if it were noise.Slice D —
peer_peek(or a richer presence note)summarizeTranscript/ the same fields/peekalready walks. Do not invent a second transcript parser.peer_peekis "what are they doing right now."Slice E — ack / thread id
in_reply_toonpeer_message.postedvsdelivered(delivered = receiver crossed a step boundary anddeliverInboundran).Later (own issues once A–E exist)
path+intent), so two announces cannot both proceed.Files that will move
src/peer_channel.zig,src/presence.zig,src/presence_chan.zigsrc/schema.zig(andpython3 sdk/generate.py --harness ./zig-out/bin/graffafter)src/commands_misc.zig,TUI/catalog.zig, TUI command dispatchsrc/agent.zig(deliverInboundcall sites — do not change timing until Input inversion (Phase 1b): approvals, pickers, mid-turn asks, Esc ownership, readline queries #430)scripts/test-pty-peer-channel.py+ newTUI/sim.zigcasesNon-goals
Related