Repository navigation
v0.18.1 — a third pane on a fresh start, duplicate spaces, and `og agents` dying on a 404
Fixed
-
A fresh
og start herdrno longer opens a third pane. Two halves were doing one job:_open_spacetypes the chat command into the pane it is about to split, and since v0.18.0og chatsplits the pane it finds itself in — so the processpane_runstarts runs the very code that splits beside it. The sharedsplit_agents_paneguarded nothing; itsknown_panesargument only ever identified the new pane afterwards.The fix is at the
og chatend. Deleting the launcher's split was the other candidate and is worse: the launcher is the half that still holds the pane when the chat command never reaches its own, and the half whose degradation reaches you before the terminal is handed to herdr. A guard keyed onknown_panesis not possible either —pane.listreturns onlypane_id,tab_idandworkspace_id, so "this tab already holds theog agentspane" and "this tab holds a pane you split yourself" are the same answer, and declining on the second is howog chatin a space you made by hand would silently lose the paneknown_panesexists to protect.So the question asked is one og can answer: did og open this pane?
og_owns_panelooks for a record naming this workspace, tab and pane. Both would-be splitters write one before they act, so it is ordered rather than raced, and both state buckets are read because a claim moves the record frompendingtospacesunchanged. All three ids must match exactly — a record with notab_idmatching every tab would stand down in a tab the launcher never touched and cost you the pane. It fails open: an unreadable state file is og having nothing to say, not og saying "mine". -
The bridge no longer opens duplicate spaces for one directory.
og startlaunches the bridge before exec'ing the launcher, and the bridge loaded its state once at construction — so the launcher'spendingrecord landed in a file it had already read, the dedupe missed, and a second space opened for a directory that already had one. Worse than a stale read:_prune_pendingsaves the whole file, so a prune against the startup snapshot deleted the launcher's record, and a record no process holds can never be found by a later pass. That is why the duplicate never healed.The bridge now re-reads at each reconcile pass, ahead of the prune, and merges per key against the last state it knew the file held — a key it has not changed belongs to whoever wrote the file last, including a writer who replaced it. A claim is a completed operation, not an unsaved edit, and is no longer mistaken for one.
-
The two writers of
og-herdr.jsontake a lock. Every interesting operation on that file is a read-modify-write, and re-reading immediately before writing narrows the window without closing it — a second read only moves the race to the other side of itself.state_updatenow does load → decide → write as one transaction under an advisoryfcntl.flock, andog chat's own pane record shares it. The lock lives on anog-herdr.json.locksidecar, never the state file: the writer finishes withos.replace, which swaps the inode, so a lock taken on the file itself would leave two holders locking different inodes and excluding nobody. Advisory, so a writer that ignores it is still last-writer-wins; wherefcntlis absent the behaviour is exactly what it was. -
og agentstalks to og's own server, and degrades instead of dying.cmd_agentswas the one caller that never forwarded--server, so on an install whose server moved off 6767 it queried the default port and read an unrelated listener's 404. Every sibling already forwarded it. The viewer also died on any non-200 —fetch_listingwas the single escaping wire call, every detail fetch already being guarded — tearing down curses with a raw traceback. It now keeps the last good rows behind a footer notice naming the server and status, and heals when the server returns. -
og startno longer types an attach for a session on another machine. A session for the same directory living on a different host matchedworkspace == cwdand won, and the attach died with "its host is offline".runner_is_offlinecannot catch it: a session on another machine has no offline runner here at all. A session whose host is known and different is now skipped. -
The host rule above could never actually fire.
host_idwas not among the fields merged onto the session row the rule tested, so the check readNoneevery time and was dead code. It is forwarded now, and this machine's host id comes from the daemon record first, falling back to/v1/hostsonly when exactly one host is online — never "the first online host", which on a multi-host account is a guess. -
A
--dry-runclaim consumes nothing, in memory either. It previously moved the record out ofpendingand declined only the disk write, leaving memory a key short with no save to reconcile it — which the new merge rule then reads as a completed claim, permanently. A run that changes nothing could durably remove a real launcher record from the bridge's view.
Docs
docs/MUX-LAUNCHER.md§2 gains the pane-ownership rule and thepane.listlimitation that forces it; the "two things open the agents pane" note was stale since v0.18.0 and now names three.