feat(provider-opencode-go): add OpenCode Go Chat Completions provider worker - #1
Open
faramirezs wants to merge 165 commits into
Open
feat(provider-opencode-go): add OpenCode Go Chat Completions provider worker#1faramirezs wants to merge 165 commits into
faramirezs wants to merge 165 commits into
Conversation
skill-check — worker0 verified, 60 skipped (no docs/).
Three for three. Nicely done. |
OpenCode Go Chat Completions provider behind llm-router. Implements the provider protocol: stream (SSE chunks to AssistantMessageEvent frames), abort, refresh_models (live GET /v1/models enriched with models.dev metadata: context window, reasoning efforts, tool/structured-output capability), and re-declaration on router::ready. Chat Completions wire format only, max_completion_tokens, strict json_schema structured output, reasoning_effort low/medium/high for deepseek-/kimi-k2.7- families. Wired into create-tag/release workflows and the harness worker deps.
README to the provider family structure (Behavior/Tests/Running), manifest tags+description to the canonical form, release wiring in alpha-release.yml and discover_changed_workers.py, llm-router README reference note.
Upstream MOT-4335 rewrote all provider identity prompts to teach the live
surface (register_trigger, harness::spawn, orchestrator: true) and stripped
orchestration-process doctrine. The fork-PR merge with the new main runs the
harness prompts sweep over every shipped prompt, which failed on our pre-
rewrite copy ("delegation is one-way" etc.). Absorb the rewritten prompt,
update the register.rs identity assertions, and apply the iii-state -> state
rename.
faramirezs
force-pushed
the
feat/provider-opencode-go
branch
from
August 4, 2026 11:10
099ac93 to
20201db
Compare
…pulation (iii-hq#689) * fix(dashboard): filter efficiency sparklines to the comparable cohort The sparklines summed raw per-scenario averages across every reported scenario, so a missing report or a newly added scenario moved the line for structural reasons while the delta chip on the same card honestly compared only the comparable cohort. Sum the same cohort the chip uses and skip executions that lack any cohort contract instead of fabricating a dip. * fix(dashboard): make efficiency cards read one coherent population The stat tiles mixed three quantities: the headline summed every reported scenario, the delta chip compared only the comparable cohort, and the sparkline plotted raw sums, so the three elements of one card could contradict each other. Value, delta, and trend now all read the comparable cohort; the sparkline gains a dashed baseline-median reference and per-point hover values, the delta names its baseline, and the section copy states the population once. * feat(dashboard): show the baseline value and enrich sparkline hover Each efficiency card now prints its baseline median under the delta chip, the dashed baseline line carries a tooltip explaining the 7-run median, and each sparkline point's hover shows run id, date, value, and its delta against the baseline.
* chore: make E2E score advisory * docs(harness): document empty reports as CI-blocking has_ci_blocking_failure() also blocks an empty report; state that alongside hard-gate and technical failures in the CI summary and README so operators do not misread a missing result as an advisory quality failure.
- iii-permissions.yaml: deny provider::opencode_go::abort (agents must not cancel router-owned streams; matches provider-claude-code) - sse.rs: relay delta.reasoning_content as thinking blocks (the OpenCode Go wire emits it, live-verified); bound tool-call index to 64 (malformed upstream could grow the vec unboundedly) - upstream.rs: data_line per SSE spec — accept data: without a space and join repeated data: lines instead of silently dropping output - curated.rs: per-model max_output_tokens from models.dev limit.output; 4096 stays the unknown-id fallback - reasoning.rs: Minimal maps to minimal then none (gpt-5.6-luna floor) - register.rs: log router::ready trigger registration failures - router_client.rs: narrow module doc claim - README: repair split provider-openai table row; III_WS_URL -> III_URL (code + engine convention); thinking-delta relay note
…ii-hq#693) The provider-deepseek worker (iii-hq#691) resolves its credential from the DEEPSEEK_API_KEY env on the router, but the E2E workflows only forwarded the anthropic/openai/zai secrets, so a deepseek subject or judge could never discover models in CI. Declare the optional secret in the reusable workflow, forward it from every caller (including the release pipeline's E2E gate from iii-hq#692), add the provider to the Rust cache workspaces, and export it in both run steps.
…le llm-router config UI (iii-hq#695) * (MOT-4353) feat(console): standalone call cards, trigger/notification cards with terminal tabs Every function call renders as its own card (accordion group removed); consecutive call-like cards stack tight. Collapsed headers carry an args preview; errored calls say 'failed' (gate denials stay verb-less). Trigger-fired and notification rows share the card language: verb-first headers, TERMINAL/RAW JSON tabs, registration recovery (harness rows -> transcript register call, entry-id-exact notification correlation), CONDITIONS met chip, copy on every JSON pane. RegisterTriggerView renders gating conditions as an ONLY IF section. Assistant header renamed 'agent'. * (MOT-4353) feat(llm-router): injectable console config form with env-var key guidance Console-injected configuration UI (docs/sops/injectable-console-ui.md): per-provider credential cards with plain-text api_key detection steering to ${ENV_VAR} references (one-click fix chip, partial-reference warning, env refs unmasked / secrets masked), console Select for provider pickers, stream budgets with human-readable echoes, and a system-prompt override flow that shows the provider-declared prompt. Assets built by build.rs (esbuild, SKIP_UI_BUILD escape) and embedded in the binary via the shared iii-console-ui crate. * (MOT-4353) fix(console): size the worker-config dialog to its content column 1180px dialog around a max-w-3xl form was ~400px of dead margin; 880px fits the column + padding, and narrow viewports keep the 100vw-2rem cap.
…c stale worker locks (iii-hq#697) * chore: regenerate provider lock files for llm-router 1.4.1 The llm-router 1.4.1 bump (531b140) updated only the router's own manifest, leaving every provider worker's Cargo.lock pinning a stale llm-router path-dependency version (1.4.0, and 1.3.2 for llamacpp/xai). The Harness E2E build compiles providers with --locked, so the first E2E run on main after the bump failed before any scenario started: 'cannot update the lock file provider-deepseek/Cargo.lock'. Sync all nine provider locks; each resolves cleanly under --locked. * fix(llm-router): gate the console UI behind a default-on feature llm-router 1.4.1's build.rs shells out to pnpm to bundle the injectable console UI, so every worker that path-depends on the router library (all nine providers) started requiring a Node toolchain just to compile llm_router::types — which broke their CI lint/test and boot-smoke jobs, whose runners only carry Rust. Introduce a default-on 'console-ui' feature carrying ui.rs, the iii-console-ui dependency, and the build.rs bundle step. Providers depend on the router with default-features = false, dropping pnpm from their build graphs entirely. The router's own binary is unaffected. Also sync the lock files left stale by today's version bumps: the nine provider locks (llm-router 1.4.1) and approval-gate (harness 1.7.0).
…terface gate (iii-hq#698) The harness 1.7.0 candidate smoke failed on untyped schemas from iii-stream — an engine-hosted builtin the harness install enables for console streaming, which landed in the workers-baseline diff and was gated as if it were part of the released surface. Engine builtins are not installable from the registry and their schemas are not this repo's to fix: exclude them from target-worker resolution. Also run the smoke's interface-gate scripts from the pipeline ref instead of the release tag, so a dispatched re-run of an already published candidate (release SOP § re-run) picks up gate fixes without re-tagging. On tag pushes both refs coincide, so tag-triggered runs are unchanged.
…ld (iii-hq#700) The harness 1.7.1 release died in the deployed-E2E validation packaging harness-e2e-stack.tar.gz: the registry-mode stack build fills a standard runner's 14GB of free disk (ENOSPC, reproduced on rerun). Drop the preinstalled toolchains this job never uses (~30GB: dotnet, Android, GHC, CodeQL, docker images) before building.
…runner (iii-hq#701) Registry mode builds only harness/target/release/harness-e2e; nothing creates the repo-root target/ directory the packaging step writes into (source mode gets it from the engine install). Both harness release attempts died on this step: 1.7.1 masked as ENOSPC by the full runner disk, 1.7.2 as a clean ENOENT once iii-hq#700 freed the space. The registry E2E path has never executed past this point.
* fix(ci): coerce registry E2E runs input * fix(ci): decouple E2E validator from source ref * fix(ci): serialize registry E2E provisioning
… drop the console trigger poll (iii-hq#775) * (MOT-4401) feat(harness,console): push trigger-binding change events, drop the console trigger poll Every open console chat tab polled harness::triggers::list on a 5s interval (each call a full-scope harness::state::list on the state worker). Replace the poll with a harness::triggers-changed doorbell — the same pattern iii-hq#452 used to kill the status poll: - harness: 5th event trigger type; {session_id, timestamp} emitted from the four BindingStore CAS primitives (reserve, attach_trigger_id, claim_fire, delete_if_unchanged), so every mutation path incl. the out-of-turn ones (expiry sweep, GC, other-tab unregister, deferred release) rings from one layer. - console: onTriggersChanged backend hook (shape of onQueuedMessage); ChatView swaps the interval for initial fetch + doorbell subscription + visibilitychange catch-up. Always-on for the selected conversation, unlike the streaming-gated queued subscription. Version skew is graceful both ways: an old harness parks the unknown trigger-type registration (console degrades to mount/visibility/post- unregister refetches); an old console keeps polling a new harness. * (MOT-4401) fix(console): coalesce trigger refetches, close subscription gaps Review fixes on iii-hq#775: - serialRefresh: doorbells arrive at-least-once and burst on rapid fires; refetches now serialize behind one in-flight list read with a single trailing rerun, so an older snapshot can never resolve after and overwrite a newer one. reset() discards in-flight responses on conversation switch/unmount (also fixes the pre-existing late-write into a switched conversation). - ChatView subscribes to the doorbell BEFORE the initial snapshot, so a mutation in the setup gap rings instead of being missed. - realOnTriggersChanged reseeds on every 'connected' transition: the SDK replays trigger registrations on reconnect but outage-window doorbells are gone (same repair TracesV2 uses). The reseed is ordered after the SDK's synchronous registration replay, so it also covers the first connect.
…q#771) * feat(code-runner): run untrusted Node.js and Python in-process One worker exposing `sandbox-code-runner`'s API — `run` / `register_function` / `teardown` / `inject-guidance` — over two in-process engines: untrusted JavaScript in deno_core V8 isolates, and untrusted Python as CPython compiled to WebAssembly inside wasmtime. No microVM and no /dev/kvm, which is the reason it exists. The engine internals are two new bus-free libraries, `crates/node-core` (`iii-node-core`) and `crates/python-core` (`iii-python-core`). Neither depends on `iii-sdk` at all — not feature-gated, dependency absent — so neither can collide with the pin the workers carry. `code-runner` owns the only bus seam and translates core errors into its own taxonomy. The load-bearing semantic, inherited from `sandbox-code-runner`: a failing script is a RESPONSE, not an error. A tenant exception returns a resolved call with `exit_code: 1` and the traceback in `stderr`; errors are reserved for infrastructure. Host-derived kill signals (`timed_out`, memory, disk) are checked BEFORE any guest-written byte is parsed, because a tenant controls every byte under its output directory and could otherwise forge past an infrastructure kill. Both languages get a capped private working directory and a guest `iii` global. Node reaches the host through ops; Python cannot — `python.wasm` imports one module and exports only `_start` — so its bridge rides `fd_write`/`fd_read` with sentinel framing, and the host publishes registered handlers on the guest's behalf. A kept Python runtime persists its interpreter as well as its files: the wrapper parks on stdin between calls rather than letting `_start` return, so globals stay bound and modules stay imported. Function ids from both engines are claimed in ONE registry, and not under the bare namespace — that is node's own pre-runtime placeholder owner, and a python id claimed there is silently reclaimed by node and then aborts the process from `op_iii_register`'s non-unwinding V8 callback. 366 tests (node-core 217, python-core 86, code-runner 63), fmt and clippy clean, plus a 24-case e2e suite that runs both languages against a real engine. Every containment test names the mutation that makes it fail. * feat(code-runner): injectable console UI for the three ops Ships two assets into any running console — `code-runner/page.js` over `console:script` and `code-runner/styles.css` over `console:style` — built from `ui/` by esbuild and embedded in the binary, so there is nothing to install and nothing to serve separately. One function-trigger renderer per op replaces the console's raw-JSON card, which turns `code` into a single escaped line and buries the verdict. Ported from `sandbox-code-runner/ui`, which serves the same API, with three deliberate divergences for this worker's wire: **`result` gets its own block.** The completion value is the field this worker's wire adds, and a null one is information rather than an absence (run.rs never skips it), so it renders explicitly with the engine's return convention beside it — node code is a function body (`return 2 + 2`), python code is a module (assign `result`). That mismatch is the usual reason a call "worked" and came back null, and it was previously invisible. **No network chip.** `sandbox-code-runner`'s `network` is a real create-time flag; this worker has no such field because neither engine has any network. An "off" chip would imply a knob exists, so there is none — asserted by a test, since the port would otherwise carry it silently. **In-process wording throughout.** Teardown disposes a V8 isolate or a CPython interpreter, not a microVM; a python namespace's interpreter is pinned, so teardown-by-namespace is the only thing that reclaims it. `runtime_id` is never rendered in full — it is a capability, so it appears only as a truncated click-to-copy chip, and every other string is filtered first: stdout, stderr, the error messages that quote it by design, the submitted source, the completion value, and the `raw json` tab the console mounts regardless of what a card does. Adding the UI reopened a process-abort hole, found by probing rather than assumed: `ConsoleUi` publishes `code-runner::ui-content`, which was not in `STATIC_IDS` and so was not seeded into the id registry. Because `register_function` lets a caller choose any namespace — `code-runner::` included — an unseeded worker id is claimable, and the claim reaches the SDK's `register_function` on an already-registered id, whose duplicate-id panic aborts the process from a non-unwinding V8 callback. `register_function("code-runner::ui-content", …)` returned `registered: true` before the fix. There is now a `seeded_ids()` that is strictly larger than `STATIC_IDS`, used by both production and the test harness so they cannot diverge, and a test that walks every owned id. 72 worker tests (up from 64) and 11 vitest cases; the mutations for the seeding fix, the result redaction, the null-result block and the convention hint were each verified to fail their test. fmt, clippy, both CI validators and biome on `ui/` all clean. * fix(code-runner): stop showing guests and callers the retired node-engine name Observed live: an agent wrote Node-idiom `global.counter`, got `ReferenceError: global is not defined`, and the stack frame said `[node-engine:eval]` — a worker name that no longer exists on the bus. Sweeping that defect class off every user-visible surface: - guidance: name the `global`/`globalThis` trap (`lang: "node"` primes the Node mental model, and 'NOT Node' alone didn't transfer), and quote the REAL id-clash wire code — `code-runner::invalid_request`, not the `id_taken` code this worker's taxonomy never had - script origins: `[code-runner:eval]`/`:prelude`/`:invoke`/`:namespace`, with the definition-error frame matcher renamed in lockstep (mutation re-verified: a leaking formatCause fails the test on the NEW names) - prelude: `iii.shutdown()` now points at `code-runner::teardown` (the old message named a function nobody can call), `String(iii)` introduces a code-runner host client, registerFunction's http refusal ditto - default namespace mint: `code-runner::<runtime_id>::` — guests were publishing live bus ids under the retired prefix - error seams: NodeEngineError::message() is now pub, and ops + translate surface it instead of Display — callers saw double-coded messages like `code-runner::invalid_request: node-engine::id_taken: …`; new test pins that re-coded messages never carry the core's own prefix - tenant log stream: the truncation notice, thread names, tempdir prefix and tracing target follow The core's internal `node-engine::<code>` tags stay: after the seam fix they reach no user surface, and re-coding is the worker's job. * docs(code-runner): RUN_DESC no longer claims register_function is node-only Stale since the python registration path landed; the catalog description steered agents away from a working feature. Golden regenerated — the one description string, no schema drift. * feat(code-runner): register_function carries request/response schemas to the catalog engine::functions::info showed REQUEST/RESPONSE "any" for every dynamic registration: the SDK auto-extracts schemas from the handler's types, and a dynamic handler is Value → Value. The SDK's escape hatch already existed — RegisterFunction::request_format/response_format override the extraction — nothing here reached it. Two optional wire fields, `request_format`/`response_format` (a superset of sandbox-code-runner's contract, like `result`), threaded to that builder: - python: one hop — the host publishes directly through `Engine::register`, which gains the two params (IIIEngine, FakeEngine, TestBus follow) - node: the whole guest chain — wire → `wrap_register` embeds them as JSON literals in the generated `__def` → the prelude's options object → `op_iii_register` (two new args, "" = absent) → `Engine::register` - byproduct: guest code gets the same surface for free — `iii.registerFunction(id, h, {request_format, response_format})` One validation rule at both trust boundaries (wire::register:: validate_format, shared so they cannot drift): a JSON OBJECT carrying at least one schema-defining keyword (an empty object IS the "any" being replaced), 16 KiB serialized cap. The worker wire checks before anything is claimed or booted; the op re-checks because the prelude's own check runs on tenant-replaceable builtins. The op's redeploy early-return is extended: it skipped the bus write whenever the DESCRIPTION was absent, which would silently discard formats — re-registration is a wholesale metadata replacement, never a merge, and the comment now says so. Mutations run for real: node arm dropping the fields from core_req → the both-languages capture test fails; the swap-branch condition left on description alone → the redeploy-republishes test fails. The final hop (IIIEngine's builder calls) has no unit seam — the new e2e case covers it against a live engine by reading functions::info back. node-core 227 tests (was 217), code-runner 68+7 (was 66+7); goldens regenerated (new request fields + REGISTER_DESC sentence, no other drift); guidance, README and worker catalog updated. * docs(code-runner): say schemas are not enforced; teach both return conventions up front Two dogfooding findings, both documentation holes: - request_format/response_format read as a contract but are catalog metadata: nothing on this bus validates payloads against them, and an agent that reads the schema in functions::info skips its own input checks. REGISTER_DESC and the guidance now say so and tell handlers to validate. - the return-vs-result convention was taught only reactively (the null card, python's SyntaxError in stderr): the injected guidance mentioned python ZERO times, and RUN_DESC never stated either convention, so an agent's first python run was written blind. RUN_DESC now names both (node = function body, return x; python = module, result = x) and the guidance gains its first python clause — lang, conventions, sync iii, /work. Guidance needles pin both clauses; goldens regenerated, descriptions only. * feat(code-runner): max_result_bytes / max_stream_bytes config knobs * feat(code-runner): pure output-cap primitives (result marker, stream head+tail) * feat(code-runner): cap result/stdout/stderr echoes at the source * feat(code-runner): serve config from the configuration worker (Tier 1) The committed config.yaml is gone; the configuration worker's code-runner entry is the authoritative runtime config (--config is a one-time seed). Output caps and timeouts are load()ed per call from an ArcSwap snapshot the configuration:updated trigger swaps, so they hot-apply; the engine-structural fields stay boot-captured and the reload handler warns that they apply at the next restart. on-config-change is seeded into the runtime-id registry and denied to agents. * feat(code-runner): custom console form for the configuration entry host.configForms.register('code-runner', …) replaces the generic schema-driven form on the Workers tab: sections carry the reload split (output caps + timeouts hot-apply on save, runtime/memory/scratch apply at the next restart), byte fields show KiB/token hints, and the scratch section computes the worst-case host footprint. * docs(code-runner): shrink run/register_function catalog descriptions The injected harness guidance already teaches the full surface; the catalog description now carries contract essentials only (~1/3 the tokens per read) and no longer points at other workers — the sandbox-code-runner mentions are gone from RUN_DESC and the Lang schema doc. * feat(code-runner): inject_guidance config knob gates the guidance hook inject_guidance (default true, hot-apply) silences the pre-generate guidance hook: disabled, the handler answers with the no-op mutation so the harness prompt is untouched. The hook stays bound — one no-op roundtrip per generation while off. Toggle ships in the console config form under a new agent-guidance section. * test(python-core): load-tolerant margins for the per-turn budget test each_turn_is_budgeted_on_its_own_timeout gave the first turn 600ms, which a trivial turn on a slow shared CI runner blows (3/3 CI failures; passes on a fast local box). First-turn budget 600ms -> 3s and the second turn's sleep 1.5s -> 4s, preserving the discrimination: the sleep still exceeds the first-turn budget, so a leaked budget still trips the assertion. * fix(code-runner): address CodeRabbit review findings on PR iii-hq#771 Registration + reload correctness: - py namespaces track in-flight registrations: a failing registration no longer destroys the interpreter a concurrent sibling is still defining on (deterministic regression test, mutation-verified), and a register racing a teardown errors cleanly instead of panicking on the map entry - py_handler loads default_timeout_ms per invocation so registered handlers honour hot config swaps, per the Manager struct contract - one post-registration refresh closes the boot window where configuration:updated could fire before the trigger existed; refreshes serialize fetch->store so an older get cannot overwrite a newer snapshot - manifest default_config is serialized from the struct (the hand list had drifted by four keys); its test now compares keysets to the schema Core crates: - python clamp_timeout/clamp_memory floor a zero ceiling instead of panicking (u64::clamp with min > max); Semaphore::new(0) hang guarded - wrapper.py: allow_nan=False so NaN/inf results become null instead of poisoning the envelope (bridge frames too); the traceback cap moved into write_error; malformed iii.json degrades to no-bridge - cache_root falls back to the temp dir when HOME/XDG_CACHE_HOME are unset instead of panicking on the boot path - capture_streams counts the appended newline against MAX_LOG_BYTES - run_with inlined into run (sole caller, work_dir always None) - wire docs: node-engine::<runtime_id>:: / node-engine::teardown -> code-runner names (goldens regenerated) Docs/catalog contract: - run.code and register.function_id descriptions state the real model: async fn body vs module, iii.files vs /work, and one runtime per (namespace, lang) — the language gate they described was rolled back - scratch_* documented as node-only (python /work is a fixed engine budget) in the schema docs, README, and config form Console UI: - CSS.escape on the focusField deep-link before querySelector, so a hostile fragment cannot unmount the form - errorInfo keeps message total when error is undefined - configForms remover joins the teardown list; copyText removes its textarea in finally; pulse honours prefers-reduced-motion - build.rs watches packages/console-ui so edits there rebuild page.js Tests/tooling: - run-tests.sh: --filter without a value exits 2 (was an infinite loop); a missing HARNESS_DONE reaches its diagnostic under set -e - stream-cap e2e asserts the real 16 KiB max_stream_bytes default (was <=2 MiB); the register e2e 'any' assertion is de-tautologized; the truncate UTF-8 test asserts the observable contract; the react import assertion matches the full specifier Verified: fmt/clippy clean; node-core 227, python-core 76, code-runner 90 tests; UI tsc + vitest 11/11; e2e suite 25/25.
…ii-hq#781) * feat(canvas): diagram worker with live console rendering New canvas worker: diagrams stored as editable source under stable ids (canvas::create/get/list/update/delete on the state bus), a per-family mermaid syntax primer (canvas::syntax), pre-render validation (canvas::validate), and injected console UI - a canvas page with Monaco editor, live mermaid preview, pan/zoom, SVG/PNG export and a freeform whiteboard mode, plus chat cards that render diagrams inline for canvas function calls. MOT-4408. * chore(canvas): retrigger ci * feat(canvas): live streaming, draggable panes, hand-drawn look The page now streams agent-side changes over the state worker's own state trigger (scope canvas, tab-scoped Message-path binding): the sidebar and any open canvas update in place, own saves are identity-suppressed so the editor never resets, and an externally deleted open canvas clears with a note. The sidebar and the editor|preview split get drag handles (persisted). Mermaid renders with the hand-drawn look and the colorful redux theme pair via one shared init used by both the page and chat cards. * feat(canvas): export options, draw-on animation, auto-open Exports gain background on/off and light/dark options on both panes (mermaid re-renders under the export theme; freeform maps them to exportBackground/exportWithDarkMode), mermaid drops htmlLabels so PNG rasterization stops blanking labels, every fresh render sketches itself in with a staggered stroke sweep (reduced-motion respected), and a canvas created by an agent auto-opens on the page when nothing is selected. * feat(canvas): element-level live drawing on freeform canvases Four new functions let an agent draw one shape at a time: canvas::element::add/update/delete/list operate on individual elements of a freeform canvas (stable element ids, verbatim scene storage, per-canvas caps, mutations serialized). The open console whiteboard applies each call in place through the editor's updateScene with remote strokes kept out of the user's undo stack, so drawings appear as they happen. Chat shows one-line cards per drawing step. Also folds in review feedback: case-insensitive family aliases, a mutation guard closing the update lost-write window, explicit syntax table arms, CI golden guard, watch-mode vendor prebuild, date and relative-time hardening, save-echo suppression for freeform, shared mermaid init, split-persist cleanup, and test dedupe. Refs MOT-4408. * fix(canvas): never auto-save an unrendered scene, normalize skeletons Opening a freeform canvas the renderer failed to restore could serialize an empty scene and save it back, destroying the stored elements. Saves and dirty-tracking are now gated on a real pointer or key interaction with the board this mount, so merely opening a canvas can never write. Stored elements are normalized on mount and on live-apply: full excalidraw elements pass through, agent skeletons run through the converter in per-run batches, and an unconvertible batch is dropped instead of sinking the whole scene. Refs MOT-4408. * fix(canvas): unique element ids, honest list, parked remote applies canvas::element::add regenerates caller-supplied ids that collide with the scene or repeat within one request; element::list returns one summary per stored element with placeholders for malformed entries so counts stay comparable; the add response counts before saving instead of re-parsing; a remote scene update that arrives mid-edit is parked and applied on the next clean transition instead of being dropped; SKILL.md states the element family is freeform-only; boundary test pins the element cap. Refs MOT-4408. * docs(canvas): current README, skill, and modules row README documents the element-by-element drawing flow with a worked example, the streaming canvas page, exports, and the freeform normalization; SKILL.md gains the draw-while-watching flow and the element list; the modules table row covers the element family. Refs MOT-4408. * fix(canvas): regenerate pnpm-lock.yaml after main merge The merge left a duplicated mapping key in pnpm-lock.yaml (stacked react specifiers), which broke every job that pre-builds the frontend bundles with --frozen-lockfile. Regenerated from main's lockfile reconciled against the workspace so canvas/ui resolves cleanly.
…cense, abort + discovery fixes - add provider::opencode_go::count_tokens (local tiktoken estimator, golden schema) — parity with openai/anthropic/codex - iii.worker.yaml: license Apache-2.0; state pin ^0.22.0 - iii-permissions.yaml: deny !provider::opencode_go::count_tokens - discovery: models_url returns Option, skips discovery when the configured URL has no /chat/completions suffix — never polls a guessed host (CodeRabbit iii-hq#3) - integration: abort lands mid-stream, cancels the in-flight upstream request, terminal done(aborted) frame, idempotent second abort (CodeRabbit C)
The stream trigger type the console's live views subscribe through was missing; add iii-stream to the harness engine stack (matches the integration/e2e stack config pattern).
This reverts commit d17ac59.
…der-PR scope Repo pattern (provider-deepseek/openrouter/github-copilot): provider PRs ship the provider dir + root README row only; no provider touches harness/. Remove the harness/Makefile BASE_STACK entry (added during the main merge) and the harness/iii.worker.yaml dependency (incidental in the original commit; the PR body's wiring section does not list it). The worker still installs via `iii worker add` like the other providers.
cargo test ran every target incl. the integration suite, which self-runs again via the explicit III_ENGINE_BIN integration command when iii is on PATH. Scope the first command to the pure targets (lib + golden schema checks), leave the integration line unchanged. (CodeRabbit review 4929864279)
…native diff surface (iii-hq#791) * feat(shell): stream workspace changes into the explorer The explorer page subscribes to the editor worker's editor::changed push channel (it observes every filesystem-touching harness call through the post-trigger hook): agent writes refresh the tree and git views live, and the active file reloads in place when the agent wrote it, with a dirty buffer always keeping the user's edits. Events coalesce in a short window; the page degrades to load-once behavior when the editor worker is absent. Refs MOT-4407. * feat(shell): system-level workspace watch behind shell::changed Reworked per review direction: instead of observing harness calls, the worker owns a real OS directory watch (FSEvents/inotify via notify). A surface binds the shell::changed trigger type with config: { path } and the worker starts one recursive watcher per binding, coalesces the raw event storm per path, filters .git internals, and fans out {path, kind, root} — so agent calls, shell::exec side effects, and edits made entirely outside the engine all stream. No harness coupling, no other-worker dependency. The explorer binds its browsed root per tab (re-binding on root change), refreshes tree and git live, reloads a clean active buffer when its file changes on disk, and follows the last visible write into a preview tab, with hidden and system paths never stealing focus. Refs MOT-4407. * feat(shell): live follow opens as a diff, deep noise filter The live follow now shows the change the way the editor feed did — through the console's shared FileDiff renderer: git baseline when the root is a repo, empty baseline for created files, and the last content this page saw for modified files outside a repo (DiffPane grows an optional baseline override). The file still opens as the preview tab underneath, and an open diff tracks further writes to its file live. The follow filter also got depth-aware: noise directories (target, node_modules, dist, build, Library, ...) are rejected at any path depth, not just the first segment, and build-artifact extensions (.o, .rlib, .lock, .log, ...) never steal the view — a cargo build under a nested worktree was follow-opening object files. Refs MOT-4407. * fix(shell): created files reach the live diff as creations macOS reports a create and the write that fills it as separate events; latest-kind-wins coalescing collapsed that to modified, so a brand-new file outside a repo arrived with no baseline and the live view fell back to dumping raw content instead of the all-added diff. Kinds now merge toward the visible outcome within a window (create+modify stays created, deletion supersedes, create-after-delete is a creation), and the page keeps its own guard: a followed file absent from the previous tree render is treated as created even when the OS kind says otherwise. Refs MOT-4407. * fix(shell): temp artifacts never steal the live follow Session scratch files (.output, .tmp, .swp, .part, .pid, .sock) join the follow exclusion list — watching a shared directory like /private/tmp streams every process's temp churn, and those writes belong in the tree refresh, not the preview. Refs MOT-4407. * docs(shell): document the shell::changed trigger type README gains a Live change feed section — binding config, payload vocabulary, coalescing semantics, and what the explorer page does with the events — and the agent skill documents the trigger type alongside the function surfaces. Refs MOT-4407. * feat(shell): review feed, lazy deep folders, nested-repo diffs, noise-free watch Four explorer gaps from live use on a home-sized root. The watcher no longer reports reads or bare metadata touches as modifications (notify Access and Modify::Metadata events are dropped at the fold) — a cat or chmod is not a workspace change, and content writes carry their own Data events. A new changes side tab keeps the last 200 visible events (kind glyph, tail-ellipsized path, age) so a fast burst stays reviewable after the auto-follow has moved on; clicking a row reopens that file's diff. Folders the budgeted tree snapshot never reached now fetch their listing on expand and splice it into the rendered tree, with fetched-and-empty markers preventing refetch loops and live changes under a fetched subtree dropping it for refresh. And diffs now find a file's OWN repository when the browsed root sits above it — git -C from the file's directory auto-discovers upward — so a worktree under the home directory shows real baselines instead of 'not a git repository'; the git tab message explains its root scope. Refs MOT-4407. * feat(shell): line-count chips and totals in the changes feed Each feed row now carries the +N/−M of its change and the header sums the session — git diff HEAD --numstat probed from the file's own directory (nested repos included), untracked files counted whole, non-repo files measured against the page's last-seen content with a prefix/suffix line delta (chip-grade arithmetic, exact for contiguous edits). Stats fill in asynchronously and are bounded per burst; a row without a chip still opens its diff. Refs MOT-4407. * feat(shell): burst-grouped changes feed The feed now keeps each coalesced burst as one group with its own file count, +N/−M totals, and age — an agent turn that edits four files reads as one reviewable unit, and the same file edited across bursts keeps its history instead of collapsing to a single row. Oldest groups fall off whole past 300 rows. Refs MOT-4407. * feat(shell): own diff renderer with folds, syntax color, and dir-aware events The diff pane is now shell's own renderer instead of the console's shared component: a Myers line diff with unmodified context folded behind expandable 'N unmodified lines' rows, dual line-number gutters, lightweight per-line syntax coloring (strings, comments, numbers, keywords per family), intraline change emphasis on replaced line pairs, and +N/−M totals in the header. shell::changed events gain a dir flag — a burst's last event was sometimes the directory creation itself, and following it into coder::read-file returned C210 raw into the pane; directories now refresh the tree but never open, and never enter the feed. The page also stops chasing its own tail: config/shell-ui.yaml (its persisted UI state) is excluded from follow — every tab change writes it, so the live view kept replacing real diffs with a -0 +0 of its own state file. splitLines treats a trailing newline as a terminator, matching git's line counts. Verified live headless: created files render all-added with syntax color and counts, the active buffer follows disk writes, and the state-file loop is gone. Refs MOT-4407. * fix(shell): own atomic-write temps never stream coder::update-file and the fs backend write through sibling temp files (.coder-tmp-, .iii-tmp-, .tmp.<uuid>) that exist for a moment between write and rename — the watcher reported them, the live view followed one, and the read raced the rename into a raw C211 in the pane. They're this worker's own write machinery, not workspace changes, and the rename lands as an event on the real path regardless; the fold now drops them (suffix-shape matched, so a user's notes.tmp.md still streams). The diff pane also says 'file no longer exists on disk' instead of echoing a raw handler error when a followed file vanishes between event and read. Verified against the live coder::create-file path: only the final file delivers. Refs MOT-4407. * feat(shell): diff pane matches the reference rendering Studied the reference TUI's diff renderer source directly and ported its remaining decisions: one gutter (the new line number for additions and context, the old one for deletions — the sign column disambiguates), a verb-led header (Edited path (+A −R)), deleted-side syntax dimmed under its tint, and a row-count guard that drops per-line tokenizing on huge diffs so the pane stays instant. Verified live headless against a nested repo under a non-repo browsed root: folds above and below the hunk, intraline emphasis on the changed span, syntax coloring both sides. Refs MOT-4407. * feat(shell): consume the open-in-shell deep link The explorer handles #/ext/shell/open/<encoded-abs>[:line] — the chat's open-in menu (console iii-hq#792) navigates here. The request is captured and stripped from the URL immediately, then applied once the root has resolved: the file opens pinned when it lives under the browsed root, and the explorer re-roots to the file's own folder when it doesn't (the effect refires on the new root and opens it there). Refs MOT-4407. * fix(shell): jailed watch paths, unstallable fan-out, bounded diffs Addresses the review findings. The watch path now goes through the coder surface's PathResolver (jail containment, operator denylist, canonicalization, hot-reload aware) — watching a tree is a read of every filename under it, so a path you can't read is a path you can't watch; README documents the policy and a test pins the jail. Deliveries ride their own tasks so one hung send can't stop the pump from draining the channel and silently dropping events. The Myers backtrack gets an edit-distance budget (memory is O(D*(N+M)) from the per-step frontier snapshots) with the coarse-replace fallback, plus a regression test. Also: poisoned-mutex recovery instead of silently skipping registration bookkeeping, literal git pathspecs for filenames containing glob metacharacters, a guarded decodeURIComponent in the deep-link capture, the feed surviving a hidden-filter toggle (root changes still clear it), inaccessible lazy folders recorded as fetched-and-empty instead of refetching every burst, malformed events without a root dropped at the guard, image previews no longer overwritten by text reloads, pointer-cancel handling on the sidebar resize handle, a preview-sized ceiling on the base64 image read, and bidi-safe tail ellipsis on feed paths. Refs MOT-4407. * (MOT-4407) feat(shell): polish live explorer review UI * (MOT-4407) feat(shell): add Codex-style workspace review * (MOT-4407) feat(shell): edit worktree diffs safely * (MOT-4407) docs(shell): add post-merge live review screenshot * (MOT-4407) fix(shell): keep explorer synced to chat root * (MOT-4407) fix(shell): retain exact working directory roots * (MOT-4407) fix(shell): avoid replaying collapse all
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
New Rust worker
provider-opencode-go: an LLM provider worker behindllm-routerspeaking the OpenCode Go API — Chat Completions(
https://opencode.ai/zen/go/v1/chat/completions), SSE streaming, live modeldiscovery, auth/error taxonomy, reasoning-effort mapping, tool calling, and
structured output. Registers
provider::opencode_go::stream/refresh_models/
abortwith the router, binds identity via registration token (state scopeprovider-opencode-go), and readsOPENCODE_GO_API_KEYas credential.Why
The stack has no way to route chat completions to an OpenCode Go subscription.
The OpenCode Go API is Chat Completions compatible, so the existing
provider-openaiworker ports directly — same protocol, same relay/pumpscaffold, same error taxonomy, with the OpenAI-only surfaces (embeddings,
Responses API) dropped as dead code.
How it works
router::provider::resolve(config slice →
OPENCODE_GO_API_KEYenv on the router → none); sent asAuthorization: Bearer.AssistantMessageEventframesinto a router-owned channel;
ping≥ every 30s of silence; a failed channelwrite (
router::abort/ caller gone) drops the SSE receiver and aborts thein-flight HTTP request. Stream-path
tracing::debugfor provider-sideobservability.
GET /v1/modelssupplies bare ids; each isenriched from a hardcoded curated metadata table (
src/curated.rs) preparedfrom models.dev (2026-08-03) — context window, reasoning support/effort
levels, tool-call and structured-output capability for the maintainer's
curated model set (24 models.dev entries +
hy3-preview). Ids outside thetable keep conservative defaults (128K, no thinking, tools on) — same
pattern as provider-openai's
curated.rs.thinking_levelmaps to the upstreamreasoning_effortonlywhen the model's curated effort list accepts the level (e.g.
grok-4.5accepts
low/medium/high;deepseek-v4-flashandglm-5.2accepthigh/max;hy3acceptsnone/low/high); toggle-only models andunknown ids stream without the field.
router::provider::registerwithbackoff until acked, re-declares on
router::ready;registration_tokenpersisted in iii-state (scope
provider-opencode-go).Scope / caveat
provider-openai: mechanical renames only in most files; theOpenAI-only surface (ApiMode, embeddings, Responses-API event handlers and
thinking deltas, curated reasoning-fallback ladder, luna guard) is dropped —
OpenCode Go has no such surface.
opencode/worker —different role (CLI wrapper vs provider), no overlap; both install side by
side.
hy3-previewis listed by the liveGET /v1/modelsbut the chat endpointcurrently returns
ModelNotFound— an upstream inconsistency; the curatedrow keeps conservative defaults and the provider surfaces the upstream error
cleanly.
no-ticketlabel applied.Repo wiring
README.mdModules row added forprovider-opencode-go(alphabetical, between
provider-openaiandprovider-xai)create-tag.yml,release.yml,alpha-release.yml, and.github/scripts/discover_changed_workers.pyllm-router/README.mdreference note (same structure as provider-openai)Verification
cargo fmt --checkandcargo clippy --all-targets --all-features -- -D warningscleancargo test --all-features: 67 pass — 57 lib unit + 2 bin unit + 4schema/golden + 4 integration
stubbed upstream: chat stream end-to-end (incl.
cache_readusage),401 →
auth_expirederror frame,refresh_models→ catalog from thecurated table, re-declare on
router::readycargo build --releaseOKrefresh_models→ 25 models;router::models::list→ 25 curated entrieswith metadata;
router::completeserved onglm-5,deepseek-v4-flash(incl.
thinking_level: high→reasoning_effort), andhy3Test plan
cargo fmt --checkcargo clippy --all-targets --all-features -- -D warningscargo test— 67 passIII_ENGINE_BIN=$(which iii) cargo test --test integration -- --test-threads=1iii worker add provider-opencode-go→refresh_models→router::models::list→router::complete