The panel stored notes, todos and plans inside one shared JSON file that
six unrelated domains also wrote to, synchronised itself through window
CustomEvents, and could only read plans. It is now Project knowledge:
server-owned storage with explicit routes, a store with rollback, a
section sidebar, plans that open and edit in place, and search across
all of it.
Notes and plans the user pins travel with every message sent in that
project. Pinning is project state, not an attachment to one message, so
it holds until unpinned and the work status panel names what is riding
along and can detach it.
Agent memory is added alongside, in two scopes: what is true about the
user, and what is true about this codebase. The split is not cosmetic —
a wrong project fact costs one project and is noticed, while a wrong
global fact quietly shapes every session everywhere and the user has no
code to check it against. It stays separate from notes so an agent
mistake cannot land in what the user wrote. Sessions receive an index of
titles only; bodies are read on demand, because an index carrying full
text grows until it crowds out the conversation.
Deciding what a session must be told, and whether it has been told, now
lives on the server. The client owned it before, which meant sessions
started without a UI — scheduled tasks, sessions the agent dispatches —
received nothing at all, and a tab's record of what it had sent outlived
the conversation: after compaction the agent no longer held the block
while the tab went on believing it did. What was delivered is recorded
in the session's own metadata, and compaction restores it through the
runtime that already restores pinned messages, in the same turn.
Agent memory ships dark behind OPENCHAMBER_MEMORY_ENABLE: unset, there
is no tool, no routes, no session index, no settings row and no panel
tab. Absent rather than switched off, so nothing invites turning on a
feature that has not been announced. Pinned notes and plans are
unaffected and ship as normal.
* fix(proxy): reuse upstream connections for OpenCode API requests
`createProxyMiddleware` was constructed without an `agent`, so `http-proxy`
fell back to `agent: false`. That disables connection pooling and forces
`Connection: close` on every proxied request, consuming one ephemeral port
per request.
Measured against a real `opencode serve` instance, 200 sequential requests
through the proxy created 201 TIME_WAIT entries (1.005 ports/request). With
a keep-alive agent the same load creates 0.
On macOS the ephemeral range is 16,384 ports and TIME_WAIT lasts 30s, so
sustained traffic around 546 req/sec exhausts the pool — after which every
process on the host fails to open outbound connections with EADDRNOTAVAIL.
`maxSockets: Infinity` preserves the unbounded concurrency of `agent: false`,
so this changes connection reuse only, not request throughput.
Partially addresses #2915.
* fix(proxy): derive proxy agent class from the target scheme
Addresses review feedback on #2916. The first commit created an
unconditional `http.Agent`, which regresses external OpenCode servers
configured over https via `OPENCODE_HOST` (accepted by env-config.js).
http-proxy dispatches through `https.request` when the target protocol is
`https:` (http-proxy/lib/http-proxy/passes/web-incoming.js:126), and
`http.Agent#createConnection` is plain `net.createConnection` — so an
http.Agent would open a plaintext socket to a TLS port and fail every
proxied request. `agent: false` previously worked for both schemes.
`createOpenCodeProxyAgent(target)` now returns an `https.Agent` for https
targets and an `http.Agent` otherwise, derived once from
`resolveProxyTarget()` at registration so the single shared instance is
preserved across `apiProxy` and `interactiveOAuthProxy`.
Guarded in both test layers, verified to fail when the selection is
reverted to an unconditional http.Agent. `https.Agent` extends
`http.Agent`, so the http cases assert `not.toBeInstanceOf(https.Agent)`.
* Round 2: fix: resolve the proxy agent lazily so cold starts honor https
Addresses the round-2 blocker on #2916. Deriving the agent class at
registration is too early: startup-pipeline-runtime.js calls setupProxy()
(line 104) before bootstrapOpenCodeAtStartup() (line 141), so on a fresh
process state.openCodePort is null, buildOpenCodeUrl() throws
(network-runtime.js:86-88), and resolveProxyTarget() returns the http
loopback fallback. An external server configured via OPENCODE_HOST=https://
only appears on state.openCodeBaseUrl after bootstrap, so it was still
getting a plain http.Agent — the regression the previous commit intended
to fix.
`agent` is now a getter backed by a per-scheme memoizing resolver.
http-proxy-middleware rebuilds per-request options with
`Object.assign({}, this.proxyOptions)` in prepareProxyRequest, which invokes
getters, so resolution happens at request time while still yielding one
shared pool per scheme.
Tests now model the production ordering — registration while the port is
null and buildOpenCodeUrl throws, then an https base URL appearing after
bootstrap — and fail against the eager implementation. A behavioral test
pins the http-proxy-middleware option re-read the fix depends on, so a
library change that froze options would fail loudly instead of silently
regressing https targets.
The resolver is module-private; `bun run dead-code` flagged it as an
unused export when it was exported.
* Round 3: docs(changelog): note upstream connection reuse under [Unreleased]
Repo precedent adds [Unreleased] bullets for comparable proxy/stability
fixes (1.18.4 Stability, 1.9.3 Reliability/Proxy). Non-blocker raised in
review on #2916.
* Round 3: docs(changelog): use repo-standard 'behavior' spelling
* Round 4: docs(changelog): don't imply a restart is the only recovery
The ephemeral port pool drains on its own once the exhausting traffic
stops (TIME_WAIT expiry), so a restart is sufficient but not necessary.
Optional nit raised in review on #2916.
* Round 5: fix: construct the proxy agent through one factory; widen the pool
Review found the https branch was mutation-uncovered: the resolver
re-implemented agent construction inline instead of calling the exported
`createOpenCodeProxyAgent(target)`, so replacing its https branch with
`new https.Agent()` — dropping OPENCODE_AGENT_OPTIONS, and with it
keep-alive — left the entire suite green. Since `createOpenCodeProxyAgent`
also had no production callers, its four tests were pinning dead code.
Delegating collapses both: the factory is now the single construction
path, and the mutation fails 2 tests including the live resolver path.
Also from review:
- maxFreeSockets 32 -> 256 (Node's own default). The lower cap evicted
pooled sockets under concurrency, reintroducing the churn this agent
exists to prevent: at 64 concurrent requests it left 303 sockets in
TIME_WAIT versus 0 at 256.
- Added `timeout` to OPENCODE_AGENT_OPTIONS. Free-socket eviction is
governed by agent.options.timeout, which was unset, so idle sockets
persisted until the peer closed them. `keepAliveMsecs` is the TCP probe
delay, not the idle lifetime.
- resolveProxyTarget() now checks openCodePort before calling
buildOpenCodeUrl instead of relying on it throwing. The port is nulled
on several runtime paths (health-check failure, failed restart), so a
degraded OpenCode made every proxied request pay for a thrown-and-caught
exception — and the getter added a second call per request.
- Test fixtures use :4096 rather than :443; WHATWG URL elides the default
port, so parseInt('') is NaN and env-config rejects that host. The
fixtures modeled a state that cannot reach production.
- The getter-read assertion is now exact (0 at construction, 1, then 2)
rather than >= 2, which would have passed if the getter were read twice
at construction and never per-request.
- listen() rejects on 'error' and servers start inside try/finally, so a
bind failure fails the test instead of hanging to timeout.
Review follow-up on the #2769 highlight caches.
Fingerprint strength. The block/highlight caches are now global and
content-addressed, so a hash collision no longer mis-colors a block — it returns
a *different* block's rendered HTML and shows the user source they never wrote.
Length + one 32-bit FNV-1a is not enough key space for that failure mode at
session scale. `contentFingerprint` now combines two independent 32-bit
multiplicative hashes with a final avalanche (~64 bits); two multiplies per
character are free next to Shiki tokenization.
Streaming churn. Content addressing made every streaming step of the trailing
`live` block insert a new cache entry, so one long message evicted the settled
`full` blocks the fix exists to keep warm. `full` and `live` blocks now use
separate caches; the live cache is small (32 entries / 2MB) because it only has
to absorb repeat renders of the same step.
Redundant worker-side caches. `markdown-worker.ts` is the only sender to the
Shiki worker, and its client cache is larger than the worker-side ones, so the
worker caches could not serve a hit the client had not already served — they
only duplicated up to 48MB of payloads in a second heap. Removed; the reason
memoization belongs on the client is now documented there, along with why only
`highlightTokens` carries a theme in its key.
Dead `cacheKey` plumbing. `renderMarkdownBlocks` kept a `cacheKey` parameter it
only `void`-ed. Removed it and the now-unused `useMorphdomMarkdown` prop; the
remaining call-site local is renamed `fadeKey` for what it actually keys.
Tests: image-mode cache identity, streaming-does-not-evict-settled-blocks,
live-cache reuse, and a 20k same-length-source fingerprint collision check.
Each new guard was verified to fail without its fix.
The token breakdown of an assistant message accumulates across every API
round-trip inside the turn: each tool call re-reads the whole cached
prompt, so input/cache.read add up to several times the context window.
Every context-usage surface summed those fields, which is why the meter
could read 330% of a 1M window whose real fill was 232,872 tokens
(23.3%), and why reopening an older session jumps the readout (#2562).
The server reports the final round-trip's window as tokens.total
(optional in the message schema; opencode 1.18.18 returns it, verified
against its live /session/:id/message API). Prefer it everywhere the
window fill is displayed and fall back to summing only when the server
did not send it: contextTokensFromBreakdown in tokenUtils now owns that
rule, and the context store extractor, sync store getter, work status
panel, context sidebar, VS Code layout, mini chat, and mobile metadata
all use it instead of their own inline sums.
Fixes#2562
Branch status resolves an open PR across the whole fork network first, so a
merged fork PR can never hide an open upstream PR for the same head. Only when
no target has an open PR does the branch's newest closed/merged PR come back,
as history.
The panel shows that history as a compact note and offers creating the next PR
below it, instead of either sticking on a terminal PR or going blank after a
merge. Terminal associations stay persisted for reload continuity but are never
treated as authority: they revalidate on the discovery cadence and on focus.
History is looked up only for the branch's own remote and name, and remembered
per repo+branch, so the extra lookup cannot exhaust the route's resolve budget.
The checks summary and merge-permission lookup are skipped for a closed or
merged PR, where neither is actionable.
Create no longer aborts when recoverStaleDraftDirectory rewrites the
implicit new-chat draft to the active project during the create probe.
Also rank the Unreleased Chat bullet below the changelog highlights.
Co-authored-by: serkraser <serkraser@gmail.com>
Keep both Unreleased bullets: the config-wipe Settings fix and the
stale merged-PR Git panel fix from main.
Co-authored-by: serkraser <serkraser@gmail.com>
Keep both Unreleased changelog bullets: the lastDirectory new-chat
fallback and the Git PR panel stale-status fix from main.
Co-authored-by: serkraser <serkraser@gmail.com>
New chats inherited a persisted lastDirectory even after that worktree
was removed, so the first message saved but the prompt never started.
Validate the implicit draft directory, fall back to the active project
only when OpenCode confirms the path is missing, and leave explicit
worktree targets and unknown probes unchanged.
Co-authored-by: serkraser <serkraser@gmail.com>
jsonc-parser was returning truncated trees (often only `$schema`) when
configs contained JSON5-style unquoted keys. Config mutations then backed
up and overwrote the full file with that stub. Check parse errors on read
and refuse to overwrite unparseable files on write in web and VS Code.
Fixes#2923
Co-authored-by: serkraser <serkraser@gmail.com>
Claude quota only worked when the user had signed into Anthropic through
OpenCode. Credentials are now discovered from Claude Code itself first: the
macOS Keychain entry, then the Linux/WSL credentials file (honouring
CLAUDE_CONFIG_DIR), then OpenCode auth.json, then CLAUDE_CODE_OAUTH_TOKEN.
All sources stay read-only and the OAuth token is never refreshed: Anthropic
allows one live refresh token per client_id, so refreshing here would sign the
user out of Claude Code. Credentials are re-read per request instead, and an
expired token reports that Claude Code needs a sign-in rather than a bare 401.
Usage is now read from the limits[] array, so model-scoped weekly limits work
again after Anthropic stopped populating seven_day_sonnet/seven_day_opus, and
new limit kinds no longer need a code change. Adds extra-usage spend and the
plan name, and holds the last good values through Anthropic's 429s with a
cooldown and an account-keyed cache.
A server running with authentication disabled pairs and connects fine, but
the saved connection has no bearer token. Auto-connect silently bailed on
the missing token and the resume reprobe reported it as 'unreachable',
so every return to the app kicked the user to the connect screen.
Treat a saved tokenless connection as valid: probe it without a bearer and
let the probe decide — auth disabled connects, auth enabled later reports
needs-login. Bail out only when an expected token cannot be read.
A single uncaught exception (e.g. a Node-internal socket error) no longer
shuts the local server down; only a sustained storm does. The dev-tunnel
client now rejects non-http(s) base URLs cleanly instead of throwing an
uncaught exception in the connection handler.
The preview panel worked by proxying a dev server through OpenChamber's own
origin and rewriting the HTML that came back. Anything the rewriter did not
anticipate broke, and pages that refuse to be embedded never loaded at all.
This deletes the proxy (-1604 lines and its tests) and merges the preview and
browser panels into one surface backed by a real Chromium view.
What the panel is now
- A `<webview>` in its own session partition: logins and cookies persist, hot
reload works because nothing is rewritten, DevTools are one click away.
- Annotation: pick one element, drag a region, or draw freehand, write a note,
and it reaches chat with a screenshot of the visible page with the marks on it.
- Toolbar: hard reload, page zoom, device sizes, a light/dark switch that
applies to the page rather than the app, and cookie/cache clearing scoped to
the panel alone.
- Several pages at once, each tab showing the page's own favicon, and an address
bar that suggests pages already visited in this project.
- Dev servers are listed from what is actually listening on the machine, checked
against what a project announced, so a server is offered no matter how it was
started. One that is still starting is waited for instead of failing.
Remote dev servers
The desktop app binds a local port and pipes raw bytes to the OpenChamber host
over the existing authenticated connection, so the page keeps its own origin at
the root of its own host. The reachable set is exactly what discovery reports
and is re-checked per connection, so an authenticated client cannot dial
arbitrary local services on the host. Links and redirects to another loopback
port stay on the machine that served the page. A tunnel that cannot be opened is
reported; it is never replaced by the plain loopback URL, which would answer
from the user's own machine under a remote address.
Agent control
Browser actions are a separate `openchamber_web` tool: open, snapshot, click,
type, scroll, inspect computed styles, resize between mobile/tablet/desktop, and
capture a screenshot into `.openchamber/screenshots/` in the project. The
existing `openchamber` tool keeps sessions, worktrees and scheduled tasks. Each
has its own setting in the new Settings -> General -> OpenChamber Tools section,
and the plugin is not injected at all when both are off.
Capability belongs to the connected client, not to configuration: a client
declares on its event stream that it can drive a page, which only a Chromium
host does. Exactly one client performs each request — it claims the request
before acting, and the first claim wins — because deciding by whose result
arrives first would be too late for a click that already happened. No client
listening is answered immediately with an explanation rather than a timeout.
Runtime boundaries
Web tabs get a plain iframe that can display a page but not inspect one. The
VS Code extension no longer offers the surface at all, since nothing that makes
the panel worth having works there. Mobile is unaffected.
Native boundary
Camera, microphone, location and device-picker requests from panel pages are
denied — Electron grants them by default when no handler is set, and the panel
loads whatever address the user types. Page capture, appearance emulation and
storage clearing verify that their target belongs to the panel's own session
instead of trusting a web-contents id from the renderer.
Persisted state
Stored `preview` tabs migrate to `browser` (v13 -> v14). Context panel tab
limits are now per surface, so filling one surface no longer evicts another's
tabs. Address history is stored per project and per runtime.
Documentation
`preview.mdx` and `desktop-browser.mdx` rewritten across all locales, the agent
tool settings path corrected, new `DOCUMENTATION.md` for the browser-control
broker and the dev tunnel, and the `ui-api-decoupling` skill updated where it
still described the deleted proxy.
With tray background mode on, the in-app minimize button hid the window,
so the taskbar entry disappeared while the native title-bar and taskbar
minimize still performed a normal minimize. Minimize now always minimizes;
the setting gates the close path only.
The persisted key stays `desktopMinimizeToTrayEnabled` so existing settings
keep working; the visible label becomes "Close to the system tray" in every
locale.
Closes#2857
A single fast probe (2.5s per transport) used to be the only chance a
connection got on cold launch and resume, so a just-woken network, a
WireGuard re-handshake, or a relay cold start (TLS + WS + E2EE) regularly
produced false "unreachable" verdicts that kicked the user to the connect
screen. Now:
- cold launch releases the splash on the fast verdict and retries once in
the background with the full connect budget — a reachable instance
reconnects on its own, and a manual connect started meanwhile wins;
- resume retries on a 4s/10s ladder, the last attempt with the full budget,
before tearing the connection down; needs-login still disconnects
immediately on every path;
- full-budget relay probes are capped at the shared 8s connect budget
instead of inheriting the 15s relay session default, so a genuinely dead
server does not pin the retry for 15 extra seconds.
Probe steps, budgets, and retry decisions all land in the connection log.
Creating a device key while the UI is open through a public https domain
(reverse proxy) dropped that domain from the QR payload whenever the
dialog passed a preferred LAN URL, leaving only the local IP and relay
as transports. Carry the non-loopback request origin as an additional
direct candidate (priority 20, between LAN and relay) so paired devices
can keep using the same domain on any network.
path-open-utils.mjs and its smoke test are byte-for-byte identical to
his PR #1335 (opened 2026-05-19, review findings addressed the same
day, community-verified on CachyOS but never given a maintainer
review), and linux-app-discovery.mjs retains most of that PR's
implementation. The [1.17.0] changelog entry credited the PR that
carried this work forward but omitted the original author.
A guided explanation is only useful in a language the reader reads, so the
panel header gets a language picker alongside the model one, defaulting to
the interface language. Like the model, it is request state rather than a
setting: the language travels with the read and the generation, and the one
a walkthrough was written in is stored with it, so reopening a review
describes what is there instead of what a fresh one would be.
Only prose is translated. Hunk aliases resolve back to hunk ids and
icon/importance are validated against fixed English values, so a translated
one would be dropped by the normalizer — silently losing an anchor or a
style. Identifiers and paths stay as they appear in the code.
The language is part of the cache key, and a read now asks the cache for the
exact request it was given before falling back to the pointer. Without that
the panel answered a request to switch languages with the text it already
had, leaving the other language unused in the cache.
Alongside it:
- The answer budget is derived from the resolved model instead of a flat 24k.
That number was the same for a 64k-context model and for one that admits to
384k output tokens, and on the latter it was the only reason generation
failed: the model spent the whole allowance reasoning and returned nothing.
It is now min(96k, max(24k, a quarter of the context)) capped by the
catalog's output limit, decided once so the input reserve and the request
cannot drift apart.
- A read no longer offers Cancel. It is a few hundred milliseconds of git with
nothing to cancel, and the button flickered on every model or language
change. When the panel is showing a fallback, a banner names what is on
screen versus what was asked for — only once the read has settled.
- The header keeps one 32px control height and drops its labels below 680px
instead of squeezing them to two letters and an ellipsis.
Docs and module documentation updated in every locale.