Skip to content

feat(host): push channel lifecycle through project lobbies - #337

Merged
iamnbutler merged 7 commits into
mainfrom
lobby-lifecycle
Oct 15, 2026
Merged

iamnbutler merged 7 commits into
mainfrom
lobby-lifecycle

Conversation

@iamnbutler

@iamnbutler iamnbutler commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Fourth of four stacked PRs for project lobbies (base: #336). Refs #17, refs #14.

Hosts push channel lifecycle through project lobbies, so project members stop polling the directory every 30 s.

  • Events follow the directory, never replace it. After a host's directory PUT succeeds, published(before, after) in apps/host/src/lobby.ts diffs its own listings into lifecycle events and sends each one to its project's lobby. The kinds are created, renamed, summarized, archived, resumed, moved, deleted, and ran (a run ended). Listings and catalogs stay the source of truth, and an event only patches the receiving host's copy of the directory (directory.apply) until its next read.
  • Activity alone sends no event. Changes to active or busy and running or idle reach teammates through peers or the directory's next read. That read is every 5 min while covered, and docs/architecture.md says so.
  • Nothing sent is lost on a dead socket. Each event stays queued, latest per channel, until the lobby echoes it back (matched by channel and at). It is resent after the next synced, because a socket can die unnoticed for up to 65 s.
  • A project's last deletion still arrives. A lobby the host no longer needs stays until the next publication, which may carry that project's deleted, and closes once its queue is echoed. emit opens a lobby if publishing outran the subscription.
  • Resume, don't miss. The lobby assigns a per-project seq. A host says hello with the last seq it applied. The lobby replays newer events, or answers resync when they are gone, and the host then does a full directory read.
  • Bursts rebuild listings once. Teammates' events are applied to the directory copy at once. ownersChanged (which re-reads catalogs, now async after fix(host): run request-path git off the gateway's event loop #346) runs once per 100 ms burst, not once per event, so a replay of 2,000 events triggers only a few rebuilds.
  • The 30 s poll slows only when it is covered. The poll skips a tick only when every wanted project's lobby is live, the last sync succeeded, and the last read was under 5 min ago. A failed publish therefore retries every 30 s. Without that, a change made while the directory was down would wait for the 5-minute read, which the smoke test caught. If any lobby drops, the next tick polls again.
  • directory.watch takes an options object. Rebased onto main's per-URL sync: Refresh, a directory change, and the heartbeat still join a sync under way, while a channel change during a sync queues one more after it, since the running one may have read channels first. Events that arrive during a GET are reapplied to its result.
  • Fewer directory writes. While covered, the heartbeat publish drops from every 30 s to every 5 min. Each PUT /hosts rewrites the host's row (fix(directory): write only changed channel listings on each publish #391 already skips unchanged channels), so a covered host writes about 288 host rows a day instead of 2,880, plus one per actual change.

Smoke test: a source host (port 4391, ACE_HOME under /tmp, Tailscale shimmed off, ACE_DIRECTORY pointed at a local wrangler dev of this stack) and a scripted teammate host in the same lobby. Run again after review:

  • Creating a channel reached the teammate about 0.1 s later, and deleting it reached the teammate too.
  • The teammate sent 300 created events and then 300 deleted events. The app listed all 300 channels, then none, after 2 client updates per burst.
  • A project the owner opened, gave one channel, and then dropped from projects.json: deleting that channel delivered deleted to its lobby, and the host then closed the lobby.
  • With the Worker stopped, a rename failed to publish. When the Worker came back, the host's lobby went live, the 30 s poll published the rename, and the teammate got renamed once, about 5 s after the restart.
  • Before review: the source host's 30 s reads stopped while its lobbies were live, and its next read came about 5.5 min later. A host against main's directory (no Lobby) got 404, retried quietly, and kept the 30 s poll.

Not checked end to end: the ran event, because a run needs model credentials on the test host. It uses the same path as the other kinds.

Deploy: wrangler deploy of services/directory from the merged stack adds the LOBBY binding and the v2 migration (#335). It needs no new secrets. The live Worker's last applied migration tag is v1. Until it is deployed, every host keeps today's behavior.

Open: changes from hosts on an older Ace, and changes made while a host's process was down, reach others only through the 5-minute read or peers.

Rebased onto main and run again with the same setup as #336: create and delete reached the teammate's lobby within 0.1 s, a 300-event burst and its 300 deletions each produced 2 client updates, a dropped project's last deletion reached its lobby, and the host's 30 s directory publish paused while its lobbies were live (after the smoke, its next sync came 5 min 6 s later instead of 30 s).

Built in Ace

No. This was built with Claude Code outside Ace by the overnight watcher, because this Mac's Ace Canary is a participant in the two-person study and can't be used for development tonight. See #5.

After each successful publish to the directory, a host diffs its channels
against its previous publication and sends each change (created, renamed,
summarized, archived, resumed, moved, deleted, or a run's end, with the new
listing) to that project's lobby. Each stays queued, latest per channel, until
the lobby echoes it back, and is resent after a reconnect, since a socket can
die unnoticed. A lobby the host no longer needs stays until the next
publication, so a project's last deletion still reaches it.

Teammates' hosts apply events to their directory listings at once and rebuild
listings once per burst, so a replay of thousands of events reads catalogs a
few times, not per event; a directory read already in flight reapplies them,
and a resync reads the directory again.

While every project a host subscribes to has a live lobby and its last publish
succeeded, the 30 s directory poll slows to a 5 min read; changes still
publish immediately, and a change that arrives during a sync runs another.
Activity alone sends no event and reaches teammates through peers or that
read. Without lobbies the poll stays at 30 s. Refs #17, #14.

Why: apps/host/src/lobby.ts#function published | events must be sent only after the directory PUT succeeds, so a teammate that reads the directory on receipt agrees with the event
Why: apps/host/src/directory.ts#function watch | the poll may slow only while every subscribed project's lobby is live and the last publish succeeded; a 404, dropped lobby, or failed PUT must keep the 30 s poll
Why: apps/host/src/directory.ts#function apply | a directory read that started before a lifecycle event must reapply it, or the stale read undoes the push
Why: apps/host/src/lobby.ts#function receive | this host's events stay queued until the lobby echoes them (same channel and at), since a socket can die unnoticed for up to SILENT
Why: apps/host/src/lobby.ts#function update | a lobby no longer wanted must stay until the next publication, or a project's last deletion is never sent
Why: apps/host/src/lobby.ts#function listed | teammates' events rebuild listings once per burst; a replay must never re-read catalogs per event
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