Orchestration: the Room, the Bridge, and Multi-Agent Conversation
For new readers
This page documents the orchestration layer of the self-model reference implementation — the scripts that bring up a running system, tear it down cleanly, and let two or more running instances hold a conversation with each other. None of this implies anything about subjective experience on either side; "speaking" here means a structured message delivered over a real socket between two real, running symbolic processes.
Bringing a system up and down: launch.patlang and shutdown.patlang
orchestrator/launch.patlang is the single-command bring-up: it reads a manifest file of name : port : script : args lines, spawns every listed component as its own real OS process via lp_spawn_component, waits for each to genuinely announce itself over signals discovery — not just for the OS process to start — then spawns the dashboard and prints its URL. It is manifest-driven for the same reason PatLang's own patmake_main.patlang is: adding or removing a component means editing one config line, not this script. It deliberately does not block waiting for components to exit, matching this project's stated preference for a distributed rather than monolithic system — each spawned process is independent and can be killed or restarted without taking the launcher down with it.
Every component this run actually spawns is recorded, in spawn order, to .patlang_queue/spawned_components.json — name, signals port, and pid — specifically so orchestrator/shutdown.patlang can do a managed shutdown rather than a blind broadcast. Its own header explains the earlier design it replaced, at the project owner's own prompting: "if you actually instrumented everything with signals APIs you could do a managed shutdown via a quit which could also tell its children to quit before wrapping up, and ensure everything is flushed and closed properly." The old signal-discovery broadcast approach could not do this: signal discovery is a single global presence beacon with no concept of "mine" versus "some other instance's" component, no ordering, and no verification that a quit signal actually took effect — kept in shutdown.patlang today only as a degraded fallback for components started by hand, outside launch.patlang, with no roster to read.
A real bug: crashing mid-shutdown on an already-dead component
shutdown.patlang sends quit to each roster entry in reverse spawn order via lib/safe_signals.patlang's safe_signal_query, not plain signal_send — the header states directly why this substitution mattered: a component can die on its own between being recorded in the roster and being sent its quit signal, and plain signal_send's fatal tcp_connect would crash the whole shutdown script the moment it hit one dead target, silently leaving every component still queued behind it never sent a quit signal at all. This is the same fatal-on-a-dead-target bug class already fixed independently in the dashboard and the auditor (each of which also polls a set of components that can die between discovery and query) — sd_quit_and_wait now sends the signal defensively and then actually waits (polling is_alive up to a fixed timeout) to confirm the process is genuinely gone before moving to the next one, rather than firing and hoping. The header records that an unverified broadcast quit had already left the auditor surviving a shutdown more than once in this project's own history, undetected until the next launch hit a port-already-in-use failure.
Room: a standing, N-ready multi-agent conversation
orchestrator/room.patlang replaces an earlier, one-shot design (bridge_chat.patlang, below) with a genuine long-lived component, matching the pattern the dashboard, the auditor, and Imagination already use — a standing process, not a script that stops talking once a fixed round count runs out. Its own header is explicit about why the replacement was necessary: bridge_chat.patlang was the whole run — once its fixed round count finished, nothing was left running to keep two instances talking, so "they only talk at the very beginning of a run" was a precise description of what that script actually did, not a bug in it.
Each participating instance's Action process only ever needs to know one thing to speak into the room: the room's own port, passed as a combined <room_port>@<room_name> sixth argv field. That combined format is itself the record of a real parsing bug: launch.patlang's manifest format already uses : as its outer field separator (name : port : script : args), so an earlier attempt at "9760:A" got split apart by that parser before Action's own code ever saw it — room_name silently became a fifth, never-read field, and Action's own str_split_char on an already-truncated string then produced a room name read past the end of the list. Confirmed live: instance B genuinely received instance A's message, but recorded the sender as interlocutor "1", A's bare numeric instance id, rather than its intended room name.
Name resolution and delivery are centralized in the room, not duplicated per instance: adding a third or fourth participant means adding one more name:port pair to the room's own roster, not touching every other instance's manifest. The roster is static per run — given at startup, not self-registering — and "room" is reserved as the broadcast target, checked at parse time (room_parse_roster) so a roster typo that collides with it fails loudly at startup rather than silently making broadcast unreachable. Speaking is addressed with a real speak(to, text) call: the when speak do ... end handler parses a JSON request carrying from, to, and text, and if to equals the reserved broadcast target it delivers to every other roster entry (room_broadcast); otherwise it looks up the named recipient's port and delivers directly (room_deliver_one) over HTTP to that instance's own dashboard /chat endpoint — the same delivery mechanism bridge_chat.patlang had already proved out, but now a direct, synchronous side effect of the speak signal rather than an external poll loop. Every delivery, successful or not, is logged to a durable room_log topic with timestamp, sender, recipient, text, and delivered count, via room_log_event.
features/room.feature's scenarios exercise the contract concretely: a malformed speak request ("not json at all") gets a clear error reply without the room dying; speaking to an unknown recipient is rejected; a direct message to a named participant reaches only that participant; a broadcast to "room" reaches every other participant but not the sender; and — tying this back to Action's own tool dispatch — a candidate that asks Action to speak to another participant, including a plain-language request phrased as "ask the room a question" rather than naming the tool explicitly, results in Action's status reporting the speak tool was actually invoked and the intended recipient genuinely receiving the message.
A quiet room is deliberately not left silent forever: if no activity has occurred for room_kickstart_idle_ms, room_maybe_kickstart broadcasts one of three rotating prompts from a synthetic room_facilitator interlocutor, the standing analogue of bridge_chat.patlang's own one-time opening message — recurring rather than one-shot, and never needed once real participants are actually talking regularly. This mechanism itself produced a real, documented bug: an earlier version read the idle threshold via a bare top-level identifier from inside room_maybe_kickstart, a function defined separately from top-level script code — the same class of "a function does not close over a top-level let" mistake the header notes has recurred across perception.patlang, launch.patlang, and bridge_chat.patlang's own run_bridge in this same codebase. The bare identifier silently resolved to something falsy rather than erroring, so the idle check was always true and kickstart fired on nearly every ten-second tick instead of waiting the real configured 300000ms threshold. Confirmed live: both participating instances were flooded with a kickstart broadcast roughly every eleven seconds, each one from a brand-new, always-zero-trust room_facilitator interlocutor — which is also why Action's own open-todo count exploded to 200 in a few minutes, since every flood message tripped the trust-gated fact-check flag documented on the Action page.
Bridge chat: the one-shot predecessor
orchestrator/bridge_chat.patlang bridges two running instances purely through each one's own dashboard HTTP API (/data, /chat), never touching queue files directly — the two instances have separate working directories and therefore separate queue state, so the dashboard HTTP boundary is the only thing that can reach both without caring where either one's files live. It is a real, documented test bed for the multi-agent PEAS scope named in Requirements Spec Section 2, and its own header records several bugs found running it for real: exec_capture_io, not exec_capture, because the latter's real calling convention is variadic trailing string arguments, not a single list value — passing a list argument silently filtered it out as "not a string" and ran curl with no arguments at all, whose only symptom was curl's own usage hint, easily misread as a network problem; a hop timeout raised from 60 seconds to 900000ms once real generations were found to queue behind each other on a shared local Ollama server; and a timed-out wait that had been silently returning its untouched empty default as though it were a genuine reply, now replaced with a distinct __BRIDGE_TIMEOUT__ sentinel the caller checks for explicitly. A separate finding reshaped the whole bootstrap: an early version only ever told instance A the shared task and relayed each side's raw reply onward from there, so instance B's every input was literally "here's what A said" — observed live as B only ever producing passive acknowledgements while A did genuine independent work. bridge_bootstrap_both now sends the task to both sides independently before either sees the other's reply, and every subsequent hop restates the task alongside the relayed reply (bridge_frame_with_task), since neither side otherwise carries any memory of the conversation beyond what the other says to it directly.
See also
For the narrative account of how the room's addressing bug and the kickstart-flood bug were actually found and fixed, see the project journey page. Action is the component that actually speaks into a room via its speak tool; the dashboard and audit trail is what every room-delivered message and every launch/shutdown event ultimately becomes visible through.