Skip to content

Release 0.19.0: message delivery into agent context, room fan-out, Grok hooks - #83

Merged
mostlydev merged 27 commits into
masterfrom
release/0.19.0
Sep 17, 2026
Merged

mostlydev merged 27 commits into
masterfrom
release/0.19.0

Conversation

@mostlydev

Copy link
Copy Markdown
Owner

Cuts 0.19.0. The operator asked for this release after live acceptance in their own terminal.

What changes for users

  • Messages arrive in the agent's context. Claude Code and Codex wakes carry the room events themselves, so an agent answers without running tt wait; tt ack <token> records receipt for exactly those events and grants no turn.
  • An operator's chat reaches every agent, standby included, as one room event. Agents' own room messages still wake nobody. @name narrows, several @names share one message, !@ is urgent.
  • Busy agents still get messages. Claude receives them at its next tool boundary without cancelling a running tool; Codex after its turn; Grok through its hooks.
  • Grok Build gets a stop guard and active-turn delivery hooks.
  • Chat shows delivery per recipient in the message header ( pending, delivered, ! failed), renders inline by default, and keeps native scrollback and selection.
  • Envelopes are compact plain text: a short chat message costs about 190 characters instead of about 600.

Not included: /invite, and a guarded herdr wake for idle Grok sessions.

Verification

  • Full suite 646 passed + 1 skipped under TERM=dumb; typecheck and build clean.
  • Live in the operator's own room: a plain chat message was delivered to and acknowledged by Claude, Codex and Grok within about five seconds; Grok confirmed hook delivery in its own session; the operator accepted the chat UI.
  • Reviewed by Codex and Grok throughout; the release notes were reconciled against shipped behaviour before generating them.

A full-screen pane has no scrollback, so herdr and other terminals could
neither scroll nor select the conversation. Print into the normal screen
with a single readline prompt instead, and keep the pinned layout behind
--fullscreen.
Bare readline submitted each line of a bracketed paste and a one-row
clear corrupted wrapped drafts. Drive inline mode through
ChatInputController and draw the composer directly, erasing every row it
drew before printing or redrawing.
A resize recomputed the erase from the new width, which stranded a copy
of the panel. Track the drawn geometry, erase the larger of the old and
reflowed heights on resize, cover a severe shrink in the terminal test,
and document the one artifact that reflow can still leave behind.
Grok Build fires a Claude-compatible Stop hook but sends camelCase keys, so
the existing guard fail-opened in every Grok session. Read both spellings,
install a dedicated ~/.grok/hooks/talking-stick-stop.json carrying the same
command so Grok's identical-handler dedup collapses it with the imported
Claude entry, and block only an ordinary turn end: a session teardown and a
subagent stop are observed, never blocked.

GROK_AGENT=1 now marks a Grok session and carries GROK_SESSION_ID as its
anchor, with the recorded hook log as fallback. GROK_SESSION_ID alone stays
inert so an inherited value cannot masquerade as a harness.

No native wake transport is added: Grok has no out-of-process inject API, so
it remains reachable only through cmux.
Grok's live payload carries session_id as well as sessionId, so the guard was
already reachable from Grok, and its Stop fires for turn ends, shutdowns and
subagent gates alike. Recognise a Grok payload by its camelCase event name and
demand reason == end_turn there, while Claude Code, which sends no reason at
all, keeps blocking as before.
The per-turn guardian joins with the agent id as its display name, and its
presence outranks the harness's when metadata merges onto the member row. For
as long as a turn was held the member was renamed to its full id, so short
chat mentions such as @claude stopped resolving. Liveness was unaffected
because it already reads the harness pid.
GROK_AGENT became a harness marker, and tt detects Grok by process ancestry
without TT_HARNESS_EXPORT, so running the suite from a Grok session promoted
every whoami and text-mode assertion to a Grok identity. Scrub GROK_AGENT
with the other harness variables and hide a grok ancestor from the process
inspector while keeping its pid and start time for liveness.
Node's readline replaces its line editor with a dumb writer whenever
TERM=dumb, even when terminal mode is requested, so tt chat submitted arrows
and Ctrl+A as literal text and drew panel escapes a dumb terminal cannot
render. Treat such a terminal as non-interactive and use plain line mode.

The terminal-mode chat tests drive readline directly and failed the same way
when run from a Grok tool shell, which exports TERM=dumb. Pin a capable TERM
in the global test setup so they no longer depend on the caller.
An operator typing in tt chat addresses the room, but room messages never
woke anyone, so only an agent already inside tt wait saw them. A room message
from a human sender now reaches every agent that is a member when it is sent,
standby included, as one event with a receipt per recipient; later joiners do
not inherit it. Agents' room messages still wake nobody so replies cannot
loop, and an agent's room interrupt still reaches only the owner.

Chat sends a plain message or @everyone once to the room, and several @NAMEs
share one event that lists them instead of one copy per agent. Each
recipient's status is reported as soon as its own wake settles, and labels say
only what is known: unreachable requires a definite transport failure.

Native envelopes become attributed plain text: a header with the room path and
ack command, a #seq sender -> you|room line per event with every content line
indented, and a closing boundary. A short chat line drops from about 600
characters to about 190.
The operator wants one block per message whose header names every recipient,
with a single-character state after each name instead of a receipt line under
the body: … not delivered yet, ✓ delivered, ! failed. Every mark is one cell
wide and reserved from the first paint, so an acknowledgement repaints the
header in place without changing how many rows it occupies. Room messages
from an operator record who they went to at send time, for display only.

Also render plain string artifacts from tt release in envelopes instead of
undefined, and stop creating native receipts for chat consoles, which never
consume them.
@mostlydev
mostlydev merged commit f0b7589 into master Sep 17, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant