Skip to main content

ADAPTER

Constant ADAPTER 

Source
pub const ADAPTER: &str = "// borg adapter for opencode, one module for both plugin APIs: opencode 1.x (`server`, written\n// against 1.18.32, verified on 1.19.0) and opencode 2.x (`setup`, written against 2.0.23, verified on\n// 2.0.29).\n//\n// Translates opencode plugin hooks and events into borg\'s one canonical, Claude-shaped hook event\n// and pipes it to `borg hook opencode <event>`:\n//\n//   opencode 1.x                  opencode 2.x                      borg\n//   chat.message (user text)      session hook \"prompt\"             prompt\n//   tool.execute.before           tool hook \"execute.before\"        pre_tool (throws borg\'s reason: a\n//                                                                   paused drone\'s tool call is refused)\n//   tool.execute.after            tool hook \"execute.after\"         post_tool\n//   tool part in state \"error\"    \"execute.after\" with an error;    post_tool_failure (incl. a rejected\n//                                 session.tool.failed \"aborted\"     permission, with or without feedback)\n//   session.idle                  session.execution.succeeded       stop\n//   session.deleted, dispose      session.deleted, setup\'s cleanup  end\n//\n// At stop, a {\"decision\":\"block\",\"reason\"} answer is sent back into the session as a user message,\n// at most once per engineer prompt (the next stop carries stop_hook_active: true, like Claude\'s Stop\n// hook). No stop when the turn was halted (Esc/abort, a provider error, a permission rejected\n// without feedback): Claude\'s Stop hook skips interrupts too. opencode 1 tells a halted turn from\n// the last assistant message; opencode 2 ends it with session.execution.failed/interrupted instead.\n//\n// At prompt, borg\'s additionalContext (e.g. a queen question) reaches the drone but not the\n// engineer\'s TUI: opencode 1 adds it to the message as a synthetic text part, opencode 2 as a\n// synthetic session input without a description, just before the prompt.\n//\n// After a tool call, borg\'s additionalContext (live mode: a check\'s questions, graded lessons) reaches\n// the model before its next step; both after-hooks are awaited. opencode 1 appends it to the tool\'s\n// output (stored with the call: the TUI hides it for built-in tools, exports show it); opencode 2 sends\n// it as a steer synthetic input and leaves the tool result alone (the TUI shows that). opencode 1 runs\n// no after-hook for a failed call, so text waiting then comes with the next successful call or prompt.\n// A shell call\'s post_tool carries the end of its output and a non-zero exit as tool_response, Codex\'s\n// way: borg tells a test run\'s pass from its failure by it.\n// An answer later than the tool call\'s budget (post_ms; a prompt\'s: prompt_ms) is kept for the next call\n// or prompt: borg already counts it delivered.\n//\n// A session that listens (`borg listen`, docs/commands.md) is woken by the plugin. opencode gives its\n// shell no session id, so the plugin binds the session itself: it sets BORG_LISTEN_BINDER in its own\n// process (every command its model runs has it, and borg\'s own run there binds no session: one pid can\n// name several), and seeing `borg listen [--off]` in a session\'s own tool.execute.after it runs\n// `borg listen [--off] --session <id>`, adding borg\'s answer to what the model reads when the shell\'s own\n// run did not say it (within the tool call\'s post_ms; a later one waits in `late`). From then on, or from\n// a stop whose answer carries `\"listen\": true` (borg says no waiter runs), it keeps the session\'s waiter,\n// `borg listen --stream`: it waits at no token cost, holds wakes off while the engineer\'s turn runs and\n// says each wake as a JSON line naming its messages. The plugin sends a wake in only while the session\n// is idle (from a prompt or tool call to its stop, also a halted one, it is not): opencode 1 with\n// session.promptAsync, opencode 2 with session.prompt({delivery: \"queue\"}), and answers the waiter on\n// its stdin: `{\"sent\": ids}`, or `{\"failed\": ids}` when opencode refused it, and the waiter wakes the\n// session again for them after its backoff. A held wake is dropped once the turn\'s hooks, one or several,\n// handed the model every message it names, and one that comes after they did as it comes (borg\'s answer\n// names them, `\"shown_ids\"`; an older borg\'s names none, so the wake goes in): when that context reached\n// the model, not when borg answered, so an answer that came late (it waits in `late`) drops nothing\n// until a call carries it. A halted turn (no stop) is sent to borg as `halted`, so the waiter holds\n// nothing off any more. Its `{\"end\"}` line ends it until the next `\"listen\": true`; `--off`, the\n// session\'s end and the plugin\'s unload stop it.\n//\n// How long each call waits for borg, and how much of a shell\'s output borg gets, are borg\'s own\n// (`budgets`, harness/opencode.rs `BUDGETS`): the plugin asks for them until an answer carried them, and\n// uses FALLBACK, the same values, until then.\n//\n// Subagent (child) sessions are ignored, like Claude\'s hooks ignore subagents, with one exception:\n// a child\'s tool call still asks pre_tool, as its root session, so a paused drone cannot carry on\n// through a subagent.\n// Fails open: if borg is missing, slow or broken, opencode carries on untouched.\n// borg is found via $BORG_BIN, else `borg` on PATH. $BORG_OPENCODE_LOG names a file that gets\n// what the adapter did and every error it swallowed.\n//\n// The module has a single default export: opencode 1 calls every exported function as a plugin.\n\nimport { spawn } from \"node:child_process\"\nimport { appendFileSync } from \"node:fs\"\nimport type { Plugin, PluginModule } from \"@opencode-ai/plugin\"\n\n/** What each call waits for borg\'s answer (ms), and how much of a shell call\'s output borg gets. */\ntype Budgets = { prompt_ms: number; pre_ms: number; post_ms: number; stop_ms: number; end_ms: number; tail_chars: number }\n\n// borg\'s budgets (harness/opencode.rs `BUDGETS`, a test keeps them equal) until its first answer carries\n// them. opencode limits no plugin hook, so each is a guard: the engineer\'s message waits 5 s and a tool\n// result 2 s for borg\'s context (file appends; a later answer is kept for the next call or prompt), a\n// tool call 2 s before it runs (a stat when nobody steers). The stop, the one call with a model call,\n// and every call queued behind it get borg\'s stop timeout, which is also its BORG_STOP_BUDGET_MS: room\n// for a live check\'s wait, the model call and an expert drone; the session\'s end, queued too, the same\n// as its own end_ms. The output\'s end holds a test run\'s summary.\nconst FALLBACK: Budgets = { prompt_ms: 5_000, pre_ms: 2_000, post_ms: 2_000, stop_ms: 90_000, end_ms: 90_000, tail_chars: 4_000 }\n\n/** The budgets borg\'s answer carries (`\"budgets\"`), over `current`: each a positive number, the rest kept;\n *  undefined when it carries none (an older borg, or none asked). */\nfunction budgetsOf(out: any, current: Budgets): Budgets | undefined {\n  const b = out?.budgets\n  if (!b || typeof b !== \"object\") return undefined\n  const next = { ...current }\n  for (const k of Object.keys(FALLBACK) as (keyof Budgets)[]) if (Number.isFinite(b[k]) && b[k] > 0) next[k] = Number(b[k])\n  return next\n}\n// Guard: a hard cap on questions sent back into a session per engineer prompt, so nothing can make\n// the plugin prompt the drone in a loop; the engineer\'s next prompt allows the next round.\nconst MAX_ROUNDS = 1\n// A wait\'s timeout, told apart from an answer without context.\nconst LATE = Symbol(\"late\")\n// Marks the text part we send back, so its chat.message echo is recognised without comparing text.\nconst MARK = { borg: true }\n// opencode\'s PermissionRejectedError: the engineer said no without feedback, which halts the turn.\nconst REJECTED = \"The user rejected permission to use this specific tool call.\"\n// opencode 1\'s rejection with the engineer\'s words after it.\nconst FEEDBACK = /rejected permission to use this specific tool call with the following feedback:\\s*([\\s\\S]*)/i\n// opencode 2\'s call the engineer declined without feedback (error type \"aborted\").\nconst DECLINED = \"The user declined this tool call\"\n// opencode 2\'s `opencode run` rejects every permission ask by itself, as a Permission.CorrectedError\n// with this \"feedback\": nobody\'s correction.\nconst AUTO_REJECTED = \"This non-interactive run cannot ask the user for permission, so the request was rejected. Continue without this action.\"\n// Claude\'s plain rejection, which borg takes for a wordless denial: the engineer\'s next prompt is the reason.\nconst DENIED = \"The user doesn\'t want to proceed with this tool use.\"\n// Guard: how long the plugin\'s own `borg listen [--off] --session <id>` may take (a few file writes).\nconst LISTEN_MS = 10_000\n// What `borg listen` prints once it opted the session in.\nconst LISTENS = \"[borg listen] This session listens\"\n// Guard: how many parents `root` follows up from a subagent session, so a cycle of parent ids, or a\n// chain deeper than any opencode builds, cannot spin a hook.\nconst MAX_DEPTH = 32\n// Display: how much of borg\'s answer one line of $BORG_OPENCODE_LOG keeps.\nconst LOG_CHARS = 200\n// Display: how many of a borg run\'s arguments such a line names (the command and its first, `listen --off`).\nconst LOG_ARGS = 2\n// Guard: the most message ids a turn\'s `handed` keeps, the newest, so a turn that never ends (a lost\n// stop event) in a busy collective cannot grow it without end. Far more than one turn hands over: one\n// hook shows at most ~90 (borg\'s 10,000-character context budget for opencode, ~110 characters or more\n// a message, msg.rs `line`), and a turn\'s hooks show only what arrived meanwhile. An id let go only\n// lets a wake that names it go in at the stop; it never drops one.\nconst HANDED = 1_024\n\n/** borg\'s context for the model and the ids of the messages it shows: an answer\'s `additionalContext`\n *  and `\"shown_ids\"` (none from a borg that names none; never a count, since one event may show a newer\n *  message and leave a held wake\'s), or several such joined. */\ntype Context = { text: string; ids: string[] }\n\n/** The ids of the messages borg\'s answer `out` showed the model (its `\"shown_ids\"`). */\nfunction shownIds(out: any): string[] {\n  const ids = out?.shown_ids\n  return Array.isArray(ids) ? ids.filter((i: unknown): i is string => typeof i === \"string\") : []\n}\n\n/** Contexts as one, in order: their texts, and every message they show. */\nfunction joined(parts: (Context | undefined)[]): Context | undefined {\n  const all = parts.filter((c): c is Context => !!c)\n  return all.length ? { text: all.map((c) => c.text).join(\"\\n\\n\"), ids: all.flatMap((c) => c.ids) } : undefined\n}\n\n/** Tells the waiter `child` whether the wake for `ids` went in (`sent`) or opencode refused it\n *  (`failed`): one JSON line on its stdin. Fails open: a waiter gone meanwhile hears nothing. */\nfunction answerWake(child: ReturnType<typeof spawn> | undefined, how: \"sent\" | \"failed\", ids: string[]) {\n  try {\n    if (ids.length && child?.stdin?.writable) child.stdin.write(`${JSON.stringify({ [how]: ids })}\\n`)\n  } catch {}\n}\n\n// opencode tool ids -> the Claude names borg matches on (borg reads Bash command, Grep/Glob pattern,\n// else file_path/path from tool_input). Unknown tools keep their id.\nconst TOOL_NAMES: Record<string, string> = {\n  bash: \"Bash\", read: \"Read\", write: \"Write\", edit: \"Edit\", multiedit: \"MultiEdit\", apply_patch: \"Edit\",\n  grep: \"Grep\", glob: \"Glob\", list: \"LS\", webfetch: \"WebFetch\", websearch: \"WebSearch\", task: \"Task\",\n  todowrite: \"TodoWrite\",\n  // opencode 2 ids (its read/write/edit/grep/glob keep their names)\n  shell: \"Bash\", patch: \"Edit\", subagent: \"Task\",\n}\n\n// Tools whose `path` (opencode 2) is the file they touch, borg\'s file_path.\nconst FILE_TOOLS = new Set([\"read\", \"write\", \"edit\"])\n\nfunction toolInput(tool: string, args: any): Record<string, unknown> {\n  const input: Record<string, unknown> = { ...(args ?? {}) }\n  if (typeof input.filePath === \"string\") input.file_path = input.filePath\n  else if (FILE_TOOLS.has(tool) && typeof input.path === \"string\") input.file_path = input.path\n  // An edit\'s new text, as Claude\'s Edit names it (borg keeps it with the change, .edits).\n  if (typeof input.newString === \"string\") input.new_string = input.newString\n  // apply_patch (opencode 1, GPT models) and patch (opencode 2) name their files only inside the patch text.\n  if ((tool === \"apply_patch\" || tool === \"patch\") && typeof input.patchText === \"string\") {\n    const files = [...input.patchText.matchAll(/^\\*\\*\\* (?:Add|Update|Delete) File: (.+)$/gm)].map((m) => m[1].trim())\n    if (files.length) input.file_path = files.join(\" \")\n  }\n  return input\n}\n\n/** borg\'s wording for a permission rejected with feedback (opencode 1\'s \"...with the following\n *  feedback: X\", opencode 2\'s Permission.CorrectedError). */\nconst corrected = (feedback: string) =>\n  `The user doesn\'t want to proceed with this tool use. The tool use was rejected. To tell you how to proceed, the user said:\\n${feedback}`\n\n// borg\'s rejection capture speaks Claude\'s wording, so opencode\'s is re-phrased: a rejection with\n// feedback carries the engineer\'s words; one without (opencode 1\'s REJECTED, opencode 2\'s DECLINED)\n// becomes Claude\'s plain rejection, a wordless denial. Every other error goes as it is.\nfunction canonicalError(error: string): string {\n  const m = FEEDBACK.exec(error)\n  if (m) return corrected(m[1])\n  const bare = error.trim()\n  return bare === REJECTED || bare === DECLINED ? DENIED : error\n}\n\n/** A shell call\'s tool_response for borg, which tells a test run\'s pass from its failure by it\n *  (hook.rs `capture_test`): the last `tail` characters of its output, after Codex\'s \"Exit code: N\" line\n *  when it exited non-zero (opencode\'s shell keeps such a call out of the error state). Undefined\n *  without output: borg records no result it cannot see. */\nfunction shellResponse(output: string | undefined, exit: unknown, tail: number): string | undefined {\n  const out = (output ?? \"\").slice(-tail)\n  if (!out.trim()) return undefined\n  return typeof exit === \"number\" && exit !== 0 ? `Exit code: ${exit}\\nOutput:\\n${out}` : out\n}\n\n/** The text of an opencode 2 tool result: its `output`, or its content\'s text blocks. */\nfunction resultText(result: any): string {\n  if (typeof result === \"string\") return result\n  if (typeof result?.output === \"string\") return result.output\n  const content = Array.isArray(result?.content) ? result.content : []\n  return content.map((b: any) => (b?.type === \"text\" ? String(b.text ?? \"\") : \"\")).filter((t: string) => t).join(\"\\n\")\n}\n\n/** The engineer- or drone-visible text of message parts (no synthetic or ignored parts). */\nfunction visibleText(parts: any[] | undefined): string {\n  return (parts ?? [])\n    .filter((p: any) => p?.type === \"text\" && !p.synthetic && !p.ignored)\n    .map((p: any) => p.text)\n    .join(\"\\n\")\n}\n\n/** Why the turn that ended with assistant message `msg` was halted rather than finished, or \"\". */\nfunction halted(msg: any): string {\n  const error = msg?.info?.error\n  if (error) return String(error.name ?? \"error\") // MessageAbortedError (Esc, abort), APIError, ...\n  const rejected = (msg?.parts ?? []).some(\n    (p: any) => p?.type === \"tool\" && p.state?.status === \"error\" && String(p.state.error ?? \"\").trim() === REJECTED,\n  )\n  return rejected ? \"permission rejected\" : \"\"\n}\n\n/** borg\'s additionalContext in an answer, if it has any text, with the messages it shows. */\nfunction contextOf(out: any): Context | undefined {\n  const t = out?.hookSpecificOutput?.additionalContext\n  return typeof t === \"string\" && t.trim() ? { text: t, ids: shownIds(out) } : undefined\n}\n\nfunction note(msg: string) {\n  const file = process.env.BORG_OPENCODE_LOG\n  if (!file) return\n  try {\n    appendFileSync(file, `${new Date().toISOString()} ${msg}\\n`)\n  } catch {}\n}\n\n/** What a shell command does to the listening of the session it runs in: `on` for `borg listen`, `off`\n *  for `borg listen --off`. Undefined for anything else: a waiter (`--wait`, `--stream`, `--queue`) or\n *  another, named session\'s (`--session`). */\nfunction listenCommand(command: string): \"on\" | \"off\" | undefined {\n  for (const part of command.split(/&&|\\|\\||[;|\\n]/)) {\n    const m = /(?:^|\\s)(?:\\S*\\/)?borg\\s+listen(?:\\s+(.*))?$/.exec(part.trim())\n    if (!m) continue\n    const args = (m[1] ?? \"\").trim().split(/\\s+/).filter(Boolean)\n    if (args.some((a) => [\"--wait\", \"--stream\", \"--queue\", \"--session\"].includes(a) || a.startsWith(\"--session=\"))) return undefined\n    return args.includes(\"--off\") ? \"off\" : \"on\"\n  }\n  return undefined\n}\n\n/** Runs `borg <args>` (no stdin); resolves to its stdout and exit code, or null (fails open). */\nfunction borgRun(args: string[], timeoutMs: number): Promise<{ out: string; code: number | null } | null> {\n  return new Promise((resolve) => {\n    let out = \"\"\n    let child: ReturnType<typeof spawn>\n    try {\n      child = spawn(process.env.BORG_BIN || \"borg\", args, { stdio: [\"ignore\", \"pipe\", \"ignore\"] })\n    } catch (e) {\n      note(`spawn borg ${args.join(\" \")} failed: ${e}`)\n      return resolve(null)\n    }\n    const timer = setTimeout(() => {\n      note(`borg ${args.slice(0, LOG_ARGS).join(\" \")}: timed out`)\n      child.kill()\n    }, timeoutMs)\n    child.on(\"error\", (e) => {\n      clearTimeout(timer)\n      note(`borg ${args.slice(0, LOG_ARGS).join(\" \")}: ${e}`)\n      resolve(null)\n    })\n    child.stdout!.on(\"data\", (d) => (out += d))\n    child.on(\"close\", (code) => {\n      clearTimeout(timer)\n      resolve({ out, code })\n    })\n  })\n}\n\n/** Runs `borg hook opencode <event>` with `payload` on stdin, killed after `timeoutMs`; resolves to its\n *  JSON answer or null. `stopMs` is the stop\'s budget borg keeps inside. */\nfunction borg(event: string, payload: Record<string, unknown>, timeoutMs: number, stopMs: number): Promise<any> {\n  return new Promise((resolve) => {\n    let out = \"\"\n    let done = false\n    const finish = (v: any) => {\n      if (done) return\n      done = true\n      clearTimeout(timer)\n      resolve(v)\n    }\n    let child: ReturnType<typeof spawn>\n    try {\n      // BORG_STOP_BUDGET_MS: at a stop borg keeps its wait for a live check, its model call and its expert\n      // drone inside this timeout, so its answer arrives.\n      const env = { ...process.env, BORG_STOP_BUDGET_MS: String(stopMs) }\n      child = spawn(process.env.BORG_BIN || \"borg\", [\"hook\", \"opencode\", event], { stdio: [\"pipe\", \"pipe\", \"ignore\"], env })\n    } catch (e) {\n      note(`spawn borg failed: ${e}`)\n      return resolve(null)\n    }\n    const timer = setTimeout(() => {\n      note(`borg ${event}: timed out`)\n      child.kill()\n      finish(null)\n    }, timeoutMs)\n    child.on(\"error\", (e) => {\n      note(`borg ${event}: ${e}`)\n      finish(null)\n    })\n    child.stdout!.on(\"data\", (d) => (out += d))\n    child.on(\"close\", () => {\n      note(`borg ${event} -> ${out.trim().slice(0, LOG_CHARS) || \"(nothing)\"}`)\n      try {\n        finish(out.trim() ? JSON.parse(out) : null)\n      } catch {\n        finish(null)\n      }\n    })\n    child.stdin!.on(\"error\", () => {})\n    child.stdin!.end(JSON.stringify(payload))\n  })\n}\n\ntype Session = {\n  chain: Promise<unknown> // borg calls of one session run in order\n  child: boolean // a subagent session: ignored (but for pre_tool, asked as its root session)\n  parent?: string // a child\'s parent session id\n  continued: boolean // we answered the last idle with borg\'s questions\n  gen: number // counts engineer prompts: a stop answer older than the latest prompt is dropped\n  rounds: number // questions sent back since the engineer\'s last prompt (<= MAX_ROUNDS)\n  echoes: number // messages we sent whose prompt echo has not come back yet\n  stopping: boolean\n  used: boolean // borg has heard of this session\n  ended: boolean\n  args: Map<string, { tool: string; args: any }> // callID -> call, for failures reported without input\n  failed: Set<string> // callIDs already reported as failures\n  worded: boolean // opencode 1: a call was rejected with the engineer\'s words since their last prompt\n  late: Context[] // context that came after post_ms or prompt_ms (or failed to go in): delivered with the next call or prompt\n  stream?: ReturnType<typeof spawn> // its waiter, `borg listen --stream`, while the session listens\n  busy: boolean // a turn runs: from a prompt or tool call to its stop (also a halted one)\n  owner: number // bumped by each mark of busy: a failed send frees only the mark it made (`occupy`)\n  wakes: { text: string; ids: string[] }[] // the waiter\'s wakes not sent yet, with their messages\' ids: sent once idle\n  handed: Set<string> // ids of the messages the turn\'s contexts handed the model (`shown`), the newest HANDED: emptied once idle\n}\n\n/** What the session state machine needs from one opencode version. */\ntype Io = {\n  directory: string\n  /** The drone\'s last assistant message: its last visible text, and why its turn was halted rather\n   *  than finished (\"\" when it finished). */\n  lastAssistant: (id: string) => Promise<{ text: string; halted: string }>\n  /** Sends borg\'s questions into the session as a user message that carries MARK. */\n  ask: (id: string, text: string) => Promise<void>\n}\n\n/** The per-session state machine both opencode versions drive: they differ only in how their hooks\n *  and events arrive and in `io`. */\nfunction drones(io: Io) {\n  const sessions = new Map<string, Session>()\n  // borg\'s budgets: FALLBACK until an answer carried them, asked for until then.\n  let budgets: Budgets = { ...FALLBACK }\n  let learned = false\n  /** `borg hook opencode <event>`, killed after `budgets[within]`; an answer\'s budgets are taken. */\n  const call = async (event: string, payload: Record<string, unknown>, within: keyof Budgets) => {\n    const out = await borg(event, learned ? payload : { ...payload, budgets: true }, budgets[within], budgets.stop_ms)\n    const b = budgetsOf(out, budgets)\n    if (b) [budgets, learned] = [b, true]\n    return out\n  }\n  const get = (id: string): Session => {\n    let s = sessions.get(id)\n    if (!s) {\n      s = { chain: Promise.resolve(), child: false, continued: false, gen: 0, rounds: 0, echoes: 0, stopping: false, used: false, ended: false, args: new Map(), failed: new Set(), worded: false, late: [], busy: false, owner: 0, wakes: [], handed: new Set() }\n      sessions.set(id, s)\n    }\n    return s\n  }\n\n  // The top-level session a (possibly nested) subagent session belongs to.\n  const root = (id: string): string => {\n    for (let i = 0; i < MAX_DEPTH; i++) {\n      const parent = sessions.get(id)?.parent\n      if (!parent || parent === id) break\n      id = parent\n    }\n    return id\n  }\n\n  const history = async (id: string): Promise<{ text: string; halted: string }> => {\n    try {\n      return await io.lastAssistant(id)\n    } catch (e) {\n      note(`reading ${id}\'s messages failed: ${e}`)\n      return { text: \"\", halted: \"\" }\n    }\n  }\n  const lastText = async (id: string) => (await history(id)).text\n\n  /** Marks the session busy; the mark\'s number, for `release`. */\n  const occupy = (s: Session) => {\n    s.busy = true\n    return ++s.owner\n  }\n  /** A send that failed frees the session, unless a prompt or tool call marked it busy since. */\n  const release = (s: Session, mark: number) => {\n    if (s.owner === mark) s.busy = false\n  }\n\n  /** Each held wake whose messages the turn\'s contexts handed the model all of (`handed`, however many\n   *  hooks borg split them over) is dropped, so no turn is spent on messages the model already read; one\n   *  with a message they left, or that names none (an older waiter\'s), keeps waiting. */\n  const drop = (id: string, s: Session) => {\n    const held = s.wakes.length\n    s.wakes = s.wakes.filter((w) => !w.ids.length || !w.ids.every((i) => s.handed.has(i)))\n    if (s.wakes.length < held) note(`${id}: ${held - s.wakes.length} held wake(s) dropped, the turn showed their messages`)\n  }\n\n  /** borg\'s context `c` reached the model: its messages join what the turn handed over (the newest\n   *  HANDED), and the held wakes they now cover are dropped. */\n  const shown = (id: string, c: Context | undefined) => {\n    if (!c?.ids.length) return\n    const s = get(id)\n    for (const i of c.ids) {\n      // Moved to the newest, so the guard lets the oldest go.\n      s.handed.delete(i)\n      s.handed.add(i)\n    }\n    for (const i of s.handed) {\n      if (s.handed.size <= HANDED) break\n      s.handed.delete(i)\n    }\n    drop(id, s)\n  }\n\n  // Queue a borg call behind the session\'s earlier ones. `build` runs in turn, so it sees their effects.\n  // Queued behind a stop, any call may wait as long as one (`within`); the end waits its own budget.\n  const send = (id: string, event: string, build: () => Promise<Record<string, unknown>>, within: keyof Budgets = \"stop_ms\"): Promise<any> => {\n    const s = get(id)\n    s.used = true\n    // pid: opencode\'s own (the plugin runs in its process), so `borg top` sees a vanished opencode.\n    const run = s.chain.then(async () => {\n      const payload = { session_id: id, cwd: io.directory, pid: process.pid, ...(await build()) }\n      return call(event, payload, within)\n    })\n    s.chain = run.catch(() => null)\n    return run.catch(() => null)\n  }\n\n  /** A subagent session, from its creation event. */\n  const child = (id: string, parent: string) => {\n    const c = get(id)\n    c.child = true\n    c.parent = parent\n  }\n\n  /** A user message: the engineer\'s prompt, or the echo of our questions (`marked`). Resolves to\n   *  borg\'s context for the prompt (e.g. a queen question), if it came within prompt_ms, after context\n   *  that came late before; the caller tells `shown` once it went in. */\n  const prompt = async (id: string, text: string, marked: boolean): Promise<Context | undefined> => {\n    const s = get(id)\n    if (s.child) return\n    // Our own stop answer coming back: not the engineer speaking.\n    // Any prompt starts a turn: no wake goes in until its stop.\n    occupy(s)\n    if (marked) {\n      s.echoes = Math.max(0, s.echoes - 1)\n      return\n    }\n    if (!text.trim()) return\n    if (s.echoes > 0) {\n      // We sent questions and their echo has not come back, yet this one is unmarked. Take it for\n      // the echo anyway: an echo mistaken for the engineer would reset the cycle and could loop.\n      s.echoes--\n      note(`${id}: unmarked message while our questions were pending, taken as their echo`)\n      return\n    }\n    // The engineer spoke: a fresh idle cycle.\n    s.gen++\n    s.continued = false\n    s.rounds = 0\n    s.worded = false\n    // Take the proposal now: by the time the queue reaches this prompt, the drone may be\n    // streaming its reply to it.\n    const prior = lastText(id)\n    // Behind a stop still thinking (a model call), the message does not wait for borg\'s context, so\n    // borg is told to hand none over (it would count as delivered, e.g. a queen question, unseen).\n    const busy = s.stopping\n    const answer = send(id, \"prompt\", async () => ({ prompt: text, last_assistant_message: await prior, ...(busy ? { context: false } : {}) }))\n    if (busy) return\n    return withLate(s, await within(id, answer.then(contextOf), budgets.prompt_ms, \"prompt\"))\n  }\n\n  /** borg\'s context if it comes within `ms`; a later answer waits in `late` for the next call or\n   *  prompt (borg already counts it delivered). */\n  const within = async (id: string, answer: Promise<Context | undefined>, ms: number, what: string) => {\n    let timer: ReturnType<typeof setTimeout> | undefined\n    const late = new Promise<typeof LATE>((r) => (timer = setTimeout(() => r(LATE), ms)))\n    const t = await Promise.race([answer, late])\n    clearTimeout(timer)\n    if (t !== LATE) return t\n    void answer.then((c) => {\n      if (!c) return\n      get(id).late.push(c)\n      note(`${id}: borg\'s ${what} context came late, kept for the next call or prompt`)\n    })\n  }\n\n  /** Context that came late before, then `t`. */\n  const withLate = (s: Session, t: Context | undefined) => joined([...s.late.splice(0), t])\n\n  /** Before a tool call: throws borg\'s reason when the drone is paused\n   *  (`borg pause`). Not queued behind the session\'s other calls, and fail open after pre_ms.\n   *  A subagent\'s call is asked as its root session: the pause holds for the drone\'s subagents. */\n  const preTool = async (id: string, callID: string, tool: string, args: any) => {\n    get(id).args.set(callID, { tool, args })\n    occupy(get(root(id)))\n    const out = await call(\"pre_tool\", {\n      session_id: root(id), cwd: io.directory, pid: process.pid,\n      tool_name: TOOL_NAMES[tool] ?? tool, tool_input: toolInput(tool, args),\n    }, \"pre_ms\")\n    const deny = out?.hookSpecificOutput\n    if (deny?.permissionDecision === \"deny\" && typeof deny.permissionDecisionReason === \"string\") {\n      throw new Error(deny.permissionDecisionReason)\n    }\n  }\n\n  /** A finished tool call. Resolves to the context for the model\'s next step: context that came late\n   *  before, then borg\'s answer, if it came within post_ms; a later answer waits in `late`. The caller\n   *  tells `shown` once it went in. A shell call\'s `output` and `exit` go along as its tool_response\n   *  (`shellResponse`). */\n  const postTool = async (id: string, callID: string, tool: string, args: any, output?: string, exit?: unknown): Promise<Context | undefined> => {\n    const s = get(id)\n    s.args.delete(callID)\n    if (s.child) return\n    const name = TOOL_NAMES[tool] ?? tool\n    const response = name === \"Bash\" ? shellResponse(output, exit, budgets.tail_chars) : undefined\n    const answer = send(id, \"post_tool\", async () => ({ tool_name: name, tool_input: toolInput(tool, args), ...(response ? { tool_response: response } : {}) })).then(contextOf)\n    // Behind a stop still thinking (a model call) the answer can only come late: the call does not wait.\n    const until = Date.now() + (s.stopping ? 0 : budgets.post_ms)\n    const context = withLate(s, await within(id, answer, until - Date.now(), \"tool\"))\n    const which = name === \"Bash\" && typeof args?.command === \"string\" ? listenCommand(args.command) : undefined\n    if (!which) return context\n    // The bind waits behind the post_tool call: the tool result waits for both no longer than post_ms.\n    const said = await within(id, listen(id, which, output ?? \"\"), Math.max(0, until - Date.now()), \"listen\")\n    return joined([context, said])\n  }\n\n  /** The session\'s model ran `borg listen [--off]`: the plugin opts this session in or out (its shell\n   *  has no session id) behind its other borg calls, and starts or stops its waiter once that ran. borg\'s\n   *  answer, for the model, when the shell\'s own run (`output`) did not say it (it shows no message). */\n  const listen = (id: string, which: \"on\" | \"off\", output: string): Promise<Context | undefined> => {\n    const s = get(id)\n    const args = [\"listen\", ...(which === \"off\" ? [\"--off\"] : []), \"--session\", id, \"--dir\", io.directory]\n    const run = s.chain.then(async () => {\n      const res = await borgRun(args, LISTEN_MS)\n      const said = (res?.out ?? \"\").trim()\n      note(`${id}: borg ${args.slice(0, LOG_ARGS).join(\" \")} -> ${res?.code} ${said.slice(0, LOG_CHARS)}`)\n      if (which === \"off\") disarm(id)\n      else if (res?.code === 0) arm(id)\n      const told = which === \"on\" ? output.includes(LISTENS) : output.includes(\"[borg listen] this session\")\n      return told || !said ? undefined : { text: said, ids: [] }\n    })\n    s.chain = run.catch(() => null)\n    return run.catch(() => undefined)\n  }\n\n  /** The session\'s waiter, unless one runs: each wake goes in once the session is idle (`flush`). */\n  const arm = (id: string) => {\n    const s = get(id)\n    if (s.child || s.ended || s.stream) return\n    let child: ReturnType<typeof spawn>\n    try {\n      // Its stdin takes the plugin\'s answers to its wakes (`answerWake`).\n      child = spawn(process.env.BORG_BIN || \"borg\", [\"listen\", \"--stream\", \"--session\", id, \"--dir\", io.directory], { stdio: [\"pipe\", \"pipe\", \"ignore\"] })\n    } catch (e) {\n      return note(`${id}: starting the waiter failed: ${e}`)\n    }\n    child.stdin!.on(\"error\", () => {})\n    s.stream = child\n    let rest = \"\"\n    child.stdout!.on(\"data\", (d) => {\n      rest += d\n      let nl: number\n      while ((nl = rest.indexOf(\"\\n\")) >= 0) {\n        const line = rest.slice(0, nl)\n        rest = rest.slice(nl + 1)\n        try {\n          const v = JSON.parse(line)\n          if (typeof v?.wake === \"string\" && v.wake.trim()) {\n            // An older waiter names neither: never taken as shown, nothing to answer.\n            const ids: string[] = Array.isArray(v.ids) ? v.ids.filter((i: unknown) => typeof i === \"string\") : []\n            s.wakes.push({ text: v.wake, ids })\n            note(`${id}: a wake${s.busy || s.stopping ? \", held while the turn runs\" : \"\"}`)\n            // The waiter may have taken its messages before a hook\'s claim of them landed, and that\n            // hook\'s context reached the model before this line: such a wake is dropped as it comes.\n            drop(id, s)\n            flush(id)\n          } else if (typeof v?.note === \"string\" || typeof v?.end === \"string\") note(`${id}: waiter: ${v.note ?? `ends: ${v.end}`}`)\n        } catch {}\n      }\n    })\n    const end = (code: number | null) => {\n      if (s.stream === child) s.stream = undefined\n      // Its stdin\'s end too, which would otherwise keep opencode\'s event loop holding a dead pipe.\n      child.stdin?.destroy()\n      note(`${id}: the waiter ended (${code})`)\n    }\n    child.on(\"error\", () => end(null))\n    child.on(\"close\", end)\n    note(`${id}: the waiter runs`)\n  }\n\n  /** The session listens no more (`--off`, its end, the plugin\'s unload): its waiter is stopped, and its\n   *  held wakes and what the turn handed over go. */\n  const disarm = (id: string) => {\n    const s = get(id)\n    const child = s.stream\n    s.stream = undefined\n    s.wakes = []\n    s.handed.clear()\n    try {\n      if (child && child.exitCode === null && child.signalCode === null) child.kill(\"SIGTERM\")\n    } catch {}\n  }\n\n  /** Sends the session\'s waiting wakes, as one marked message, while it is idle and no stop runs, and\n   *  answers its waiter whether they went in. One that fails to go in is the waiter\'s to send again\n   *  (after its backoff): the messages stay unread until a wake goes in. */\n  const flush = (id: string) => {\n    const s = get(id)\n    if (s.busy || s.stopping || s.ended) return\n    // Idle: the turn is over. Its messages drop no later wake: borg claimed them shown before it\n    // answered, so the waiter names none of them again.\n    s.handed.clear()\n    if (!s.wakes.length) return\n    const sent = s.wakes.splice(0)\n    const ids = sent.flatMap((w) => w.ids)\n    const mark = occupy(s)\n    io.ask(id, sent.map((w) => w.text).join(\"\\n\\n\")).then(\n      () => {\n        answerWake(s.stream, \"sent\", ids)\n        note(`woke ${id}: messages wait`)\n      },\n      (e) => {\n        note(`waking ${id} failed: ${e}; its waiter wakes it again`)\n        release(s, mark)\n        answerWake(s.stream, \"failed\", ids)\n      },\n    )\n  }\n\n  /** The session\'s turn was halted with no stop (opencode 2\'s session.execution.failed or\n   *  .interrupted; opencode 1\'s halted turn still reaches `stop`, which says so): it is idle, and its\n   *  waiting wakes go in, so an Esc never leaves a listening session that nothing wakes. */\n  const idle = (id: string) => {\n    const s = get(id)\n    if (s.child) return\n    s.busy = false\n    halt(id)\n    flush(id)\n  }\n\n  /** A listening session\'s turn was halted with no stop: borg ends the engineer\'s turn on its side too\n   *  (`borg hook opencode halted`), so its waiter holds nothing off. Nothing for any other session. */\n  const halt = (id: string) => {\n    const s = get(id)\n    if (!s.stream || s.ended) return\n    send(id, \"halted\", async () => ({}))\n  }\n\n  /** A failed tool call; `args` undefined takes the input seen before the call. */\n  const failure = (id: string, callID: string, tool: string | undefined, args: any, error: string) => {\n    const s = get(id)\n    const call = s.args.get(callID)\n    s.args.delete(callID)\n    if (s.child || s.failed.has(callID)) return\n    s.failed.add(callID)\n    // When the engineer rejects a call with words, opencode 1 rejects every other pending call with\n    // the plain REJECTED: that cascade is nobody\'s denial (borg assimilate drops it too).\n    if (FEEDBACK.test(error)) s.worded = true\n    else if (s.worded && error.trim() === REJECTED) return\n    const name = tool ?? call?.tool ?? \"\"\n    send(id, \"post_tool_failure\", async () => ({\n      tool_name: TOOL_NAMES[name] ?? name,\n      tool_input: toolInput(name, args ?? call?.args),\n      error: canonicalError(error),\n      last_assistant_message: await lastText(id),\n    }))\n  }\n\n  /** The drone\'s turn ended: borg\'s conscience may send questions back, at most MAX_ROUNDS per prompt. */\n  const stop = async (id: string) => {\n    const s = get(id)\n    if (s.child || s.stopping) return\n    s.stopping = true\n    s.busy = false\n    let sent = 0 // the busy mark of borg\'s questions sent back, once they are\n    try {\n      const gen = s.gen\n      const { text, halted } = await history(id)\n      if (halted) {\n        // The engineer (or the provider) stopped the drone: not a turn ending on its own.\n        note(`${id} halted (${halted}): no stop`)\n        halt(id)\n        return\n      }\n      const active = s.continued\n      s.continued = false\n      const out = await send(id, \"stop\", async () => ({ last_assistant_message: text, stop_hook_active: active }))\n      // The session listens and no waiter runs.\n      if (out?.listen === true) arm(id)\n      if (out?.decision !== \"block\" || typeof out.reason !== \"string\") return\n      if (active) return\n      // The engineer spoke while borg thought: the questions are about a turn already superseded.\n      if (s.gen !== gen) return note(`${id}: engineer spoke during stop, questions dropped`)\n      if (s.rounds >= MAX_ROUNDS) return note(`${id}: ${s.rounds} question round(s) this prompt, no more`)\n      s.rounds++\n      s.continued = true\n      s.echoes++\n      sent = occupy(s)\n      await io.ask(id, out.reason)\n      note(`sent borg\'s questions into ${id}`)\n    } catch (e) {\n      note(`sending borg\'s questions into ${id} failed: ${e}`)\n      if (sent) {\n        s.continued = false\n        s.echoes = Math.max(0, s.echoes - 1)\n        release(s, sent)\n      }\n    } finally {\n      s.stopping = false\n      // Wakes that came during the turn go in now that it is idle.\n      flush(id)\n    }\n  }\n\n  const end = (id: string) => {\n    const s = get(id)\n    if (s.child || !s.used || s.ended) return\n    s.ended = true\n    // borg\'s end forgets the opt-in; the waiter is stopped here at once.\n    return send(id, \"end\", async () => ({}), \"end_ms\").then((out) => {\n      disarm(id)\n      return out\n    })\n  }\n\n  /** opencode is shutting down (e.g. `opencode run` finished): end every session borg has heard of. */\n  const endAll = async () => {\n    await Promise.all([...sessions.keys()].map(end))\n  }\n\n  return { get, child, prompt, preTool, postTool, shown, failure, stop, idle, end, endAll }\n}\n\n// ---- opencode 1.x: a `server` function that returns hooks; bus events through `event`.\n\nconst server: Plugin = async ({ client, directory }) => {\n  process.env.BORG_LISTEN_BINDER = \"opencode\"\n  const d = drones({\n    directory,\n    // The last assistant message ({info, parts}) tells a halted turn; its last visible text is borg\'s\n    // \"proposal\" for captured corrections and the plan the conscience reads at stop.\n    lastAssistant: async (id) => {\n      const res: any = await client.session.messages({ path: { id } })\n      const msgs: any[] = res?.data ?? []\n      let last: any\n      for (let i = msgs.length - 1; i >= 0; i--) {\n        if (msgs[i]?.info?.role !== \"assistant\") continue\n        last ??= msgs[i]\n        const text = visibleText(msgs[i].parts).trim()\n        if (text) return { text, halted: halted(last) }\n      }\n      return { text: \"\", halted: halted(last) }\n    },\n    ask: async (id, text) => {\n      const body = { parts: [{ type: \"text\" as const, text, metadata: MARK }] }\n      const session: any = client.session\n      const res = await (session.promptAsync ?? session.prompt).call(session, { path: { id }, body })\n      if (res?.error) throw new Error(JSON.stringify(res.error))\n    },\n  })\n\n  return {\n    \"chat.message\": async (input, output) => {\n      const parts: any[] = output.parts ?? []\n      const id = input.sessionID\n      const context = await d.prompt(id, visibleText(parts), parts.some((p) => p?.metadata?.borg === true))\n      if (!context) return\n      // opencode gave the parts their ids already; ours sorts right after the last one.\n      const last = parts[parts.length - 1]\n      parts.push({\n        ...(last?.id ? { id: `${last.id}0` } : {}),\n        sessionID: last?.sessionID ?? id,\n        messageID: last?.messageID ?? (input as any).messageID,\n        type: \"text\",\n        text: context.text,\n        synthetic: true,\n      })\n      d.shown(id, context)\n      note(`${id}: borg\'s prompt context added (${context.text.length} chars)`)\n    },\n\n    \"tool.execute.before\": async (input, output) => {\n      await d.preTool(input.sessionID, input.callID, input.tool, output.args)\n    },\n\n    // Awaited before the next step; the output is what the model reads (and what opencode stores).\n    \"tool.execute.after\": async (input, output) => {\n      const t = await d.postTool(input.sessionID, input.callID, input.tool, input.args, output.output, output.metadata?.exit)\n      if (!t) return\n      output.output = `${output.output}\\n\\n${t.text}`\n      d.shown(input.sessionID, t)\n    },\n\n    event: async ({ event }) => {\n      const e: any = event\n      const p = e?.properties ?? {}\n      switch (e?.type) {\n        case \"session.created\":\n        case \"session.updated\":\n          if (p.info?.id && p.info.parentID) d.child(p.info.id, p.info.parentID)\n          break\n        case \"message.part.updated\": {\n          // A failed tool call (opencode does not run tool.execute.after for it).\n          const part = p.part\n          if (part?.type === \"tool\" && part.state?.status === \"error\")\n            d.failure(part.sessionID, part.callID, part.tool, part.state.input, String(part.state.error ?? \"\"))\n          break\n        }\n        case \"session.next.tool.failed\":\n          // The same failure on opencode\'s newer session event stream.\n          if (p.sessionID && p.callID) d.failure(p.sessionID, p.callID, undefined, undefined, String(p.error?.message ?? \"\"))\n          break\n        case \"session.idle\":\n          // Also after a halted turn: stop tells it from the last assistant message (and sends `halted`).\n          if (p.sessionID) await d.stop(p.sessionID)\n          break\n        case \"session.deleted\":\n          if (p.info?.id) await d.end(p.info.id)\n          break\n      }\n    },\n\n    dispose: d.endAll,\n  }\n}\n\n// ---- opencode 2.x: `setup(ctx)` registers hooks and reads the event stream.\n\nasync function setup(ctx: any) {\n  process.env.BORG_LISTEN_BINDER = \"opencode\"\n  const d = drones({\n    directory: ctx.location?.directory ?? process.cwd(),\n    // A stop only follows session.execution.succeeded, so the turn finished: nothing to tell apart.\n    lastAssistant: async (id) => {\n      const res: any = await ctx.session.context({ sessionID: id })\n      const msgs: any[] = Array.isArray(res) ? res : (res?.data ?? [])\n      for (let i = msgs.length - 1; i >= 0; i--) {\n        if (msgs[i]?.type !== \"assistant\") continue\n        const text = visibleText(msgs[i].content).trim()\n        if (text) return { text, halted: \"\" }\n      }\n      return { text: \"\", halted: \"\" }\n    },\n    // \"queue\": should the engineer have typed meanwhile, the questions wait for that turn to end.\n    ask: async (id, text) => {\n      await ctx.session.prompt({ sessionID: id, text, metadata: MARK, delivery: \"queue\" })\n    },\n  })\n\n  // Every prompt into a session: the engineer\'s, a subagent\'s task, or the echo of our questions.\n  // The hook is awaited before the prompt is stored. Text added to the prompt would show in the\n  // engineer\'s message, so borg\'s context goes in as a synthetic input without a description: the\n  // drone reads it, the TUI hides it, and it lands just before the prompt it belongs to.\n  await ctx.session.hook(\"prompt\", async (e: any) => {\n    const id = e.sessionID\n    const context = await d.prompt(id, String(e.prompt?.text ?? \"\"), e.metadata?.borg === true)\n    if (!context) return\n    try {\n      await ctx.session.synthetic({ sessionID: id, text: context.text, delivery: e.delivery, resume: false })\n      d.shown(id, context)\n      note(`${id}: borg\'s prompt context added (${context.text.length} chars)`)\n    } catch (err) {\n      // borg already counts it shown (a message\'s seen claim): the next call or prompt carries it.\n      d.get(id).late.unshift(context)\n      note(`adding borg\'s prompt context to ${id} failed: ${err}`)\n    }\n  })\n\n  await ctx.tool.hook(\"execute.before\", async (e: any) => {\n    await d.preTool(e.sessionID, e.id, e.tool, e.input)\n  })\n\n  await ctx.tool.hook(\"execute.after\", async (e: any) => {\n    if (e.status !== \"error\") {\n      // Awaited before the next step. A steer, not the tool result: the TUI shows the result.\n      const t = await d.postTool(e.sessionID, e.id, e.tool, e.input, resultText(e.result), e.result?.metadata?.exit ?? e.metadata?.exit)\n      if (!t) return\n      try {\n        await ctx.session.synthetic({ sessionID: e.sessionID, text: t.text, delivery: \"steer\", resume: false, metadata: MARK })\n        d.shown(e.sessionID, t)\n        note(`${e.sessionID}: borg\'s tool context steered in (${t.text.length} chars)`)\n      } catch (err) {\n        // borg already counts it delivered: the next call or prompt carries it.\n        d.get(e.sessionID).late.unshift(t)\n        note(`steering borg\'s tool context into ${e.sessionID} failed: ${err}`)\n      }\n      return\n    }\n    const cause = e.error?.error\n    // `opencode run`\'s own auto-reject arrives as the same error: no feedback, an ordinary failure.\n    const feedback =\n      cause?._tag === \"Permission.CorrectedError\" && typeof cause.feedback === \"string\" && cause.feedback.trim() !== AUTO_REJECTED ? cause.feedback : undefined\n    d.failure(e.sessionID, e.id, e.tool, e.input, feedback !== undefined ? corrected(feedback) : String(e.error?.message ?? \"\"))\n  })\n\n  const events = new AbortController()\n  void (async () => {\n    try {\n      for await (const event of ctx.event.subscribe({ signal: events.signal })) {\n        const e: any = event\n        const p = e?.data ?? {}\n        switch (e?.type) {\n          case \"session.created\":\n            if (p.sessionID && p.parentID) d.child(p.sessionID, p.parentID)\n            break\n          case \"session.tool.failed\":\n            // A call the engineer declined without feedback: no execute.after, the turn is interrupted.\n            // Its DECLINED reaches borg as Claude\'s plain rejection (canonicalError).\n            if (p.sessionID && p.id && p.error?.type === \"aborted\") d.failure(p.sessionID, p.id, undefined, undefined, String(p.error.message ?? \"\"))\n            break\n          // Each busy period ends with exactly one of these; only a finished turn gets a stop\n          // (Esc, a provider error or a declined call halt it: Claude\'s Stop hook skips interrupts too).\n          case \"session.execution.succeeded\":\n            if (p.sessionID) await d.stop(p.sessionID)\n            break\n          case \"session.execution.failed\":\n          case \"session.execution.interrupted\":\n            if (p.sessionID) {\n              note(`${p.sessionID} halted (${e.type.slice(\"session.execution.\".length)} ${p.reason ?? p.error?.type ?? \"\"}): no stop`)\n              d.idle(p.sessionID)\n            }\n            break\n          case \"session.deleted\":\n            if (p.sessionID) await d.end(p.sessionID)\n            break\n        }\n      }\n    } catch (e) {\n      if (!events.signal.aborted) note(`event stream failed: ${e}`)\n    }\n  })()\n\n  // The plugin is unloaded (reload, location closed, shutdown): end every session borg has heard of.\n  return async () => {\n    events.abort()\n    await d.endAll()\n  }\n}\n\n// One module for both: opencode 2 reads `id` and `setup`, opencode 1 (>= 1.18.29) `id` and `server`.\nexport default { id: \"borg\", setup, server } satisfies PluginModule & { setup: typeof setup }\n";