Resolve conflicts after 914 upstream commits:
- CHANGELOG.md: keep brew opencode fix entry in Unreleased
- .gitignore: keep superpowers docs exclusion, take upstream additions
Drop /usr/local/ TOOLCHAIN_SEGMENTS addition — /usr/local/bin is part
of the default macOS PATH, so treating it as user-configured would skip
the login-shell fallback that this fix relies on. Upstream tests
(pass 1602) confirm minimal system PATH must not look user-configured.
* feat(skills): remove ClawHub catalog integration
Drop the ClawHub registry as a skills catalog source across web server,
shared UI, VS Code, docs, and locales. The catalog now serves git-based
sources only: the curated Anthropic repo and user-defined repositories.
Also removes the now-unused adm-zip dependency.
* feat(skills): redesign catalog around curated GitHub repositories
Replace the single-source dropdown with a card grid of curated GitHub
repositories (Anthropic, OpenAI, Cursor pstack/skills, Matt Pocock) plus
user-defined sources. Source cards show skill counts, GitHub stars, and
last-updated time; a global search covers all loaded sources.
Server: curated sources gain GitHub repo metadata (stars, pushed_at)
fetched best-effort with a 3-hour in-memory and on-disk cache; scans
run through a concurrency-limited, deduplicated cache with 3-hour TTL
persisted across restarts. Refresh still bypasses the cache.
Shared UI: source cards, global search with clear button, per-skill
GitHub links, install/installed states. VS Code curated list updated
to match. All new copy translated across 12 locales.
* fix(skills): address catalog review findings
- GitHub metadata fetch timeout drops to 1.5s (under the catalog
client's 3s deadline) and failed lookups cache briefly (5 min) so
repeated catalog loads do not re-hit a failing API.
- Disk cache files are written with owner-only permissions (0o600);
rename preserves the mode.
- loadSource deduplicates concurrent in-flight requests per source and
the shared isLoadingSource flag now clears only when the last active
source load finishes.
Plugin providers are registered from a plugin's `config` hook and credentialed
from its `auth` loader, both inside the running OpenCode process. Nothing about
them reaches `opencode.json` or `auth.json`, so resolution that only reads files
could not see them: selecting such a model failed with "has no known API base
URL" while the same model worked in chat (#2666).
`GET /provider` is where that state is visible. A new `runtime-providers`
module keeps one cached snapshot of it and reports, per provider, the
credential and endpoint OpenCode itself resolved. Credential resolution becomes
config -> runtime -> auth.json, and endpoint resolution config -> openai default
-> runtime -> models.dev catalog.
Providers with a dedicated wire format (Copilot, ChatGPT-plan OpenAI, Anthropic,
Google) are excluded from the runtime credential: for them OpenCode reports an
OAuth access token that their real transport does not accept.
opencode zen is excluded when the user has no zen login. OpenCode then reports
the sentinel `apiKey: "public"` and trims its catalog to free models that run on
its own infrastructure; the sentinel is never read as a credential.
Claude Code stays refused for background actions even when a plugin publishes an
OpenAI-compatible endpoint for it, because that endpoint is a facade over the
Claude Agent SDK and spawns the CLI per request.
No capability probe. Asking `GET /models` does identify a plugin whose protocol
lives in its own `fetch`, but measured across the 166 providers with an `api`
URL in the models.dev catalog it also denies six that work and simply have no
`/models` route. A provider that vanishes from the picker explains nothing,
while one that fails on use says why, so availability stops at credential and
endpoint.
The same list drives the Small Model and Changes Walkthrough pickers.
Validated against a real OpenCode with four plugin providers loaded: offered
providers went from 3 to 7, zen and Claude Code stayed out, and a generation
through a plugin-backed model that previously failed now returns.
* fix: reconcile busy sessions after managed OpenCode restart
Forced health-check restarts previously rebound the event stream without
settling in-flight turns, so sessions stayed busy with no terminal state.
Interrupt those sessions, classify health failures, and retain bounded
process diagnostics for post-restart diagnosis.
Fixes#2943
Co-authored-by: serkraser <serkraser@gmail.com>
* fix: surface interrupted chats after OpenCode restart
Complete unfinished assistant turns as aborted once the session is
authoritatively idle, and show a persistent toast so users can continue
instead of remaining silently stranded.
Fixes#2943
Co-authored-by: serkraser <serkraser@gmail.com>
* fix: redact Basic auth credentials in restart diagnostics
The key/value sanitizer stopped at whitespace, so Authorization: Basic
credentials survived in stderr tails and health snapshots. Redact the
scheme token before that rule runs.
Co-authored-by: serkraser <serkraser@gmail.com>
* fix(git): create worktrees from forked PRs via refs/pull/<n>/head fallback
A worktree created from a linked GitHub PR whose head branch lives in a fork
failed when the fork's head repository was missing (deleted fork) or
unfetchable (auth, network): the dialog threw 'PR head repository URL is
unavailable' before any git command ran, and the server had no fallback to
refs/pull/<n>/head, which GitHub serves on the base repository.
- NewWorktreeDialog: when pr.headRepo is absent, send a prRef config
(refs/pull/<n>/head from origin) instead of throwing; the fork config now
also carries prRef so the server can fall back when the fork fetch fails.
- git service: fetchPullRequestHeadRef fetches refs/pull/<n>/head into
refs/remotes/<remote>/pr-<n>-head (same refspec shape as
fetchRemoteBranchRef) and both validateWorktreeCreate and
attachGitWorktreeToCandidate fall back to it when the fork path fails;
fallback worktrees get --no-track and no upstream config because a PR ref
is not pushable. When both paths fail the original fork error surfaces.
- Focused tests cover the prRef-only path and the fork-unreachable fallback.
Fixes#2422
* fix(git): harden PR worktree fallback against stale fork refs (#12)
After a fork fetch fails, resolve immediately from refs/pull/<n>/head
instead of accepting a cached remotes/<fork>/<branch> tracking ref.
Match the PR base repository by URL (not a hardcoded origin remote),
store fetched PR heads under refs/openchamber/pull/<n>/head, and share
one existing-mode resolver between validate and create.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* fix(git): make PR head SHA authoritative and namespace private refs
Reuse local/remote branches for linked PRs only when their tip matches
pr.headSha; otherwise fall through to fork fetch / refs/pull. Store PR
heads under refs/openchamber/github/<owner>/<repo>/pull/<n>/head, prefer
HTTPS for direct base-repo fallback, and surface composite fork+fallback
errors when both paths fail.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* test(ui): assert validate/create forward deleted-fork PR payload fields
Guards the dialog wiring regression where validate omitted prRef while
create included it, by asserting worktreeManager forwards prRef,
prBaseRepoUrl, and related fields for deleted-fork configs.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* refactor(git): always checkout linked PRs from refs/pull/<n>/head
Move PR worktree resolution to the server. The UI now sends only
pullRequest identity (number + baseRepoUrl + optional head fields);
the server always fetches the authoritative PR head and best-effort
configures fork upstream afterward.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* refactor(ui): drop PrWorktreeConfig; send PR identity only
Delete the prWorktreeConfig module. NewWorktreeDialog maps linked PRs
straight to pullRequest identity, skips upstream defaults for that path,
and leaves checkout + optional fork tracking to the server.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* refactor(git): linked PRs are {number, baseRepoUrl} only
Drop fork upstream / tracking and head/base owner-repo fields from the
linked-PR worktree path. Fetch refs/pull/<n>/head, create --no-track, done.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* fix(git): meet #2422 Must/Should without refs/pull fallback
Linked PRs send fork identity only; the server provisions pr-<owner>,
fetches the head branch, and fails clearly when the fork is missing or
unreachable. Local reuse requires a matching headSha. Prefer HTTPS for
headRepoUrl. Do not write upstream tracking when the upstream ref was
never fetched.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* fix(git): drop invalid upstream fallback and PR branch collisions
Remove setBranchTrackingFallback: if upstream fetch fails, leave tracking
unset. When a linked PR's head branch already exists locally with a
different tip, create pr-<number> instead of git worktree add -b on the
colliding name.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* fix(git): strip PR worktree create back to fork-remote provision (#15)
Keep the original ensureRemoteName/Url path for linked fork PRs, prefer
HTTPS clone URLs, fail clearly when the fork is unreachable, and leave
upstream tracking unset when the upstream ref was never fetched.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
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.
Treating every undefined parse as empty config let a file that is not JSON
at all (YAML, plain text) read as {}, so a later write would back it up and
replace it - the same data loss this fix is meant to prevent. Only a
comment-only parse, where ValueExpected is the sole error, counts as empty.
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.
Auto-derived project labels were title-cased, turning .ssh into .Ssh and
opencode-claude into Opencode Claude. Show the folder name verbatim in the
sidebar, window title, settings selector and notification templates, and
migrate persisted legacy labels back to the folder name (manual renames
are preserved).
Plugin listing still called readConfigFile per layer, so one unparseable
project file made GET /api/config/plugins and VS Code listPluginEntries
fail. Reuse readConfigLayer isolation and pin comment-only empty parse
in the VS Code suite.
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>
Address review findings on the #2923 fail-closed parse fix:
- Comment-only files produce ValueExpected with no JSON value; treat that
as empty config instead of INVALID_JSONC. Partial object trees still throw.
- readConfigLayers no longer lets one unparseable layer abort valid sibling
layers. Mutations still fail closed on the custom/user write target.
Co-authored-by: serkraser <serkraser@gmail.com>
Add store persist/hydrate regressions for terminal branch associations
and a server test that a complete empty open list does not query closed
PRs. Inline the open-only matcher state so the next repo target can win.
Co-authored-by: serkraser <serkraser@gmail.com>
Branch PR status now resolves open PRs only, revalidates closed/merged
associations on a discovery cadence, and clears authoritative empty
results so the panel can self-heal without a manual refresh.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.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>
The Skills Catalog source dropdown showed "ClawdHub"; the registry brands
itself as ClawHub. Update curated/fallback labels across web, UI, and VS Code,
align docs, and add regression tests.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
The opencode-claude integration registers its provider as `claude-code`, which
never matched the `claude` quota provider, so the collapsed Usage section in
the work status panel showed no limit for a model from that integration.
Claude windows also reported no duration, leaving the headline to fall back to
whichever row came first instead of the limit that runs out soonest. The
session and weekly windows now carry their length; extra usage stays without
one because it is a monthly spend cap.
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 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.
* fix(scheduled-tasks): claim schedule occurrences across server instances
Two OpenChamber servers sharing project config each armed timers and both
dispatched the same daily/weekly/cron/once slot (#2710). Claim the occurrence
in shared config under a cross-process write lock before creating a session.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* fix(scheduled-tasks): harden occurrence claim failure and lock ownership
Address PR review blockers: release running-slot bookkeeping when claim
throws, avoid silently dropping an armed occurrence after a due-slack sync,
verify lock-file ownership on release, and cover real on-disk lock behavior.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* fix(scheduled-tasks): always release running slot on state-write failures
Wrap runTask bookkeeping in finally so claim, manual-start, and completion
lock timeouts cannot stuck-run a task; drop the diskNext claim guard that
suppressed later occurrences; recover unparseable locks via mtime age.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* fix(scheduled-tasks): stop re-arming past nextRunAt and clear stuck running
Only schedule future nextRunAt values so once-task losers and claim-failed
paths cannot spin delay-0 retries. Clear past once nextRunAt on claim, and on
completion-write failure retry terminal status so manual runNow still returns
the session instead of a hard 500 with lastStatus stuck running.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* fix(scheduled-tasks): release write chain on lock acquire timeout
withProjectWriteLock left the in-process promise chain pending when
acquireProjectFileLock timed out, wedging every later project write and
stranding runTask before finally. Always release the chain; surface
persistError on run; record once claim failures in task state.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
---------
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* chore: remove verified dead declarations
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* chore: narrow unused internal exports
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* chore: remove newly exposed dead helpers
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* chore: remove unused deep-link serializer
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* test: drop two tests that assert on copies of the code
mainLayoutMobileSidebarMount read MainLayout.tsx and SessionSidebar.tsx as
strings and asserted on source substrings down to exact indentation, so it
failed on formatting rather than behaviour. useProjectSessionSelection.test
reimplemented the hook's visitNodes logic inside the test file and asserted
against that copy, so it could not observe the hook at all.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* test: repair sync suites that had rotted while unrunnable
No runner executed packages/ui, so these drifted from the source unnoticed:
two imported helpers that are no longer exported, one directory-store stub
predated the session field routeMessage reads, and the WebSocket fake missed
the mandatory url-token mint plus the close event the socket wrapper reads.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* test: stop the web suite failing on timeouts and a hand-copied mock
The Git suites drive a real git binary, so the 5s default made a valid suite
fail differently per run. The gitApiHttp mock listed ~70 export names by hand
and fell behind the source; it now derives every stub from the real module,
which the added shared-UI aliases make resolvable.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* test: run every suite from one command and in CI
packages/ui (232 files) and packages/vscode (22) had no test script at all, CI
ran neither, and 9 vscode files could never run because Node cannot resolve
their extensionless TypeScript imports. Three electron files sat outside every
script list, one of them importing vitest, which that package does not depend
on. A runner gives each file its own process, since these suites keep
module-level singletons and fail by load order when sharing one.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* chore: delete a superseded repro harness and a completed plan
The issue-2638 harness needed lsof, overrode process.platform and spawned real
servers, and nothing referenced it; event-stream/rebind.test.js now covers the
same hub-pinned-to-the-old-port behaviour. The pairing v2 plan described relay
and the pairing UI as out of scope, both of which shipped.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* docs: point at the theme tools and record the github barrel invariant
convert-vscode-theme and harmonize-theme were referenced nowhere, so the
theme-authoring reference now names them. The github barrel is loaded through
await import('./index.js') and destructured per route, which no static report
can see; documenting that is what stops the next cleanup from deleting it.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* test: repair merge drift in bridge and route-registry mocks
upstream/main gained upsertProviderConfig on bridge-system-runtime and a
PATCH scheduled-task route after this branch forked. Their test doubles
were never updated to match:
- bridge-system-runtime.test.js: add upsertProviderConfig to the
opencodeConfig mock so the import resolves.
- sse-routes.test.js: add app.patch to the route registry stub.
---------
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
Settle the buffered-body wait when the delivery deadline fires so the
stream's buffered chunks are freed immediately, and make the deadline
injectable for tests. Covers the stalled-mid-body path with a test.
When the relay drops mid-request, the prompt_async body frames can be
lost. The tunnel host forwarded the request to loopback as an
empty/truncated chunked body, which the server rejects with a bare 400
(empty response body) — the mobile app's 'Failed to send message (400)'.
Host now buffers request bodies (<512KB) and forwards the complete body
only once StreamEnd arrives; larger bodies still stream live. A new
hasBody flag on the request head lets the host detect a body that
delivered zero frames and abort it as an ambiguous transport failure
(which the client already retries) instead of forwarding an empty body.
A 15s body-delivery deadline converts stalled tunnels into clean aborts.
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.