feat(opencode): local LAN provider discovery + auto-discover models - #27554
feat(opencode): local LAN provider discovery + auto-discover models#27554androidand wants to merge 297 commits into
Conversation
|
The following comment was made by an LLM, it may be inaccurate: Based on the search results, here are the potentially related PRs: Most Related:
Related by Pattern:
Note: PR #27554 (the current PR) appears as the top result in all searches, which is expected. The most directly related duplicate candidate is #26756, which already implements discovery from |
c368353 to
a56a8fe
Compare
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
|
Please have a look, I think people will appreciate this, especially in combination with the updated llama-swap which adds feature parity (and more) with ollama. |
|
It would be very much appreciated. Thanks! |
A machine with multiple LAN interfaces was being added as a new provider for each non-primary interface (probeLAN only skips one IP per subnet). Skip any discovered host that matches a local network interface address. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
scanMDNSOnly() exposes just the Bonjour browse phase without the localhost/LAN fallback — useful for verifying mDNS advertisement. The integration test (skipped in CI) runs both mDNS-only and full discovery scans, prints all found providers with models and online status, and asserts at least one provider is reachable. Run with: bun test test/local/discovery.integration.test.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
m3 was appearing twice — once via mDNS (192.168.1.237:11435) and once via localhost probe (127.0.0.1:11435). Both paths set name to the hostname, so dedup by name catches the duplicate. mDNS results are preferred; localhost/LAN hits with the same name are skipped.
Replace /Users/andreas/dev/ paths with ~/dev/ throughout. Add ecosystem position diagram and multi-repo rules section covering: llama-skein client codegen protocol, --agent flag preservation requirement, mDNS discovery protection, and upstream sync policy. Collect code generation commands and branch conventions into dedicated sections. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix dialog-model-ctx: use sdk.client.local.model.setCtxSize (correct path per generated SDK), replace broken getSuggestions with PRESETS, remove prompt()-based custom input (invalid in terminal TUI), fix all TypeScript errors from prior incomplete implementation - Sidebar context: make ctx window clickable for local providers, accent color on ctx value, hide cost display for local (cost not applicable), add isLocal detection via provider baseURL option - Rename llama-swap refs to llama-skein in AGENTS.md, mdns.ts - theme.tsx: theme registration additions - provider.ts: local provider fetch improvements Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…alog - Poll llama-skein /api/hardware every 15s when dialog is open; detect GPU (discrete VRAM) vs unified/Apple-Silicon memory automatically - Show free/total memory in dialog title: "Context — modelName · 4.2/18.6 GB Unified free" - Add per-preset descriptions: - < 65k: warns "too small — MCP tools fill this before meaningful work" - 131k: marks as recommended (sweet spot for MCP workflows) - > current: shows +delta and OOM warning when free memory is low - Normalize base URL for control API (strip /v1 suffix, matches server handler) - Import from correct llama-skein gen/client path (createClient/createConfig) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Poll /api/hardware every 30s when a local llama-skein provider is active. Render a 20-char two-segment bar: used (amber █) vs free (muted ░). Show GB label: "10.2/18.6 GB Unified · 8.4 free" below the bar. Auto-detects GPU VRAM vs Apple Silicon unified memory from the snapshot. Bar only appears for local providers; cloud sessions are unaffected. Three-segment (model/KV/free) requires API extension to ResourceSnapshot — tracked separately. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ware - Sidebar bar: model weights (amber █) / KV cache (accent ▓) / free (░) Falls back to two-segment used/free when loaded_model is absent - Label row: "1.7 model · 0.8 KV · 6.2 free" when breakdown available Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Restructure the sidebar Context panel into two distinct sections with bold headers, moving speed to the bottom: Tokens ← clickable header (opens ctx dialog) 15,320 / 128k 12% ████████░░░░░░░░░░░░ ← accent fill = used, muted = free, clickable VRAM / Unified / RAM ← label from hardware data ████████████░░░░░░░░ ← amber=model, accent=KV, muted=free 15.7 · 5.1 · 3.6 GB ← with GB unit 1.1 t/s ← speed below both bars The memory section degrades gracefully: shows used/free bar when /api/hardware is available but no loaded_model data yet (e.g. model not yet loaded), and disappears entirely for non-local providers. The GB unit was previously missing from the memory text line. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Merges 20 upstream commits — model catalogue updates (GLM 5.3, Gemini 3.7, Grok endpoint), a unicode fix in grep previews, v1 database compatibility, and generated/nix churn. Merged without conflicts. Only one of the 80 files upstream touched is fork-registered (tool/registry.ts), which is why this was a clean merge rather than the seven-file reconciliation the previous sync needed. fork:verify flagged 46 unregistered divergences before the baseline bump — all of them the stale ref, not drift: owned 150/150 present and patched 95/95 intact throughout, so no fork feature was lost. Bumping upstreamRef to the newly merged commit clears them, exactly as the manifest's own comment prescribes. Verified after: fork:verify clean (150 owned, 95 patched, 0 unregistered), typecheck 0 errors in both packages, loop suite 149/0.
fork:verify became enforcing this week and immediately failed its first sync with 46 unregistered divergences — while owned files sat at 150/150 and patch markers at 95/95. Nothing was lost; the baseline ref was stale. Reading that correctly is the difference between bumping a field and hunting a phantom regression, and nothing wrote it down. The skill records that distinction, the sync procedure that produced a conflict-free merge of 20 upstream commits, and three traps: - measure the upstream delta from the merge-base. `git diff HEAD..upstream/dev` includes the fork's own ~270 commits and reported 245 fork-registered files for a sync whose real answer was 1. - merge, do not rebase. A rebase replays one decision once per local commit. - grep upstream/dev before calling a symbol fork-specific. McpTool looked like ours by name and by its consumers; it is upstream's, and mistaking it cost a wrong diagnosis of 35 type errors. Also carries the maintainer's standing rule for conflicting hunks — upstream wins ties, the fork wins where it adds something, ambiguous cases go on a list rather than being guessed — and the worktree recipe for landing a commit without touching another agent's working tree.
Verifying the loop-stall fix required actually running test/loop/*,
which uncovered that the fork's LayerNode/defaultLayer plumbing had
been broken for a while and this test suite had likely never passed:
- LayerNode.make(layer, [...]) positional-call sites (should be the
object form { service, layer, deps })
- 9 services missing an exported `layer` binding their own tests
reference directly (SessionStatus, SessionRunState, Question, Todo,
ToolRegistry, Truncate, SessionProcessor, SessionCompaction)
- 77 services missing a `defaultLayer` export entirely; added one that
mirrors each service's own `deps` list via
`layer.pipe(Layer.provide(dep.defaultLayer), ...)`, referencing each
dependency's own canonical defaultLayer so shared singletons
(Database in particular) stay memoized as one instance instead of
getting rebuilt per subtree
- 4 of those had a `node.layer` field that wasn't the bare `layer`
identifier (database.ts, runtime-flags.ts, llm.ts, core session.ts)
the generated defaultLayer wired the wrong construction
- 4 files transitively depend on unbound (tag-hoisted) nodes and can't
have a standalone defaultLayer at all (acp/usage.ts, acp/directory.ts,
project/instance-store.ts, worktree/index.ts) - left unexported
- tool/registry.ts's defaultLayer chained 21 separate Layer.provide()
calls in one .pipe(), over TS's pipe-overload arity limit - switched
to the array form Layer.provide([...])
- loop.test.ts and queue-mode.test.ts were both missing SessionProjector
(the event-driven service that persists sessions to SQLite) from
their hand-built layer graphs, so sessions were "created" but never
written to disk before the first prompt looked them up
- session/prompt.ts used `new Prompt(...)` where Prompt is a
Schema.Struct, not a class; crashed any prompt once the event-sourced
session path actually ran. Fixed to Prompt.make(...), matching the
convention used elsewhere (core/src/session.ts)
test/loop/ now passes clean (143 tests, 8 files).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The no-progress guard flagged genuinely-progressing iterations as stalled whenever their output was long and structurally repetitive (file dumps, boilerplate) - character-bigram similarity scores high on that kind of text regardless of the actual edit, and the old 0.92 threshold with a default noProgressLimit of 10 gave real progress too little room before the guard fired. - Raise the near-identical similarity threshold 0.92 -> 0.96 - Raise the default noProgressLimit 10 -> 15 - Add a length-change veto: an output whose length differs from the previous iteration's by more than 10% is treated as progress even when bigram similarity alone would call it near-identical, since a large length swing means real content was added or removed - Bump the SDK's noProgressLimit default in loop-args.ts to match (3 -> 10) fix-loop-stall tasks 1-5 done; task 6 (verify with real /loop usage) left open pending a live TUI run. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ran two real /loop sessions through the CLI against a running server and live llama-skein backends (rocky/qwen3-35b-a3b): one file-writing task (8 files, 17 tool calls, completed correctly) and one text-only task explicitly designed to produce long, near-identical output across iterations. Both completed cleanly via the promise token, no false stalls, no regressions from the LayerNode/defaultLayer fixes. Neither run naturally produced multiple no-tool-call iterations - a capable model just finishes an unconstrained task in one turn - so the precise similarity/length-veto boundary condition stays covered by the existing deterministic unit tests rather than a live run, which is the more reliable way to pin an exact numeric threshold anyway. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
sync.tsx and local.tsx both call usePermission() from their own init(), but nothing in app.tsx's provider tree ever wrapped it in <PermissionProvider> — permission.tsx (and its two consumers) landed via the Aug 7 upstream sync, and the wrapper never got added during that merge. Silent until now: existing long-running sessions had already booted past this code path before today, so the gap only surfaces on a fresh boot into an existing session (`opencode -s <id>`) - crashing with "Permission context must be used within a context provider" before any prompt is even reachable. Reproduced and confirmed fixed against a full isolated copy of a real, affected session. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same issue as core/src/session.ts: LocationServiceMap.node is LayerNode.unbound (tag-hoisted, replaced only by app-node-builder's real graph), so it has no defaultLayer and never can. The earlier defaultLayer sweep referenced LocationServiceMap.defaultLayer here anyway - missed in that pass, caught now by the pre-push typecheck gate actually running. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…gration, provider capabilities, and TUI
Fixes the ~15 pre-existing typecheck error clusters the newly-enforced
pre-push fork:verify+typecheck gate exposed (never run before today, so this
debt accumulated silently). All 30 turbo packages now pass `bun typecheck`.
- mcp/index.ts: reshape MCP.tools() to return {def, client, timeout} per-tool
(matching what session/tools.ts, code-mode.ts and tests already expected);
implement instructions() via the MCP SDK's Client.getInstructions();
fix getAuthStatus to compute expiry inline instead of calling a
nonexistent isTokenExpired
- provider.ts: widen ProviderInterleaved.field to a plain string and add
normalizeInterleaved() so config/catalog-supplied interleaved values
(bare string or {field}) both normalize correctly; cast at the two
plugin/SDK boundaries where our internal Model type is wider than the
generated SDK's still-narrow capabilities.interleaved
- share-next.ts: cast outbound share data to the generated SDK's Model[]
at the same interleaved-widening boundary
- beads/sync.ts, effect/app-runtime.ts: wire BeadsSync into AppLayer via a
LayerNode `node` export - it was never registered, so `opencode beads
sync` would defect at runtime on a missing service
- tool/code-mode.ts: migrate off the old @modelcontextprotocol/sdk types
(CallToolResultSchema arg, 3-arg callTool) to the new split SDK client
API already used by mcp/catalog.ts
- tool/shell.ts, tool/shell/prompt.ts: add the optional `description`
field to the shell tool's parameter schema - referenced at every
ctx.ask/metadata call site since the tool's introduction but never
declared, so it was always undefined; fall back to the command itself
for the result title
- session/prompt.ts: drop a stray `messages` argument to
compaction.isOverflow that the interface never accepted
- cli/cmd/attach.ts: --mini pointed at a runMini export that never
existed; fail explicitly instead of a silent dead import
- cli/cmd/mcp.ts: authenticate() now opens the browser itself; drop the
obsolete callback-based call
- tui/app.tsx: wrap the app in LocationProvider (paired with the
already-committed PermissionProvider fix) - both providers were added
by the Aug 7 upstream sync but never wired into the component tree,
crashing on session resume
- tui/component/dialog-move-session.tsx: break a circular generic
inference in createResource (T needed from both the explicit return
annotation and the untyped info param) that collapsed the resolved
type to {} and cascaded into ~11 errors
- test/mcp/oauth-provider.test.ts, test/session/prompt.test.ts: remove
two tests that reached for SDK/service surfaces removed by the v1->v2
MCP SDK migration and an architecturally-unbindable SessionV2.defaultLayer
respectively; both were already skip'd or testing internals no longer
exported
- test/mcp/catalog.test.ts, test/tool/code-mode*.test.ts,
test/tool/registry.test.ts: migrate remaining old-SDK imports
(Server/Client/InMemoryTransport/schemas) to the new split packages
Verified: bun run typecheck clean across all 30 packages, bun run
fork:verify clean, and a stash-based control test (same 44-file suite
with/without this commit) shows 972/1047 pass vs 953/1040 on the
pre-fix baseline - net improvement, no regressions introduced.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…typecheck fixes Re-baselines fork/manifest.json's accepted-divergence list after rebasing onto origin/dev's fork-sync (4643e65, fork/2026-08-14.1). fork:verify was reporting 66 unregistered divergences purely from files already in this branch's history, not anything newly lost in the rebase (verified: origin/dev alone reports 0 unregistered). Sampled across the flagged files to classify before accepting, per the fork-sync skill's rule against blind --accept-divergence: - ~55 files (core services + opencode services): the mechanical defaultLayer-export sweep from an earlier commit in this branch — `const layer` -> `export const layer` plus a `defaultLayer` composing each service's own deps' defaultLayers. Purely additive, one consistent pattern, verified via direct diff sampling (process.ts, session/store.ts, question/index.ts, session/compaction.ts, tool/truncate.ts, tool/registry.ts) — no upstream behavior altered or removed. - The rest: today's typecheck-fix work and the loop-stall fix, both already verified via typecheck, fork:verify's owned/patched counts, and (for the typecheck fixes) a stash-based control test against the pre-fix baseline. owned 150/150, patched 95/95 unaffected throughout — nothing was lost, only unclassified. baseline.upstreamRef/syncedAt/forkTag unchanged (still accurate). Individual markers for the defaultLayer sweep are left as a followup, not blocking here — it is one coherent, understood pattern repeated across files, not 55 separate decisions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… defaultLayers Rebasing onto origin/dev surfaced real breakage that merged silently (no conflict markers, since both sides touched different lines of the same files): - beads/sync.ts, effect/app-runtime.ts: duplicate BeadsSync import/node export — upstream/dev independently wired BeadsSync into AppLayer too; kept the one with the explanatory fork comment, dropped my duplicate. - mcp/index.ts: duplicate McpTool declaration — mine (from today's typecheck fixes) vs upstream's canonical, better-documented readonly interface. Dropped mine. - test/loop/loop.test.ts, queue-mode.test.ts: duplicate SessionProjector import and duplicate `instructions` mock property, both from git splicing in two independent additions of the same line. - test/session/snapshot-tool-race.test.ts: MCP mock was missing `resourceTemplates`, now a required Interface member. Also: provider.ts, mcp/index.ts, session/prompt.ts, auto-mode/service.ts were missing `defaultLayer` even on origin/dev itself (confirmed: these errors exist on origin/dev alone once files elsewhere reference Provider.defaultLayer/MCP.defaultLayer/SessionPrompt.defaultLayer that never existed) — added, following the established pattern, only after confirming every dependency in each node's `deps` list already has its own defaultLayer. bun typecheck: 0 errors across all 30 packages. fork:verify: 150/150 owned, 95/95 patched, 0 unregistered. test/loop/: 149/149, matching the pre-rebase dev baseline exactly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…eter The shell tool's description parameter (added to fix a pre-existing typecheck gap where the field was referenced at every ctx.ask/metadata call site but never declared in the schema) changes its JSON Schema wire shape. Snapshot updated to match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
llama-skein's max_fit_ctx has vramSafetyFrac/promptMarginFrac headroom built in, so it's conservative by design — but this dialog was also using it as the hard block in apply(), which meant the recommended value and the refusal threshold could disagree: pick the recommended ctx, get told it doesn't fit. max_physical_ctx (new llama-skein field, no discretionary margin) is now the block in apply() and the ceiling used for aboveCeiling(). The dialog surfaces both: the physical max as a selectable option and in the title (`max X`), and max_fit_ctx alongside it (`safe ~Y`) so a user who wants headroom still sees it. - packages/*/gen/types.gen.ts: regenerated (opencode) / hand-added (tui, which has no dedicated regen script for this client copy) - packages/tui/src/local/model-fit.ts: aboveCeiling doc updated for the semantic shift, logic unchanged - packages/tui/src/component/dialog-model-ctx.tsx: maxPhysical memo, new selectable option, apply() gate, titleSuffix shows both numbers
The foreign-turn guard skips prompting when the target session already has a turn in flight that this loop didn't start (the user's own message, or a slow previous turn not yet settled) — otherwise ensureRunning would join that turn and attribute its output to this loop. But the skip result still advanced info.iteration and counted against maxIterations. With the 2s default poll interval, starting a loop against a busy session burned the whole iteration budget in seconds without ever sending the loop's own first prompt, landing on "max_reached" while the model was still generating unrelated output. Both call sites (prompt mode's run(), queue mode's queueTurn()) now retry a skip without touching the counter or the iterations history. queueTurn retries internally so its three call sites keep treating a returned result as a real gate attempt — a skipped result surfacing as "0 tool calls, empty output" would have read as a failing turn and cost the change a strike it didn't take. Added a regression test that reproduces the bug (a foreign turn held open on the loop's session) and fails without the fix.
ae9cc41 deleted this as part of tearing out a fork-authored 4-state autonomy ladder (manual/skip-ask/continue/auto). Two of those states really were dead duplicates of /loop and /auto, correctly removed — but the permission toggle wasn't a duplicate of anything, it was the one real setting in that ladder, and it got swept out with the rest because by then it only existed as one branch of a shared dispatcher rather than its own command. This is upstream's own permission.mode command (context/permission.tsx's toggle()/set() were never touched by that deletion, only the palette entry and keybind calling them were), restored verbatim: same command name, same title strings, same behavior. <leader>p is the same binding it had before the deletion. fixes the "briefly saw an auto indicator, can't find how to toggle it again" gap — the indicator in prompt/index.tsx was always live, this was the only thing missing.
/auto collided with upstream's own "Auto mode" (permission auto-approve, opencode.ai/docs/permissions, just restored in the previous commit) — not an internal naming preference this time, a real feature upstream documents and ships today. One word, two unrelated meanings. /backlog names what the command works on rather than how it's built, which also avoids reintroducing the "queue is an implementation detail" complaint that the original /queue -> /auto rename existed to fix. /queue remains as an alias, unchanged. Renamed the command, its slash name, status-pill label, toasts, and every doc/comment reference. Left mode: "queue", QueueAuthority, experimental.queue_gate/queue_personas, and other internal identifiers alone — the collision was in the discoverable verb, not the implementation. Also fixed the auto_mode config field's doc comment, which still described a "Toggle auto mode" TUI command that no longer exists.
loop.create was called with no sessionID from the command palette entry, so it always fell back to minting a new hidden session. The run really did work — it just had nowhere visible to show up: no status pill, no way to find it from the session you were looking at, and a second attempt from the same directory hit the one-queue-run-per-directory guard with no visible reason why. Now scopes to the session you're viewing, matching what /backlog's prompt intercept already did. Navigates to the new session when started from the home screen (no session open to attach to).
180s covered a cold model load, which is what it was calibrated against. It didn't cover a multi-day /loop or /backlog session whose context only grows — prefill on that much context can genuinely take longer than 180s on real local hardware with the backend perfectly healthy. Observed killing an otherwise-fine generation mid-run. 600s keeps the same purpose (turn a truly-dead connection into a clear timeout instead of an indefinite hang) with realistic margin for long-running unattended work. Per-provider options.headerTimeout still overrides this for anyone who wants a different value.
<leader>p was defined in keybind.ts and the command existed, but appBindingCommands — the list app.tsx actually uses to turn keybind definitions into live bindings — never had "permission.mode" added to it. The keybind silently did nothing; only selecting the command by hand from the palette worked at all.
Weak local orchestrator models reliably ignore a "prefer background=true" tool description and just call task() with the (blocking) default, delegating large work to one foreground subagent while the rest of the fleet sits idle. Flip runInBackground to opt-out (params.background !== false) once OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS is on, keeping the flag-off fallback to foreground intact. Fixes #76 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…inspector
The "always allow" TUI prompt only ever approved a pattern for the running
instance's lifetime; there was no path from an interactive approval to a
persisted opencode.json rule. Add a "Remember (save to config)" choice
alongside the existing until-restart approval, and give the until-restart
option an accurate label ("Until restart") instead of the outgoing text's
"until OpenCode is restarted" claim getting quietly narrowed to "this
session" — the approval is still instance-wide (InstanceState is keyed by
project directory, not session), so the label needed to stay honest about
scope.
Add `opencode permission status`/`scan` as the batch-oriented counterpart:
`status` reports the effective ruleset (global + optional per-agent) plus
the always-on loop/backlog deny ceiling; `scan` mines session history for
repeatedly-asked bash prefixes and drafts additive allow rules, mirroring
the fewer-permission-prompts skill's allowlist logic against opencode's own
permission schema.
Also give the permission-auto-approve prompt badge its own accent
(theme.warning) so it reads as distinct from the loop/backlog badge
(theme.success) when both are active at once.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Raise the stream-inactivity watchdog default from 600s to 1800s (llm.ts). 600s was tied exactly to SUBAGENT_TASK_TIMEOUT_MS, so a long subagent or a compaction pass (one giant single-shot summary call with no tool ticks, firing exactly when the prompt is largest) raced the same deadline and lost. Also fixes the config schema doc, which still claimed the old 300s default. - loop.ts: a stalled stream resolves as a completed assistant message carrying `.info.error` rather than an Effect failure, so it was scored as "no progress" and silently re-prompted every iteration up to maxIterations instead of failing the loop immediately. - local.ts setModelCtxSize: stop collapsing "backend rejected the patch" into the same `false` used for "no baseURL" / "above ceiling" — the TUI toast was blaming the wrong cause. A genuine backend failure now surfaces as a server error with a ref instead of a misleading message. - dialog-model.tsx: an exact/prefix hit on a provider's own id or name (e.g. a local host like "m3") now shows all of that provider's models directly, instead of fuzzy-matching the same short needle against every model title too and burying the provider's models in unrelated OpenRouter-catalog noise. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
It was meant to catch a truly wedged provider connection, but every
real case it fired on was a slow-but-alive stream: host-paced hybrid
models faulting weights in, foreground subagents, compaction's single
big call, cold-load chunks. Each got a special-cased exemption; this
finishes that trend — no genuinely-wedged-socket case ever showed up
to justify keeping it, and the false positives were abandoning working
turns fleet-wide ("Provider stream stalled: no events for 1800s").
Removes stream-stalled.ts, tool-activity.ts, and every seam that fed
or consumed them (llm.ts stream wrapping, message-v2.ts error mapping,
tools.ts liveness ticks, loop.ts no-progress heuristic comment).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FK9MDUfpqc2D2jLF1ya8yR
…work Plans the full scope from the loop/backlog reliability and cross-session messaging discussion as linked, dependency-ordered openspec changes: loop-eternal-by-default, peer-messaging, subagent-notification-reliability, free-model-subagent-pool, claude-peer-protocol-spike, and a gated claude-peer-protocol stub that does not get real tasks until the spike reports findings. Also extends the existing fleet-instance-presence change (Phase 6) tying its unfinished wedge/heartbeat work directly to the reported "backlog suggests wrong/unreachable agents" symptom. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNLPKoYAazB85eSHgToec1
Plain `/loop` used to stop the moment its own prompt completed, even with unchecked openspec work still sitting in the backlog — the top complaint driving this change. On completion, a prompt-mode loop now checks the disk-derived eligible-change queue (`resolveQueue`) before finalizing; if work remains, it transitions into queue-style continuation under the same QueueDenyRules authority ceiling a `/backlog` run gets (never more push/ deploy authority than before), instead of just stopping. `--no-eternal` (`eternal: false`) restores the old stop-on-completion behavior exactly. Also adds one bounded stall reprieve — a single harder-worded retry before finalizing as `stalled`, never unbounded — using the same eternal opt-in. Two other documented `loop-spec-queue` follow-ups turned out to already be fixed in the current tree (priority-aware queue ordering in queue.ts, whole-turn tool-call counting in loop.ts) — verified and the tracking corrected rather than reimplementing them. Wired through the CLI (--eternal/--no-eternal), TUI (/loop --no-eternal), and SDK loop-args parser; SDK client types regenerated via the scoped packages/sdk/js/script/build.ts (avoids script/generate.ts's whole-repo reformat side effect). 153/153 loop tests pass (3 new), typecheck clean across opencode/sdk/tui. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNLPKoYAazB85eSHgToec1
…ng (peer-messaging) The existing `peers` tool only ever shipped discovery (session-peer-awareness deliberately scoped out messaging). Nothing filled that gap except an untracked, unregistered, broken sketch (plugin/skein-peers.ts: hardcoded localhost:4096, an invalid fetch() call for a local file, a parallel list_agents tool duplicating `peers` under a new name) — now deleted and superseded. send_peer_message extends the same roster `peers` already computes rather than inventing a second one: resolve by exact session id or an unambiguous title prefix (ambiguous match refused, never guessed), deliver via the same ctx.extra.promptOps primitive task.ts uses for background-subagent result injection, with a structured provenance envelope so a peer message can't be mistaken for a user prompt or a permission grant. Delivery is fire-and-forget (forked), matching task.ts's own pattern, so the tool honestly reports "accepted for delivery" rather than a stronger claim it can't back up without blocking the sender on the peer's whole reply. A literally mid-turn (busy) target is refused outright rather than risked — the same foreign-turn hazard loop.ts already guards against. Cross-instance delivery stays out of scope here, gated on fleet-instance-presence Phase 4 per openspec/changes/peer-messaging. Also fixes a pre-existing stale snapshot in test/tool/__snapshots__ left over from subagent-background-default's already-shipped description reword (confirmed present on a clean tree before this change; unrelated one-line --update-snapshots fix, bundled here since it was found while running this change's test suite). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNLPKoYAazB85eSHgToec1
fork:verify was failing on push with 10 regressions, blocking everything in this session. Investigated each one against actual diffs before touching anything: - 3 "DROPPED owned files" + 3 "LOST patch markers" all trace to a single commit, a90c498 ("fix(session): remove the stream-inactivity watchdog"), already on dev before this session started. Its own commit message gives a full, reasoned account of a deliberate removal (the watchdog only ever fired on false positives, never once caught a genuinely wedged socket) — confirmed against the actual diffs for llm.ts, message-v2.ts, and tools.ts, all fully self-contained with no partial/accidental collateral. The manifest was simply never updated at deletion time. Recorded under _ownedRemoved_2026-09-05, matching this file's own established convention for exactly this situation. llm.ts and tools.ts still diverge from upstream for unrelated, live fork reasons (defaultLayer export; the loop-spec-queue authority ceiling) and were re-registered in `patched` with fresh markers rather than dropped — message-v2.ts no longer diverges at all and was removed outright. - 4 "UNREGISTERED divergence": packages/opencode/src/cli/cmd/permission.ts and packages/tui/test/component/dialog-model.test.ts predate this session (already on dev); packages/opencode/src/tool/send-peer-message.ts and .txt are this session's own new tool. Verified none exist in upstream/dev, then registered all four as owned. `bun run fork:verify` now reports 151/151 owned, 94/94 patched, 0 unregistered divergence. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNLPKoYAazB85eSHgToec1
Direct answer to "does peer messaging actually work": no, not until this
commit. Writing a real end-to-end test (not just the pure-function unit
tests from the previous commit) instead of trusting typecheck + mocked
tests surfaced that the model itself said "Model tried to call unavailable
tool 'send_peer_message'."
Root cause: packages/opencode/src/tool/registry.ts builds each tool via an
Effect.all({...}) object (session-peer-awareness added `peers` there
2026-08-07, this session added `send_peer_message` the same way), but the
tools a model can actually call come from a SEPARATE, hand-maintained
`builtin` array a few lines below. `peers` was never added to it — meaning
the already-shipped, fully unit-tested `peers` tool has never once been
callable by a real model since it landed. `send_peer_message` inherited
the identical gap. Both added to `builtin` now.
Also fixed while writing the real test: target resolution was reusing
`resolvePeers`'s "actively working" filter, which correctly excludes idle
sessions from the awareness roster (alert-fatigue) but wrongly excluded
them as message targets — messaging an idle session ("update your client
when you get back to this") is the normal case for an explicit send, not
an edge case. Added `resolveMessageTargets` in session/peers.ts as a
sibling projection (same caller/descendant/directory exclusions, no
idle filter) rather than changing resolvePeers's behavior for `peers`.
New tests in test/session/prompt.test.ts drive the real ToolRegistry /
SessionTools.resolve path with a real (mocked-provider) model turn, not a
direct call to the tool's execute() function: "peers tool is reachable by
the model and reports another active session" and "send_peer_message
delivers into a second, idle session in the same directory" (the latter
polls the actual receiving session's message history for the delivered,
provenance-tagged text — proving the forked delivery path lands, not just
that the tool call returns "accepted").
bun test test/loop/ test/session/ test/tool/: 926 pass, 0 fail.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VNLPKoYAazB85eSHgToec1
Issue for this PR
Closes #6231
Closes #27553
Type of change
What does this PR do?
Adds
Local (LAN)discovery in/connectfor local OpenAI-compatible servers.Auto-discovers models for OpenAI-compatible providers from
/models, including limits like context window and max output tokens when available.Compatible with and preserves manually configured/edited providers and models and respects
discoverModels: true|false.Adds context
current / context_lengthwhen available (see llama-swap PR below) to Context in sidebar and footer.Fixes unrelated bugs introduced by rebase, --agent parameter would crash opencode.
Related
Server-side companion: mostlygeek/llama-swap#755 adds
context_lengthandmax_output_tokensto the/v1/modelsresponse. When those fields are present, this PR reads them to configure each model's context window and output token limits automatically — no manual config needed.How did you verify your code works?
bun run typecheckbun test test/provider/provider.test.ts --timeout 30000bun turbo typecheck/connect -> Local (LAN)against multiple local OpenAI-compatible servers on localhost and LAN.Screenshots / recordings
Provider config example
Checklist