import { tool, type Plugin } from "@opencode-ai/plugin"; import { join } from "node:path"; import { createOpenCodeBackend, defaultRunsDir, buildOpenCodeCompletionEvent } from "../backends/opencode.ts"; import { isAutopilotOn, opencodeHostConfigDir, resolveStateDir, autopilotCommand, autopilotModeMessage, loadAutopilotConfig, type AutopilotConfig, probeStateDir, logStateDirProbe, type StateDirProbeResult } from "../config.ts"; import { createScheduleManager, scheduledOffDue } from "../framework/scheduled-off.ts"; import { CONTRACTS } from "../tools/contracts.ts"; import { Autopilot } from "../core.ts"; import { loadStoreOrNew, ensureMigrated } from "../queue-store.ts"; import { queueList, queueAdd, queueUpdate, queueDispatch, queueReview, queueSteer, repoCheck, type QueueOpsCtx, type ToolResult, } from "../tools/queue-ops.ts"; import { installOpenCodeAgent } from "../agents/install.ts"; import { createFrameworkRunner } from "../framework/runner.ts"; import { flagForReview } from "../framework/flag-review.ts"; import { logTickDelivery } from "../framework/tick-telemetry.ts"; // opencode-plugin.ts — the opencode HOST (one file per host, symmetric with // hosts/pi-extension.ts). Two layers in one file: // // 1. HOST LOGIC — createOpenCodeFramework: wires the opencode backend + the // six shared queue tools (tools/queue-ops.ts) + completion (backend // process-exit → core.handleAsyncComplete → queue flips + Verdict: // PASS/FAIL/NO-CHANGE routing) + the periodic capacity sweep + the framework agent // projections. No @opencode-ai/plugin dependency — the logic is the // framework's, testable without the opencode runtime. // 2. ADAPTER — OrchestratorAutopilot (the Plugin opencode loads): maps the // tools to the tool() API, tracks the orchestrator session via the event // hook, and delivers ticks into it (client.session.promptAsync). // // Wire `orchestrator-autopilot` into opencode.jsonc's `plugin` array (npm // spec) or point it at this file in a local config; the state dir resolves // from the config dir's /orchestrate command per mode layer (see // opencodeStateDir — the SAME store the TUI decision panel reads). // // TICK DELIVERY (verified): the sweep → onTick → promptAsync pipeline works // end-to-end (the API accepts the injection). LIMITATION: a headless one-shot // `oc run` never PROCESSES queued prompts (it runs its initial prompt and // exits), so live ticks only reach a persistent session — the interactive TUI // orchestrator (opencode's prompt_async is the async-prompting path live // sessions consume). A serve-based orchestrator would also receive them. export interface ArgSpec { name: string; type: "string" | "number" | "boolean"; required: boolean; description?: string; } export interface OpenCodeToolDef { description: string; args: ArgSpec[]; execute(args: Record): Promise; } export interface OpenCodeFrameworkOptions { stateDir: string; /** This host's config dir ($OPENCODE_CONFIG_DIR) — the state-dir probe's * host-parity input (its autopilot.host.json declares the profile's state * dir). Omit when the host has no config dir. */ hostConfigDir?: string; runsDir?: string; ocBin?: string; config?: Partial; /** Tick delivery — the plugin's promptAsync injector (session-targeted). */ delivery?: TickDelivery; /** Domain-event sink (orch:reviewer-dispatched, orch:verdict, ...). */ onDomainEvent?: (e: { name: string; data?: Record }) => void; /** STATE-DIR PROBE (AUTOPILOT-3) warning hook — called once at framework * startup when the resolved dir is unhealthy (missing store, promised-but- * absent config, host-parity mismatch). The plugin logs it loudly; a host * with a UI can additionally notify. */ onProbeWarning?: (r: StateDirProbeResult) => void; sweepIntervalMs?: number; // 0 disables the timer (default 10 min, like the pi extension) } export interface OpenCodeFramework { backend: ReturnType; autopilot: Autopilot; tools: Record; /** The scheduled-shutdown timer owner (framework module) — the autopilot * tool arms/cancels it from the shared command result. */ schedules: ReturnType; /** The orchestrator's session settled (session.idle) → settled sweep. */ onSettled(): void; /** Explicit activation sweep (/autopilot on) — the harness fills free slots * immediately instead of waiting for the first idle/timer (AUTOPILOT-9). */ activate(): void; /** Feed a session event (busy tracking + settled + tick target). */ handleSessionEvent(event: unknown): void; dispose(): void; } export function createOpenCodeFramework(opts: OpenCodeFrameworkOptions): OpenCodeFramework { const stateDir = opts.stateDir; const runsDir = opts.runsDir ?? defaultRunsDir(); const cfg = loadAutopilotConfig(stateDir); ensureMigrated(stateDir); // legacy state.md → queue.json (host-agnostic; a no-op without one) // KEY: AUTOPILOT-116 — the engine gets the WHOLE resolved host config (`cfg`), // not a hand-picked subset. Before this, the opencode host built the engine // with `{ stateDir, ...opts.config }` and NO production caller supplies // `opts.config`, so maxSlots / queueLowThreshold / workerAgents / // reviewerAgents / reviewCap / zombieGraceMinutes / intakeSuppressionHours // ALL silently ran the engine's built-in defaults while the file's own // values were accepted and ignored. Spreading the resolved config is the // class-ending fix: every engine key that loadAutopilotConfig resolves // reaches the engine, so a future key cannot go inert here without a // conscious removal. `opts.config` stays the LAST-wins test/injection seam // (it was never supplied in production). Do NOT go back to naming keys one // at a time — that is exactly how this defect (and two earlier single-key // instances) survived. const autopilot = new Autopilot({ stateDir, ...cfg, ...opts.config }); // STATE-DIR PROBE (AUTOPILOT-3) at STARTUP (plugin load = this host's // session start): verify the resolved dir is REAL before anyone operates on // a phantom/empty store (observed three ways: pre-migration projections → // empty-queue phantoms; a missing config → unreadable workspace facts; // drifted env/fallback vs the orchestrator's projection). ONE telemetry // log; NEVER throws — findings are reported (console = the opencode server's // log; a host with a UI can notify via onProbeWarning), the harness keeps // running fail-open. const probe = probeStateDir({ stateDir, hostConfigDir: opts.hostConfigDir, expectWorkspaceConfig: true }); if (!probe.ok) { logStateDirProbe(stateDir, probe, { hook: "opencode-startup" }); for (const f of probe.findings) console.warn(`[orchestrator-autopilot] state-dir probe: ${f}`); opts.onProbeWarning?.(probe); } const storeOrNew = () => loadStoreOrNew(stateDir); const isHeadlessRun = process.argv.includes("run"); // oc run = one-shot (no live ticks) let busy = false; // session.status busy/idle tracking // The completion event is built by the BACKEND (the run record + the last // text live there; the pi backend owns the same seam via buildCompletionEvent). // This host only wires the backend's onComplete to the shared runner. const backend = createOpenCodeBackend({ runsDir, ocBin: opts.ocBin, onComplete: (runId, rec) => runner.onCompletion(buildOpenCodeCompletionEvent(runId, rec) as never) }); // The shared runner: identical trigger machinery in both hosts (see // src/framework/runner.ts). The opencode host supplies its gate state + // the promptAsync delivery; the pi host supplies the same shape with // sendMessage. // Scheduled shutdown ("off in ") — parsing, persistence and the due // check are the framework's (config.autopilotCommand + scheduledOffDue); // this manager only owns the in-process timer whose firing replays today's // immediate-"off" delivery. The plugin process persists, so its armed timer // fires exactly; the enabled() backstop covers a restart. /** The ONE fire path (timer AND enable-gate backstop): the framework's * due-check flips OFF + clears the persisted deadline first, so the * delivery below happens EXACTLY ONCE per schedule. */ const fireScheduledOffIfDue = (sid: string): void => { try { if (!sid || !scheduledOffDue(stateDir, sid)) return; opts.delivery?.deliver(autopilotModeMessage("off")); } catch { // silent — a delivery failure must not break the framework's OFF flip } }; const schedules = createScheduleManager({ onFire: fireScheduledOffIfDue }); const runner = createFrameworkRunner({ stateDir, autopilot, backend, host: { interactive: () => !!opts.delivery?.target() && !isHeadlessRun, loaded: () => true, // the plugin IS the orchestrator context in opencode busy: () => busy, compacting: () => false, }, deliver: (message) => opts.delivery?.deliver(message), emit: (e) => e.forEach((x) => opts.onDomainEvent?.(x as { name: string; data?: Record })), // The toggle means the same as on pi: OFF stops the PROACTIVE lane // (auto-dispatch / auto-review / auto re-dispatch / recovery / the intake // tick) while the INFORMATIONAL ticks and completion recording keep // running — the runner owns that class split (KEY: AUTOPILOT-120). The // session id is the tick-delivery target (the session this plugin serves). enabled: () => { const sid = opts.delivery?.target() ?? ""; // Scheduled-off BACKSTOP: a persisted deadline in the past flips OFF // before any sweep runs — covers a process restart losing the armed // timer. fireScheduledOffIfDue(sid); return isAutopilotOn(stateDir, sid); }, sweepIntervalMs: opts.sweepIntervalMs ?? cfg.sweepIntervalMs, }); const ctx: QueueOpsCtx = { stateDir, backend, storeOrNew, autopilot: () => autopilot, cfg: () => loadAutopilotConfig(stateDir), emit: (e) => e.forEach((x) => opts.onDomainEvent?.(x as { name: string; data?: Record })), repoCheck, sessionCwd: process.cwd(), }; const def = (description: string, args: ArgSpec[], execute: (a: Record) => Promise): OpenCodeToolDef => ({ description, args, execute }); const tools: Record = { queue_list: def( "List the orchestrator task queue (opencode host). Filter by status/since/sort; compact view by default, heavy fields only with includeNotes. Returns per-status counts + fleet occupancy + matching items.", [ { name: "status", type: "string", required: false, description: "status filter (proposal|approved|active|ai-review|human-review|failed|done)" }, { name: "since", type: "string", required: false, description: "ISO timestamp — only items with updatedAt >= since" }, { name: "sort", type: "string", required: false, description: "updatedAt|createdAt|key (default updatedAt desc)" }, { name: "limit", type: "number", required: false, description: "max items (default 50)" }, { name: "includeNotes", type: "boolean", required: false, description: "include scope/evidence/value/urgency/risk/notes" }, ], (a) => queueList(ctx, a), ), queue_add: def(CONTRACTS.queue_add, [ { name: "key", type: "string", required: false, description: "unique key; OMIT to auto-allocate the next sequential id (with `series`, e.g. \"B\" → B-; default series Q)" }, { name: "series", type: "string", required: false, description: "id series for auto-allocation when key is omitted (e.g. \"B\" → B-)" }, { name: "title", type: "string", required: true }, { name: "status", type: "string", required: false, description: "proposal (default) | approved" }, { name: "scope", type: "string", required: false, description: "draft worker scope (free-form); " + CONTRACTS.approval_gate_param }, { name: "cwd", type: "string", required: false, description: "repo the worker runs in; " + CONTRACTS.approval_gate_param }, { name: "timeoutMs", type: "number", required: false, description: "requested wall-clock budget (ms) — recorded on the item and passed to every spawned run (dispatch, re-dispatch, review)" }, { name: "model", type: "string", required: false, description: CONTRACTS.model_param }, { name: "tier", type: "string", required: false, description: CONTRACTS.tier_param }, { name: "after", type: "string", required: false, description: `${CONTRACTS.after_param} (comma-separated keys)` }, { name: "evidence", type: "string", required: false }, { name: "value", type: "string", required: false }, { name: "urgency", type: "string", required: false }, { name: "risk", type: "string", required: false }, { name: "notes", type: "string", required: false }, ], (a) => queueAdd(ctx, a), ), queue_update: def(CONTRACTS.queue_update, [ { name: "key", type: "string", required: true }, { name: "status", type: "string", required: false }, { name: "blocker", type: "string", required: false }, { name: "title", type: "string", required: false }, { name: "scope", type: "string", required: false }, { name: "cwd", type: "string", required: false, description: "the repo the work lands in; " + CONTRACTS.approval_gate_param }, { name: "completedRun", type: "string", required: false, description: "COMPLETED-deliverable evidence for blocked → ai-review: the run id that finished this item's work (recorded as the item's runId). The transition is REFUSED without a completed run (runId/lastRunId) or a recorded branch/sha (runWorktree/landedEvidence/shippedAt) — a parked item must not become reviewable with no run that produced anything" }, { name: "evidence", type: "string", required: false }, { name: "value", type: "string", required: false }, { name: "urgency", type: "string", required: false }, { name: "risk", type: "string", required: false }, { name: "timeoutMs", type: "number", required: false, description: "requested wall-clock budget (ms) recorded on the item; every dispatch/review passes it to the spawned run" }, { name: "model", type: "string", required: false, description: CONTRACTS.model_param }, { name: "tier", type: "string", required: false, description: CONTRACTS.tier_param }, { name: "dispatchClass", type: "string", required: false, description: "worker (default) | finisher — finisher declares the run writes OUTSIDE its worktree, into the declared cwd's checkout (merge finisher)" }, { name: "finisherSource", type: "string", required: false, description: "for a finisher item: WHAT it lands (branch/tag/sha in the cwd). Required for landed evidence — without it a finisher's runtime failure verdict stands as usual" }, { name: "overrideReason", type: "string", required: false, description: "RECORD an override of the run's failure verdict on the item (append-only overrides[]) instead of hand-writing it into notes — say WHY the failure is not believed" }, { name: "after", type: "string", required: false, description: `${CONTRACTS.after_param} (comma-separated keys)` }, { name: "notes", type: "string", required: false }, { name: "findings", type: "string", required: false, description: "REQUIRED to re-dispatch a human-review item (status:'active'): what needs fixing. Sets the structured handoff the harness's human re-dispatch sweep reads to spawn the redo — do not also call queue_dispatch" }, ], (a) => queueUpdate(ctx, a), ), queue_dispatch: def(CONTRACTS.queue_dispatch, [ { name: "key", type: "string", required: true, description: "queue key of an approved item" }, { name: "task", type: "string", required: true, description: "the scoped worker prompt (self-contained; KEY: as first line is recommended)" }, { name: "cwd", type: "string", required: false, description: "repo the worker operates on (worktree isolation runs THERE). REQUIRED when the session cwd is not the target repo." }, { name: "timeoutMs", type: "number", required: false }, { name: "model", type: "string", required: false, description: CONTRACTS.model_param }, { name: "tier", type: "string", required: false, description: CONTRACTS.tier_param }, { name: "dispatchClass", type: "string", required: false, description: "worker (default) | finisher — use 'finisher' for a MERGE FINISHER that writes into the cwd's checkout instead of its worktree; success is judged on the declared source landing there, not on worktree edits" }, { name: "finisherSource", type: "string", required: false, description: "the branch/tag/sha this finisher must land in cwd (e.g. pi-parallel--0). REQUIRED for the landed-evidence path: without it a runtime 'no edits in the worktree' verdict fails the run as usual" }, { name: "overrideCapacity", type: "boolean", required: false, description: "dispatch even though the fleet is at/above maxSlots. Default false = the dispatch is REFUSED at capacity; true is a deliberate over-subscription (recorded in the result)" }, ], (a) => queueDispatch(ctx, a), ), queue_review: def( CONTRACTS.queue_review, [ { name: "key", type: "string", required: true, description: "queue key of an `ai-review` item" }, { name: "task", type: "string", required: false, description: "optional reviewer prompt; the verdict contract is injected if omitted" }, { name: "timeoutMs", type: "number", required: false }, { name: "model", type: "string", required: false, description: CONTRACTS.model_param }, { name: "tier", type: "string", required: false, description: CONTRACTS.tier_param }, ], (a) => queueReview(ctx, a), ), queue_steer: def( CONTRACTS.queue_steer, [ { name: "key", type: "string", required: true, description: "queue key of the running worker/reviewer" }, { name: "message", type: "string", required: true, description: "the steering instruction (interrupt-and-deliver)" }, { name: "mode", type: "string", required: false, description: "steer (interrupt, default) | follow_up (queued until idle)" }, { name: "ackTimeoutMs", type: "number", required: false }, ], (a) => queueSteer(ctx, a), ), }; // Ensure the framework-owned agents are projected into opencode's agents dir // (idempotent, version-stamped). Never throws — a missing agent surfaces as // a spawn error downstream (queue_dispatch would fail with 'agent not found'). try { installOpenCodeAgent("reviewer", { log: (l) => opts.onDomainEvent?.({ name: "orch:reviewer-installed", data: { log: l } }) }); installOpenCodeAgent("worker", { log: (l) => opts.onDomainEvent?.({ name: "orch:worker-installed", data: { log: l } }) }); } catch { // silent } // Periodic capacity sweep — owned by the shared runner (identical machinery // in both hosts). runner.start(); return { backend, autopilot, tools, schedules, onSettled: () => runner.onSettled(), activate: () => runner.activate(), handleSessionEvent(event: unknown) { const e = event as { type?: string; properties?: { sessionID?: string; status?: { type?: string } } }; const sid = e.properties?.sessionID; if (sid) opts.delivery?.setTarget(sid); if (e.type === "session.status") busy = e.properties?.status?.type === "busy"; if (e.type === "session.idle") { busy = false; runner.onSettled(); } }, dispose() { runner.stop(); schedules.dispose(); // no leaked timers }, }; } type ToolArgsShape = Parameters[0]["args"]; function argsSchema(specs: ArgSpec[]): ToolArgsShape { const out: Record = {}; for (const s of specs) { let v: ToolArgsShape[string] = s.type === "number" ? tool.schema.number() : s.type === "boolean" ? tool.schema.boolean() : tool.schema.string(); if (s.description) v = (v as { describe?: (d: string) => ToolArgsShape[string] }).describe?.(s.description) ?? v; out[s.name] = s.required ? v : (v as { optional?: () => ToolArgsShape[string] }).optional?.() ?? v; } return out; } /** The opencode SDK client surface the delivery needs (injectable for tests). */ export interface PromptClient { session: { promptAsync(options: { path: { id: string }; body: { parts: Array<{ type: "text"; text: string }> } }): Promise; }; } export interface TickDelivery { /** The current tick target session id (undefined until a session event is seen). */ target(): string | undefined; /** Record the session id from an event (session.* events carry it). */ setTarget(sessionID: string | undefined): void; /** Deliver a tick: inject into the target session, or console-fallback. */ deliver(message: string): void; } /** * The opencode tick-delivery mechanism (extracted for hermetic testing): * tracks the orchestrator session id and injects tick messages via * client.session.promptAsync. Never throws — a delivery failure degrades to * the console log. */ export function createTickDelivery( client: PromptClient, log: (line: string) => void = console.log, /** TICK AUDIT TRAIL (AUTOPILOT-51, optional — tests omit it): the ONE * choke point every tick this host delivers passes through. Logging here * with the SAME appendTelemetry writer Autopilot.logEvent uses (no second * logger — see docs/AUTOPILOT-51-tick-audit-gap.md) is what finally gets * `type: "tick"` lines into autopilot.jsonl for the opencode host. */ telemetry?: { stateDir: string; isAutopilotOn: (sessionId?: string) => boolean }, ): TickDelivery { let target: string | undefined; return { target: () => target, setTarget(sessionID) { if (typeof sessionID === "string" && sessionID) target = sessionID; }, deliver(message) { if (telemetry) { logTickDelivery({ stateDir: telemetry.stateDir, message, autopilotOn: telemetry.isAutopilotOn(target), sessionId: target }); } if (!target) { log(`[orch-tick] ${message}`); return; } void client.session .promptAsync({ path: { id: target }, body: { parts: [{ type: "text", text: message }] }, }) .catch((err) => log(`[orch-tick] inject failed: ${err instanceof Error ? err.message : String(err)}`)); }, }; } export const OrchestratorAutopilot: Plugin = async (ctx) => { // The SAME resolution the TUI plugin uses (it adds its api.state.path.config // rung, which this input point does not have). Before this was shared the // server skipped the host-config fact whenever OPENCODE_CONFIG_DIR was unset // while the TUI still read it — two entry points, two stores. const hostConfigDir = opencodeHostConfigDir(); const stateDir = resolveStateDir(hostConfigDir); const delivery = createTickDelivery(ctx.client as unknown as PromptClient, undefined, { stateDir, isAutopilotOn: (sessionId) => isAutopilotOn(stateDir, sessionId), }); const fw = createOpenCodeFramework({ stateDir, hostConfigDir, delivery, }); const { backend, schedules } = fw; // the framework pieces the tools below wire to // `tools` is declared BEFORE its first assignment — assigning first is a // temporal-dead-zone crash that kills the plugin AT LOAD TIME (the queue // tools never even registered). const tools: Record> = {}; tools.autopilot = tool({ description: "Toggle or query the orchestrator autopilot for THIS session. on: enable the proactive harness (auto-dispatch, auto-review, auto-recovery) + all [orch-tick] messages; off: stop the proactive harness + the intake/dispatch nudges, while the INFORMATIONAL ticks (review, failure, budget, decision, harness, ship, main-write, config) keep arriving — or schedule with 'off in ' (e.g. 'in 90s', 'in 30m', 'in 1h30m', max 24h — autopilot stays ON until then); status: show the current state + capacity; capacity : set the worker slot limit. The toggle is per-session.", args: { action: tool.schema.string().describe("on | off | status | capacity"), value: tool.schema.string().optional().describe("capacity value (when action=capacity); for action=off: an optional duration like 'in 30m' to schedule the shutdown"), }, execute: async (args) => { const sid = delivery.target(); const action = String(args.action ?? "").trim(); // ONE shared toggle implementation (config.autopilotCommand) — this host // only returns its message (pi's /autopilot notifies + injects instead). try { // PREREQUISITE PROBE (fail-closed, BEFORE the toggle writes state): // the harness spawns through the backend — without that runtime, // dispatch/review would fail downstream in confusing ways. if (action === "on") { const fleet = await backend.fleetStatus(); if (fleet === null) { return "Autopilot NOT activated: the subagent backend is unreachable (fleet status timed out). Install/enable the required backend extension and retry — the queue tools work without it, but ticks/dispatch do not."; } } const r = autopilotCommand(action, String(args.value ?? "").trim() || undefined, { stateDir, sessionId: sid ?? "" }); if (!r.ok) return r.message; let probeNote = ""; if (r.mode === "on") { // STATE-DIR PROBE (AUTOPILOT-3) at ACTIVATION: the ON message points // the orchestrator at this state dir for workspace facts — verify it // is REAL before the harness relies on it. Fail-open, EXACTLY like // the pi host (hosts/pi-extension.ts): ONE telemetry log, findings // ride the tool return, and control FALLS THROUGH — a finding must // never cost the schedule handling or the activation sweep below // ("never a throw, never a block", docs/queue-model.md). const probe = probeStateDir({ stateDir, hostConfigDir, expectWorkspaceConfig: true }); if (!probe.ok) { logStateDirProbe(stateDir, probe, { hook: "autopilot-on" }); probeNote = `STATE-DIR PROBE: ${probe.findings.join(" | ")}\n\n`; } } if (r.scheduledOffAt !== undefined && sid) { schedules.schedule(sid, r.scheduledOffAt); // arm/replace the in-process timer } else if ((r.mode === "on" || r.mode === "off") && sid) { schedules.cancel(sid); // explicit toggle cancels a pending schedule } if (r.mode === "on") fw.activate(); // activation sweep: the harness fills // free slots immediately instead of waiting for the first idle/timer // (AUTOPILOT-9). Tick delivery is gated on the session, so pre-idle it // only dispatches. return `${probeNote}${r.message}`; } catch (err) { return `autopilot failed: ${err instanceof Error ? err.message : String(err)}`; } }, }); tools.flag_for_review = tool({ description: CONTRACTS.flag_for_review, args: { summary: tool.schema.string().describe("What was done (one line)"), risk: tool.schema.string().describe("low | medium | high"), blast_radius: tool.schema.string().describe("What breaks if this is wrong — be specific"), review_targets: tool.schema.array(tool.schema.string()).describe("WHERE to review: file paths, path:line ranges, commit SHAs, MR/PR links"), self_reviewed: tool.schema.boolean().describe("true if an automated review passed, else false"), review_method: tool.schema.string().describe("commit-hook | reviewer-subagent | none"), action_needed: tool.schema.string().optional().describe("What the user should DO after reviewing"), residual_risks: tool.schema.string().optional().describe("What was NOT verified / could change"), queue_key: tool.schema.string().optional().describe("The queue item this flag came from"), }, execute: async (args) => { const { deliveryNote } = flagForReview(args as never, { logPath: join(stateDir, "reviews.jsonl") }); return `Flagged for review. Risk: ${args.risk}. ${deliveryNote}`; }, }); for (const [name, def] of Object.entries(fw.tools)) { tools[name] = tool({ description: def.description, args: argsSchema(def.args), execute: async (args) => (await def.execute(args as Record)).text, }); } return { tool: tools, // Feed session events to the framework: busy tracking, the tick target, // and the settled sweep (session.idle). event: async ({ event }) => { fw.handleSessionEvent(event); }, // opencode unloading (config reload / shutdown) stops the runner timer + // any scheduled-off timers — no leaked intervals across sessions. dispose: async () => { fw.dispose(); }, }; };