@gorlitzer-labs/apiary 1.13.9 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -3
- package/dist/agent/index.d.ts +73 -0
- package/dist/agent/index.js +1 -1
- package/dist/{chunk-BCQQJR5E.js → chunk-3UQBSELW.js} +7 -178
- package/dist/chunk-3UQBSELW.js.map +1 -0
- package/dist/{chunk-7JXPJO3R.js → chunk-74XU7HHN.js} +6 -6
- package/dist/{chunk-4A7MREYX.js → chunk-N2LPKMPO.js} +185 -2
- package/dist/chunk-N2LPKMPO.js.map +1 -0
- package/dist/chunk-OEAHTZCA.js +994 -0
- package/dist/chunk-OEAHTZCA.js.map +1 -0
- package/dist/{chunk-MEWZT5KA.js → chunk-W4KWQKPP.js} +2 -2
- package/dist/{chunk-WJD5EKO6.js → chunk-Z4J6TRJY.js} +154 -41
- package/dist/chunk-Z4J6TRJY.js.map +1 -0
- package/dist/cli/index.js +39 -386
- package/dist/cli/index.js.map +1 -1
- package/dist/{invites-GRROUSCE.js → invites-SJ4ZVKVX.js} +4 -4
- package/dist/{join-H62VB4HU.js → join-5UFFMSLS.js} +4 -4
- package/dist/{room-ID4DQDPT.js → room-S6ULVAEH.js} +10 -10
- package/dist/run-5PSUQJF4.js +270 -0
- package/dist/run-5PSUQJF4.js.map +1 -0
- package/package.json +3 -2
- package/dist/chunk-4A7MREYX.js.map +0 -1
- package/dist/chunk-BCQQJR5E.js.map +0 -1
- package/dist/chunk-WJD5EKO6.js.map +0 -1
- package/dist/chunk-XLUTCAY6.js +0 -288
- package/dist/chunk-XLUTCAY6.js.map +0 -1
- package/dist/run-C22WQZVK.js +0 -91
- package/dist/run-C22WQZVK.js.map +0 -1
- /package/dist/{chunk-7JXPJO3R.js.map → chunk-74XU7HHN.js.map} +0 -0
- /package/dist/{chunk-MEWZT5KA.js.map → chunk-W4KWQKPP.js.map} +0 -0
- /package/dist/{invites-GRROUSCE.js.map → invites-SJ4ZVKVX.js.map} +0 -0
- /package/dist/{join-H62VB4HU.js.map → join-5UFFMSLS.js.map} +0 -0
- /package/dist/{room-ID4DQDPT.js.map → room-S6ULVAEH.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli/composer.ts","../src/cli/claude/tmux-bridge.ts","../src/agent/wait-for-agent.ts","../src/cli/rejoin.ts","../src/cli/runtime-handlers.ts"],"sourcesContent":["/**\n * Did the text we injected actually get submitted?\n *\n * Both bridges type into a TUI and then assume it worked. On 2026-09-10 that\n * assumption cost seven minutes of apparent silence: a room message reached a\n * Codex agent, the `@`-mention popup swallowed the Enter, and the message sat in\n * the composer while apiary reported success and moved on. Nobody learned it had\n * failed until a human read the pane.\n *\n * The check is deliberately narrow. After a submit, the text appears in the\n * *transcript* — that is success, not failure — so the presence of our text on\n * screen proves nothing. What distinguishes the two is where it sits: an\n * unsubmitted message is still in the composer, which both TUIs render as the\n * last prompt-marked block at the bottom of the screen.\n */\n\n/** Prompt markers: Codex renders the composer with ›, Claude with ❯ (> on older builds). */\nconst PROMPT_MARKER = /^\\s*[›❯>]\\s?/;\n\n/**\n * The composer's contents: everything from the last prompt marker down.\n *\n * Long messages wrap, so the marker line alone is not the whole composer — the\n * rest of the block below it belongs to the same input.\n */\nexport function composerBlock(lines: string[]): string {\n const tail = lines.slice(-25);\n let start = -1;\n for (let i = tail.length - 1; i >= 0; i--) {\n if (PROMPT_MARKER.test(tail[i])) {\n start = i;\n break;\n }\n }\n if (start === -1) return \"\";\n return tail.slice(start).join(\" \");\n}\n\n/**\n * A stable fragment of the message to look for.\n *\n * The head of the text rather than the whole of it: the composer wraps, the pane\n * truncates, and a trailing `@mention` may already have been rewritten by the\n * very popup we are checking for — so the tail is exactly the part that cannot\n * be relied on. Whitespace is collapsed because wrapping introduces its own.\n */\nexport function submissionProbe(text: string): string {\n return text.replace(/\\s+/g, \" \").trim().slice(0, 40);\n}\n\n/**\n * True when the message is still sitting in the composer, i.e. it was NOT\n * submitted. A message short enough to produce no usable probe is reported as\n * submitted rather than guessed at — a false \"undelivered\" would have the\n * caller resend, and a duplicate room message is worse than an unchecked one.\n */\nexport function composerStillHolds(lines: string[], text: string): boolean {\n const probe = submissionProbe(text);\n if (probe.length < 8) return false;\n const composer = composerBlock(lines).replace(/\\s+/g, \" \");\n return composer.includes(probe);\n}\n","/**\n * TmuxBridge — state-aware event injection into Claude Code.\n *\n * Reads the Claude Code TUI screen via `tmux capture-pane`, detects the\n * current UI state, and applies the right injection strategy:\n *\n * idle → inject directly\n * typing → Ctrl+U (cut), inject, Ctrl+Y (restore)\n * dialog → queue and poll\n * permission → queue and poll\n * streaming → queue and poll\n * unknown → queue and poll (safe default)\n *\n * Events that can't be injected immediately are queued and drained\n * when the state becomes safe.\n */\n\nimport {\n tmuxPaneIsShell,\n tmuxCapturePane,\n tmuxCapturePaneStyled,\n tmuxInjectText,\n tmuxSendEnter,\n tmuxSendKey,\n} from \"../tmux.js\";\nimport { composerStillHolds } from \"../composer.js\";\nimport { contentPartsToString } from \"../../agent/prompts.js\";\nimport type { ContentPart } from \"../../agent/types.js\";\n\nexport type TuiState =\n /** The CLI is not running — the pane has fallen back to a shell. */\n | \"absent\"\n | \"idle\"\n | \"typing\"\n | \"dialog\"\n | \"permission\"\n | \"streaming\"\n | \"unknown\";\n\n// Spinner characters used by Claude Code during streaming\nconst SPINNER_CHARS = \"⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏\";\n\n/**\n * Claude Code is mid-turn but shows none of the braille spinner glyphs.\n *\n * Current Claude Code (checked on v2.1.299) draws its live status with other\n * glyphs — \"✢ Perusing… (2s · ↓ 88 tokens · thinking)\" — so the spinner scan\n * misses it. The shape is stable even though the verb rotates: a word, an\n * ellipsis, and a parenthesised elapsed time. The finished form (\"✻ Brewed for\n * 1s · done 11:33 AM\") has no ellipsis and does not match.\n *\n * \"← 1 agent\" in the footer used to be read as \"a subagent is running\". On\n * v2.1.299 it is the opposite: every idle session shows it (four live panes\n * checked), and it disappears while a background agent runs. Treating it as\n * busy made every idle Claude Code pane look busy forever, so events only got\n * through via the 30s force-inject. A background agent also no longer blocks\n * submission — a room nudge typed while one ran was answered normally.\n */\nconst BUSY_PATTERNS: RegExp[] = [\n /[A-Za-zÀ-ÿ]+…\\s*\\(\\d+(?:m\\s*\\d+)?s\\b/,\n];\n\n/**\n * Does Claude Code's composer hold nothing the human typed?\n *\n * A plain capture cannot tell a draft from the dim placeholder or suggestion\n * Claude Code shows in an empty composer (\"Try \\\"fix typecheck errors\\\"\",\n * \"check the room for messages\") — both read as text after ❯. The styled\n * capture can: the placeholder is drawn dim (ESC[2m), a draft is not.\n */\nexport function composerHoldsOnlyGhost(styledLines: string[]): boolean {\n const SGR = /\\x1b\\[[0-9;]*m/g;\n for (let i = styledLines.length - 1; i >= 0; i--) {\n const plain = styledLines[i].replace(SGR, \"\").trimStart();\n if (!/^[❯›](\\s|$)/.test(plain)) continue;\n const afterPrompt = styledLines[i].slice(styledLines[i].search(/[❯›]/) + 1);\n const typed = afterPrompt\n .replace(/\\x1b\\[2m[\\s\\S]*?(?:\\x1b\\[(?:0|22)?m|$)/g, \"\")\n .replace(SGR, \"\")\n .trim();\n return typed.length === 0;\n }\n return false;\n}\n\n// Patterns that indicate a selection/question dialog\nconst DIALOG_PATTERNS = [\n \"Enter to select\",\n \"to navigate\",\n \"Esc to cancel\",\n \"Ready to code?\",\n \"Review your answers\",\n \"ctrl+g to edit in\",\n];\n\n// Patterns that indicate a permission/confirmation prompt\nconst PERMISSION_PATTERNS = [\n \"(Y)\",\n \"Allow \",\n \"Deny \",\n \"approve\",\n \"Yes / No\",\n // Current Claude Code wording, captured from a live prompt. Without these a\n // permission dialog reads as idle and the bridge pastes a room event into\n // it, answering on the agent's behalf.\n \"Do you want to proceed?\",\n \"don't ask again\",\n // The folder-trust modal, which no flag skips.\n \"Yes, I trust this folder\",\n];\n\n/**\n * Patterns specific enough to put \"⏸ needs you\" in front of a human.\n *\n * DIALOG_PATTERNS and PERMISSION_PATTERNS above deliberately over-match, and\n * for their original consumer that is correct: they gate DELIVERY, where a\n * false positive only queues a room event for the drain loop to retry. Cheap.\n *\n * That reasoning was then reused for a second consumer with the opposite cost\n * — the agent state reported to the room — where a false positive stops the\n * operator and makes them wait on an agent that needs nothing. Observed: the\n * bare substring `\"approve\"` matched an agent's own tool output, \"hard cap, set\n * by the approved top\", and the room strip read \"⏸ needs you\" at an agent 23\n * minutes into a harness run. `\"to navigate\"` is the same trap and worse — it\n * matches any sentence about navigating, in a project whose entire subject is\n * navigating by bearings.\n *\n * So: two lists. Over-match to protect the agent, under-match before\n * interrupting the human. These are UI chrome a prompt actually renders —\n * wordings and shapes, not vocabulary that can occur in prose.\n */\nconst BLOCKING_PATTERNS: Array<string | RegExp> = [\n \"Do you want to proceed?\",\n \"don't ask again\",\n \"Yes, I trust this folder\",\n \"Ready to code?\",\n \"Enter to select\",\n /❯\\s*1\\.\\s*Yes/,\n /\\(y\\s*\\/\\s*n\\)/i,\n // The arrow glyphs claude renders beside it. Bare \"to navigate\" is prose.\n /[↑↓⬆⬇]\\s*(?:[↑↓⬆⬇]\\s*)?to navigate/,\n];\n\n/** Match a mixed string/RegExp pattern list, case-insensitively for strings. */\nfunction matchesAny(text: string, patterns: Array<string | RegExp>): boolean {\n const haystack = text.toLowerCase();\n for (const pattern of patterns) {\n if (typeof pattern === \"string\") {\n if (haystack.includes(pattern.toLowerCase())) return true;\n } else if (pattern.test(text)) {\n return true;\n }\n }\n return false;\n}\n\nexport interface DeliverOptions {\n /**\n * Drop this text if an identical copy is already queued. For callers that\n * retry a delivery they cannot observe landing (the room invite loop).\n */\n dedupe?: boolean;\n}\n\nexport interface TmuxBridgeOptions {\n /** How often to poll when events are queued (ms). Default: 200 */\n pollIntervalMs?: number;\n /** How long to wait between Ctrl+U/inject/Ctrl+Y steps (ms). Default: 50 */\n keystrokeDelayMs?: number;\n /**\n * Called when a message could not be got into the agent, after the retry.\n *\n * Delivery used to be assumed: apiary typed, reported success and moved on,\n * so a swallowed submit looked exactly like an agent choosing not to answer.\n * This is the hook that makes the difference visible to someone.\n */\n onUndelivered?: (text: string) => void;\n /**\n * A human drives this pane (`apiary mcp` inside their own session). Default\n * false, for panes apiary launched and nobody types in.\n *\n * When true, text goes in only when the composer is empty — never cut and\n * restored around a draft (Ctrl+U clears one line, so a wrapped draft was\n * split in two and half of it sent with the event, seen live) — and never\n * forced after ~30s: a human session can sit on a real permission prompt for\n * minutes, and typing into it would answer the prompt on their behalf.\n */\n attended?: boolean;\n}\n\n/**\n * Last resort when no one has wired a handler: say it on stderr, which lands in\n * the launcher pane. Better than nothing, and a great deal better than success.\n */\nfunction defaultUndeliveredWarning(text: string): void {\n const preview = text.replace(/\\s+/g, \" \").slice(0, 80);\n process.stderr.write(`apiary: message NOT delivered to the agent — still in its composer: ${preview}\\n`);\n}\n\nexport class TmuxBridge {\n private session: string;\n private queue: string[] = [];\n private pollTimer: ReturnType<typeof setInterval> | null = null;\n private pollIntervalMs: number;\n private keystrokeDelayMs: number;\n private onUndelivered: (text: string) => void;\n private attended: boolean;\n private stopped = false;\n private blockedCount = 0;\n private static readonly BLOCKED_THRESHOLD = 150; // ~30s at 200ms poll — force inject if stuck\n\n constructor(session: string, opts?: TmuxBridgeOptions) {\n this.session = session;\n this.pollIntervalMs = opts?.pollIntervalMs ?? 200;\n this.keystrokeDelayMs = opts?.keystrokeDelayMs ?? 50;\n this.onUndelivered = opts?.onUndelivered ?? defaultUndeliveredWarning;\n this.attended = opts?.attended ?? false;\n }\n\n /**\n * Delivery callback — drop-in replacement for the raw tmuxDeliver lambda.\n * Pass `bridge.deliver.bind(bridge)` to EventProcessor.run().\n */\n async deliver(parts: ContentPart[], opts?: DeliverOptions): Promise<void> {\n const text = contentPartsToString(parts);\n if (!text.trim()) return;\n\n this.inject(text, opts?.dedupe === true);\n }\n\n /**\n * Detect the current TUI state by reading the screen.\n * Exported for testing — the heuristic logic is in detectStateFromLines().\n */\n detectState(): TuiState {\n // See the Codex bridge: a shell prompt is indistinguishable from an idle\n // composer once you are only looking at rendered text.\n if (tmuxPaneIsShell(this.session)) return \"absent\";\n const lines = this.captureScreen();\n return detectStateFromLines(lines);\n }\n\n /**\n * State to REPORT to the room, as opposed to the state used to decide\n * whether it is safe to type. Same screen, stricter about claiming a human\n * is needed — see detectReportedStateFromLines.\n */\n detectReportedState(): TuiState {\n if (tmuxPaneIsShell(this.session)) return \"absent\";\n return detectReportedStateFromLines(this.captureScreen());\n }\n\n /**\n * Extract claude's live status label from the pane (the \"Sautéed for 12s\",\n * \"Compacting conversation… 23%\" lines under the prompt). Returns null when\n * the agent is idle / no status visible.\n *\n * The room TUI calls this through the agent runtime's poll loop to surface\n * \"is the agent actually working or stuck?\" without making the user attach\n * to each tmux pane.\n */\n getActivityLabel(): string | null {\n return extractActivityLabel(this.captureScreen());\n }\n\n /**\n * Try to inject text, choosing strategy based on TUI state.\n * If the state is unsafe, queues the text and starts polling.\n *\n * Text is flattened to a single line before injection to avoid triggering\n * Claude Code's paste detection. When multi-line text arrives via\n * `send-keys -l`, Claude Code detects it as a paste and collapses it into\n * \"[Pasted text #1 +N lines]\" which may not reliably submit with Enter.\n */\n private inject(text: string, dedupe: boolean): void {\n const flat = text.replace(/\\n/g, \" \");\n const state = this.deliveryState();\n\n switch (state) {\n case \"idle\":\n this.injectIdle(flat);\n break;\n case \"typing\":\n this.injectWhileTyping(flat);\n break;\n default:\n // dialog, permission, streaming, unknown — queue it\n this.enqueue(flat, dedupe);\n break;\n }\n }\n\n /**\n * The state delivery acts on. Unattended it is the screen state. Attended,\n * \"typing\" splits in two: only placeholder text is idle; a real draft is the\n * human's, so the event waits (\"unknown\" queues it).\n */\n private deliveryState(): TuiState {\n const state = this.detectState();\n if (!this.attended || state !== \"typing\") return state;\n return composerHoldsOnlyGhost(tmuxCapturePaneStyled(this.session)) ? \"idle\" : \"unknown\";\n }\n\n /** Capture the screen via tmux capture-pane. */\n private captureScreen(): string[] {\n return tmuxCapturePane(this.session);\n }\n\n /**\n * Inject into an idle prompt: type text + Enter, then check it went.\n *\n * This used to fire a second Enter unconditionally as a \"safety net\" against\n * Claude Code's paste detection swallowing the first. That is a guess in both\n * directions: it cannot tell whether the first Enter worked, and it cannot\n * tell whether the second one did either. Reading the composer back answers\n * the question instead, and only sends the second Enter when it is needed.\n */\n private injectIdle(text: string): void {\n tmuxInjectText(this.session, text);\n tmuxSendEnter(this.session);\n this.confirmSubmitted(text);\n }\n\n /**\n * Check the message left the composer, and press Enter once more if it did not.\n *\n * A submitted message moves into the transcript; one still in the composer was\n * never sent. If it is still there after the retry, the room is told rather\n * than left to read the agent's silence as a choice.\n */\n private confirmSubmitted(text: string): void {\n this.sleep(80);\n if (!composerStillHolds(this.captureScreen(), text)) return;\n\n tmuxSendEnter(this.session);\n this.sleep(120);\n if (!composerStillHolds(this.captureScreen(), text)) return;\n\n this.onUndelivered(text);\n }\n\n /**\n * Inject while the user is typing:\n * 1. Ctrl+U — cut line to kill ring\n * 2. Inject our text + Enter\n * 3. Ctrl+Y — paste the user's text back\n */\n private injectWhileTyping(text: string): void {\n // Cut user's current input\n tmuxSendKey(this.session, \"C-u\");\n this.sleep(this.keystrokeDelayMs);\n\n // Inject our event, then confirm rather than hope\n tmuxInjectText(this.session, text);\n tmuxSendEnter(this.session);\n this.confirmSubmitted(text);\n this.sleep(this.keystrokeDelayMs);\n\n // Restore user's text\n tmuxSendKey(this.session, \"C-y\");\n }\n\n /** Add to queue and start polling if not already. */\n private enqueue(text: string, dedupe: boolean): void {\n // Off by default — room events must never be collapsed. Only callers that\n // retry a delivery they cannot observe landing (the invite loop) opt in.\n if (dedupe && this.queue.includes(text)) return;\n this.queue.push(text);\n this.startPolling();\n }\n\n /** Start the polling timer to drain queued events. */\n private startPolling(): void {\n if (this.pollTimer || this.stopped) return;\n this.pollTimer = setInterval(() => this.drainQueue(), this.pollIntervalMs);\n }\n\n /** Stop the polling timer. */\n private stopPolling(): void {\n if (this.pollTimer) {\n clearInterval(this.pollTimer);\n this.pollTimer = null;\n }\n }\n\n /**\n * Try to drain one queued event if the state is safe.\n *\n * Drains one event at a time rather than batching all into one multi-line\n * string — multi-line text triggers Claude Code's paste detection which\n * collapses it into \"[Pasted text #1 +N lines]\".\n *\n * After injecting one event, the poll continues. The next cycle re-checks\n * state: if Claude is busy (streaming), remaining events wait. If idle,\n * the next event is injected. Events are already flattened in inject().\n */\n private drainQueue(): void {\n if (this.queue.length === 0) {\n this.stopPolling();\n this.blockedCount = 0;\n return;\n }\n\n const state = this.deliveryState();\n if (state === \"idle\" || state === \"typing\") {\n this.blockedCount = 0;\n const text = this.queue.shift()!;\n\n if (state === \"idle\") {\n this.injectIdle(text);\n } else {\n this.injectWhileTyping(text);\n }\n\n if (this.queue.length === 0) {\n this.stopPolling();\n }\n // else: keep polling to drain remaining events\n } else {\n // Any non-injectable state (streaming, dialog, permission, unknown)\n this.blockedCount++;\n if (!this.attended && this.blockedCount >= TmuxBridge.BLOCKED_THRESHOLD) {\n // Stuck too long — force inject to unblock\n this.blockedCount = 0;\n const text = this.queue.shift()!;\n this.injectIdle(text);\n if (this.queue.length === 0) this.stopPolling();\n }\n }\n }\n\n /** Cleanup. */\n stop(): void {\n this.stopped = true;\n this.stopPolling();\n this.queue.length = 0;\n }\n\n /** Synchronous sleep — only used for tiny keystroke delays. */\n private sleep(ms: number): void {\n if (ms <= 0) return;\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);\n }\n}\n\n// ── Activity label extraction ────────────────────────────────────────────────\n\n/**\n * Pull claude's status string out of a captured pane.\n *\n * Claude's TUI shows these markers above the input prompt while it's working:\n * ✻ Fluttering… (38s · ↓ 2.1k tokens) <- the LIVE form, present throughout\n * ✻ Sautéed for 12s <- the FINISHED form, a moment at the end\n * ✻ Compacting conversation… 23%\n * ✻ Thinking…\n *\n * Three patterns, tried narrowest-first so the broad one cannot swallow the\n * others:\n * - PROG: a known long-op verb (Compacting/Thinking/Loading/Connecting)\n * followed by an optional description and optional N%. Stops at the first\n * `(` so it doesn't capture a trailing metadata blob.\n * - COOK: cooking-verb + \"for Ns\", the form claude leaves behind once a turn\n * ends. Matches unicode letters (Sautéed) via the Latin-1 supplement range.\n * - LIVE: any word, an ellipsis, and a parenthesised progress group. Matched\n * on SHAPE rather than vocabulary, because the verb is whimsical and\n * rotates — Improvising, Fluttering, Reticulating, Cogitating. A word list\n * goes stale the next time someone adds one, which is exactly how this\n * broke: with only COOK and PROG, a *working* agent matched nothing and\n * reported no label, so the room strip showed a bare glyph for as long as\n * the work took. The one moment the operator most needs to know what is\n * happening was the one moment it said nothing.\n *\n * LIVE deliberately keeps its parenthesised group — \"(38s · ↓ 2.1k tokens)\" is\n * the elapsed time and stream progress, which is what distinguishes \"moving\"\n * from \"wedged\". PROG still stops at `(` because its verbs carry their own\n * detail, and it is tried first so that stays true.\n *\n * Scan the last ~25 lines, return the newest match. Null when none → idle.\n */\nconst ACTIVITY_LIVE = /([A-Za-zÀ-ÿ]+…\\s*\\([^)\\n]*\\))/;\nconst ACTIVITY_COOK = /([A-Za-zÀ-ÿ]+ed\\s+for\\s+\\d+s)/;\nconst ACTIVITY_PROG = /((?:Compacting|Thinking|Loading|Connecting)[^()]*?(?:\\s+\\d+%)?)(?=\\s*[(\\n]|\\s*$)/;\nexport function extractActivityLabel(lines: string[]): string | null {\n if (lines.length === 0) return null;\n let best: string | null = null;\n for (const line of lines.slice(-25)) {\n // PROG first, then COOK, then LIVE. LIVE is the broadest — any word plus a\n // parenthesised group — so it must go last or it swallows lines the narrower\n // patterns own: \"Compacting conversation… (2m 9s · ↑ 3.3k tokens)\" is\n // reported as \"Compacting conversation…\" on purpose, and LIVE-first turned\n // that into \"conversation… (2m 9s · ↑ 3.3k tokens)\".\n const m = line.match(ACTIVITY_PROG) ?? line.match(ACTIVITY_COOK) ?? line.match(ACTIVITY_LIVE);\n if (m) best = m[1].trim();\n }\n return best;\n}\n\n// ── State detection heuristics ──────────────────────────────────────────────\n\n/**\n * Detect TUI state from capture-pane output lines.\n * Exported separately so it can be unit-tested without tmux.\n *\n * Claude Code's TUI layout (v2.1+):\n * ────────────────────────\n * ❯ <user input here>\n * ────────────────────────\n * PR #2 /ide ...\n *\n * The ❯ prompt sits between separator lines (─). No ❯❯ footer in v2.1+.\n */\nexport function detectStateFromLines(\n lines: string[],\n opts?: { ignorePrompts?: boolean },\n): TuiState {\n if (lines.length === 0) return \"unknown\";\n\n // Work with the last ~30 lines (the visible bottom of the screen)\n const tail = lines.slice(-30);\n const tailText = tail.join(\"\\n\");\n\n // `ignorePrompts` asks \"what is this screen doing APART from any prompt?\" —\n // used by detectReportedStateFromLines to answer the human's question once\n // the loose lists have been found to be matching prose rather than chrome.\n if (opts?.ignorePrompts !== true) {\n // 1. Dialog: selection/question/plan approval\n for (const pattern of DIALOG_PATTERNS) {\n if (tailText.includes(pattern)) return \"dialog\";\n }\n\n // 2. Permission prompt\n for (const pattern of PERMISSION_PATTERNS) {\n if (tailText.includes(pattern)) return \"permission\";\n }\n }\n\n // 3. Streaming: spinner characters in the last few lines\n const lastFew = tail.slice(-5).join(\"\");\n for (const ch of SPINNER_CHARS) {\n if (lastFew.includes(ch)) return \"streaming\";\n }\n\n // 3b. Busy with no spinner — a subagent is running. Checked after the\n // dialog and permission patterns on purpose: a prompt that needs a human\n // outranks \"also busy\", because only one of those the user can clear.\n // Only the bottom of the screen: the live status sits just above the\n // composer, and the same shape further up is output the agent printed.\n const statusArea = tail.slice(-10).join(\"\\n\");\n for (const pattern of BUSY_PATTERNS) {\n if (pattern.test(statusArea)) return \"streaming\";\n }\n\n // 4. Look for the ❯/› prompt line near the bottom.\n // Supports both old layout (❯❯ footer) and new layout (separator lines).\n const promptChar = /^[❯›](\\s|$)/;\n const footerChar = /^[❯›]{2}\\s/;\n const separatorLine = /^[─━─\\-]{10,}/;\n\n // Strategy A: old layout — find ❯❯ footer then ❯ prompt above it\n for (let i = tail.length - 1; i >= 0; i--) {\n const line = tail[i].trimStart();\n if (footerChar.test(line)) {\n // Found old-style footer, look for prompt above\n for (let j = i - 1; j >= 0; j--) {\n const above = tail[j].trimStart();\n if (promptChar.test(above)) {\n const content = above.replace(/^[❯›]\\s*/, \"\").trim();\n return content.length === 0 ? \"idle\" : \"typing\";\n }\n }\n break;\n }\n }\n\n // Strategy B: new layout — find ❯ prompt between/near separator lines\n for (let i = tail.length - 1; i >= 0; i--) {\n const line = tail[i].trimStart();\n if (promptChar.test(line)) {\n // Verify it's Claude's prompt by checking for separator line nearby\n const above = i > 0 ? tail[i - 1].trimStart() : \"\";\n const below = i < tail.length - 1 ? tail[i + 1].trimStart() : \"\";\n if (separatorLine.test(above) || separatorLine.test(below)) {\n const content = line.replace(/^[❯›]\\s*/, \"\").trim();\n return content.length === 0 ? \"idle\" : \"typing\";\n }\n }\n }\n\n return \"unknown\";\n}\n\n/**\n * The same screen read, but tuned for a human rather than for the delivery\n * queue: only claim a prompt is waiting when one is actually on screen.\n *\n * `detectStateFromLines` leans toward \"there is a prompt\" because a false\n * positive there merely queues an event. This one leans the other way, because\n * its false positive is \"⏸ needs you\" in the operator's face at an agent that\n * needs nothing — which costs a person's attention, and teaches them to stop\n * trusting the strip. When the loose lists fire but no specific prompt wording\n * is present, the screen is re-read with the prompt scan disabled so the\n * spinner and composer get to answer instead.\n */\nexport function detectReportedStateFromLines(lines: string[]): TuiState {\n const loose = detectStateFromLines(lines);\n if (loose !== \"dialog\" && loose !== \"permission\") return loose;\n if (matchesAny(lines.slice(-30).join(\"\\n\"), BLOCKING_PATTERNS)) return loose;\n return detectStateFromLines(lines, { ignorePrompts: true });\n}\n","/**\n * Wait for another agent to reach a state.\n *\n * Without this, an agent that needs to act after another one finishes has two\n * options, both bad: re-read the room on a loop (a full LLM turn per check,\n * which is the expensive thing), or guess and collide.\n *\n * It became buildable once runtimes started reporting the state they read off\n * their own CLI's screen, because \"working\" and \"blocked\" look identical from\n * the room's side and only one of them ever resolves on its own.\n *\n * Deliberately bounded rather than open-ended: the call has to return inside\n * the MCP client's tool timeout, so it reports the state it last saw and lets\n * the caller decide whether to wait again. A tool that hangs past the client's\n * timeout fails the turn, which is worse than one that says \"still working\".\n */\n\n/** The states a runtime reports. `unknown` means it reports nothing. */\nexport type AgentWaitState = \"idle\" | \"working\" | \"blocked\" | \"unknown\";\n\n/** What the caller is waiting for. */\nexport type WaitTarget = \"idle\" | \"blocked\" | \"working\" | \"change\";\n\nexport interface WaitForAgentOptions {\n /** Read the target's current state. `unknown` if it isn't reporting. */\n readState: () => Promise<AgentWaitState>;\n /** What to wait for. Default \"idle\" — \"they finished, my turn\". */\n until?: WaitTarget;\n /** Give up after this long. Default 60s, capped by `maxWaitMs`. */\n timeoutMs?: number;\n /** Hard ceiling, to stay inside the MCP client's tool timeout. */\n maxWaitMs?: number;\n /** Gap between reads. Default 2s — this is cheap, unlike an LLM turn. */\n pollIntervalMs?: number;\n sleep?: (ms: number) => Promise<void>;\n now?: () => number;\n}\n\nexport type WaitOutcome =\n | { result: \"satisfied\"; state: AgentWaitState; waitedMs: number }\n | { result: \"timeout\"; state: AgentWaitState; waitedMs: number }\n /** The target reports no state at all — waiting would never end. */\n | { result: \"not-reporting\"; state: \"unknown\"; waitedMs: number };\n\nexport const DEFAULT_WAIT_MS = 60_000;\nexport const MAX_WAIT_MS = 120_000;\nconst DEFAULT_POLL_MS = 2_000;\n\n/**\n * Clamp a requested wait to something that returns inside the MCP client's\n * tool timeout. Exported so the clamp is testable without actually waiting.\n */\nexport function resolveBudgetMs(timeoutMs: number | undefined, maxWaitMs: number = MAX_WAIT_MS): number {\n return Math.min(Math.max(timeoutMs ?? DEFAULT_WAIT_MS, 0), maxWaitMs);\n}\n\n/** Is `state` what the caller asked for? */\nexport function satisfies(state: AgentWaitState, until: WaitTarget, initial: AgentWaitState): boolean {\n if (state === \"unknown\") return false;\n if (until === \"change\") return state !== initial;\n return state === until;\n}\n\n/**\n * Poll until the target agent reaches `until`, or the deadline passes.\n *\n * Returns `not-reporting` immediately if the agent reports no state, rather\n * than burning the whole timeout on something that cannot change: a runtime\n * with no heartbeat (or one that has died) would otherwise look exactly like\n * an agent that is simply taking a long time.\n */\nexport async function waitForAgent(opts: WaitForAgentOptions): Promise<WaitOutcome> {\n const {\n readState,\n until = \"idle\",\n maxWaitMs = MAX_WAIT_MS,\n pollIntervalMs = DEFAULT_POLL_MS,\n sleep = (ms: number) => new Promise<void>((r) => { setTimeout(r, ms).unref?.(); }),\n now = () => Date.now(),\n } = opts;\n\n const budget = resolveBudgetMs(opts.timeoutMs, maxWaitMs);\n const started = now();\n\n const initial = await readState();\n if (initial === \"unknown\") {\n return { result: \"not-reporting\", state: \"unknown\", waitedMs: now() - started };\n }\n if (satisfies(initial, until, initial)) {\n return { result: \"satisfied\", state: initial, waitedMs: now() - started };\n }\n\n let latest: AgentWaitState = initial;\n // Counted rather than wall-clocked so an injected instant sleep cannot spin.\n const steps = Math.max(1, Math.ceil(budget / pollIntervalMs));\n for (let step = 0; step < steps; step++) {\n await sleep(pollIntervalMs);\n latest = await readState();\n if (satisfies(latest, until, initial)) {\n return { result: \"satisfied\", state: latest, waitedMs: now() - started };\n }\n }\n\n return { result: \"timeout\", state: latest, waitedMs: now() - started };\n}\n\n/** One line for the agent, saying what happened and what to do next. */\nexport function describeOutcome(\n participant: string,\n until: WaitTarget,\n outcome: WaitOutcome,\n): string {\n const secs = Math.round(outcome.waitedMs / 1000);\n switch (outcome.result) {\n case \"satisfied\":\n return `${participant} is now ${outcome.state} (waited ${secs}s).`;\n case \"timeout\":\n return `${participant} is still ${outcome.state} after ${secs}s — not ${until} yet. `\n + `Call again to keep waiting, or get on with something else.`;\n case \"not-reporting\":\n return `${participant} does not report its state, so there is nothing to wait for. `\n + `Only agents started by apiary with a Claude Code or Codex runtime report it. `\n + `Ask them directly instead.`;\n }\n}\n","/**\n * Re-join a room whose session was lost.\n *\n * An agent holds one session token per room. Two things kill it while the\n * agent keeps running:\n *\n * 1. The room server restarts. Its session tokens live in memory, so every\n * call answers 401 \"Invalid session token\" and the event stream never\n * comes back — retrying with the same token can never work.\n * 2. The URL the agent joined with dies (a trycloudflare tunnel expires)\n * while the server is still up on 127.0.0.1. Every call fails with\n * \"fetch failed\", and the agent's seat sits in the room as a ghost.\n *\n * The rejoiner keeps what joining took (share token, name, type) and, on a 401\n * from any call or the event stream, joins again under the SAME name — the\n * server replaces the old seat rather than adding one (seatsToEvict) — then\n * swaps the new token into the data source, the SSE connection and the\n * JoinResult. The failed call is retried once by the caller.\n *\n * For a dead URL it looks at the room's local session file\n * (~/.apiary/sessions/room_<name>.json) and, if that names a 127.0.0.1 or\n * localhost address that answers, moves there. No other host is ever tried.\n *\n * A room server that restarted also mints new share tokens. If the agent had\n * joined with one of the room's own tokens recorded in that local file, the\n * rejoiner may use the token of the SAME tier the file now holds — the agent\n * already had that access, so this never raises it. Any other share token that\n * is rejected (403 \"Invalid share token\") is final: the room really changed,\n * and the agent is told to get a new link instead of being left to loop.\n *\n * Loops are bounded three ways: retries back off (and fail fast during a\n * cooldown); a seat taken over again within 30 s of re-joining is left to the\n * other session, so two processes with one name cannot evict each other\n * forever; and an agent an admin kicked is never re-joined.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { RoomConnectionError, type SessionRecovery } from \"../agent/remote-room-data-source.js\";\nimport type { RemoteRoomDataSource } from \"../agent/remote-room-data-source.js\";\nimport type { EngagementMode } from \"../agent/engagement.js\";\nimport type { Participant } from \"../core/types.js\";\n\n/** What joining took, kept so the room can be joined again unattended. */\nexport interface RejoinInfo {\n /** The share URL join_room was given. */\n joinUrl: string;\n /** Share token from that URL (absent for a token-less join). */\n shareToken?: string;\n /** Name the agent joined under — re-joining with it replaces the old seat. */\n name: string;\n type: \"agent\";\n /** The room's name on the server (the agent's alias may differ). */\n serverRoomName: string;\n /**\n * Set when the share token was one of the room's own persisted tokens in the\n * local session file: the tier it was. Lets a re-join after a server restart\n * use the same-tier token the file now holds.\n */\n localTier?: \"admin\" | \"member\";\n}\n\n/** The parts of a JoinResult the rejoiner reads and rewrites. */\nexport interface RejoinableRoom {\n serverUrl: string;\n sessionToken: string;\n participantId: string;\n roomName: string;\n roomId: string;\n authority: string;\n participants: Participant[];\n dataSource: RemoteRoomDataSource;\n rejoin: RejoinInfo;\n /** Set when the stream reported this agent kicked: never re-join. */\n removedBy?: string;\n}\n\n/** The fields of ~/.apiary/sessions/room_<name>.json the rejoiner uses. */\nexport interface LocalRoomSession {\n serverUrl?: string;\n publicUrl?: string;\n adminToken?: string;\n memberToken?: string;\n}\n\n/** Read the local session file of a room hosted on this machine, if any. */\nexport function readLocalRoomSession(roomName: string): LocalRoomSession | null {\n // The name comes from the server: keep it to a plain file name.\n if (!/^[A-Za-z0-9._-]+$/.test(roomName) || roomName.includes(\"..\")) return null;\n const path = join(homedir(), \".apiary\", \"sessions\", `room_${roomName}.json`);\n if (!existsSync(path)) return null;\n try {\n const data = JSON.parse(readFileSync(path, \"utf-8\")) as Record<string, unknown>;\n const str = (v: unknown) => (typeof v === \"string\" && v ? v : undefined);\n return {\n serverUrl: str(data.serverUrl),\n publicUrl: str(data.publicUrl),\n adminToken: str(data.adminToken),\n memberToken: str(data.memberToken),\n };\n } catch {\n return null;\n }\n}\n\n/** Only these hosts are ever used as a fallback. */\nexport function isLoopbackUrl(url: string): boolean {\n try {\n const host = new URL(url).hostname;\n return host === \"127.0.0.1\" || host === \"localhost\";\n } catch {\n return false;\n }\n}\n\nfunction sameOrigin(a: string, b: string): boolean {\n try {\n return new URL(a).origin === new URL(b).origin;\n } catch {\n return a === b;\n }\n}\n\n/** Which of the room's own tokens (if any) the agent joined with. */\nexport function localTierOf(shareToken: string | undefined, session: LocalRoomSession | null): \"admin\" | \"member\" | undefined {\n if (!shareToken || !session) return undefined;\n if (session.adminToken === shareToken) return \"admin\";\n if (session.memberToken === shareToken) return \"member\";\n return undefined;\n}\n\nexport interface RejoinDeps {\n sseMux: { updateConnection(roomId: string, serverUrl: string, sessionToken: string): void };\n processor: {\n participantId: string;\n setRoomParticipantId(roomId: string, participantId: string): void;\n getModeForRoom(roomId: string): EngagementMode;\n };\n /** One line per reconnect, to stderr in `apiary mcp`. */\n log?: (line: string) => void;\n /** Told once when a room is lost for good. */\n onLost?: (room: RejoinableRoom, error: RoomConnectionError) => void;\n fetch?: typeof fetch;\n readRoomSession?: (roomName: string) => LocalRoomSession | null;\n sleep?: (ms: number) => Promise<void>;\n now?: () => number;\n /** Waits between attempts inside one re-join. Default [500, 1500]. */\n retryDelaysMs?: number[];\n /** Longest fail-fast cooldown after failed re-joins. Default 30 s. */\n maxCooldownMs?: number;\n /** A seat lost again this soon after re-joining is left alone. Default 30 s. */\n replacedWindowMs?: number;\n /** Least time between local-URL probes on network errors. Default 5 s. */\n probeIntervalMs?: number;\n}\n\ntype Attempt =\n | { ok: true; url: string; shareToken?: string; data: Record<string, unknown> }\n | { ok: false; kind: \"share-rejected\" | \"transient\" | \"unreachable\" | \"wrong-room\"; error: string };\n\nexport class RoomRejoiner implements SessionRecovery {\n private inflight: Promise<void> | null = null;\n private lost: RoomConnectionError | null = null;\n private failures = 0;\n private cooldownUntil = 0;\n private lastRejoinAt = Number.NEGATIVE_INFINITY;\n private lastProbeAt = Number.NEGATIVE_INFINITY;\n private readonly fetchFn: typeof fetch;\n private readonly now: () => number;\n private readonly sleep: (ms: number) => Promise<void>;\n private readonly readSession: (roomName: string) => LocalRoomSession | null;\n\n constructor(private room: RejoinableRoom, private deps: RejoinDeps) {\n this.fetchFn = deps.fetch ?? ((...args) => fetch(...args));\n this.now = deps.now ?? Date.now;\n this.sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));\n this.readSession = deps.readRoomSession ?? readLocalRoomSession;\n }\n\n /** Why the room is lost for good, if it is. */\n get lostError(): RoomConnectionError | null {\n return this.lost;\n }\n\n // ── SessionRecovery (RemoteRoomDataSource) ─────────────────────────────\n\n async sessionInvalid(usedToken?: string): Promise<void> {\n // Another caller already re-joined while this request was in flight.\n if (usedToken !== undefined && usedToken !== this.room.sessionToken) return;\n await this.rejoin(\"session token rejected (401)\");\n }\n\n async unreachable(): Promise<boolean> {\n if (this.lost || this.room.removedBy) return false;\n const now = this.now();\n if (now - this.lastProbeAt < (this.deps.probeIntervalMs ?? 5000)) return false;\n this.lastProbeAt = now;\n\n const local = this.readSession(this.room.rejoin.serverRoomName)?.serverUrl;\n if (!local || !isLoopbackUrl(local) || sameOrigin(local, this.room.serverUrl)) return false;\n const base = local.replace(/\\/$/, \"\");\n\n // Same server on another address? The current session may still be valid\n // there, which keeps the seat as it is.\n let res: Response;\n try {\n res = await this.fetchFn(`${base}/participants`, {\n headers: { Authorization: `Bearer ${this.room.sessionToken}` },\n signal: AbortSignal.timeout(5000),\n });\n } catch {\n return false;\n }\n await res.body?.cancel().catch(() => {});\n if (res.ok) {\n const from = this.room.serverUrl;\n this.swapSession(base, this.room.sessionToken);\n this.log(`room \"${this.room.roomName}\": ${from} unreachable, moved to ${base} (same session)`);\n return true;\n }\n if (res.status === 401) {\n try {\n await this.rejoin(`${this.room.serverUrl} unreachable`);\n return true;\n } catch {\n return false;\n }\n }\n return false;\n }\n\n // ── SseRecoveryHandler (per room) ──────────────────────────────────────\n\n async authRejected(status: number, usedToken?: string): Promise<boolean> {\n if (usedToken !== undefined && usedToken !== this.room.sessionToken) return true;\n try {\n await this.rejoin(`event stream rejected (${status})`);\n return true;\n } catch (err) {\n if (err instanceof RoomConnectionError && err.permanent) throw err;\n return false;\n }\n }\n\n // ── Re-join ────────────────────────────────────────────────────────────\n\n /**\n * Join the room again and swap the new session in everywhere. Concurrent\n * callers share one attempt. Throws RoomConnectionError: `permanent` when\n * retrying cannot help, otherwise after bounded retries.\n */\n rejoin(reason: string): Promise<void> {\n if (this.lost) return Promise.reject(this.lost);\n const roomLabel = `\"${this.room.roomName}\"`;\n if (this.room.removedBy) return Promise.reject(this.removed());\n if (this.inflight) return this.inflight;\n\n const now = this.now();\n if (now - this.lastRejoinAt < (this.deps.replacedWindowMs ?? 30_000)) {\n return Promise.reject(this.loseFor(\n `Your seat in room ${roomLabel} was taken over again right after apiary re-joined it — another ` +\n `session is probably using the name \"${this.room.rejoin.name}\". Not re-joining automatically, so the ` +\n `two do not keep evicting each other. Call join_room to take the seat back.`,\n ));\n }\n if (now < this.cooldownUntil) {\n const secs = Math.ceil((this.cooldownUntil - now) / 1000);\n return Promise.reject(new RoomConnectionError(\n `Lost the session in room ${roomLabel} and the last re-join failed; apiary retries in ${secs}s. ` +\n `If this persists, call join_room again with the room's share URL.`,\n false,\n ));\n }\n\n this.inflight = this.runRejoin(reason).finally(() => {\n this.inflight = null;\n });\n return this.inflight;\n }\n\n private async runRejoin(reason: string): Promise<void> {\n const roomLabel = `\"${this.room.roomName}\"`;\n this.log(`room ${roomLabel}: ${reason}; re-joining as \"${this.room.rejoin.name}\"`);\n const delays = [0, ...(this.deps.retryDelaysMs ?? [500, 1500])];\n let lastError = \"\";\n\n for (const delay of delays) {\n if (delay > 0) await this.sleep(delay);\n if (this.room.removedBy) break;\n const outcome = await this.attemptOnce();\n if (outcome.permanent) {\n throw this.loseFor(\n `Room ${roomLabel} no longer accepts the share link this agent joined with (${outcome.error}). ` +\n `The room was recreated or the link revoked — ask for a new share URL and call join_room with it.`,\n );\n }\n if (outcome.done) {\n this.failures = 0;\n this.cooldownUntil = 0;\n this.lastRejoinAt = this.now();\n return;\n }\n lastError = outcome.error;\n }\n\n if (this.room.removedBy) throw this.removed();\n this.failures += 1;\n const cooldown = Math.min(1000 * 2 ** (this.failures - 1), this.deps.maxCooldownMs ?? 30_000);\n this.cooldownUntil = this.now() + cooldown;\n this.log(`room ${roomLabel}: re-join failed (${lastError}); next try in ${Math.ceil(cooldown / 1000)}s`);\n throw new RoomConnectionError(\n `Lost the session in room ${roomLabel} (${reason}) and could not re-join yet: ${lastError}. ` +\n `apiary keeps retrying; if this persists, call join_room again with the room's share URL.`,\n false,\n );\n }\n\n /** One pass over the candidate addresses and tokens. */\n private async attemptOnce(): Promise<{ done: boolean; permanent: boolean; error: string }> {\n const info = this.room.rejoin;\n const candidates: Array<{ url: string; shareToken?: string }> = [\n { url: this.room.serverUrl, shareToken: info.shareToken },\n ];\n const session = this.readSession(info.serverRoomName);\n const local = session?.serverUrl && isLoopbackUrl(session.serverUrl)\n ? session.serverUrl.replace(/\\/$/, \"\")\n : undefined;\n if (local && !sameOrigin(local, this.room.serverUrl)) {\n candidates.push({ url: local, shareToken: info.shareToken });\n }\n // After a restart the room's own tokens are new. Same tier only.\n const fresh = info.localTier === \"admin\" ? session?.adminToken\n : info.localTier === \"member\" ? session?.memberToken\n : undefined;\n if (local && fresh && fresh !== info.shareToken) {\n candidates.push({ url: local, shareToken: fresh });\n }\n\n let shareRejected = \"\";\n let transient = \"\";\n let unreachable = \"\";\n for (const c of candidates) {\n const r = await this.joinAt(c.url, c.shareToken);\n if (r.ok) {\n this.apply(r.url, r.shareToken, r.data);\n return { done: true, permanent: false, error: \"\" };\n }\n if (r.kind === \"share-rejected\") shareRejected = r.error;\n else if (r.kind === \"unreachable\") unreachable ||= r.error;\n else transient ||= r.error;\n }\n if (shareRejected && !transient) return { done: false, permanent: true, error: shareRejected };\n return { done: false, permanent: false, error: transient || unreachable || shareRejected || \"no address answered\" };\n }\n\n private async joinAt(url: string, shareToken: string | undefined): Promise<Attempt> {\n const body: Record<string, unknown> = { type: this.room.rejoin.type, name: this.room.rejoin.name };\n if (shareToken) body.token = shareToken;\n let res: Response;\n try {\n res = await this.fetchFn(`${url}/join`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(10_000),\n });\n } catch (err) {\n return { ok: false, kind: \"unreachable\", error: `${url} unreachable (${err instanceof Error ? err.message : String(err)})` };\n }\n if (!res.ok) {\n const text = (await res.text().catch(() => \"\")).slice(0, 200);\n if (res.status === 403 && /invalid share token/i.test(text)) {\n return { ok: false, kind: \"share-rejected\", error: `403 Invalid share token at ${url}` };\n }\n return { ok: false, kind: \"transient\", error: `${res.status} from ${url}/join${text ? `: ${text}` : \"\"}` };\n }\n const data = (await res.json().catch(() => ({}))) as Record<string, unknown>;\n if (String(data.roomName ?? \"\") !== this.room.rejoin.serverRoomName || String(data.roomId ?? \"\") !== this.room.roomId) {\n // Not the room this agent was in. Leave the seat we just took.\n await this.fetchFn(`${url}/disconnect`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${String(data.sessionToken ?? \"\")}` },\n body: \"{}\",\n }).catch(() => {});\n return { ok: false, kind: \"wrong-room\", error: `${url} serves a different room (${String(data.roomName ?? \"?\")})` };\n }\n return { ok: true, url, shareToken, data };\n }\n\n /** Swap the new session into the JoinResult, data source, processor and SSE stream. */\n private apply(url: string, shareToken: string | undefined, data: Record<string, unknown>): void {\n const room = this.room;\n const oldId = room.participantId;\n const token = String(data.sessionToken ?? \"\");\n const newId = String(data.participantId ?? \"\");\n const participants = (data.participants as Participant[]) ?? [];\n\n room.participantId = newId;\n room.authority = String(data.authority ?? room.authority);\n room.participants = participants;\n if (shareToken !== undefined) room.rejoin.shareToken = shareToken;\n room.dataSource.setParticipants(participants);\n room.dataSource.setSelf(newId, room.rejoin.name);\n\n const { processor } = this.deps;\n processor.setRoomParticipantId(room.roomId, newId);\n if (processor.participantId === oldId) processor.participantId = newId;\n\n this.swapSession(url, token);\n this.log(`room \"${room.roomName}\": re-joined as \"${room.rejoin.name}\" at ${url}`);\n\n // The server starts a new seat in its default mode; tell it the one in use.\n const mode = processor.getModeForRoom(room.roomId);\n if (mode !== \"everyone\") {\n this.fetchFn(`${url}/set-mode`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${token}` },\n body: JSON.stringify({ mode }),\n }).catch(() => {});\n }\n }\n\n private swapSession(url: string, token: string): void {\n this.room.serverUrl = url;\n this.room.sessionToken = token;\n this.room.dataSource.setSession(url, token);\n this.deps.sseMux.updateConnection(this.room.roomId, url, token);\n }\n\n private removed(): RoomConnectionError {\n return this.loseFor(\n `You were removed from room \"${this.room.roomName}\" by ${this.room.removedBy}, so apiary will not ` +\n `re-join it. Call join_room again only if you are invited back.`,\n );\n }\n\n private loseFor(message: string): RoomConnectionError {\n if (!this.lost) {\n this.lost = new RoomConnectionError(message, true);\n this.log(`room \"${this.room.roomName}\": giving up — ${message}`);\n try {\n this.deps.onLost?.(this.room, this.lost);\n } catch { /* a notifier must not break recovery */ }\n }\n return this.lost;\n }\n\n private log(line: string): void {\n this.deps.log?.(line);\n }\n}\n","/**\n * Shared MCP-server option builder for the CLI runtime.\n *\n * Both entry points wire the SAME set of local MCP tool handlers against an\n * EventProcessor + SseMultiplexer:\n * - `setupAgentRuntime` (apiary claude / codex / opencode — tmux/HTTP delivery)\n * - `runMcpServer` (apiary mcp — stdio, pull-based)\n *\n * The handlers proxy tool calls (join/leave/set-mode/ping + admin\n * set-mode-for/mute/unmute/kick/promote/demote) to the apiary server over HTTP.\n * Keeping them here stops the two entry points from drifting — which they had:\n * the stdio path was missing admin promote/demote and the room-rules fetch on\n * join. This builder is the single source for both.\n */\n\nimport { extractToken } from \"./auth.js\";\nimport { can } from \"../core/authority.js\";\nimport { RemoteRoomDataSource } from \"../agent/remote-room-data-source.js\";\nimport { EventProcessor } from \"../agent/event-processor.js\";\nimport { SseMultiplexer } from \"../agent/sse-multiplexer.js\";\nimport { buildCatchUpLines } from \"../agent/tool-handlers.js\";\nimport {\n waitForAgent,\n describeOutcome,\n type AgentWaitState,\n type WaitTarget,\n} from \"../agent/wait-for-agent.js\";\nimport { type EngagementMode } from \"../agent/engagement.js\";\nimport type { Participant, AuthorityLevel } from \"../core/types.js\";\nimport type { RoomEvent } from \"../core/events.js\";\nimport type { RuntimeMcpServerOptions, JoinRoomResult } from \"../agent/mcp/runtime.js\";\nimport type { RoomConnectionError } from \"../agent/remote-room-data-source.js\";\nimport {\n RoomRejoiner,\n localTierOf,\n readLocalRoomSession,\n type RejoinDeps,\n type RejoinInfo,\n} from \"./rejoin.js\";\n\n/**\n * A room the agent has joined, tracked for cache updates + cleanup.\n *\n * `serverUrl`, `sessionToken` and `participantId` change when the room is\n * re-joined (see rejoin.ts): read them from here each time, never copy them.\n */\nexport interface JoinResult {\n serverUrl: string;\n sessionToken: string;\n participantId: string;\n roomName: string;\n roomId: string;\n authority: string;\n participants: Participant[];\n dataSource: RemoteRoomDataSource;\n /** What joining took (share URL and token, name, type), to re-join unattended. */\n rejoin: RejoinInfo;\n /** Restores a lost session; installed on the data source and the SSE stream. */\n rejoiner?: RoomRejoiner;\n /** Set when the event stream reported this agent kicked: it is never re-joined. */\n removedBy?: string;\n}\n\n/**\n * Keep a joined room's local view in step with one of its events: the\n * participant cache, and whether this agent was kicked (so it is not\n * re-joined behind the admin's back). Both runtimes' event wrappers call it.\n */\nexport function trackRoomEvent(jr: JoinResult, event: RoomEvent): void {\n if (event.type === \"ParticipantJoined\") {\n jr.dataSource.addParticipant(event.participant);\n } else if (event.type === \"ParticipantLeft\") {\n jr.dataSource.removeParticipant(event.participant_id);\n } else if (event.type === \"ParticipantKicked\") {\n if (event.participant_id === jr.participantId) jr.removedBy = safeName(event.kicked_by);\n jr.dataSource.removeParticipant(event.participant_id);\n }\n}\n\nfunction safeName(name: string): string {\n return String(name ?? \"\").replace(/[^A-Za-z0-9 _.-]/g, \"\").trim().slice(0, 40) || \"an admin\";\n}\n\nexport interface RuntimeHandlerContext {\n processor: EventProcessor;\n sseMux: SseMultiplexer;\n /** Mutable — join/leave handlers push and splice as the agent moves rooms. */\n joinResults: JoinResult[];\n agentName: string;\n authority: AuthorityLevel | undefined;\n /** Fired after a successful join (the tmux/HTTP path repaints its UI). */\n onRoomJoined?: () => void | Promise<void>;\n /** Re-join and URL-fallback lines (stderr in `apiary mcp`). Default: silent. */\n log?: (line: string) => void;\n /** Told once when a room is lost for good and will not be re-joined. */\n onRoomLost?: (jr: JoinResult, error: RoomConnectionError) => void;\n /** Test seam for the rejoiner (timings, fetch, session-file reader). */\n rejoinOverrides?: Partial<Omit<RejoinDeps, \"sseMux\" | \"processor\" | \"log\" | \"onLost\">>;\n}\n\n/**\n * Build the `RuntimeMcpServerOptions` shared by every CLI runtime entry point.\n * Pass the result straight to `createRuntimeMcpServer` / `createStdioRuntimeMcpServer`.\n */\nexport function buildRuntimeMcpOptions(ctx: RuntimeHandlerContext): RuntimeMcpServerOptions {\n const { processor, sseMux, joinResults, agentName, authority, onRoomJoined } = ctx;\n\n // A dropped event stream that retrying alone cannot fix (401/403 after a\n // server restart, or a dead URL) goes to that room's rejoiner.\n const rejoinerFor = (roomId: string) => joinResults.find((j) => j.roomId === roomId)?.rejoiner;\n sseMux.setRecoveryHandler({\n authRejected: async (roomId, status, usedToken) => {\n const r = rejoinerFor(roomId);\n return r ? r.authRejected(status, usedToken) : false;\n },\n unreachable: async (roomId) => (await rejoinerFor(roomId)?.unreachable()) ?? false,\n });\n\n /** POST to the room server through the data source (re-joins on a lost session). */\n const post = async (ds: RemoteRoomDataSource, path: string, body: Record<string, unknown>) => {\n try {\n const res = await ds.request(path, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n if (!res.ok) return { success: false, error: await res.text() };\n return { success: true };\n } catch (err) {\n const msg = err instanceof Error && err.name === \"RoomConnectionError\" ? err.message : \"Server unreachable.\";\n return { success: false, error: msg };\n }\n };\n\n /** Resolve a room + named participant, then POST to the apiary server. */\n const adminAction = (build: (id: string) => { path: string; body: Record<string, unknown> }) =>\n async (room: string, participant: string) => {\n const conn = processor.resolve(room);\n if (!conn) return { success: false, error: `Unknown room \"${room}\".` };\n const ds = conn.dataSource as RemoteRoomDataSource;\n const p = conn.dataSource.listParticipants().find((pp) => pp.name === participant);\n if (!p) return { success: false, error: `Unknown participant \"${participant}\".` };\n const { path, body } = build(p.id);\n return post(ds, path, body);\n };\n\n return {\n resolver: processor,\n toolOptions: {\n isEventSeen: (id) => processor.isEventSeen(id),\n markEventsSeen: (ids) => processor.markEventsSeen(ids),\n assignRef: (id) => processor.assignRef(id),\n resolveRef: (ref) => processor.resolveRef(ref),\n },\n authority,\n\n onSetMode: async (room, mode) => {\n const conn = processor.resolve(room);\n if (!conn) return { success: false, error: `Unknown room \"${room}\".` };\n processor.setModeForRoom(conn.dataSource.roomId, mode as EngagementMode, false);\n const result = await post(conn.dataSource as RemoteRoomDataSource, \"/set-mode\", { mode });\n if (!result.success && result.error !== \"Server unreachable.\") {\n return { success: false, error: `Server rejected: ${result.error}` };\n }\n // Server unreachable: the local mode is still set.\n return { success: true };\n },\n\n /**\n * Wait for another agent to reach a state, by polling the room server's\n * participant list. Two seconds of HTTP is far cheaper than the LLM turn\n * the agent would otherwise spend re-reading the room to check.\n */\n onWaitForAgent: async (room, participant, until, timeoutSec) => {\n const conn = processor.resolve(room);\n if (!conn) return { success: false, error: `Unknown room \"${room}\".` };\n const ds = conn.dataSource as RemoteRoomDataSource;\n const target = conn.dataSource.listParticipants().find((pp) => pp.name === participant);\n if (!target) return { success: false, error: `Unknown participant \"${participant}\".` };\n if (target.id === conn.dataSource.selfId) {\n return { success: false, error: \"You cannot wait for yourself.\" };\n }\n\n const readState = async (): Promise<AgentWaitState> => {\n try {\n const res = await ds.request(\"/participants\", { signal: AbortSignal.timeout(10_000) });\n if (!res.ok) return \"unknown\";\n const data = (await res.json()) as {\n participants?: Array<{ id: string; agentState?: string }>;\n };\n const found = data.participants?.find((pp) => pp.id === target.id);\n const state = found?.agentState;\n return state === \"idle\" || state === \"working\" || state === \"blocked\" ? state : \"unknown\";\n } catch {\n // A transient failure must not read as a state change.\n return \"unknown\";\n }\n };\n\n const outcome = await waitForAgent({\n readState,\n until,\n timeoutMs: timeoutSec === undefined ? undefined : timeoutSec * 1000,\n });\n return { success: true, message: describeOutcome(participant, until ?? \"idle\", outcome) };\n },\n\n onPing: async (room, participant) => {\n const conn = processor.resolve(room);\n if (!conn) return { success: false, error: `Unknown room \"${room}\".` };\n const ds = conn.dataSource as RemoteRoomDataSource;\n const p = conn.dataSource.listParticipants().find((pp) => pp.name === participant);\n if (!p) return { success: false, error: `Unknown participant \"${participant}\".` };\n return post(ds, \"/ping\", { participantId: p.id });\n },\n\n onJoinRoom: async (url, alias, nameOverride) => {\n const token = extractToken(url);\n let serverUrl: string;\n try {\n const parsed = new URL(url);\n parsed.search = \"\";\n serverUrl = parsed.toString().replace(/\\/$/, \"\");\n } catch {\n serverUrl = url.replace(/\\/$/, \"\");\n }\n\n try {\n const joinName = nameOverride ?? agentName;\n const joinBody: Record<string, unknown> = { type: \"agent\", name: joinName };\n if (token) joinBody.token = token;\n\n const res = await fetch(`${serverUrl}/join`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(joinBody),\n signal: AbortSignal.timeout(15_000),\n });\n if (!res.ok) return { success: false, error: `Failed to join: ${await res.text()}` };\n\n const data = await res.json() as Record<string, unknown>;\n const sessionToken = String(data.sessionToken ?? \"\");\n const roomName = alias ?? String(data.roomName ?? \"\");\n const roomId = String(data.roomId ?? \"\");\n const roomAuthority = String(data.authority ?? \"member\");\n const participants = (data.participants as Participant[]) ?? [];\n const newParticipantId = String(data.participantId ?? \"\");\n\n // Joining a room this agent is already in (e.g. with a new share URL\n // after its old one stopped working) replaces that entry: the\n // processor and SSE stream ignore a second registration for the room.\n const existingIdx = joinResults.findIndex((j) => j.roomId === roomId);\n if (existingIdx >= 0) {\n sseMux.removeConnection(roomId);\n processor.disconnectRemoteRoom(roomId);\n joinResults.splice(existingIdx, 1);\n }\n\n const dataSource = new RemoteRoomDataSource(serverUrl, sessionToken, roomId);\n dataSource.setParticipants(participants);\n dataSource.setSelf(newParticipantId, joinName);\n\n // Set global selfId on first join; always set per-room selfId\n if (joinResults.length === 0) {\n processor.participantId = newParticipantId;\n }\n processor.setRoomParticipantId(roomId, newParticipantId);\n\n // Register in EventProcessor and SSE multiplexer\n const mode = processor.getModeForRoom(roomId) ?? \"everyone\";\n processor.connectRemoteRoom(dataSource, roomName);\n sseMux.addConnection(serverUrl, sessionToken, roomName, roomId);\n\n // Track for cleanup\n const serverRoomName = String(data.roomName ?? \"\");\n const jr: JoinResult = {\n serverUrl,\n sessionToken,\n participantId: newParticipantId,\n roomName,\n roomId,\n authority: roomAuthority,\n participants,\n dataSource,\n rejoin: {\n joinUrl: url,\n shareToken: token ?? undefined,\n name: joinName,\n type: \"agent\",\n serverRoomName,\n localTier: localTierOf(\n token ?? undefined,\n (ctx.rejoinOverrides?.readRoomSession ?? readLocalRoomSession)(serverRoomName),\n ),\n },\n };\n jr.rejoiner = new RoomRejoiner(jr, {\n ...ctx.rejoinOverrides,\n sseMux,\n processor,\n log: ctx.log,\n onLost: ctx.onRoomLost ? (_room, err) => ctx.onRoomLost!(jr, err) : undefined,\n });\n dataSource.setRecovery(jr.rejoiner);\n joinResults.push(jr);\n\n // Build recent activity lines for the response\n const conn = processor.resolve(roomName);\n let recentLines: string[] = [];\n if (conn) {\n const catchUp = await buildCatchUpLines(conn, {\n isEventSeen: (id) => processor.isEventSeen(id),\n markEventsSeen: (ids) => processor.markEventsSeen(ids),\n assignRef: (id) => processor.assignRef(id),\n });\n recentLines = catchUp.lines;\n // TODO: catchUp.imageBlocks not surfaced here — the runtime injection\n // path doesn't support image content blocks yet. Wire up when adding\n // image delivery to the tmux/HTTP agent path.\n }\n\n await onRoomJoined?.();\n\n // Fetch this room's rules so they show up in the join response. Best\n // effort — older servers without /rules return 404 and we just skip.\n let rules: string[] | undefined;\n try {\n const rulesRes = await fetch(`${serverUrl}/rules`, {\n headers: { Authorization: `Bearer ${sessionToken}` },\n });\n if (rulesRes.ok) {\n const rulesData = (await rulesRes.json()) as { rules?: string[] };\n if (Array.isArray(rulesData.rules) && rulesData.rules.length > 0) rules = rulesData.rules;\n }\n } catch {\n /* old server or transient — skip */\n }\n\n return {\n success: true,\n roomName,\n agentName: joinName,\n authority: roomAuthority,\n mode,\n participants: participants\n .filter((p) => p.id !== newParticipantId)\n .map((p) => ({ name: p.name, authority: (p as any).authority ?? \"member\" })),\n recentLines,\n rules,\n } as JoinRoomResult;\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { success: false, error: `Unable to connect. Is the server running? (${serverUrl}) — ${msg}` };\n }\n },\n\n onLeaveRoom: async (room) => {\n const conn = processor.resolve(room);\n if (!conn) return { success: false, error: `Unknown room \"${room}\".` };\n const roomId = conn.dataSource.roomId;\n\n const idx = joinResults.findIndex((jr) => jr.roomId === roomId);\n if (idx >= 0) {\n const jr = joinResults[idx];\n jr.dataSource.setRecovery(null); // leaving on purpose: never re-join\n sseMux.removeConnection(roomId);\n processor.disconnectRemoteRoom(roomId);\n\n try {\n await fetch(`${jr.serverUrl}/disconnect`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${jr.sessionToken}` },\n body: JSON.stringify({}),\n });\n } catch {\n // Server may be down\n }\n\n joinResults.splice(idx, 1);\n }\n return { success: true };\n },\n\n // Admin actions differ only by endpoint + target tier — one helper covers all.\n onAdminSetModeFor: can(authority, \"set_mode_for\")\n ? (room, participant, mode) =>\n adminAction((id) => ({ path: \"/set-mode\", body: { participantId: id, mode } }))(room, participant)\n : undefined,\n onAdminMute: can(authority, \"mute\")\n ? adminAction((id) => ({ path: \"/set-authority\", body: { participantId: id, authority: \"guest\" } }))\n : undefined,\n onAdminUnmute: can(authority, \"unmute\")\n ? adminAction((id) => ({ path: \"/set-authority\", body: { participantId: id, authority: \"member\" } }))\n : undefined,\n onAdminKick: can(authority, \"kick\")\n ? adminAction((id) => ({ path: \"/kick\", body: { participantId: id } }))\n : undefined,\n onAdminPromote: can(authority, \"promote\")\n ? adminAction((id) => ({ path: \"/set-authority\", body: { participantId: id, authority: \"product_owner\" } }))\n : undefined,\n onAdminDemote: can(authority, \"demote\")\n ? adminAction((id) => ({ path: \"/set-authority\", body: { participantId: id, authority: \"member\" } }))\n : undefined,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
INVITES_DIR
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-3UQBSELW.js";
|
|
4
4
|
|
|
5
5
|
// src/cli/invites.ts
|
|
6
6
|
import { existsSync, readFileSync, unlinkSync } from "fs";
|
|
@@ -92,4 +92,4 @@ export {
|
|
|
92
92
|
invitePrompt,
|
|
93
93
|
deliverInvite
|
|
94
94
|
};
|
|
95
|
-
//# sourceMappingURL=chunk-
|
|
95
|
+
//# sourceMappingURL=chunk-W4KWQKPP.js.map
|
|
@@ -17,8 +17,6 @@ var LocalRoomDataSource = class {
|
|
|
17
17
|
this._room = _room;
|
|
18
18
|
this._channel = _channel;
|
|
19
19
|
}
|
|
20
|
-
_room;
|
|
21
|
-
_channel;
|
|
22
20
|
get roomId() {
|
|
23
21
|
return this._room.roomId;
|
|
24
22
|
}
|
|
@@ -139,23 +137,40 @@ function classifyEvent(event, mode, selfId, senderType, senderId) {
|
|
|
139
137
|
}
|
|
140
138
|
|
|
141
139
|
// src/agent/remote-room-data-source.ts
|
|
140
|
+
var RoomConnectionError = class extends Error {
|
|
141
|
+
constructor(message, permanent) {
|
|
142
|
+
super(message);
|
|
143
|
+
this.permanent = permanent;
|
|
144
|
+
this.name = "RoomConnectionError";
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
function isAbort(err) {
|
|
148
|
+
return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
|
|
149
|
+
}
|
|
142
150
|
var RemoteRoomDataSource = class {
|
|
143
151
|
constructor(_serverUrl, _sessionToken, _roomId) {
|
|
144
152
|
this._serverUrl = _serverUrl;
|
|
145
153
|
this._sessionToken = _sessionToken;
|
|
146
154
|
this._roomId = _roomId;
|
|
147
155
|
}
|
|
148
|
-
_serverUrl;
|
|
149
|
-
_sessionToken;
|
|
150
|
-
_roomId;
|
|
151
156
|
_participants = [];
|
|
152
157
|
_selfId = "";
|
|
153
158
|
_selfName = "";
|
|
159
|
+
_recovery = null;
|
|
154
160
|
/** Set own identity for populating outgoing message stubs. */
|
|
155
161
|
setSelf(id, name) {
|
|
156
162
|
this._selfId = id;
|
|
157
163
|
this._selfName = name;
|
|
158
164
|
}
|
|
165
|
+
/** Swap in a new server URL and/or session token (after a re-join or URL fallback). */
|
|
166
|
+
setSession(serverUrl, sessionToken) {
|
|
167
|
+
this._serverUrl = serverUrl;
|
|
168
|
+
this._sessionToken = sessionToken;
|
|
169
|
+
}
|
|
170
|
+
/** Install the handler that restores a lost session. */
|
|
171
|
+
setRecovery(recovery) {
|
|
172
|
+
this._recovery = recovery;
|
|
173
|
+
}
|
|
159
174
|
get roomId() {
|
|
160
175
|
return this._roomId;
|
|
161
176
|
}
|
|
@@ -185,17 +200,49 @@ var RemoteRoomDataSource = class {
|
|
|
185
200
|
listParticipants() {
|
|
186
201
|
return [...this._participants];
|
|
187
202
|
}
|
|
203
|
+
// ── HTTP ──────────────────────────────────────────────────────────────────
|
|
204
|
+
/**
|
|
205
|
+
* An authenticated request to the room server, with the CURRENT URL and
|
|
206
|
+
* token. On a network error or a 401 it asks the recovery handler once, then
|
|
207
|
+
* retries once with whatever URL and token are in place afterwards.
|
|
208
|
+
*
|
|
209
|
+
* Throws RoomConnectionError when the session was invalid and could not be
|
|
210
|
+
* restored; rethrows a network error that no fallback could route around.
|
|
211
|
+
*/
|
|
212
|
+
async request(path, init = {}) {
|
|
213
|
+
let usedToken = this._sessionToken;
|
|
214
|
+
const attempt = () => {
|
|
215
|
+
usedToken = this._sessionToken;
|
|
216
|
+
return fetch(`${this._serverUrl}${path}`, {
|
|
217
|
+
...init,
|
|
218
|
+
headers: { ...init.headers ?? {}, Authorization: `Bearer ${usedToken}` }
|
|
219
|
+
});
|
|
220
|
+
};
|
|
221
|
+
let res;
|
|
222
|
+
try {
|
|
223
|
+
res = await attempt();
|
|
224
|
+
} catch (err) {
|
|
225
|
+
if (isAbort(err) || !this._recovery) throw err;
|
|
226
|
+
if (!await this._recovery.unreachable()) throw err;
|
|
227
|
+
res = await attempt();
|
|
228
|
+
}
|
|
229
|
+
if (res.status === 401 && this._recovery) {
|
|
230
|
+
await res.body?.cancel().catch(() => {
|
|
231
|
+
});
|
|
232
|
+
await this._recovery.sessionInvalid(usedToken);
|
|
233
|
+
res = await attempt();
|
|
234
|
+
}
|
|
235
|
+
return res;
|
|
236
|
+
}
|
|
188
237
|
// ── HTTP-backed data access ───────────────────────────────────────────────
|
|
189
238
|
async getMessage(id) {
|
|
190
239
|
try {
|
|
191
|
-
const res = await
|
|
192
|
-
`${this._serverUrl}/message/${encodeURIComponent(id)}`,
|
|
193
|
-
{ headers: { Authorization: `Bearer ${this._sessionToken}` } }
|
|
194
|
-
);
|
|
240
|
+
const res = await this.request(`/message/${encodeURIComponent(id)}`);
|
|
195
241
|
if (!res.ok) return null;
|
|
196
242
|
const data = await res.json();
|
|
197
243
|
return data.message;
|
|
198
|
-
} catch {
|
|
244
|
+
} catch (err) {
|
|
245
|
+
if (err instanceof RoomConnectionError) throw err;
|
|
199
246
|
return null;
|
|
200
247
|
}
|
|
201
248
|
}
|
|
@@ -203,12 +250,11 @@ var RemoteRoomDataSource = class {
|
|
|
203
250
|
const params = new URLSearchParams({ query, count: String(limit) });
|
|
204
251
|
if (cursor) params.set("cursor", cursor);
|
|
205
252
|
try {
|
|
206
|
-
const res = await
|
|
207
|
-
headers: { Authorization: `Bearer ${this._sessionToken}` }
|
|
208
|
-
});
|
|
253
|
+
const res = await this.request(`/search?${params}`);
|
|
209
254
|
if (!res.ok) return { items: [], has_more: false, next_cursor: null };
|
|
210
255
|
return await res.json();
|
|
211
|
-
} catch {
|
|
256
|
+
} catch (err) {
|
|
257
|
+
if (err instanceof RoomConnectionError) throw err;
|
|
212
258
|
return { items: [], has_more: false, next_cursor: null };
|
|
213
259
|
}
|
|
214
260
|
}
|
|
@@ -216,12 +262,11 @@ var RemoteRoomDataSource = class {
|
|
|
216
262
|
const params = new URLSearchParams({ count: String(limit) });
|
|
217
263
|
if (cursor) params.set("cursor", cursor);
|
|
218
264
|
try {
|
|
219
|
-
const res = await
|
|
220
|
-
headers: { Authorization: `Bearer ${this._sessionToken}` }
|
|
221
|
-
});
|
|
265
|
+
const res = await this.request(`/messages?${params}`);
|
|
222
266
|
if (!res.ok) return { items: [], has_more: false, next_cursor: null };
|
|
223
267
|
return await res.json();
|
|
224
|
-
} catch {
|
|
268
|
+
} catch (err) {
|
|
269
|
+
if (err instanceof RoomConnectionError) throw err;
|
|
225
270
|
return { items: [], has_more: false, next_cursor: null };
|
|
226
271
|
}
|
|
227
272
|
}
|
|
@@ -230,12 +275,11 @@ var RemoteRoomDataSource = class {
|
|
|
230
275
|
if (category) params.set("category", category);
|
|
231
276
|
if (cursor) params.set("cursor", cursor);
|
|
232
277
|
try {
|
|
233
|
-
const res = await
|
|
234
|
-
headers: { Authorization: `Bearer ${this._sessionToken}` }
|
|
235
|
-
});
|
|
278
|
+
const res = await this.request(`/events/history?${params}`);
|
|
236
279
|
if (!res.ok) return { items: [], has_more: false, next_cursor: null };
|
|
237
280
|
return await res.json();
|
|
238
|
-
} catch {
|
|
281
|
+
} catch (err) {
|
|
282
|
+
if (err instanceof RoomConnectionError) throw err;
|
|
239
283
|
return { items: [], has_more: false, next_cursor: null };
|
|
240
284
|
}
|
|
241
285
|
}
|
|
@@ -245,9 +289,9 @@ var RemoteRoomDataSource = class {
|
|
|
245
289
|
if (image) body.image = image;
|
|
246
290
|
if (attachments?.length) body.attachments = attachments;
|
|
247
291
|
if (recipients?.length) body.recipients = recipients;
|
|
248
|
-
const res = await
|
|
292
|
+
const res = await this.request("/message", {
|
|
249
293
|
method: "POST",
|
|
250
|
-
headers: { "Content-Type": "application/json"
|
|
294
|
+
headers: { "Content-Type": "application/json" },
|
|
251
295
|
body: JSON.stringify(body)
|
|
252
296
|
});
|
|
253
297
|
if (!res.ok) {
|
|
@@ -265,9 +309,9 @@ var RemoteRoomDataSource = class {
|
|
|
265
309
|
};
|
|
266
310
|
}
|
|
267
311
|
async emitEvent(event) {
|
|
268
|
-
const res = await
|
|
312
|
+
const res = await this.request("/event", {
|
|
269
313
|
method: "POST",
|
|
270
|
-
headers: { "Content-Type": "application/json"
|
|
314
|
+
headers: { "Content-Type": "application/json" },
|
|
271
315
|
body: JSON.stringify({ event })
|
|
272
316
|
});
|
|
273
317
|
if (!res.ok) {
|
|
@@ -1342,7 +1386,9 @@ function registerTools(server, opts) {
|
|
|
1342
1386
|
}
|
|
1343
1387
|
return textResult(lines.join("\n"));
|
|
1344
1388
|
}
|
|
1345
|
-
|
|
1389
|
+
const result = await handleCatchUp(resolver, { room }, toolOptions);
|
|
1390
|
+
opts.onCatchUp?.(room);
|
|
1391
|
+
return result;
|
|
1346
1392
|
}
|
|
1347
1393
|
);
|
|
1348
1394
|
server.tool(
|
|
@@ -1643,6 +1689,17 @@ var SseMultiplexer = class {
|
|
|
1643
1689
|
_waiters = [];
|
|
1644
1690
|
_connections = /* @__PURE__ */ new Map();
|
|
1645
1691
|
_closed = false;
|
|
1692
|
+
_recovery = null;
|
|
1693
|
+
_initialBackoff;
|
|
1694
|
+
_maxBackoff;
|
|
1695
|
+
constructor(options = {}) {
|
|
1696
|
+
this._initialBackoff = options.initialBackoffMs ?? 1e3;
|
|
1697
|
+
this._maxBackoff = options.maxBackoffMs ?? 3e4;
|
|
1698
|
+
}
|
|
1699
|
+
/** Install the handler for lost sessions and dead URLs. */
|
|
1700
|
+
setRecoveryHandler(handler) {
|
|
1701
|
+
this._recovery = handler;
|
|
1702
|
+
}
|
|
1646
1703
|
/**
|
|
1647
1704
|
* Add an SSE connection to a stoop server.
|
|
1648
1705
|
* Starts streaming events immediately.
|
|
@@ -1650,8 +1707,32 @@ var SseMultiplexer = class {
|
|
|
1650
1707
|
addConnection(serverUrl, sessionToken, roomName, roomId) {
|
|
1651
1708
|
if (this._connections.has(roomId) || this._closed) return;
|
|
1652
1709
|
const abortController = new AbortController();
|
|
1653
|
-
const
|
|
1654
|
-
|
|
1710
|
+
const conn = {
|
|
1711
|
+
serverUrl,
|
|
1712
|
+
sessionToken,
|
|
1713
|
+
roomName,
|
|
1714
|
+
roomId,
|
|
1715
|
+
abortController,
|
|
1716
|
+
attempt: null,
|
|
1717
|
+
loopPromise: Promise.resolve()
|
|
1718
|
+
};
|
|
1719
|
+
this._connections.set(roomId, conn);
|
|
1720
|
+
conn.loopPromise = this._sseLoop(conn);
|
|
1721
|
+
}
|
|
1722
|
+
/** Is there a live (not stopped) connection for this room? */
|
|
1723
|
+
hasConnection(roomId) {
|
|
1724
|
+
return this._connections.has(roomId);
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Point a connection at a new URL and/or session token. The current attempt
|
|
1728
|
+
* is dropped so the stream reconnects with the new session straight away.
|
|
1729
|
+
*/
|
|
1730
|
+
updateConnection(roomId, serverUrl, sessionToken) {
|
|
1731
|
+
const conn = this._connections.get(roomId);
|
|
1732
|
+
if (!conn) return;
|
|
1733
|
+
conn.serverUrl = serverUrl;
|
|
1734
|
+
conn.sessionToken = sessionToken;
|
|
1735
|
+
conn.attempt?.abort();
|
|
1655
1736
|
}
|
|
1656
1737
|
/** Remove a connection by room ID. */
|
|
1657
1738
|
removeConnection(roomId) {
|
|
@@ -1672,28 +1753,45 @@ var SseMultiplexer = class {
|
|
|
1672
1753
|
}
|
|
1673
1754
|
this._waiters = [];
|
|
1674
1755
|
}
|
|
1675
|
-
async _sseLoop(
|
|
1676
|
-
const
|
|
1677
|
-
const
|
|
1678
|
-
let backoff =
|
|
1756
|
+
async _sseLoop(conn) {
|
|
1757
|
+
const { roomId, roomName } = conn;
|
|
1758
|
+
const signal = conn.abortController.signal;
|
|
1759
|
+
let backoff = this._initialBackoff;
|
|
1679
1760
|
while (!signal.aborted && !this._closed) {
|
|
1761
|
+
let retryNow = false;
|
|
1762
|
+
const attempt = new AbortController();
|
|
1763
|
+
conn.attempt = attempt;
|
|
1764
|
+
const attemptSignal = AbortSignal.any([signal, attempt.signal]);
|
|
1765
|
+
const usedToken = conn.sessionToken;
|
|
1680
1766
|
try {
|
|
1681
|
-
const res = await fetch(`${serverUrl}/events`, {
|
|
1767
|
+
const res = await fetch(`${conn.serverUrl}/events`, {
|
|
1682
1768
|
method: "POST",
|
|
1683
1769
|
headers: {
|
|
1684
1770
|
Accept: "text/event-stream",
|
|
1685
|
-
Authorization: `Bearer ${
|
|
1771
|
+
Authorization: `Bearer ${usedToken}`
|
|
1686
1772
|
},
|
|
1687
|
-
signal
|
|
1773
|
+
signal: attemptSignal
|
|
1688
1774
|
});
|
|
1775
|
+
if ((res.status === 401 || res.status === 403) && this._recovery) {
|
|
1776
|
+
await res.body?.cancel().catch(() => {
|
|
1777
|
+
});
|
|
1778
|
+
try {
|
|
1779
|
+
retryNow = await this._recovery.authRejected(roomId, res.status, usedToken);
|
|
1780
|
+
} catch {
|
|
1781
|
+
if (this._connections.get(roomId) === conn) this._connections.delete(roomId);
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
if (retryNow) backoff = this._initialBackoff;
|
|
1785
|
+
throw new Error(`SSE auth rejected: ${res.status}`);
|
|
1786
|
+
}
|
|
1689
1787
|
if (!res.ok || !res.body) {
|
|
1690
1788
|
throw new Error(`SSE connect failed: ${res.status}`);
|
|
1691
1789
|
}
|
|
1692
|
-
backoff =
|
|
1790
|
+
backoff = this._initialBackoff;
|
|
1693
1791
|
const reader = res.body.getReader();
|
|
1694
1792
|
const decoder = new TextDecoder();
|
|
1695
1793
|
let buffer = "";
|
|
1696
|
-
while (!
|
|
1794
|
+
while (!attemptSignal.aborted) {
|
|
1697
1795
|
const { done, value } = await reader.read();
|
|
1698
1796
|
if (done) break;
|
|
1699
1797
|
buffer += decoder.decode(value, { stream: true });
|
|
@@ -1711,10 +1809,24 @@ var SseMultiplexer = class {
|
|
|
1711
1809
|
}
|
|
1712
1810
|
} catch (err) {
|
|
1713
1811
|
if (signal.aborted) break;
|
|
1812
|
+
if (attempt.signal.aborted) {
|
|
1813
|
+
retryNow = true;
|
|
1814
|
+
backoff = this._initialBackoff;
|
|
1815
|
+
} else if (!retryNow && !(err instanceof Error && err.message.startsWith("SSE ")) && this._recovery) {
|
|
1816
|
+
try {
|
|
1817
|
+
if (await this._recovery.unreachable(roomId)) {
|
|
1818
|
+
retryNow = true;
|
|
1819
|
+
backoff = this._initialBackoff;
|
|
1820
|
+
}
|
|
1821
|
+
} catch {
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
} finally {
|
|
1825
|
+
if (conn.attempt === attempt) conn.attempt = null;
|
|
1714
1826
|
}
|
|
1715
|
-
if (!signal.aborted && !this._closed) {
|
|
1827
|
+
if (!signal.aborted && !this._closed && !retryNow) {
|
|
1716
1828
|
await new Promise((r) => setTimeout(r, backoff));
|
|
1717
|
-
backoff = Math.min(backoff * 2,
|
|
1829
|
+
backoff = Math.min(backoff * 2, this._maxBackoff);
|
|
1718
1830
|
}
|
|
1719
1831
|
}
|
|
1720
1832
|
}
|
|
@@ -1757,6 +1869,7 @@ export {
|
|
|
1757
1869
|
parseEngagementMode,
|
|
1758
1870
|
StoopsEngagement,
|
|
1759
1871
|
classifyEvent,
|
|
1872
|
+
RoomConnectionError,
|
|
1760
1873
|
RemoteRoomDataSource,
|
|
1761
1874
|
buildCatchUpLines,
|
|
1762
1875
|
EventMultiplexer,
|
|
@@ -1768,4 +1881,4 @@ export {
|
|
|
1768
1881
|
createStdioRuntimeMcpServer,
|
|
1769
1882
|
SseMultiplexer
|
|
1770
1883
|
};
|
|
1771
|
-
//# sourceMappingURL=chunk-
|
|
1884
|
+
//# sourceMappingURL=chunk-Z4J6TRJY.js.map
|