This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

covibe/omp: supervise collab hosting so a lost room comes back

The autostart was one-shot, and an omp collab host never returns on its own:
CollabSocket treats 4001/4004/4009/4029 as fatal (no reconnect), and
CollabHost.#broadcast self-stops with "session switched" when the session id
changes — which happens seconds into a fresh session, as the first prompt
creates the real session. Observed on ignavia: hosts died 21s and 88s in
(logging one debug line, "dropping frame, socket closed" t=bye), so the
dashboard listed sessions whose room no longer existed and every browser link
answered "session ended: no such room" for the rest of a multi-hour session.

superviseAutostartCollab() now keeps hosting: it re-hosts on the same preset
room whenever the host disappears without the user asking (/collab stop and
/leave set a flag), with exponential backoff on connect failures. Because
covibe fixes the room id and key, re-hosting restores the identical link, so
QR codes already scanned keep working; retrying also recovers from a stale
4009 host once the relay's keepalive frees the dead room.

readCollabAutostartEnv() also consumes OMP_COLLAB_* from process.env: children
inherit it, so a nested omp would race the host for the same room and one of
them loses with 4009.

Rebased onto omp 17.1.4 (the previous patch only applied with fuzz) and
typechecked with tsgo.

+108 -26
+108 -26
2configs/omp-collab-autostart.patch
··· 1 1 diff --git a/packages/coding-agent/src/collab/host.ts b/packages/coding-agent/src/collab/host.ts 2 - index 74a021a..45df271 100644 2 + index 5d9d103..722b01b 100644 3 3 --- a/packages/coding-agent/src/collab/host.ts 4 4 +++ b/packages/coding-agent/src/collab/host.ts 5 5 @@ -21,6 +21,7 @@ import type { ··· 29 29 this.#writeToken = writeToken; 30 30 this.#link = formatCollabLink(relayUrl, roomId, rawKey, writeToken); 31 31 this.#webLink = formatCollabWebLink(relayUrl, roomId, rawKey, writeToken, webUrl); 32 - @@ -688,3 +693,32 @@ export class CollabHost { 32 + @@ -289,6 +294,9 @@ export class CollabHost { 33 + /** Broadcast a goodbye, detach all taps, and close the socket. */ 34 + async stop(reason: string): Promise<void> { 35 + if (this.#stopped) return; 36 + + // Only the slash commands stop with this reason; record it so the covibe 37 + + // supervisor treats it as intent and does not resurrect the room. 38 + + if (reason === USER_STOP_REASON) userStoppedHosting = true; 39 + this.#socket?.send({ t: "bye", reason }); 40 + await this.#teardown(); 41 + } 42 + @@ -689,3 +697,103 @@ export class CollabHost { 33 43 this.#ctx.ui.requestRender(); 34 44 } 35 45 } 36 46 + 37 47 +/** 38 - + * covibe integration: when OMP_COLLAB_RELAY is set, start hosting headlessly on 39 - + * boot using a caller-provided room id + key (OMP_COLLAB_ROOM / OMP_COLLAB_KEY, 40 - + * base64url of key‖writeToken) so the orchestrator owns a stable, enumerable 41 - + * link with no minting or scraping. Returns the live host, or undefined when no 42 - + * relay is configured. 48 + + * covibe integration: host headlessly on a caller-owned room. 49 + + * 50 + + * OMP_COLLAB_RELAY/_ROOM/_KEY (key = base64url of key‖writeToken) let the 51 + + * orchestrator own a stable, enumerable link instead of minting or scraping one. 52 + + * Hosting is supervised because a host that goes away never comes back on its 53 + + * own: CollabSocket treats 4001/4004/4009/4029 as fatal (no reconnect), and the 54 + + * host also stops itself on a session switch — which happens seconds into a 55 + + * fresh session, when the first prompt creates the real session id. A one-shot 56 + + * start therefore loses the room for the rest of a multi-hour session. Since the 57 + + * room id and key are fixed by the orchestrator, re-hosting restores the very 58 + + * same link, so already-shared links and QR codes keep working. 43 59 + */ 44 - +export async function maybeAutostartCollab(ctx: InteractiveModeContext): Promise<CollabHost | undefined> { 45 - + const relay = process.env.OMP_COLLAB_RELAY; 46 - + if (!relay) return undefined; 60 + + 61 + +/** Reason the /collab stop and /leave commands pass to {@link CollabHost.stop}. */ 62 + +const USER_STOP_REASON = "host stopped"; 63 + +const AUTOSTART_POLL_MS = 2_000; 64 + +const AUTOSTART_BACKOFF_BASE_MS = 1_000; 65 + +const AUTOSTART_BACKOFF_MAX_MS = 30_000; 66 + + 67 + +/** Set once the user explicitly stops hosting, disabling re-hosting. */ 68 + +let userStoppedHosting = false; 69 + + 70 + +export interface CollabAutostartConfig { 71 + + relayUrl: string; 72 + + webUrl: string; 73 + + preset?: { roomId: string; key: Uint8Array; writeToken: Uint8Array }; 74 + +} 75 + + 76 + +/** 77 + + * Read the autostart env and remove it from process.env. Consuming it matters: 78 + + * children inherit the environment, so a nested omp (or a worker daemon) would 79 + + * otherwise try to host the same room and the relay rejects one of them with 80 + + * 4009 — if the child wins the race, the real session is locked out. 81 + + */ 82 + +export function readCollabAutostartEnv(): CollabAutostartConfig | undefined { 83 + + const relayUrl = process.env.OMP_COLLAB_RELAY; 47 84 + const webUrl = process.env.OMP_COLLAB_WEB ?? ""; 48 85 + const roomId = process.env.OMP_COLLAB_ROOM; 49 86 + const keyB64 = process.env.OMP_COLLAB_KEY; 50 - + let preset: { roomId: string; key: Uint8Array; writeToken: Uint8Array } | undefined; 87 + + delete process.env.OMP_COLLAB_RELAY; 88 + + delete process.env.OMP_COLLAB_WEB; 89 + + delete process.env.OMP_COLLAB_ROOM; 90 + + delete process.env.OMP_COLLAB_KEY; 91 + + if (!relayUrl) return undefined; 92 + + let preset: CollabAutostartConfig["preset"]; 51 93 + if (roomId && keyB64) { 52 94 + const secret = new Uint8Array(Buffer.from(keyB64, "base64url")); 53 95 + if (secret.byteLength === ROOM_KEY_BYTES + WRITE_TOKEN_BYTES) { ··· 56 98 + key: secret.subarray(0, ROOM_KEY_BYTES), 57 99 + writeToken: secret.subarray(ROOM_KEY_BYTES), 58 100 + }; 101 + + } else { 102 + + logger.warn("collab autostart: OMP_COLLAB_KEY has unexpected length", { 103 + + bytes: secret.byteLength, 104 + + want: ROOM_KEY_BYTES + WRITE_TOKEN_BYTES, 105 + + }); 59 106 + } 60 107 + } 61 - + const host = new CollabHost(ctx); 62 - + await host.start(relay, webUrl, preset); 63 - + return host; 108 + + return { relayUrl, webUrl, preset }; 109 + +} 110 + + 111 + +/** 112 + + * Host on the configured room and keep hosting for the life of the process: 113 + + * whenever the host goes away without the user asking, start a new one on the 114 + + * same room. No-ops when no relay is configured. 115 + + */ 116 + +export async function superviseAutostartCollab(ctx: InteractiveModeContext): Promise<void> { 117 + + const cfg = readCollabAutostartEnv(); 118 + + if (!cfg) return; 119 + + let attempt = 0; 120 + + for (;;) { 121 + + if (userStoppedHosting) return; 122 + + // Joining someone else's session takes precedence over hosting ours. 123 + + if (!ctx.collabHost && !ctx.collabGuest) { 124 + + try { 125 + + const host = new CollabHost(ctx); 126 + + await host.start(cfg.relayUrl, cfg.webUrl, cfg.preset); 127 + + ctx.collabHost = host; 128 + + attempt = 0; 129 + + logger.info("collab autostart: hosting", { room: cfg.preset?.roomId, relay: cfg.relayUrl }); 130 + + } catch (err) { 131 + + attempt++; 132 + + const delay = Math.min(AUTOSTART_BACKOFF_MAX_MS, AUTOSTART_BACKOFF_BASE_MS * 2 ** (attempt - 1)); 133 + + logger.warn("collab autostart: host start failed, retrying", { 134 + + error: String(err), 135 + + attempt, 136 + + delayMs: delay, 137 + + room: cfg.preset?.roomId, 138 + + }); 139 + + await Bun.sleep(delay); 140 + + continue; 141 + + } 142 + + } 143 + + await Bun.sleep(AUTOSTART_POLL_MS); 144 + + } 64 145 +} 65 146 diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts 66 - index c2008d5..447f69b 100644 147 + index 9a444a4..d38f4a2 100644 67 148 --- a/packages/coding-agent/src/modes/interactive-mode.ts 68 149 +++ b/packages/coding-agent/src/modes/interactive-mode.ts 69 - @@ -54,6 +54,7 @@ import chalk from "chalk"; 150 + @@ -55,6 +55,7 @@ import chalk from "chalk"; 70 151 import { reset as resetCapabilities } from "../capability"; 71 152 import type { CollabGuestLink } from "../collab/guest"; 72 153 import type { CollabHost } from "../collab/host"; 73 - +import { maybeAutostartCollab } from "../collab/host"; 154 + +import { superviseAutostartCollab } from "../collab/host"; 74 155 import { KeybindingsManager } from "../config/keybindings"; 75 - import type { ResolvedModelRoleValue } from "../config/model-resolver"; 156 + import { formatModelString, type ResolvedModelRoleValue } from "../config/model-resolver"; 76 157 import { applyProviderGlobalsFromSettings } from "../config/provider-globals"; 77 - @@ -1091,6 +1092,14 @@ export class InteractiveMode implements InteractiveModeContext { 158 + @@ -1137,6 +1138,15 @@ export class InteractiveMode implements InteractiveModeContext { 78 159 this.statusLine.watchBranch(() => { 79 160 this.ui.requestRender(); 80 161 }); 81 162 + 82 - + // covibe: headless collab autostart from env (OMP_COLLAB_*). Fire-and-forget 83 - + // so the TUI is not blocked on the relay connect; attaches when it opens. 84 - + void maybeAutostartCollab(this) 85 - + .then(host => { 86 - + if (host) this.collabHost = host; 87 - + }) 88 - + .catch((err: unknown) => logger.warn(`collab autostart failed: ${String(err)}`)); 163 + + // covibe: headless collab hosting from env (OMP_COLLAB_*). Supervised and 164 + + // fire-and-forget: the TUI never waits on the relay, and an involuntary 165 + + // stop (relay drop, freed room, or the session-switch self-stop that fires 166 + + // when the first prompt creates the real session id) re-hosts the same 167 + + // room, so links and QR codes already shared stay valid. 168 + + void superviseAutostartCollab(this).catch((err: unknown) => 169 + + logger.warn(`collab autostart supervisor failed: ${String(err)}`), 170 + + ); 89 171 } 90 172 91 173 /** Reload the title-generation system prompt override for the provided working