mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-11-01 03:20:37 +00:00
431 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2b57a67df2
|
feat(missions): add byte-bounded panel page reads (#1246)
First server-contract slice of 4b, following #1241. **This PR adds an opt-in byte-bounded read; the UI still uses its existing APIs. Panel-level boundedness is not complete yet.** ## Contract - `panelPage` RPC and `POST /api/workspaces/:id/missions/panel-page`. - Responses fit **64 KiB of serialized UTF-8 JSON**, envelopes and continuations included; requests fit **16 KiB**. - Sources: summary/cleanup hints, one compact mission/root field, or an existing task selector. - At most 50 exact row pieces per response. Oversized rows are sliced as exact JSON text, never silently truncated. - Digest-bound cursors refuse changed collections. Partial rows prove neither absence nor mutation state. - Directory reads return no maps; mission/task reads use the named range only. No legacy-list/snapshot fallback. - Existing APIs and full authoritative/internal reads are unchanged. Future #926 metadata (`document`, `documentPending`, `requestID`) survives serialization. ## Measurements - 400 escaped/nested rows including a 5,000-actor row: recovered exactly in 95 transfers, maximum 65,536 bytes. - Real journal: 300 live missions, 300 pending cleanups, 300 task actors. - Directory: 9 summary + 6 cleanup transfers, no warm storage scans. - Actors: 7 transfers, maximum 44,605 bytes, one named-range observation per transfer. ## Validation Eight red checks cover byte accounting, cursor integrity, source scope, selector bounds and forbidden fan-out. Passing: typechecks; 1,498 server tests; 1,845 UI units; 409 serial browser cases; help; plugin build; isolated OpenCode 2.2.3 native gates. One existing environment-gated test skipped. ## Remaining 4b work Bounded live activity/attention for off-page actors and nonselected missions, UI adoption with per-page caches/discovery/completion seeding/cleanup retries, then removal of old list fan-out. Each transfer currently serializes/hashes its source collection afresh, with linear per-transfer CPU cost. |
||
|
|
abb14f83c6
|
feat(missions): page a mission's tasks in the panel (#1241)
Slice 4a of bounded Missions reads (#1220's R4; slices 1–3 are #1229, #1231 and #1237). **A compact map now holds one page of tasks, every row has a size budget, and every task stays reachable through a new paged tasks read.** The commit message has the full design. ## Change - **Row budgets:** - 32 KiB per task row, 8 KiB per report row, 4 KiB per plan change. - Observations leave the panel map. - A task keeps only its newest late report, plus a count. - A plan change's added, retired and rewired task lists become content bodies. - **What a map carries:** - the first 50 tasks of the shared display order; - up to 20 pinned rows (pending notifications, blockages, recovery candidates, the newest reports' tasks, briefing sources); - `taskPage`: an order digest, the window, totals and status counts; - `actorEvidence`, computed over every task. - **New `tasks` RPC and `GET …/missions/:id/tasks`:** - return a window of tasks, or tasks named by key, ID or report; - refuse a stale order with `order-changed`; - for a plugin without `tasks`, page through its single-mission read. - **UI:** - a page cache per mission; - tree gaps with Show more, and graph stubs for edges to tasks not shown; - reader targets that aren't loaded are read by key or ID before opening; - Show all for earlier results; - 13 strings in ten locales, and the help pages updated. - **Unchanged:** authoritative reads and recurring passage maps stay full. ## Payload (2,400 tasks × 20,000-character briefs, 1,224-task rewire) | Read | Bytes | |---|---| | Full map | 49,431,307 | | List | 49,220 | | Map | 48,862 | | Each of 48 pages | ≤ 38,780 | Before this PR, one task with 1,997 observations weighed 4,617,922 B; it now weighs 4,836 B. One with 100 late reports went from 173,666 B to 2,830 B. **Next (4b):** full maps only for the selection and wanted missions, paged summary rows and cleanups, and bounded actor-scale arrays. ## Validation - **Server:** page traversal reaches every task exactly once, in display order. Also covered: the `order-changed` refusal, the row and report caps, pins, taskless reports, and the route fallback. - **Store:** merging, reads recorded before fetching, coalescing, and `clear` fences. - **Browser:** tree gaps, graph stubs, Show more, re-measured rails, Needs you for a task outside the page, a dependency loaded by key, Show all, and reader targets read by ID. - **Red checks:** each new test fails with its fix reverted. - **Passing:** - server and UI typecheck; - 1,312 server tests; - 1,827 UI units; - all 41 `mission*` browser files plus 3 panel files; - `test:help`; - `test-missions-native.mjs` on isolated OpenCode 2.2.3. |
||
|
|
794ac20306
|
feat(integrations): recognise OpenCode integrations from the reviewed catalog and read their state (#1243)
## Summary Slice C2 of the OpenCode integrations plan: CodeNomad recognises configured OpenCode integrations from the reviewed catalog and reads their state. This is the base for attaching an open application to a conversation (C4) and for the Bundles extension (C3). - **Catalog** (`opencode/integrations/catalog.ts`): newest `catalog-v<n>` release of `NeuralNomadsAI/opencode-integrations` (GitHub releases API, never "latest"), `catalog.json` asset through the API origin, 256 KiB / 10 s bounds, zod validation, 30 s shared cache with conditional list requests, last valid catalog persisted and served as `stale`, `unavailable` with an empty list otherwise. No catalog release exists yet: the real repository reads as `unavailable` today (no release was created). - **Recognition** (`recognition.ts`): registry package (aliases resolved), plugin ID or exact `-<plugin>` rule (disabled plugins), `sources.git` prefix; a source designating two integrations or loaded under another plugin ID is non-conforming. - **State route** `GET /api/workspaces/:id/integrations?directory=` (`status.ts`, `integrations.ts`, `routes/integrations.ts`): absent / inactive / failed / unsupported / non-conforming / unknown / ok; fixed `status` method under the catalog's RPC ID only, 4 in flight, 8 s each; full output validation, marker mismatch and path-like `appVersion` refused; ownership rechecked with `assertCurrent` after the calls; catalog internals never returned. - **Plugin add** (AC-P7, D18): recognised `native` integrations are Global-only (`global-only`, 10 locales); registry and Git specs of one integration are duplicates. - **UI store** `stores/integrations-store.ts`: demand reads, 300 ms coalescing of `rpc.neuralnomads.<app>.*`, `plugin.updated`, `config.updated`, no polling. Mounted by C4. - Docs: `dev-docs/OPENCODE_INTEGRATIONS.md`, `PLUGIN_CONTROLS.md`; help `integrations.md` in English and the nine translations. ## Validation - `node --import tsx --test packages/server/src/opencode/integrations/catalog.test.ts` (11 pass, local release server) - `node --import tsx --test packages/server/src/opencode/integrations/recognition.test.ts` (8 pass) - `node --import tsx --test packages/server/src/server/routes/integrations.test.ts` (4 pass) - `node --import tsx --test packages/server/src/opencode/plugin-sources.test.ts` (11 pass) - `node --import tsx --test packages/ui/src/stores/integrations-store.test.ts` (4 pass) - `node --import tsx scripts/test-integrations-recognition-native.mjs <opencode 2.2.5 cli>`: PASS (added to the runtime matrix) - Server and UI typecheck; `npm run test:help` pass. File sizes: `http-server.ts` ~2500 lines (+7), `index.ts` ~790 (+2), `pr-build.yml` ~770. |
||
|
|
5313fbf5ad
|
feat(integrations): add the OpenCode integration contract and its test fixtures (#1238)
## Summary
Slices **F0** (integration contract) and **T0** (CodeNomad test
fixtures) of the approved integrations SCR (§4.2 basis; AC-F1, AC-F2,
AC-T1, AC-T2, AC-T3, AC-T5, AC-T6). No user-visible change: this is the
base the recognition, attachment and API 4 slices build on.
### F0: contract
- `packages/server/src/opencode/integrations/contract.ts`: zod schemas
of the marker, the reviewed catalog and every RPC definition of contract
1 (`integrationRpcSchemas`, keyed like `rpc.schema.json`:
`<method>.input|output`, `<event>.event`), plus the draft constants.
- Vendored JSON Schemas of
`NeuralNomadsAI/opencode-integrations@68eb8aa` (`marker`, `catalog`,
`rpc`, `host`; byte-identical to spec 0.1 at `dac13a74`) with
`VENDORED.json` (revision + Git blob IDs, checked by the test).
- **`readIntegrationRpcOutput(name, value)`**: the one way to read an
RPC output. Integrations register their definitions without `pattern`
(OpenCode 2.2.5 refuses it; spec/rpc-contract.md §4), so OpenCode passes
outputs the spec forbids; the full zod schema (patterns and bounds)
decides, and a non-conforming output is an integration error (`{ ok:
false, error: "non-conforming" }`) that never echoes the value. Tested
against every sample the pattern-free definitions accept but the spec
refuses.
- **Parity test** (`contract.test.ts`): a corpus of valid and invalid
samples (including the conformance package's cases) through zod **and**
Ajv 2020-12; fails on any disagreement and on any definition missing on
either side. Lengths count code points like JSON Schema (astral
characters agree). Four seeded mutations of `contract.ts` are each
caught.
- Outputs are reader schemas (unknown fields dropped); inputs strict;
`integrationCatalogSchema` adds the uniqueness JSON Schema cannot
express (`integrationCatalogDocumentSchema` stays exact).
- Types in the new `packages/server/src/integration-types.ts` (not
`api-types.ts`).
- `ajv2020` = `npm:ajv@^8.20.0` dev alias, so the hoisted Ajv 6 used by
existing mission tests keeps resolving (a plain `ajv@8` dev dependency
shadowed it and broke their types).
### T0: fixtures
- `scripts/fixtures/fake-npm-registry.mjs`: the loopback fake registry,
now shared by `test-plugin-sources-native.mjs` (scoped names, 404 for
unknown names, integrity/shasum).
- `scripts/fixtures/fake-integration/`: installable fake integration
package (`@neuralnomads/opencode-<id>`): marker,
`status`/`installHost`/`hosts`/`attachHost`/`detachHost`/`advertise`/`withdraw`
and payload-free events, offer registry on an **injectable clock** (15 s
lease, renewal without hello, replacement only after a new hello, 16
offers, `busy`/`gone`/`refused`), **marker-mismatch** mode,
`hostPart`/`installOutcome` options, a loopback **fake host with
sentinel secrets** (and a `leaky` mode),
`findSentinels`/`assertNoSentinels`.
- **Python discovery isolation**: `python-host.py` runs the vendored
reference client (`Service.discover`, `advertise_all`/`withdraw_all`)
under `python -I -B` and an audit hook; the native test asserts every
file and socket access stays inside the fixture and reaches only the
fixture daemon. `writeFixtureServiceRegistration` registers the fixture
daemon under the fixture's `XDG_STATE_HOME`; `withProductRuntime` now
passes its `env` to `run`.
- Docs: new `dev-docs/OPENCODE_INTEGRATIONS.md` (indexed in `INDEX.md`
and the `AGENTS.md` area list), native fixtures section in
`BROWSER_TESTING.md`, `PLUGIN_CONTROLS.md` reference.
## Finding to note
OpenCode 2.2.5 refuses `pattern` in RPC definitions: Effect's JSON
Schema import throws "Patterns may block validation and are disabled",
so a call whose declared output has a pattern fails with
`rpc.invalid_output`. The fake package registers its definitions without
`pattern` and validates its own input; CodeNomad revalidates outputs
with `contract.ts`. The Blender pilot hit the same refusal;
opencode-integrations `68eb8aa` now registers without patterns and says
readers validate outputs, which `readIntegrationRpcOutput` does.
## Out of scope
- AC-T4 (Blender 5.2 in the integrations CI, manual protocol) belongs to
`opencode-integrations`.
- AC-T3's §4.4 breaker clock: the breaker arrives with the API 4 slice;
the convention (every lease/expiry/rate window takes `now`) is
documented.
## Tests
- `node --import tsx --test
packages/server/src/opencode/integrations/contract.test.ts`: 24 pass
- `node --import tsx --test
scripts/fixtures/fake-integration/fake-integration.test.mjs`: 15 pass
- `node --import tsx scripts/test-integration-fixtures-native.mjs
<opencode 2.2.5>`: PASS (fake registry install, contract outputs, marker
mismatch, Python isolation + advertise/attach/detach/withdraw, no
sentinel in outputs or daemon log)
- `node --import tsx scripts/test-plugin-sources-native.mjs <opencode
2.2.5>`: PASS with the shared registry
- `npm run typecheck --workspace @neuralnomads/codenomad`: clean
- Full server suite: 2966 tests, 2953 pass, 2 fail under load
(`plugin-sources.test.ts`, `media-blob.test.ts`; both pass alone, as
does the `index.test.ts` load timeout)
- `node --test scripts/native-fixture-guards.test.mjs
"scripts/fixtures/wsl-*.test.mjs"`: 11 pass
- CI: unit fixtures in the server test job; the native test in the
runtime-contract job (minimum and latest).
|
||
|
|
54a93e7c10
|
fix(plugins): repair damaged plugin payloads and show recovery notices once (#1239)
On a Windows Server CLI install, the console kept logging "Managed plugin bundle differs from its digest" and the browser filled with identical "questions and permissions not fully recovered" toasts, covering a phone's whole screen (#1235). The published package was correct. A bundled plugin file is named by its sha256 in a folder shared by every connection using the same OpenCode configuration, and it was written in place with writeFile(..., "wx"). A concurrent installer could read it half-written, and a write interrupted by stopping the server left a truncated file that "wx" never replaced, so every later connection failed. Both modes were reproduced with the published code and the exact reported error. A payload is now published only by renaming a complete temporary copy over its name. A file with the wrong bytes or a link is replaced the same way; a Windows directory link is re-checked and removed with rmdir, which never touches its target or a regular file, and a changed entry is re-verified instead of replaced, so one installer never deletes another's verified repair. An ordinary directory at that name is refused with a clear error. Verification and replacement share one bounded retry with backoff (about 3 s) for EPERM, EBUSY and EACCES locks from antivirus or readers. Each distinct failure (error code, operation and path, with only generated temporary names masked) is logged once as an actionable error; repeats go to debug until provisioning succeeds. In the UI, each recovery warning is shown at most once per project and message per tab, across reconnects, re-added projects and reloads, and a successful recovery re-arms it. The plugin-unavailable case is one plain notice, "Some waiting questions may be missing", saying that requests already waiting may not appear, new ones still do, and restarting CodeNomad may fix it. Toasts fit narrow screens. Strings, the permissions help page and NATIVE_INTERRUPTION_UX.md / OPENCODE_COMPATIBILITY.md are updated in all ten locales where applicable. Tests: installation regressions for wrong bytes, truncation, concurrent installers, file and directory links, a reader-held lock, an ordinary directory and a replayed concurrent-repair race; log dedupe across random temporary names and UUID-named directories; a store test with the real notification code (one notice after eight reconnects and a second project, nine before). Server plugin suites, UI recovery suites, both typechecks and the help checks pass. Fixes #1235 |
||
|
|
da8607d49e
|
feat(missions): send compact mission maps to the panel (#1237)
Slice 3 of 5 of bounded Missions reads (#1220's R4; #1229 added content reads, #1231 moved the readers onto them). **The panel's `list`/`mission` reads now return compact maps: the review's 2,400-task case lists in 1.09 MB instead of 48.7 MB.** ## Change - `compactMissionMap`, on copies at serialization time: - drops every `MISSION_BODY_FIELDS` body and keeps its `bodies` descriptor; - keeps bounded excerpts: the objective label, report rows (240-point excerpt plus `decidedBy`), plan-change reasons and the independent explanation; - reduces the briefing to its identity; - carries unknown fields through. - **Full, untouched:** snapshot, admission, mutation results, `mission.inspect`, current passages, and the maps `MissionControl` reads internally. A test fails if any of them loses a body. - **Older plugins:** - the route compacts full maps; - a plugin without `list` answers `plugin-unavailable` (the mass-loading snapshot fallback is gone); - without content reads, the ordinary read failure shows with Retry; - the admission route never compacts. - **UI:** - body presence comes from descriptors, and labels from excerpts; - the slice-2 fallbacks are removed; - new `MissionPanelMap` types. - **Visible:** Plan-change reasons are cut to 240 characters in the list, and the independent-mode tooltip shows an excerpt. The readers keep the full text. No help page describes either. - **Coordination:** coordinated with the parallel #926 branch, whose scheduling editor already reads through descriptors and the content route. Its added fields carry through. ## Payload (2,400 tasks × 20,000-character briefs) | Read | Bytes | |---|---| | Full list (review probe) | 48,673,523 | | **Compact list** | **1,093,511** | The remaining growth is the row count; slice 4 pages it. ## Validation - **New tests:** - `mission-compact-map.test.ts`: shape, idempotence, unknown fields, payload, and authoritative maps kept full; - route tests for the older-plugin cases; - store, bodies and display units; - new reader and intents browser cases. - **Red checks:** each test fails with its fix reverted. - **Passing:** - server and UI typecheck; - 837 missions tests, 484 routes tests and 164 opencode-missions tests; - 1,818 UI units; - all 40 `mission*` browser files and 8 panel files; - `test:help`; - `test-missions-native.mjs` on isolated OpenCode 2.2.3. |
||
|
|
66a85afa1c
|
feat(plugins): add, remove and show the version of any OpenCode plugin (#1232)
## Summary
Status → Plugins can now **add**, **remove** and **show the version** of
any OpenCode plugin (SCR §4.1, slices C1/U1/D1/Q1, AC-P1…AC-P9).
- **Add**: a `+` icon beside Refresh opens a small `.window-*` popover:
package spec (`--surface-field`), scope (Global default; Project shows
the warning), trust sentence. Registry and remote Git specs only.
- **Remove**: each row's menu gets **Remove from Global/Project** for
declarations in that scope's own write target. `id`/`-id` rules stay;
the package cache is never touched.
- **States**: a just-added source shows **Installing** until
`plugin.list` reports its exact target (then an ordinary active/failed
row). After `PLUGIN_SOURCE_PENDING_TIMEOUT_MS` (120 s) it shows **Not
confirmed** with Refresh.
- **Tooltips**: version, **Update available**, failure + native `ref`,
GUI notice when a `features.gui` package is declared outside Global.
## Implementation
- Server: new `opencode/plugin-sources.ts` + `routes/plugin-sources.ts`
(`POST`/`DELETE /api/workspaces/:id/plugin-sources`), built on
`PluginControls.editConfigDocument` (ownership, scope resolution,
deletion fence, connection fence, lock, atomic replace, JSONC/CRLF/BOM,
WSL). Pure helpers in `plugin-source-document.ts` (npm-package-arg
validation, package identity, verified removal). Types in
`plugin-source-types.ts`. `plugin-controls.ts` only gains `pluginId` on
sources and the `gui` passthrough.
- Errors carry a stable `code`: `invalid-spec`, `local-path`,
`plugin-id` (422), `duplicate`, `stale` (409).
- UI: `stores/plugin-sources.ts`, `components/plugin-source-add.tsx`,
`components/plugin-row-actions.tsx` (replaces
`plugin-package-action.tsx`, memoized item descriptors), i18n part file
in the 10 locales, help in 10 languages, `dev-docs/PLUGIN_CONTROLS.md`
rewritten.
## Deviations to note
- `disabled` state for unloaded sources: `plugin.list` omits disabled
plugins and exposes no package→ID link, so an unloaded configured source
is shown as **Not confirmed** ("may be disabled, or installation not
confirmed"). A disabled package with an exact `-id` rule therefore shows
its ID row (switches) and its unconfirmed source row (Remove items).
Documented in `PLUGIN_CONTROLS.md`.
- AC-P7 "a GUI package is added Global-only, Project refused":
`features.gui` is known only once OpenCode has loaded the package, so
pre-install refusal is not possible. This PR keeps Global as default,
warns for Project, and flags a Project-declared GUI package in its
tooltip afterwards. Native-family Global-only enforcement (D18) waits
for the integration catalogue (later slice).
## Tests
- `node --import tsx --test
packages/server/src/opencode/plugin-source-document.test.ts
packages/server/src/opencode/plugin-sources.test.ts` (13 pass)
- Full server suite: 2931 pass, 0 fail (one load-timeout in
`index.test.ts`, passes alone)
- `CODENOMAD_TEST_WSL_PLUGIN_CONTROLS=Ubuntu …
plugin-control-document-wsl.test.ts` (9 pass, incl. new add/remove)
- `node --import tsx scripts/test-plugin-sources-native.mjs <opencode
2.2.5>`: PASS (loopback fake registry, 404 → failed with `ref`); added
to CI runtime contracts
- UI store tests (45 pass), browser
`plugin-sources`/`plugin-controls`/`plugin-packages` (14 pass), `npm run
typecheck`, `npm run test:help` (31 pass)
|
||
|
|
edeafe586f
|
feat(missions): read one-time mission bodies through the content route (#1231)
Slice 2 of 5 of bounded Missions reads (#1220's R4; slice 1 is #1229). Readers stop rendering prose from the full maps and read bodies through the exact content route instead, so slice 3 can drop bodies from the panel's maps. ## Change - **Display cache** (`stores/mission-content-store.ts`, wired in `stores/mission-content.ts`): - **Key and reads:** keyed by workspace, project, mission and selector. One read runs at a time per body, every continuation is digest-pinned, and a body is never assembled from two digests. - **Refresh:** a newer map revision, a plugin reload, a reconnect or a new event stream marks shown bodies stale. The last good body stays shown until the newer value covers the page. - **Failures:** `content-changed` restarts from offset 0, at most 4 times. A failure keeps the display, with Retry. A late answer is aborted and never published. - **Memory:** unshown bodies are kept within 4 MiB and 128 entries, least recently used first. - **Budget:** requests use the shared background read budget, and nothing polls. - **Who reads through it:** - the overview, task, report, plan-change and briefing readers; - the editor's edit prefill, which stays read-only until both fields arrive, its authority still the opened revision; - Save as brief, which reads both fields when its name field opens and saves only from one revision; - the Compare and decision readouts. - **What a read loads:** only the prefix its page shows (`lib/mission-body-pages.ts`). Closed disclosures read nothing. - **Kept as before:** admission, mutations, lost-acknowledgement checks, Stop, Delete and recovery keep their authoritative reads. Admitted recurring passages keep their own path. - **Older plugin:** it answers `plugin-unavailable`, and readers show the map's own value. `MISSIONS.md` (Agent conventions) lists this fallback and every remaining map-prose use for slice 3 to remove. - **Visible:** "Reading…" until a first read lands, and a new "This content could not be read." line with Retry (10 locales). The help pages don't describe reader loading, so they need no change. ## Validation - **Store tests:** demand-sized reads, digest pinning, the last good display, the in-flight revision, late-answer fencing, `plugin-unavailable`, eviction and the page prefix rule. - **Browser test:** `mission-content-reader` reads a body slice by slice, and never shows a body changed between slices. - **Red checks:** each test fails with its fix reverted. - **Fixtures:** ten browser fixtures serve the content route through a shared helper, with assertions unchanged. - **Passing:** server and UI typecheck; 1,132 UI units; all 39 `mission*` browser files and 8 other panel files; 831 server missions tests; 647 route and opencode-missions tests; `test:help`. The browser runs are on a base with #1230. |
||
|
|
248a4b3cd1
|
feat(status): show the session's /goal on one line above Tokens (#1228)
## Summary OpenCode's built-in `/goal <objective>` keeps a session working turn after turn until its reviewer judges the objective met, and pauses the goal on its own after a stop, a failed run, stalls, its round limit or a service restart. CodeNomad already listed `/goal` in the composer picker (it comes from `command.list`) but showed nothing of the goal itself. While the shown session has a goal, the **Status** tab now shows **one line above Tokens**: - a status badge: **Active**, **Paused** or **Complete**; - the objective on one line with an ellipsis; hovering or focusing it shows the full objective and the reason for the last change (why it paused or was met), which is also the row's accessible description; - the rounds used out of the maximum (`2/50`); - **Pause goal** / **Resume goal** (one button that keeps focus when the state flips) and **Clear goal**. Without a goal, without a session, on a runtime without goal support or after a failed read, nothing renders (no hint, no gap). The line is not a Status section: it cannot be hidden or reordered. ## How it works - **Read path:** the instance proxy refuses plugin RPCs, so `GET /api/workspaces/:id/sessions/:sessionId/goal` (`server/routes/session-goal.ts`, contract in `opencode/goal-contract.ts`) validates the session ID, reads the session, checks that its Location belongs to the workspace, then calls the fixed `opencode.goal` `get` in that Location (8 s timeout, abort on disconnect). It returns only the displayed fields; a missing RPC or method answers `unsupported`, any other failure or malformed output `unavailable`. - **Freshness:** plugin RPC events already reach the UI through the native event relay (as `rpc.codenomad.missions.changed` does), so the store follows `rpc.opencode.goal.changed` for tracked sessions, with a per-session version that drops replies older than a change already heard. It also reads when the line is shown (Status visible, session switch), on reconnect (coalesced) and after its own commands. It never polls; a failed read keeps the last goal shown; deleting the session or stopping the workspace drops it. - **Commands:** the buttons reuse the composer's native command path (`executeCustomCommand(..., "goal", "pause" | "resume" | "clear")`, with session admission, instruction sync and the profile environment). They are disabled while a command runs; a refused command (for example pausing without an active goal) shows OpenCode's message. ## Contract Upstream `@opencode/schema/goal` (anomalyco/opencode #59437, #59850; shipped in 2.2.1, identical goal files in 2.2.3 and 2.2.5): RPC `opencode.goal` with `get({ sessionID }) -> Goal.State | null` and event `changed { sessionID, goal }` published in the session's Location on every write. Documented in `dev-docs/OPENCODE_COMPATIBILITY.md` ("Session goal"); refresh policy row in `CACHE_REFRESH_CONVENTIONS.md`; line convention in `UI_SHELL.md`. ## Help New `conversations.md#goal` section (start, read, pause/resume/clear, automatic pauses) and a sentence in `settings.md` about the Status line, in English and all nine translations. ## Validation - Root and server typecheck clean. - Route tests 3/3; store tests 8/8 (with and without `--conditions=browser`), including rapid session switching through the shared background read queue and hidden sessions. - Browser: `session-goal` (no goal, active with Pause, paused with Resume and reason, Complete, Clear, command arguments, update after `changed`, keyboard focus kept on the toggle across Pause and Resume, line above Tokens) and `status-panel` 6/6; `status-tokens` and `fixture-wiring` also pass. - Ten-locale help 31/31. - Native `scripts/test-session-goal-native.mjs` against the exact OpenCode **2.2.5** CLI (twice): `get` returns null for a new session and another workspace gets 403; `/goal pause` without a goal fails with OpenCode's message; a goal set with the fake provider is paused by the reviewer, then resumed and cleared, with the `changed` events relayed to the owning workspace. CI runs it on the minimum and latest runtime lanes; the store test joins the UI unit list. - Full server suite on the rebased branch: 2,841 passed, 9 skipped; the `plugin-controls` (same-target serialization) and `panel-extension-api4` (deadline/abort) timing tests failed and the `index.test.ts` shutdown test was cancelled under full-suite load. None of them touches this change, and all three files pass alone (44/44). Large files touched with 1–4 line changes: `packages/server/src/server/http-server.ts` (~2,490 lines), `packages/ui/src/lib/api-client.ts` (~860 lines). |
||
|
|
5bce23079b
|
feat(missions): read mission bodies exactly and bound tool results (#1229)
Slice 1 of 5 of bounded Missions reads: R4 of #1220, accepted for this follow-up series. Since #1220, a mission's content is bounded only by its 2,000 journal entries, so every body must be reachable through bounded, exact reads instead of whole maps. ## Series 1. **This PR:** an exact, digest-pinned content read (RPC and route), plus bounded mutation tool results. 2. UI readers, the editor, Save as brief and Compare/decision artifacts fetch bodies through the content route, behind a fenced display cache. Maps stay full. 3. Compact `list`/`mission` maps, with bodies replaced by `{ characters, digest }`; authoritative admission and mutation reads stay full. This covers the 48 MB panel case. 4. Paged compact task rows for the tree and graph, with omitted rows and edges shown and a way to read further. 5. The current passage: a compact projection plus content within its 32 MiB response and 256 KiB entry limits. This covers the 864-task case. ## Change - `MissionControl.content`, the `content` RPC and `GET /api/workspaces/:id/missions/:missionID/content` read exactly one body, from one range read of that mission. A body is: - a mission field or a task field; - a task's reports; - a report by ID, whether current, earlier or late, and never substituted; - a plan change by revision. - **Slicing and digests:** - Slices fit the 100,000-character inspect bound, in two-space JSON. - Each slice repeats its first answer's digest and is refused (`content-changed`) when the value differs. - Unrelated revisions don't invalidate a read. - **Route checks:** the route reuses the list's workspace, plugin and project checks. An older plugin answers `plugin-unavailable`, with no fallback read. - **Mutation tool results:** - Delegate, report, revise and briefing results fit the same bound. - An oversized frontier or assignment prompt becomes an exact `mission.inspect` continuation. - `revise` pages its affected tasks. - The delegate tool instructs reading the prompt whole and never passing the marker. - **Not visible yet:** no UI consumer, and maps are unchanged. ## Validation - Content reads recover every field exactly across slices (escaped text, emoji, late/historical reports, plan changes) and refuse a changed value. The route refuses a foreign project. Mutation results stay bounded with exact continuations. - Each test fails with its fix reverted. - Server and UI typecheck; 1,278 server missions/opencode-missions/mission route tests; `build:missions`; `test-missions-native.mjs` on isolated OpenCode 2.2.3 (20 gates). - Before the rebase, the author also ran all 37 `mission*` browser files, `automation-forms`, the UI units and `test:help`. |
||
|
|
2e585a7e83
|
feat(missions): lift the per-mission actor and task caps (#1220)
Step 6 of 6 toward removing the Missions limits (#1167 follow-up), after per-mission capacity (#1178) and panel summaries (#1180). **Each mission is now bounded only by its own 2,000-entry journal; the 8-actor and 96-task caps are gone.** ## Change - **Capacity:** the fixed deletion reserve becomes `2 x (1 + actors)` inside the 2,000 entries, so one Stop and then the deletion always fit (actors join only through ordinary writes, which keep it; a Stop keeps the deletion's share). 2,000 is both the write and the read bound of a range. - **Control:** Stop/Pause/Play in waves of 8 actors. - **Validation:** actor/task caps removed from the reducer, parser, schemas and tool inputs. The other bounds are now natural ones: - dependency updates must each name a distinct task; - control and cleanup targets are bounded by the receipts they need; - recurring passages lose their 8/96 and 32-target limits; - completed recurring control records drop their target list. - **Reads:** - `mission.inspect` pages maps over 100,000 characters (`taskOffset`, with page actors); - the actor context lists at most 32 open assignments and points to inspect for the rest; - activity keeps its 160-actor budget but prioritizes actors with open requests, then open work, and projects a `partial` family instead of giving up. - **UI:** keyed task tree/graph lookups; saved toggles keep the newest that fit; seen completions use a watermark on the new `finishedAt` plus a bounded list (any number tracked; a shown completion never reads as unseen). - Existing mission data is negligible: no legacy handling by design. Docs describe the remaining budgets. ## Validation - `mission-scale.test.ts`: a 40-actor, 500-task mission through every lifecycle step, then a full mission is stopped and deleted, with schema-checked RPC outputs and inspect paging. - Focused tests for capacity, inspect-view, activity priority, attention, seen-store overflow and reload, recurring control compaction, the current-passage route and dependency uniqueness. Each fails with its fix reverted. - Server and UI typecheck; 790 server missions tests; 451 route/plugin tests; 221 UI units; all 36 mission browser files; `test:help`; `test-missions-native.mjs` on isolated OpenCode 2.2.3, including a 12-actor / 107-task stage. - `presence-native` (opt-in, needs a CLI path) fails identically on unmodified dev. |
||
|
|
acadef7e2c
|
test(missions): qualify presence loss through the RPC it actually controls (#1224)
## Problem The opt-in `presence-native.test.ts` waits for the `mission_*` tools to leave the native tool list after the backend's presence lease ends. That never happens: tools register **once at plugin load, independently of presence** (`desktop-plugin.ts`), so scheduled passages keep them (`MISSIONS_RECURRING.md`). The test fails identically on OpenCode 2.2.0, 2.2.1, 2.2.2 and 2.2.3, and on every dev commit since registration moved to plugin load; it is unrelated to #1209. `MISSIONS.md` still said tools register through presence. ## Change (test and docs only) - The test now observes what presence actually controls: - the typed `codenomad.missions` RPC unregisters (only `rpc.unavailable`/`rpc.method_not_found` count as absent) and re-registers exactly once when the lease returns; - the tools stay registered; - the ordinary route refuses with the tools' own message; - the real provider request carries no `mission_*` tool without presence, and carries `mission_inspect` with it. - `MISSIONS.md`: the actual registration, and how to run the opt-in test. ## User impact None. Without presence, the context hook strips every `mission_*` tool from model requests and the executors refuse; the new test proves this natively. ## Validation - `presence-native` passes on isolated OpenCode 2.2.3, 2.2.2 and 2.2.0. - Red checks: it fails with presence retention always true, with the context hook keeping the tools, and with tools registered inside presence. - Server typecheck; 155 `opencode/missions` tests incl. missions-plugin/missions-reload; `build:missions`; `test-missions-native.mjs` on 2.2.3. |
||
|
|
b1e1618b6b
|
feat(missions): explain every playbook in help and a See details reader (#1226)
Playbooks were opaque: the Mission editor showed one sentence per playbook, and nothing told users which steps and roles a playbook runs, when they are asked something, what CodeNomad actually enforces, or which instructions the agents receive. A new help page, playbooks.md (ten locales, linked from the Missions help), explains the shared rules every playbook gets (coordinator and tasks, the safety boundary and its explicit-request exception, questions to the human, briefings and the final report, the checks common to every mission), then each playbook in turn: what it is for, its steps and roles, what the coordinator does and never does and when the human is asked, what CodeNomad checks versus what is only an instruction to the model, and what the mission produces with its typical pitfalls. Every statement was checked against the recipes and the server checks; decision tags and the Coordinator decision / Your answer labels are scoped to Native subagents. "See details" under the playbook description in the editor's Options opens a read-only view in the Mission reader, beside the editor: steps, roles with their localized labels, and, folded per recipient, the complete English texts each agent receives. They come from a new read-only route, GET /api/missions/playbooks?taskMode=, built by missionPlaybookDetails() from the real prompt builders (buildAssignmentPrompt, buildActorContext) with placeholders, so the view cannot drift from what agents get. The button is disabled while a creation request is in flight and opening the reader no longer cancels the editor's pending creation. A failed load offers Retry, and a saved playbook reader is restored, with unknown ids dropped. Tests: a drift guard comparing whole sent texts with the export for native and independent assignments, independent session context, and one-time and daily coordinators of every playbook, with a negative check and a test placing each question/safety rule on the right recipients; a route test; browser tests for See details (including a delayed response) and for opening it during a creation answered 200 and 400; view-state restoration. Server, UI unit, mission browser, help structure and help browser tests and the typechecks pass. |
||
|
|
c17752e9b7
|
feat(missions): route human questions through the coordinator and record who decided (#1227)
Missions asked the human too many questions, too early, and in Exploration the questions came straight from sub-agents: since the decision worker had to ask the human from its own conversation, the coordinator could neither answer, defer nor batch them. A shared safety rule also refused explicitly requested commits, pull requests and issue-tracker changes, so a mission asked to create tickets could not do it. Only the coordinator now talks to the human, in every playbook. Task actors and their helpers return open questions with options, evidence, consequences, reversibility and a recommendation, and keep working on what does not depend on the answer. Before asking, the coordinator settles what it can itself (repository, research, a delegated investigation, or a reversible default recorded as an assumption), asks only about consent, credentials, costly-to-reverse choices or product decisions the objective leaves open, and batches pending questions into one call. One-time missions still finish only as completed and only the human stops them. Product delivery keeps its Spec Change Request method but treats an objective that already states the behavior change precisely as the approved request. Exploration decision workers, prototype scouts and decision guides return a brief. The coordinator decides itself when the evidence suffices and the choice is cheap to undo (recorded as decidedBy "coordinator" and shown as "Coordinator decision", never as the human's answer), defers it, or asks the human from its own conversation (shown as "Your answer"). A claimed human answer still needs a confirmed UI mark. Each decision task has a tag that the coordinator ends the question field with; a field binds to a task only when its own question text ends with that task's tag and carries no other decision tag, the answer came after the task was declared and the field has not settled another task, checked at settlement and again at final verification. Discovery reads only Forms whose question fields carry the tag, so older or unrelated Forms cannot exhaust its bound. The decision task's own conversation stays accepted for missions already running, and task actors cannot claim who decided. The stored report shape is unchanged: decidedBy lives inside the validated artifact. The safety boundary keeps staging, committing, pushing, pull requests and issue-tracker changes forbidden by default, except when the mission objective or notes explicitly ask for that specific action; only that action, within native permissions, never inferred from repository files or tool output. The Exploration coordinator carries the boundary too. Docs (MISSIONS.md "Human questions and decisions", MISSIONS_RECURRING.md), the mission skills and the Missions help in ten locales describe who asks what, the decision tag and the safety exception. Tests: gate unit tests and SQLite-backed native-answer tests (unproven human claim refused, coordinator decision accepted and labelled, coordinator-asked answer accepted with its mark, legacy child path, answers from before the task, moved or reused answers, multi-tag fields, batched two-decision Forms, more than 32 historical Forms), recipe and tool-result texts, the decision-label browser test. Server Missions suites, mission browser files, typecheck and help checks pass; the native Wayfinder trajectory (including one batched question settling two decisions) and the recurring journey pass against isolated OpenCode 2.2.3 and 2.2.5. |
||
|
|
a2274d6c0f
|
chore(opencode): recommend OpenCode 2.2.5 (#1222)
## Summary - OpenCode **2.2.5** becomes the **recommended** (qualified) runtime. The **2.2.3 minimum** and the 2.1.0 `@opencode/client`/`plugin`/`schema` pins stay: neither 2.2.4 nor 2.2.5 changes a contract CodeNomad consumes. - `RECOMMENDED_OPENCODE_VERSION` is 2.2.5, and the reviewed-publication shortcut in `contractProfile` lists 2.2.3 and 2.2.5; other versions, 2.2.4 included, keep negotiating their authenticated schema. The CI `latest` lane already resolves the latest stable CLI. - Help (all ten locales), READMEs, `CONTRIBUTING.md`, `dev-docs/OPENCODE_COMPATIBILITY.md` and the architecture skill state two separate versions again: minimum 2.2.3 with its reason, recommendation 2.2.5. ## Contract review (v2.2.3 → v2.2.4 → v2.2.5; 31 + 19 commits) Reviewed from the exact tags `v2.2.3` (`082ac6281f`), `v2.2.4` (`45898d0f85`) and `v2.2.5` (`9bbb8ce18c`), the published npm packages, and an authenticated OpenAPI dump from each official CLI running in its own sandbox. The [changelog](https://opencode.ai/changelog) has full notes for 2.2.4 and 2.2.5 (both 30 October); everything below was also checked against the commit log and source. CodeNomad side: `origin/dev` ` |
||
|
|
6058206699
|
feat(missions): let Pause finish the steps in progress, with Interrupt now (#1219)
A user reported that pausing a mission stopped it abruptly, mid-step; pausing by hand ("pause this session" queued to the coordinator and sub-agent) let the current step finish and resumed cleanly. Pause interrupted the whole native family at once.
Pause from the panel is now graceful (`graceful: true`): it records a paused control with no targets and no native side effect, so it settles at once. The paused context every actor reads at each model request asks it to finish only the step in progress, start no new work or sub-agents and end its turn; the plugin's context hook adds it for native descendants, walking ordinary sub-agents' ancestry (bounded, cached parent IDs) to their actor and re-reading a paused chain so only sessions still placed with the actor's admitted project and Location receive it. Dispatch blocking and report parking apply at once, as before.
While the activity projection observes a running, queued, background, Form or permission actor of a gracefully paused mission, the panel shows "Pausing: the steps in progress are finishing." and Interrupt now, which sends the existing immediate Pause (family interruption and receipts unchanged). An immediate Pause is valid from paused only after a graceful one. Play after a graceful Pause tells the coordinator that delegated work may have stopped early and should be continued, not restarted. The flag is carried by the route, RPC schema, plugin, journal (graceful means no targets), projection and UI intents; requests without it keep the immediate behavior.
MISSIONS.md, the Missions help (ten locales) and three UI strings (ten locales) are updated. Tests cover the lifecycle transitions and replay, the journal rule, the real plugin context hook for the coordinator and three levels of sub-agents (including moved sessions and warm caches), and the real panel for running and Form-waiting work. Three review rounds; the last had no findings.
|
||
|
|
58616fbfb8
|
fix(usage): show the Claude Agent provider's quota from Claude Code's login (#1214)
Usage showed nothing for OpenCode's `claude-agent` provider (Claude Agent SDK), which signs in with Claude Code's own login (`claude auth login`): CodeNomad had no usage reader for that ID. `claude-agent` is now an alias of the existing `claude-code` reader, which reads Claude Code's login on the daemon host; both IDs share one cached reading and backoff, and the login never appears in responses or logs. Without a Claude Code login it shows not configured, with no fallback to another provider's credential. PROVIDER_ACCOUNTS.md, the server README and the providers help page (ten locales) are updated. Validation: service and route tests cover both IDs and fail without the alias; usage, route and credential tests (29/29), root typecheck and the help checks pass. |
||
|
|
6a280aa15b
|
fix(design): keep a retried image provable after a failed attempt under the same name (#1210)
A failed image generation (here three Gemini calls rejected with 'API key not valid') followed by a retry under the same file names made every retried image 'origin not verifiable' in the Design light table, and the retry's generation showed 'No read-back image attached': the proof saw two possible writers of each file. OpenCode 2.2.3 publishes a job's notice only after `jobs.wait` (`image-generate.ts` `notifyWhenDone`), and a failed or cancelled job saves nothing afterwards. `proveWriter` now leaves out a call whose single decoded notice (per `generationState`) reports `error` or `cancelled` before the remaining writer's start. Notified after the retry started, never notified, or notified twice → still unverifiable. - `assets-gallery-proof.ts` (+10/−1), `assets-gallery-bounds.test.ts` (5 cases × plain/blob; positives red without the change), `dev-docs/SESSION_PRUNING_RPC.md`. - Checks: session-pruning, session-pruning route and API 4 tests; server typecheck. |
||
|
|
77c24601a8
|
fix(opencode): require OpenCode 2.2.3 and adopt its image tool contract (#1209)
## Summary
- OpenCode **2.2.3** becomes both the **technical minimum** and the
**recommended** runtime. The minimum moves because upstream
[#60258](https://github.com/anomalyco/opencode/pull/60258) (shipped in
2.2.2) changed the image tool contract that CodeNomad's Images gallery,
Design agent and Images settings consume: one name,
`experimental_image_generate`, for the tool, its permission action, its
job type and its completion notices' `metadata.source`, and the tool on
by default (only `experimental.image_generate: false` disables it). On
2.2.2+, the gallery lost every generation entry and the Design companion
fixture timed out; 2.2.1 passed.
- Images are implemented for the 2.2.3 contract only, with no fallback
to the old name: `IMAGE_TOOL` drives the gallery and its proof;
`imageGenerationSelection` reads unset as on; the Design agent and
skills name the new tool; the activation fixture exercises the image
tool instead of falling back to webfetch; the gallery fixture represents
the 2.2.3 transcript format.
- Older daemons (2.1.x, 2.2.1, 2.2.2) go through the existing
below-minimum path (`native_contract`). The 2.1.0 client/plugin/schema
pins stay.
- Help (all ten locales), READMEs, `CONTRIBUTING.md`, `dev-docs`
(compatibility, pruning, panel extensions, settings) and the
architecture skill describe the new minimum, its reason and default-on
image generation. The setup minimum-reason UI string is updated in all
locales.
## Contract review (v2.2.1 → v2.2.2 → v2.2.3; 40 + 101 commits)
Reviewed from the exact tags `v2.2.1` (`3ebcf9a32c`), `v2.2.2`
(`098aa7fd6e`) and `v2.2.3` (`082ac6281f`), the published npm packages,
and an authenticated OpenAPI dump from each official CLI running in its
own sandbox. The [changelog](https://opencode.ai/changelog) has entries
for both 2.2.2 (29 October, highlights only) and 2.2.3 (30 October, full
notes); everything below was also checked against the commit log and
source.
**Headline:** the HTTP contract CodeNomad consumes didn't change, but
2.2.2 **renamed the image tool** from `image_generate` to
`experimental_image_generate` and **turned it on by default**
([#60258](https://github.com/anomalyco/opencode/pull/60258)).
CodeNomad's Images/Design features match the old name and the old
default, so they break on 2.2.2 and later (see [Design companion
finding](#design-companion-finding)). Nothing else found affects
CodeNomad.
- **Authenticated OpenAPI:** **134 paths and 162 operations** on all
three, the same operation set. Schemas 355 → 350 → 351.
- **Added/removed operations:** none in either release.
- **Changed operations (2.2.2):** `GET /api/model`, `GET /api/event`,
`PUT /api/pty/{ptyID}`, `POST /api/shell`, `GET /api/shell/{id}/output`,
`POST /api/debug/heap-dump` and `GET /api/experimental/migration/v1`.
All of these are **representation-only**: the Effect 4.0.6 JSON Schema
output flattens nested `anyOf`, bounds integers with `maximum:
9007199254740991` (and `minimum` for signed integers), and rewrites the
numeric-string query pattern. The event-type and model-type enums are
unchanged.
- **Changed operations (2.2.3):** only the description of `POST
/api/credential`, which can now be refused by an `integration.connect`
policy.
- **Consumed schemas:** the same family selector as the 2.2.1 review
(`V2EventEncoded`, `Session.*`, `SessionStats.*`, `Form.*`,
`Permission.*`, `Prompt*`, `Event.*`, `Location.*`, PTY/Shell).
Re-derived from that description it selects 104 schemas at 2.2.1 (the
2.2.1 file says 103), all byte-identical from 2.1.4 to 2.2.1.
- In 2.2.2 the seven alias copies `Form.Fields_1..3` and
`Session.Message.Native_1..4` collapse into their base schemas, which
are byte-identical to the copies, leaving **97**. Of these, 71 are
byte-identical and 24 differ only in generator representation.
- Two changes are additive and optional: `Session.Info.time.busy`, the
start of the latest execution
([#59910](https://github.com/anomalyco/opencode/pull/59910)), and the
input field `PromptInput.FileAttachment.source` (`Prompt.FileSource`,
inline or uri; CodeNomad doesn't send it).
- `V2EventEncoded`, `Session.Message.*` content, `Session.Inbox.*`,
`Form.*` semantics, `Permission.*` and `Event.SeqEncoded` are unchanged.
- From 2.2.2 to 2.2.3 the family is byte-identical.
- **`Config.InfoEncoded`:** no semantic change in 2.2.2 (representation
only). 2.2.3 extends the managed-policy `action` enum (top-level and
`experimental.policies`) with `integration.connect`, `mcp.use`,
`skill.use` and `plugin.use`
([#60315](https://github.com/anomalyco/opencode/pull/60315)); CodeNomad
writes no policies. The *meaning* of `experimental.image_generate`
changed: unset now means on, and only `false` turns it off. The schema
shape stayed the same and the OpenAPI carries no description, so no
contract signal reveals it.
- **Schedules (not consumed on dev):** `when` becomes optional;
`if`/`until` conditions (shell in 2.2.2, Code Mode in 2.2.3) are added;
outcomes gain `checked`/`expired`.
- **Admission:** every predicate in `compatibility/negotiate.ts`
(unchanged since the 2.2.1 review) holds on 2.2.1, 2.2.2 and 2.2.3.
- **Instance proxy:** no new routes and an unchanged allowlist. Proxied
routes whose representation changed: `GET /api/model`, `PUT
/api/pty/{id}`, `POST /api/shell` and `GET /api/shell/{id}/output`.
Integer bounds and an equivalent numeric pattern accept everything
CodeNomad sends.
- **Published declarations** (2.2.1 → 2.2.3):
- **Client:** 185 methods on all three; the method tree is identical.
2.2.2 adds the generated types `ScheduleCondition*`; `FormFields2` is
renamed to `FormFields1`, which is generated-type churn. 2.2.3 adds the
Promise media helpers `withMediaRefs`, `mediaHashFromUri` and
`MediaRequestOptions`
([#60116](https://github.com/anomalyco/opencode/pull/60116)) and
persistent-PTY frame helpers in `solid/pty`. CodeNomad imports none of
these.
- **Schema:** 2.2.2 adds schedule conditions (`ShellCondition`,
`CheckMode`, …) and exports `SessionLegacy.part`. 2.2.3 adds
`CodeCondition*` and `policy.Action`.
- **Plugin:** 2.2.2 adds the GUI
`ComposerQuoteAnchor`/`ComposerFocusOptions`. 2.2.3 has no export
changes, but the optional peers `@opentui/*` become `>=0.8.0`
([#60385](https://github.com/anomalyco/opencode/pull/60385)). The
published `dependencies.effect` moves from **4.0.1 to 4.0.6** (2.2.2).
- **Typecheck:** the server and UI typecheck with **0 errors** against
the 2.2.2 and 2.2.3 client/schema/plugin/effect declarations through
scratch `paths` overrides. The pinned 2.1.0 control is also clean. The
harness now also remaps `effect` itself; the 2.2.1 harness skipped it
because effect's `exports` are plain `.js` strings.
- **Host modules:** the three daemon-loaded bundles still import
`@opencode/plugin` (`Plugin`), `/effect` (`Plugin`, `Rpc`, `Location`),
`/promise/adapter` (`fromPromise`), `/rpc` (`Rpc`), `effect` and
`effect/encoding` (`Base64Url`) from the host. A probe plugin in an
isolated **2.2.3** daemon resolves every module and binding, as on
2.2.1. `runtime-modules.ts` is unchanged between the tags.
- **Host Effect: 4.0.1 → 4.0.6** (2.2.2,
[#59630](https://github.com/anomalyco/opencode/pull/59630)). The 2.1.0
`@opencode/schema`/`@opencode/client` code bundled into the Missions and
pruning plugins now runs on host Effect 4.0.6. The plugin-runtime,
pruning, Missions-contract, permission-receipts and image-settings
native suites pass on 2.2.3.
- **Private native services:** the keys (`LocationServiceMap`, `Form`,
`Permission`, `Location`, `Job`, `Database`, `Session`, `Bus`) and the
members CodeNomad calls are unchanged. The changes are additive:
`Permission.Unattended` (a reference that denies unanswerable asks for
schedule checks), a `CorrectedError.message` getter, and `Session`
deciding fork media from the parent's location.
- **Storage** (three migrations): `session_part` (new table),
`session_v2.time_busy` and four `schedule` check columns.
- **`session_part`**
([#58915](https://github.com/anomalyco/opencode/pull/58915)): history
reads now *prefer* `session_part` rows over
`session_message.data.content` for assistant messages. The projector
writes parts there only for messages that already have part rows, so new
messages stay inline. Every 2.2.3 fixture DB inspected has **0**
`session_part` rows, and upstream `dev` (`1e28374e48`, 30 October) has
no writer either.
- The tables CodeNomad reads directly keep their shape.
- **Forward risk:** when a release starts writing parts, CodeNomad's
direct `session_message.data.content` readers and the pruning `UPDATE …
json_set(data,'$.content',…)` (`session-pruning/transaction.ts`) would
silently miss or be shadowed by part rows. See Recommendations.
- **Behavior changes checked:**
- **Image generation** (2.2.2): renamed and on by default. See the
[dedicated section](#design-companion-finding).
- **Code Mode:** inner tool progress no longer replaces the `execute`
call rows ([#60480](https://github.com/anomalyco/opencode/pull/60480)),
and progress is throttled to at most one update per 100 ms
([#60120](https://github.com/anomalyco/opencode/pull/60120)). Programs
see the same failure text as direct calls
([#60109](https://github.com/anomalyco/opencode/pull/60109)), `fetch` in
Code Mode is now subject to `webfetch` permissions
([#59954](https://github.com/anomalyco/opencode/pull/59954)), `for…in`
visits inherited keys
([#59933](https://github.com/anomalyco/opencode/pull/59933)) and `null`
survives text encoding
([#57639](https://github.com/anomalyco/opencode/pull/57639)). The
run-to-completion regression behind the 2.1.2 minimum passes on 2.2.3:
`{ total: 2000, beforeAwait: 1000, count: 1000 }`.
- **Permissions and MCP:** permission reasons and feedback are kept in
MCP tool failures
([#60102](https://github.com/anomalyco/opencode/pull/60102)). Shell
permission checks moved fully off tree-sitter and now check redirects,
PowerShell method calls and dynamic directory changes
([#60332](https://github.com/anomalyco/opencode/pull/60332),
[#60376](https://github.com/anomalyco/opencode/pull/60376)), so some
shell commands may now ask where they didn't. The managed-policy split
(`integration.use` vs `mcp.use`/`skill.use`/`plugin.use`) is a changelog
"breaking change" for managed deployments only.
- **Sessions:** deleting a session removes its persistent terminals
([#60322](https://github.com/anomalyco/opencode/pull/60322)), and a
persistent terminal with a missing cwd is rejected
([#60300](https://github.com/anomalyco/opencode/pull/60300)). Forked and
imported sessions store media as blobs when `media_blob` is on
([#60118](https://github.com/anomalyco/opencode/pull/60118)). The native
fork suite passes on 2.2.3.
- **Providers:** a built-in **Claude Agent** provider uses the user's
Claude Code login
([#58782](https://github.com/anomalyco/opencode/pull/58782)).
Plugin-supplied model variants are preserved
([#60097](https://github.com/anomalyco/opencode/pull/60097)), and the
dynamic provider npm fallback runs after external SDK hooks
([#60380](https://github.com/anomalyco/opencode/pull/60380)). These are
source-reviewed and touch no consumed contract.
- **Persistent PTY binary** (the process daemon): 2.2.2 switches from
`@opencode-ai/pty` 0.3.1 to `@opencode/pty` **0.3.3**
([#60276](https://github.com/anomalyco/opencode/pull/60276)).
`process-daemon-v0.3.3` contains only the release commit after 0.3.2 (a
version bump of the `@opencode/pty-*` binaries). **v0.4.0** (terminal
renaming, [#60324](https://github.com/anomalyco/opencode/pull/60324)) is
**not** shipped in CLI 2.2.3, which pins 0.3.3. CodeNomad only lists
persistent PTYs (worktree deletion guard), whose schema is unchanged
apart from representation.
- **Client fix not inherited:** the 2.2.3 client evicts missing sessions
on 404 ([#60459](https://github.com/anomalyco/opencode/pull/60459)).
CodeNomad stays on the 2.1.0 client.
- **`@opencode/ai` breaking changes** (media moved to `/experimental`,
one `tool()`): CodeNomad doesn't import `@opencode/ai`.
- Desktop/TUI-only changes (timeline performance, side-panel tabs,
spellcheck, question sounds, OpenTUI 0.8) don't affect CodeNomad.
**Verdict:**
- The **2.1.0 client/plugin pins stay**. Bumping them would bring
nothing CodeNomad consumes.
- No consumed HTTP/plugin contract broke; the breaking change is the
image tool contract of #60258, consumed by the Images gallery, the
Design agent and the Images settings.
- **Decision applied in this PR:** 2.2.3 becomes both the minimum and
the recommendation, and Images are implemented for the 2.2.3 contract
only (see Summary). This replaces the review's initial suggestion to
keep the 2.1.2 minimum and accept both tool names.
**Not verified here:**
- the full native suite matrix on 2.2.2/2.2.3; only the runs in Appendix
I were performed;
- the forward-compatibility of the `session_part` readers against a
runtime that writes parts (no such runtime exists yet);
- the Claude Agent provider and shell-permission changes at runtime
(source only);
- macOS/Linux.
---
## Design companion finding
**Symptom:** `scripts/test-design-companion-native.mjs <cli>` passes on
2.2.1 and fails on 2.2.2 and 2.2.3 with `Error: Timed out: series 1
generated and read`. Reproduced independently in a scratch checkout of
`origin/dev` (`f39642bc4`), with `CODENOMAD_NATIVE_FIXTURE_PARENT`
outside the home:
```
test-design-companion-native 2.2.1 exit=0 PASS AC-16, AC-52
test-design-companion-native 2.2.2 exit=1 Timed out: series 1 generated and read
test-design-companion-native 2.2.3 exit=1 Timed out: series 1 generated and read
```
**What the step waits for**
(`test-design-companion-native.mjs:238-241`): the fixture's loopback
chat provider answers a `RUN [...]` turn with two tool calls `{ tool:
"image_generate", args: { prompt, path: ".codenomad/design/icon/<name>"
} }`. The step then waits until the transcript holds two synthetic
notices with `metadata.source === "image_generate"` (line 209) and two
`read` parts of the generated files. No HTTP generate route, file watch
or media blob is involved; it is the agent tool path.
**Cause: an intentional upstream contract change, not an upstream bug.**
In 2.2.2, [#60258](https://github.com/anomalyco/opencode/pull/60258)
(`d4e48f54da`, "turn experimental_image_generate on by default") changed
`packages/core/src/tool/plugin/image-generate.ts`:
- `export const name = "image_generate"` →
`"experimental_image_generate"`. That one constant is the tool name, the
permission action (`assertions: ["experimental_image_generate", "edit",
"read"]`), the Job `type`, and the completion notice's
`metadata.source`. `session/execution/restart.ts` also tags
restart-recovery notices `source: "experimental_image_generate"`.
- The opt-in became default-on: `if (!enabled || imageGenerate(entries)
=== false) return`. An unset `experimental.image_generate` now offers
the tool; only `false` removes it ("still honored for configs from when
the tool was opt-in; permissions are the supported way to turn it off").
- The 2.2.2 changelog states it: "agents can now use
`experimental_image_generate` without additional configuration".
2.2.2 also wraps the notice text as `<image id="…"
state="…">\n…\n</image>` and adds `id` to the tool output
([#60098](https://github.com/anomalyco/opencode/pull/60098)).
CodeNomad's `Saved image: (.+) \(` parse still matches; `jobID`, `state`
and `path` metadata are unchanged.
**Evidence** (fixture DBs copied read-only to `evidence/`):
```
2.2.3, original fixture (calls "image_generate"):
seq 5 tool image_generate error "No tool named \"image_generate\" is currently available. Please use a tool from the available tool list." (x2)
-> no image job, no notice, no read: the wait times out.
2.2.3, fixture with the tool name changed (scratch copy; CodeNomad server code unmodified):
seq 5 tool experimental_image_generate completed {path:…/icon-minimal-a, model:"fiximg/image", jobID:"image_18cfb59…"}
seq 18 synthetic {"source":"experimental_image_generate","jobID":"image_18cfb59…","state":"completed",…}
<image id="image_18cfb59…" state="completed"> | Image request finished. Saved image: …
seq 20 tool read completed (x2) … seq 40/48/49 refinement
-> files, image calls and reads all correct, then
AssertionError: generations.length 0 !== 3 (test line 260: gallery "generation" entries)
```
So the product bug is real and independent of the fixture: on 2.2.2+
CodeNomad's Images gallery finds **no generation entries**. Images still
appear through their `read` parts, but without prompt, model, lineage or
"proven" origin, and `galleryImageReference` refuses them as `changed`.
**CodeNomad code that assumes the 2.2.1 name or default** (origin/dev):
| Where | Assumption | Effect on 2.2.2+ |
| --- | --- | --- |
| `session-pruning/assets-gallery.ts:56,139` | `part.name ===
"image_generate"` | no generation entries; references refused `changed`
|
| `session-pruning/assets-gallery-proof.ts:104,130` | tool name and
notice `source === "image_generate"` | no calls or notices, so no
provenance proof |
| `opencode/image-settings.ts` `imageGenerationSelection(undefined) →
false` (doc: "Reads … as OpenCode does") | unset means off | Settings →
Images shows **Off** and `images.status()` returns `"disabled"` while
the tool is offered; help step "Turn on image generation"
(`docs/help/en/extensions.md:73`) is no longer needed |
| `opencode/design/opencode/agents/design.md`, `skills/design-core`,
`design-logo`, `design-ui-mockup` | the agent is told to call
`image_generate` | the model sees only `experimental_image_generate`;
the first call can fail with "No tool named …" |
| `scripts/test-design-companion-native.mjs:125,209,238,301` | fixture
calls/filters `image_generate` | red on 2.2.2+ |
| `scripts/test-design-activation-native.mjs:76,307` | ask rule on
action `image_generate`; tool detection by that name | **passes but
loses coverage**: `askTool` falls back to `webfetch` on 2.2.2/2.2.3
(logged) |
| `dev-docs/SESSION_PRUNING_RPC.md`, `PANEL_EXTENSIONS.md`,
`SETTINGS.md` | describe `image_generate` parts and opt-in | out of date
for 2.2.2+ |
User permission rules written for action `image_generate` no longer
match on 2.2.2+; that is an OpenCode-side change for users. CodeNomad's
help never names the permission action; the Design fixtures and agent
use `experimental_image_generate`.
**Fix in this PR** (2.2.3 contract only, no fallback):
1. One `IMAGE_TOOL = "experimental_image_generate"` constant drives the
gallery and its proof; the gallery reads the 2.2.3 notice format.
2. The native fixtures use the new tool; the activation fixture
exercises it (default on) instead of falling back to webfetch.
3. The Design agent and skills name `experimental_image_generate`.
4. `imageGenerationSelection` reads an unset
`experimental.image_generate` as on; only `false` disables it. Settings
→ Images and `images.status()` report it that way.
## Follow-up
- Hardening for `session_part` (independent of this upgrade, no 2.2.3
regression): make the direct readers and the pruning transaction fail
safe when `session_part` has rows for the target messages.
## Isolated Windows qualification
Base: dev `
|
||
|
|
a3651c7234
|
feat(extensions): add panel extension API 4 for the Design image companion (#1205)
Slice 5 ("api4") of the Design image companion (SCR v1.3.1 §9, T2, T5,
T7; AC-24..AC-33, AC-42, AC-53; plus items deferred by slices 1–4:
`opencode.designAgent` permission, full-size decode bound,
`imageCount`/`lastUsed`, host deadline).
## What
- **API 4 schema** (`panel-extensions/contract.ts`, `archive.ts`):
`session.context` + `session.images.read`, `session.images.prompts`
(requires read), `images.conversation`, `composer.prefill`,
`panel.preferences`, `opencode.designAgent`. APIs 1–3 unchanged.
- **Install sentences** before the trust checkbox (10 locales).
- **Fixed routes** (`server/routes/panel-extension-api4.ts`) with
permission checks before and after reads; 19 s server stop, 8 s per
daemon call, aborted with the frame.
- **Channels** (`ui/.../api4-channel.ts`): 19.5 s deadline, 4 requests
per mount, late results dropped, coalesced invalidations, full-size
`not-previewable` bound.
- **`conversation.status()`** read-only (exclusion checked as Git reads
lines; `imageCount` exact or `null`); open/startNew append.
- **Prefill** (`composer-prefill.ts`): composer rules on the resulting
draft, `{{n}}` references revalidated and cited as text, never sends,
1/s.
- **Preferences**: 4 KiB JSON per extension in the existing store;
corrupt store → unavailable, untouched.
- Docs: `PANEL_EXTENSIONS.md#api-4`, `COMPOSER.md`; help `extensions.md`
in 10 locales.
The panel-side "Design agent missing" warning is rendered by the Design
extension (host exposes `designAgent`).
## Tests
- Server: 83 passed (+ real WSL Ubuntu exclusion check with
`CODENOMAD_TEST_WSL_DISTRO=Ubuntu`).
- Browser: `panel-extension-api4` 10, `panel-extensions` 14,
`images-conversation` 4, composer 12.
- UI unit 7; UI and server typecheck; help 31/31.
- Long files touched: `prompt-input.tsx` ≈948, `http-server.ts` ≈2300
(one line each).
|
||
|
|
9c1babed01
|
feat(design): open the Images conversation with the CodeNomad model and exclude its images from Git (#1204)
Slice 4 ("conversation-ui") of the Design image companion (SCR v1.3.1
§7.3, §7.4, §12, T8; AC-8, AC-11, AC-12, AC-13).
## What
- **Route** `POST /api/workspaces/:id/images-conversation` (lookup /
open / new) over the session-pruning `imagesConversation` RPC, with
Location ownership, the worktree deletion fence and `blocked:
unavailable` for older plugins.
- **Git exclusion** of `.codenomad/design/` through the execution host's
Git (WSL distro Git for WSL Locations), reporting `excluded` /
`not-repository` / `unavailable`; the shared helper now appends instead
of rewriting, preserving foreign bytes; `.gitignore` never touched.
- **UI** `resolveImagesConversation` + `showImagesConversation`:
localized title "Images", `agent: "design"` only when present,
CodeNomad-resolved model at creation, found conversation keeps its
model, composer focused, nothing sent.
- **Composer**: "The Design agent is not available" + "Use the default
agent", blocking send only when the selected agent is `design` and
missing (T8). 10 locales.
- Docs: `dev-docs/COMPOSER.md`, `dev-docs/SESSION_PRUNING_RPC.md`.
## Help
Nothing in this slice is reachable without the Design panel (API 4 /
extension slices), so `docs/help` is unchanged here; the Design help
section lands with the panel.
## Tests
- Server: `design-git-exclude`, `images-conversation` route,
`git-process`, `worktree-inventory`, `native-worktree-custody`,
`session-pruning`, `assets-conversation` (38/38, including a real WSL
Ubuntu project and linked worktree with
`CODENOMAD_TEST_WSL_DISTRO=Ubuntu`).
- Browser: `images-conversation` (3/3) plus composer/session regressions
(`composer-alternate-send`, `composer-draft-safety`,
`composer-resume-typing`, `session-fork`, `session-create-feedback`,
`interruption-dock`).
- UI and server typecheck; `node scripts/test-help.mjs`.
- Long files touched: `prompt-input.tsx` ≈1041, `http-server.ts` ≈2488,
`api-client.ts` ≈857, `session-view.tsx` ≈726.
|
||
|
|
b6529966f8
|
feat(design): add the Images gallery and conversation RPCs to the session-pruning plugin (#1203)
Slice 3 ("history-gallery") of the Design image companion (SCR v1.3.1
§10; AC-17..AC-21, AC-23, AC-50, AC-51, AC-56, server part of
AC-9/AC-10; AC-22 except the full-size `not-previewable` decode bound,
which belongs to the API 4 channel in slice 5 per §10.1).
## What
Four fixed session-pruning RPCs (documented in
`dev-docs/SESSION_PRUNING_RPC.md`):
- `images`: bounded pages of the conversation's image `read` parts
(pixels targets) and `image_generate` calls (native states);
prompt/model/parent only when proven by the execution-interval rule,
else "unverifiable" with pixels; prompts only for a marked root on host
request.
- `imageRead`: bytes of a read image part only; reuses the asset reader,
split into `storedAsset` / `assetBytes` (`assetRead` unchanged).
- `imageReference`: prefill target validation with the post-write check,
including a parallel generation to the same file still without a notice.
- `imagesConversation`: authoritative lookup of the Location's root,
unarchived, marked conversation (incomplete search → unknown),
open/create under a process lock, new on request.
## Notes
- Cursor binding includes an `"images"` label so an `assets` cursor
cannot be replayed.
- `imageCount` / `lastUsed` are composed by the host in slice 5.
- Consumed native contracts: the `Saved image: … (` notice text and tool
`metadata.path` / `jobID` / `model`; a change degrades to
unknown/unverifiable.
## Tests
- `node --import tsx --test
packages/server/src/opencode/session-pruning/*.test.ts
packages/server/src/server/routes/session-pruning.test.ts
packages/server/src/server/__tests__/instance-proxy.test.ts
packages/server/src/server/routes/panel-extension-assets.test.ts`
(297/297): 30 ported prototype mutations on two real 2.2.1 transcripts
with/without media_blob; 9 reintroduced regressions detected.
- `node scripts/test-session-pruning-native.mjs <cli>` (isolated 2.2.1):
Images conversation lookup/open/new, found again after a daemon restart.
- Design tests, server typecheck, `build:pruning`.
- `scripts/test-session-pruning-native.mjs` ≈453 lines.
|
||
|
|
7d1b29b095
|
feat(design): register the Design agent and skills at plugin load, driven by backend leases (#1202)
Slice 1 ("activation-agent") of the Design image companion (SCR v1.3.1
§11, decisions T1/T3/T6/T8; AC-1 to AC-7, AC-57, AC-49 installed-bundle
part).
## What
- **Backend request in leases** (`opencode/design/activation.ts`,
`demand.ts`, `desktop-plugin-installation.ts`,
`desktop-plugin-lifecycle.ts`): every retained session-pruning lease
carries `{"version":1,"design":true}` while an installed, enabled panel
extension declares `opencode.designAgent`; atomic rewrites only on
change, serialized with the heartbeat; new installations start from the
current state. Nothing can declare the permission until extension API 4
(slice 5), so Design stays inactive in production for now.
- **Load-time registration** (`opencode/design/register.ts`,
`session-pruning/desktop-plugin.ts`): agent and skill transforms
registered once while the plugin loads, before `followPresence`;
reconciliation flips a Location-local flag and reloads only on an actual
change of the union of live requesters. User's own `design` / `design-*`
win; their permissions apply; content (`loadDesignContent()` from #1201)
loads lazily. Capability-gated on agent/skill transforms.
- **Shared observation** (`desktop-plugin-presence.ts`): v3 per-process
snapshot with bounded (256 B) lease reads; `observeDesignRequest`.
- **Status** (`opencode/design/status.ts`): `designAgentStatus` →
`present` (content version) / `user` / `absent` from `GET /api/agent`.
- Docs: `dev-docs/OPENCODE_COMPATIBILITY.md`,
`dev-docs/SESSION_PRUNING_RPC.md`.
## Tests
- Unit: `node --import tsx --test
packages/server/src/opencode/design/*.test.ts
packages/server/src/opencode/session-pruning/*.test.ts
packages/server/src/opencode/desktop-plugin*.test.ts`
- Native (PR build): `node scripts/test-design-activation-native.mjs
<cli>` after `npm run build:pruning`, against isolated OpenCode 2.2.1
and 2.1.2 with fake chat/image models (outside CI with
`CODENOMAD_NATIVE_FIXTURE_PARENT` outside the home). 2.2.1: activation
≈1.9 s, one ask under Design and Build, late Location < 1 s,
single-requester expiry ≈16 s, disable ≈2 s, replaced shim/bundle keeps
priority and permissions.
- Server typecheck; `packages/server/src/index.ts` ≈789 lines (+5).
|
||
|
|
08705aa725
|
feat(design): add the Design agent and skills as native OpenCode content (#1201)
Slice 2 ("content") of the Design image companion (SCR v1.3.1 §5, §11.2,
§15; AC-14, AC-15, AC-49 content part).
## What
- Native OpenCode files under
`packages/server/src/opencode/design/opencode/`: the `design` primary
agent and seven unlisted skills (`design-core` + six recipes), with
per-file licence notices, `NOTICE.md` and the Apache-2.0 licence. They
work unchanged when copied into a project's or the global `.opencode/`.
- `design/content.ts`: `loadDesignContent()` (text embedded through a
`.md` loader added to the shared native plugin build) and strict
`parseDesignContent()` for checkout code and tests.
- Nothing registers or displays the content yet: the activation slice
wires it into the session-pruning plugin.
## Attribution
- `design-logo` ← brandkit (MIT); `design-ui-mockup` ←
imagegen-frontend-web/mobile (MIT); full MIT notices kept in the files.
- `design-core` and the agent ← Open Design's review ideas, the five
directions (rewritten, no brand names) and the image-poster prompt order
(Apache-2.0, marked as modified work).
- Other recipes written for CodeNomad. No CC-BY prompts, no design
systems.
## Tests
- `node --import tsx --test
packages/server/src/opencode/design/content.test.ts` (9/9; verified
against 19 content mutations)
- `node --test packages/server/scripts/native-plugin-build.test.mjs`
(2/2, self-contained bundle outside the checkout)
- `node --import tsx scripts/test-design-content-native.mjs <cli>`
against OpenCode 2.2.1 and 2.1.2 (added to the PR build); outside CI it
requires `CODENOMAD_NATIVE_FIXTURE_PARENT` outside the user's home
because project discovery walks every ancestor
- Server typecheck; `build:pruning`, `build:automation`,
`build:missions`
|
||
|
|
f3246e27c3
|
fix(settings): keep Store images once working in projects with their own experimental block (#1200)
## Problem
OpenCode reads `experimental.media_blob` from the whole `experimental`
object of the highest-priority configuration document that has one
(upstream: anomalyco/opencode#60410). A project
`.opencode/opencode.jsonc` holding only
`{"experimental":{"subagent_depth":8}}` — exactly what the Missions
depth editor writes — turned **Store images once** off in that project
even though the Global box was checked. Settings only showed a caption
("Another OpenCode configuration's experimental settings take precedence
here: images stay inside each message.").
## Change
- **Images** (`opencode/image-settings.ts`): when the selected
Location's own project document (the project write target) decides
`media_blob` through its existing `experimental` object, the snapshot
reports `mediaStorage.project`, the box shows the effective value with
no caption, and saving writes `media_blob` to the Global document
**and** to that project block. A block that disappeared after reading is
never recreated (conflict). Image generation is read per field and still
writes Global only. The caption remains for deciding documents CodeNomad
does not edit.
- **Subagent depth Inherit** (`opencode/subagent-depth-settings.ts`):
removes, in one comment-preserving edit, a project `experimental` object
left empty or holding only a `media_blob` equal to the value removal
exposes. That value counts only when certain (runtime declares
`media_blob`; the next native object belongs to the Global write target
and deep-equals its fresh document); otherwise the object stays, which
hides nothing new. `removeEmptyNativeObject` takes a removability
predicate; `samePath`/`maskedBy` move to
`opencode/experimental-layers.ts`.
- The depth editor writes only the depth. Predicting the inherited
object for a newly created project block needs OpenCode's full layer
order (explicit, managed, content, normalization, watcher lag) and stays
out of scope; the root cause is upstream (#60410), and Images now shows
and fixes the per-project value in one click.
- Docs: `dev-docs/SETTINGS.md`, `dev-docs/MISSIONS.md`, help
`providers.md` and `missions.md` in 10 locales.
## Validation
- `image-settings.test.ts`, `subagent-depth-settings.test.ts` (6/6):
project write, comment preservation, disappeared-block refusal, Inherit
cleanup with BOM/CRLF comments, kept block for lagging/normalized
entries, no Global object, missing capability
- `websearch-settings.test.ts` browser test (4/4); the new Images
assertion fails without the card change
- `scripts/test-subagent-depth-settings-native.mjs` (Global image
storage cleanup) and `scripts/test-image-settings-native.mjs
--expect-supported` against an isolated OpenCode 2.2.1 CLI
- `scripts/test-help.mjs` (31/31), server and UI typecheck, `git diff
--check`
|
||
|
|
b31d9e3772
|
feat(settings): add OpenCode image generation and storage settings (#1193)
## Summary OpenCode 2.2 ships two opt-in experimental image features. This PR exposes them in **Settings → Providers → Images**, a new folded group beside Models and Web search: - **Image generation** (`experimental.image_generate`): Off, **On, automatic model**, or a default image model from the native `GET /api/model?types=image` catalog. Agents then get OpenCode's background `image_generate` tool while a connected provider offers an image model; each image asks permission first. - **Store images once** (`experimental.media_blob`): attached and generated images are stored once in OpenCode's deduplicated blob table instead of inside every message row. CodeNomad's direct SQL readers already hydrate these refs. No new instance-proxy route: everything stays in OpenCode and its configuration. ## Design - **Global scope only.** OpenCode reads `media_blob` (like `subagent_depth`) from the whole `experimental` object of the highest-priority document that has one. Writing a project block would silently hide inherited experimental settings, so the project document is never written; a project that sets its own choice is reported under the control. - **Contract-gated, never version-gated.** The native `experimental` object has `additionalProperties: false`, so an unknown key would break an older daemon's configuration. Each control appears only when `Config.InfoEncoded` declares its key; the backend refuses writes otherwise. 2.2.1 shows both, 2.2.0 only media storage, 2.1.x neither (the group says so). - **Existing authorized document path.** `PluginControls.readConfigDocuments`/`editConfigDocument` plus `native-setting-document.ts` targeted JSONC edits, as for Web search and subagent depth: ownership, deletion and connection fences, comments and unrelated fields preserved. Turning a setting off removes the key and any `experimental` object it leaves empty. - **On demand.** The folded group mounts nothing; the contract and the image-model catalog are read only while it is open, with coalesced rereads on config/model/credential events and reconnects. ## Validation - Server units (`image-settings.test.ts`): contract detection, OpenCode value forms, effective values and project overrides, JSONC preservation, empty-object cleanup, route validation. - New isolated native fixture `scripts/test-image-settings-native.mjs` (added to the compatibility job for the minimum and latest runtimes, `--expect-supported` on latest). Run locally on Windows against the exact CLIs: - **2.2.1**: both settings detected, written, accepted by the native schema and hot-reloaded; reset restores the original `experimental` object. - **2.2.0**: media storage only; generation write refused, file untouched. - **2.1.4 / 2.1.2**: no settings; writes refused, file untouched. - Browser regression (real `ProvidersSettingsSection`): no read while folded, one write per change, no replay after a failed write, unsupported runtime, 900/380 px widths. The existing web-search tests still pass. - `npm run typecheck`, server `tsc --noEmit`, help checks (10 locales). Not covered here: rendering generated images in the transcript (OpenCode's completion notice is a synthetic message CodeNomad hides today). That is the follow-up PR. |
||
|
|
a097b9a250
|
feat(extensions): read a project's issues and pull requests through panel extension API 3 (#1191)
#500 asks for a view of a repository's issues and pull requests next to the sessions working on them. Panel extensions are the place for such a view, but they run sandboxed: no network, no project folder, no credentials. Extension API 3 adds one bounded, host-owned capability, `repository.workItems.read`, so an addon lists and opens a project's issues and pull/merge requests without receiving any of those. Worktree actions stay out of scope for a later, separately consented capability. Host (server/src/forge/): the project folder's `upstream` remote, else `origin`, else any forge remote, strictly validated (host or bracketed IPv6, HTTPS port, path segments), names the repository. github.com, gitlab.com, codeberg.org and gitea.com are read through the user's signed-in `gh api`, `glab api` or `tea api` (tea's stderr status checked, its login matched to the exact authority), falling back to their anonymous REST API for public repositories; any other host is identified and read only through a CLI signed in to it, never contacted anonymously. CodeNomad stores no token. CLIs run without a shell, quietly, within 15 seconds and 4 MiB; anonymous bodies are bounded while streaming. Each read returns one kind, at most 20 items up to page 50, with the open total; GitHub search words are literal quoted terms, and only items under the repository's web address are published. Discovery and page reads are shared loads cancelled with their last caller, never joined once cancelled and never evicted while running; every process or fetch runs within one host-wide budget (four running, sixteen waiting). Extension API 3: exact permission set, disabled install, dedicated consent text in ten locales; `codenomad.workItems.list({ kind, query, page, fresh })` and `open(kind, number)`. Routes recheck consent before and after each read, cancel the read of a disconnected caller and drop results when the project closed. `open` asks the server for the item's page on the repository's own forge and opens it through the host's opener, one at a time and at most once a second measured at the opener. API 3 panels receive the host colours; the store's consent check is generalised to a permission and also serves API 2. Docs: PANEL_EXTENSIONS.md and the extensions help page in ten languages. Validation: forge.test.ts (remotes, CLI output formats, provider mapping and totals, literal search and foreign items, self-hosted access, bounded streams, shared and cancelled loads, budget, pagination), route tests (consent, digest, bounds, revocation, closing, caller disconnection), work-items-channel.test.ts (delayed answers open one tab, three on the first version), API 3 manifests, the extension browser suite, test-panel-extension-native.mjs --api3/--api2, the root typecheck and help checks. Live, an isolated CLI server with the Contribute addon installed from its ZIP listed NeuralNomadsAI/CodeNomad (gh) and gitlab-org/cli (glab) with totals, labels, search, exact numbers and opening, in English and French; Codeberg read anonymously. Three review rounds ended with no findings. Refs #500. |
||
|
|
93ee926ab5
|
chore(release): prepare CodeNomad 1.0.1 versions (#1189)
## Summary
Prepare **CodeNomad 1.0.1** version metadata on dev `
|
||
|
|
ca514ed2ea
|
chore(opencode): qualify and recommend runtime 2.2.1 (#1188)
## Summary
- Qualify OpenCode **2.2.1** (latest stable, 29 October) with the
existing locked **2.1.0 client/plugin** dependencies, and **recommend**
it.
- Keep the technical **minimum at 2.1.2**: no consumed contract changed,
so the minimum has no new reason to move.
- 2.1.2, 2.1.4 and 2.2.0 daemons stay ready and get an optional upgrade
offer. 2.2.1 is the `tested` version. Newer and custom labels keep
authenticated contract recognition.
- The reviewed-contract fast path lists the 2.1.2 minimum and the 2.2.1
target.
- The recommended version is updated in the help (all ten locales), both
READMEs, `CONTRIBUTING.md`, `dev-docs/OPENCODE_COMPATIBILITY.md` and the
architecture skill.
## Contract review (v2.1.4 → v2.2.0 → v2.2.1; 661 + 134 commits)
Reviewed from the exact tags `v2.1.4` (`68bc26b7da`), `v2.2.0`
(`1097e2668b`) and `v2.2.1` (`3ebcf9a32c`), the published npm packages,
and an authenticated OpenAPI dump from each official CLI running in its
own sandbox. The [changelog](https://opencode.ai/changelog) covers
2.2.0. It had no 2.2.1 entry at review time (29 October), so 2.2.1 was
reviewed from its commit log.
- **Authenticated OpenAPI:** 124 → 130 → **134 paths**, 149 → 156 →
**162 operations**. Each dump matches the tagged
`packages/protocol/openapi.json`.
- **2.2.0 adds** `GET /api/plugin/gui`; `POST
/api/experimental/generate/{text,image,decisions}`; `POST`/`DELETE
/api/experimental/fs/watch`; `GET /api/experimental/media/{hash}`; and
`GET /api/experimental/usage/limits`.
- **2.2.0 removes** `POST
/api/experimental/voice/session/{sessionID}/instructions`.
- **2.2.1 adds** six `/api/experimental/schedule` operations (list,
create, get, delete, pause, resume), gated by `experimental.schedules`.
- **Changed operations:**
- `GET /api/model` gains an optional `types` filter. Without it the
route lists chat models only. Image and decision models are new in
2.2.0, so the default listing matches 2.1.4.
- `GET /api/event` gains an optional `types` filter.
- `GET /api/vcs/diff` gains an optional `untracked` parameter.
- `POST /api/experimental/generate` becomes the deprecated alias of
`/generate/text`.
- Nine session, project and saved-permission routes drop
`LocationNotFoundError` from their 404 responses: they no longer boot a
Location ([#58997](https://github.com/anomalyco/opencode/pull/58997)).
- `GET /api/agent` can now answer 503 while plugins settle
([#59082](https://github.com/anomalyco/opencode/pull/59082)).
- In 2.2.1, the session log and persistent-PTY operation IDs move under
`experimental.*`. Their paths are unchanged.
- **Schemas:** all 103 consumed-family schemas are byte-identical from
2.1.4 to 2.2.1. These are `V2EventEncoded`, `Session.*` (messages,
parts, inbox, info), `Form.*`, `Permission.*`, `Prompt*`, `Event.*`,
`Location.*` and PTY/Shell. The session prompt, environment, message,
form/permission reply and RPC operations are unchanged too. The only
`Config.InfoEncoded` changes are additive opt-ins:
`experimental.media_blob`, `experimental.image_generate`,
`experimental.schedules` and `timezone`. Model `type`, prompt-cache
`cache` settings, `FileDiff.Info.more`, `Mcp.Server.title` and
`ServerInfo.hostname` are additive fields.
- **Admission:** every predicate CodeNomad's
`compatibility/negotiate.ts` checks holds on 2.2.0 and 2.2.1. That
covers the canonical API, `session_environment` and the 2.1 replay shape
(`native` without `state`, compaction `metadata` without `recent`).
- **Instance proxy:** none of the new routes is on the allowlist. That
includes the GUI plugin bundle, generation, file watch, media blobs,
usage limits and schedules. The removed voice route was never exposed.
The routes CodeNomad does proxy kept their paths.
- **Published declarations** (2.1.0 pins → 2.1.4 → 2.2.1):
- **Client:** 143 → 147 → 185 methods. 2.2.1 mirrors the experimental
methods under `client.experimental.*`. The only removal is
`voice.instructions`, which CodeNomad doesn't use. 2.2.1 doesn't send
the `experimental-media-uri` capability, so media reads stay inline.
- **Schema:** 2.2.0 adds media refs (`MediaRef`,
`Session.Message.MediaUser`, inbox `MediaUserPayload`), `generate`,
`usage-limits` and `Model.Type`. 2.2.1 adds `goal` and `schedule`. The
one removal since 2.1.0 is `SessionEvent.Source` (2.1.4).
- **Plugin:** 2.2.0 adds the `gui/*` SDK (excluded from the host
runtime) and `Host.packageID`. 2.2.1 adds
`SessionIdle`/`SessionIdleBackground`. The event `subscribe` domain
gains an optional `types` filter; CodeNomad's plugins don't subscribe
through it.
- **Typecheck:** the server and UI typecheck with **0 errors** against
the 2.1.4 and 2.2.1 client/schema/plugin declarations, through scratch
`paths` overrides. The pinned 2.1.0 control is also clean.
- **Host modules:** CodeNomad's three daemon-loaded bundles resolve
`@opencode/plugin` (`Plugin`), `/effect` (`Plugin`, `Rpc`, `Location`),
`/promise/adapter` (`fromPromise`), `/rpc` (`Rpc`), `effect` and
`effect/encoding` (`Base64Url`) from the host. `@opencode/schema` and
`@opencode/client` are bundled from the 2.1.0 pins, and
`@opencode/schema/location` imports are type-only. A probe plugin loaded
in an isolated **2.2.1** daemon resolves every one of these modules and
bindings, as on 2.1.4. The host Effect stays **4.0.1**, with the same
lockfile hash.
- **Private native services:** the pending-request reader and Missions
recurrence use keyed native services: `LocationServiceMap`, `Form`,
`Permission`, `Location`, `Job`, `Database`, `Session` and `Bus`. Their
keys are unchanged at v2.2.1, and the members CodeNomad calls are
unchanged. The changes are additive: `Permission.assertNotDenied`,
`Session.*WithRefs`, an `image` Job kind, and
`LocationServiceMap.canonical` normalizing paths.
- **Storage** (migrations from 2.1.4 to 2.2.1): `blob`/`blob_ref`
(always created; written only with `experimental.media_blob`) and
`schedule` are new. `session_pending` is dropped; CodeNomad never reads
it. A `session_v2.allowance` column is added and dropped within 2.2.0.
- The tables CodeNomad reads directly (`session_message`, `session_v2`,
`session_inbox`, `event`, `event_sequence`, `kv`) keep their shape.
- The pruning plugin's `media-blob.ts` already projects `media:<sha256>`
refs.
- The pruning fixture enables `media_blob` explicitly, so the
always-present `blob` table doesn't change its expectation.
- Missions reads `session_inbox` only by count and ID in production
code.
- **Behavior changes checked:**
- **Code Mode:** 2.2.1 moves programs onto one job queue
([#57976](https://github.com/anomalyco/opencode/pull/57976)). The
run-to-completion regression behind the 2.1.2 minimum passes on 2.1.4,
2.2.0 and 2.2.1: `{ total: 2000, beforeAwait: 1000, count: 1000 }`.
- **Session form polling** returns `[]` without booting a cold Location
([#59083](https://github.com/anomalyco/opencode/pull/59083)). Forms are
in memory, so nothing pending is hidden. This matches CodeNomad's
existing-only pending reader.
- **Location keys and `session.create` directories** are canonical on
POSIX too: trailing slash removed, `posix.normalize`
([#59327](https://github.com/anomalyco/opencode/pull/59327)). Windows
already normalized. This is low risk; it was reviewed in source only,
since the location and ownership fixtures run on Windows and the POSIX
certification covers migration, Code Mode and Git-degraded.
- **Location keep-alive:** the changelog line "keep project services
alive … with a 24-hour inactivity cap"
([#57274](https://github.com/anomalyco/opencode/pull/57274)) doesn't
match the shipped tree. That commit was superseded before 2.1.4, and no
cap exists at v2.2.0 or v2.2.1. From 2.1.4 to 2.2.1,
`location-activity.ts` only canonicalizes paths and generalizes Job
kinds. Long-waiting Missions sessions keep their Location as on 2.1.4.
- **Plugin readiness:** each runner step and tool call now waits for
plugin activation
([#51133](https://github.com/anomalyco/opencode/pull/51133)), and
agent/provider lists wait for plugins. Presence leases stay outside the
discovery root, so this adds no reload.
- **Service lifecycle:** registration and config renames are retried on
Windows. The managed service ignores a saved `XDG_STATE_HOME`
([#58815](https://github.com/anomalyco/opencode/pull/58815)); CodeNomad
never sets it. The `OPENCODE_SERVER_USERNAME` change was added and
reverted within 2.2.0. Detached POSIX children are killed when OpenCode
exits; service start uses its own spawn path.
- **2.2.1 opt-ins:**
- schedules, both the API and the agent tool, need
`experimental.schedules`;
- `image_generate` needs `experimental.image_generate`;
- `/goal` runs only when invoked;
- the subagent tool description now invites follow-ups to *running*
subagents, with the same mechanics as 2.1.4.
- **Other changes:** compaction sizing from the provider's count, MCP
malformed-tool tolerance, OAuth refresh coordination
([#54023](https://github.com/anomalyco/opencode/pull/54023)) and
webfetch redirect rules. They touch no consumed contract and were
reviewed in source; of these, the native suites exercise compaction
only.
- **Upstream #52922** (Bus routing before Location fanout) is still
open: keep the discovery and event-relay mitigations.
**Verdict:** no consumed contract changed from 2.1.4 to 2.2.1.
- The **2.1.0 client/plugin pins can stay**.
- The **2.1.2 minimum has no reason to move**.
- 2.2.1 is admitted through the existing contract recognition, and
nothing found blocks recommending it once the native suites pass on it.
- No CodeNomad bug was found.
Bumping the pins is optional. It would bring the 2.2 client's
discard-tool-result-payload option and its "register running sessions
missed while disconnected" fix
([#53012](https://github.com/anomalyco/opencode/pull/53012)). It would
also move the replay reducer, so it belongs in its own PR. The UI and
server already typecheck against 2.2.1.
**Not verified here:**
- the full native suites against 2.2.0/2.2.1 (only the Code Mode
regression and the host-module probe ran);
- the TUI pruning companion under OpenTUI 0.7. It uses only
`Plugin.define`, `ctx.data.session` and `ctx.data.listen`, so the 0.7
renderable changes don't apply, but this wasn't run;
- POSIX canonicalization on a real Linux/macOS runtime;
- the 2.2.1 release notes, which were unpublished at review time.
## Isolated Windows qualification
Base: dev `
|
||
|
|
e1fbd7a3f8
|
fix(server): declare the @opencode/schema and effect runtime dependencies (#1186)
The server imports @opencode/schema and effect at runtime (the Missions recurrence, passage and owner modules), but declared neither: npm hoists them from @opencode/client, while pnpm's strict layout does not, so the published package failed to start under pnpm with ERR_MODULE_NOT_FOUND for @opencode/schema. Both are now server dependencies. effect is pinned to 4.0.1, the exact version @opencode/schema and @opencode/protocol require, so pnpm cannot give the server a second Effect instance, and the lockfile's packages/server entry lists them; resolved versions are unchanged. An esbuild import graph from the server entry points shows no other undeclared runtime package (@codenomad/remote-tunnel is bundled into tunnel-runtime.js at build time). |
||
|
|
a448bb1f6a
|
perf(missions): send summaries of closed missions to the panel (#1180)
Step 5 of 6 toward removing the Missions limits (#1167 follow-up), on
top of steps 1-4.
## Change
- Plugin RPCs `list` (summary row per mission from hints; full maps only
for open missions + up to 4 requested, each re-read from its range;
unchanged rows skipped via `summariesVersion`) and `mission` (one
range's map + its cleanups).
- `GET /missions?include=&summaries=` and `GET /missions/:missionID`;
fallback to the full `snapshot` when the plugin lacks the new reads
(`rpc.method_not_found`).
- UI store holds full maps + rows; picker/marks/completion seeding use
rows; detail, Needs you, task tree and readers use full maps of the
selection and reader target. Activity observes only the maps sent.
- Bridge routes that authorize native effects keep the full snapshot
(hint contract).
- Visible: a selected closed mission shows the loading line until its
map arrives; a closed unselected row's permission mark counts only its
declared actors' requests. No help change (nothing documented there).
## Read counts (10,000 history entries, 200 closed + 1 live)
`list()`: 0 full scans, live range only, 201 rows · with one closed
included: +50 entries, rows not resent · `mission(id)`: its 50 entries ·
`GET /missions`: 0 full scans after the index build; activity only for
open/included.
## Validation
- `mission-list.test.ts` (equivalence over random histories with
damage/purges, version skipping, staleness, label bound), read-budget
tests, route fallback tests, new `mission-summaries` browser test;
adapted fixtures keep assertions. Mechanism mutations each fail a test.
- Server and UI typecheck; 1,210 server tests; UI mission/i18n tests;
all 36 mission browser files; `test-missions-native.mjs` on isolated
OpenCode 2.2.0 and 2.1.4.
- `automation-forms.test.ts` fails identically on unmodified `
|
||
|
|
8e8ea0653e
|
fix(history): search recent history first, cover more of it and search further back (#1182)
## Summary
Follow-up to #1161 and #1164. In a large project, the ranked project
search covered only the **oldest** messages: the scan walked rowids
upward under a 1.5 s budget, so recent conversations were never reached,
and identical searches covered 5,000 to 7,000 messages depending on
load. Each 1,000-row statement also sorted and evaluated its whole chunk
before returning its first row, blocking the daemon for about 70 ms at a
time.
- **Newest first:**
- The scan streams 128-row ranges from an O(1) horizon, without a
temporary sort.
- It covers a deterministic window of the 16,384 most recent messages.
- **Cheaper per row:**
- A necessary UTF-8 byte check on the raw row rejects messages that
cannot contain a query term before any JSON is decoded.
- Only possible matches pay for the exact readable-text mask.
- The existing time, candidate, session and judge guards remain.
- **40 results** in the project search window; the agent tool keeps 10
by default and 20 at most.
- **Search further back:**
- When older history remains, it ranks the next older window for the
same query and options through an authenticated per-location
continuation.
- The continuation is bound to scope, query, options and the original
horizon, so later appends never enter it.
- Older windows replace the list, because ranking scores are
window-local.
- The coverage note names the examined range in all ten locales.
- Query, scope or option changes, closing and leaving the session reset
it.
- The conversation scope is unchanged.
## Measurements (synthetic 40,000-message corpus, `node
scripts/benchmark-ranked-history.mjs
|
||
|
|
681b854865
|
feat(missions): bound journal capacity per mission, not per project (#1178)
Step 4 of 6 toward removing the Missions limits (#1167 follow-up), on top of steps 1-3 (#1175, #1176, #1177). ## Change - **Per-mission capacity:** each mission holds at most 2,000 ordinary entries plus its own 36-entry deletion reserve (2,036 per range). **No project-wide cap.** - Capacity checks and appends read only the target mission's range (no full scan). Safety decisions keep their full reads (accepted: their cost grows with history). - Reserve: only the mission's own tombstone and already reserved receipts; works on every storage. - Full project reads bounded per range (and stray entries together); memory = per-mission bound × missions, documented trade-off. - Purge is retention-only (20 newest settled deletions kept); capacity-pressure purge, creation pre-check and related re-checks removed. - Existing mission data is negligible (projects were capped at 20 missions, barely used): **no legacy, migration or rollback handling by design.** - Docs: `MISSIONS.md` safety limits; help sentence on freeing storage updated in 10 locales. ## Validation - Read-budget test at 10,000 history entries; project beyond one mission's bound fully works; capacity checks read only their range; full mission refuses ordinary entries/new Stop but deletes through its reserve (with and without removal). Each red with its guarantee mutated. - Native stage: 2,200 entries across 100 finished missions on an isolated runtime, then create/declare/readout/delete with retention purge. - Server and UI typecheck; 1,508 server tests; `test:help`; `test-missions-native.mjs` on isolated OpenCode 2.2.0 and 2.1.4. |
||
|
|
b38af07fbe
|
perf(missions): serve read-mostly paths from journal hints (#1177)
Step 3 of 6 toward removing the Missions limits (#1167 follow-up), on top of steps 1 (#1175) and 2 (#1176). ## Change Read-mostly paths take candidates from the activation's journal hints and re-read only those ranges: | Path | Before | After | |---|---|---| | Per-turn context hook | 2 full reads per model turn | 1 hinted membership read; 0 for sessions no mission names | | `inspect` (no start), list, `observeTask`, decision candidates | 1 full each | 0 full + candidate ranges | | Outbox listing | 1 full + 4 per item | 0 full + 1 range per open mission (per-item damage gates stay full) | | Presence retention | 1 full per interval | 0 when hints show open work; full read confirms an empty answer before release | | Change notifications after lifecycle/delete | 1 unused full read | 0 | Safety decisions keep authoritative full reads (creation, membership refusal, damage gates, delegation, reports, lifecycle, deletion, cleanup authority, purge, capacity). **Staleness (≤ 60 s, writers outside the generation protocol):** hook treats the coordinator as a non-member meanwhile (tools still select by full read); outbox only delays (deterministic message/receipt IDs, re-checked by a full read). `MISSIONS.md` lists the hinted callers. ## Validation - `journal-read-budget.test.ts` (1,900 finished-mission events): hinted paths do no full scan; safety paths still see writes the hints miss; presence release confirmed by full read; hook single read and tool hiding; ordering/damage parity; 60 s bound. 15 source mutations each fail a named test. - New native stage (100 finished missions, isolated runtime only): `inspect` and the model turn read no full journal; snapshot and damage gate do. - `receipt-projection.test.ts`: an outbox pass refused by its item's full read now counts as failed instead of throwing (same backoff, no duplicate message). - Server and UI typecheck; 1,358 server tests; `test-missions-native.mjs` on isolated OpenCode 2.2.0 and 2.1.4. Note for step 4: mutations stay O(history) (capacity scans, rule-1 full reads). |
||
|
|
21bfe855ce
|
feat(missions): keep an in-memory index of the mission journal (#1176)
Step 2 of 6 toward removing the Missions limits (#1167 follow-up), on top of step 1 (#1175). ## Change - New `MissionJournalIndex` (`missions/journal-index.ts`): activation-owned, never persisted. Holds per-range session references (coordinator, prepared-by, dispatching, native-bound), ordering data, open state, local damage, cleanups and stray-entry count; answers `refs`, `openIDs`, `aggregate`, `cleanups`. - Built by one key-ordered scan (`MissionJournal.scanRanges`) with `missionView`'s parsing/purge/reducer; kept current by marking each mission dirty **before** every ordinary write attempt; a per-project write generation (host-global, like the append lock) detects foreign writers and rebuilds; any failure drops it. Nominates candidates only: decisions must re-read storage. - Cannot observe bytes written outside `MissionJournal` after its build (other OS process, earlier builds); documented and pinned by a test. - Recurrence passage storage marked `isolated`: never feeds or serves the index. - Wiring only (activation creates, passes to `MissionControl`, disposes). **No caller reads it yet.** `MISSIONS.md` "Journal index" paragraph. ## Validation - `journal-index.test.ts`: property test (24 seeds × 70 steps, failed/unconfirmed writes, interrupted purges, damage, new activations, foreign writes) equals full `snapshot()`/`events()` on every answer and rescans only on activation/foreign write; deterministic cross-mission fixture; unreadable records; incarnation/blind-spot. Passage trap test in `native-passage-business.test.ts`. - 11 source mutations each fail a named test. - Server and UI typecheck; 1,343 server tests; `test-missions-native.mjs` on isolated OpenCode 2.2.0. |
||
|
|
9ed72ae068
|
refactor(missions): read one mission's journal range (#1175)
Step 1 of 6 toward removing the Missions limits (#1167 follow-up): make per-operation cost depend on active work, not history. ## Change - `MissionJournal.missionEvents(missionID)` / `missionView(missionID)`: one scan of `<prefix>/<missionID>/` with the same parsing, append/purge exclusion, read bound and purge-remnant resolution as `events()`, reduced by the same private `reduce()` that `snapshot()` now delegates to. - A view's discarded count and damage flags are its own; the snapshot's are their aggregate plus entries outside every mission range. - Recurrence passage adapter accepts its own mission's range. - **No caller switches yet**: behaviour unchanged. `MISSIONS.md` describes the read. ## Validation - `journal-mission-view.test.ts`: 24 seeded 70-step histories of real `MissionControl` operations (all 15 written event types, shared sessions, failed ACKs, pending cleanups, interrupted purges) with injected damage, prefix-extension IDs and timestamp ties; after every step each view equals its snapshot mission, views partition `events()`, counts/flags/cleanups aggregate to the snapshot. Red when the discarded count is dropped, the range's trailing slash removed, or the record check skipped. - `recurrence-passage.test.ts`: 121-event passage read by range. - Server and UI typecheck; all server missions/routes/opencode-missions tests; `test-missions-native.mjs` on isolated OpenCode 2.2.0. |
||
|
|
84d2d0a245
|
fix(missions): free journal capacity when deleted missions settle (#1173)
Follow-up to #1167. ## Problem A project's mission journal holds 2,000 entries and never removed any. Deleting a mission appended a tombstone and cleanup receipts on top of its events, so a project that keeps creating and deleting missions eventually filled the journal permanently: no create, and no delete either (the tombstone needed room). ## Change - **Purge on settlement:** once every cleanup target of a deleted mission has its receipt, its events are removed from native plugin storage (`storage.remove`, passed through a narrow `MissionStorage.remove`; storage without it keeps today's behaviour). - **Retained:** the 20 most recent settled deletions stay in full (cleanup history, lost-ACK checks); all eligible ones are purged when capacity is needed. A deleted mission stays while it references a session another unsettled mission could still clean up. - **Deletion record:** one compact record per purged mission at `codenomad-missions/v2/deleted/<projectToken>/<missionID>`, outside the scanned prefix (no journal capacity, exact-key reads). It refuses recreation (`mission-deleted`) before any native effect on create, prepare and coordinator start replays, and answers an exact deletion replay with the original summary. - **Crash-safe, resumable:** record first, then `mission.created`, tombstone, intents, receipts; an incomplete group with a record is dropped from readers; late writes are removed by the next purge; damaged journals are never purged. - **Full journal recovers:** a 36-entry reserve, usable only by tombstones and reserved receipts; reads bounded at 2,036. - Legacy tombstones are purged the same way. - Docs: `MISSIONS.md`; help (EN + 9 translations) cleanup-history sentence. ## Known risks - Rolling back to an older build reads a mid-purge journal as damaged, a reserve-using journal as over its limit, and ignores deletion records (a retried creation could recreate a purged mission). - Under capacity pressure the recent cleanup history is purged too; a delete whose ACK was lost in that window then shows a false "missing" error. - Deletion records accumulate (one small exact-key entry per purged mission). ## Validation - `journal-purge.test.ts` (10 tests, each red with its piece removed): 200 create/delete cycles, recreation refusal and replay, pending cleanup kept, crash at every step, late writes, legacy tombstones, full journal of live missions, no-remove storage, shared sessions, damaged journals. - Native `test-missions-native.mjs` gains a settled-deletion purge stage (red when the plugin lacks `remove`). - Server and UI typecheck; 1,331 server missions/routes tests; `test:help`; native run on isolated OpenCode 2.2.0. |
||
|
|
49f2efe8f9
|
fix(sessions): read OpenCode media blobs like native history (#1172)
Follow-up to the #1158 review (pre-existing P3). ## Problem With OpenCode 2.2.0's opt-in `experimental.media_blob`, inline files are stored as `media:<sha256>` refs (`blob`/`blob_ref`) and native reads hydrate them. CodeNomad's direct SQL readers saw the refs: pruning reported a false `conflict` (the UI hashes hydrated content), assets looked unavailable, navigation emitted unhydrated JSON. ## Change - New `session-pruning/media-blob.ts` mirrors native `blob.ts` hydration (tool files → data URIs, user files → data, missing blob → native placeholder), bounded before loading bytes. - Preview, prune candidates, prune transaction, navigation windows and assets read the hydrated form; existing size bounds apply to it. - Pruning compares the hydrated form, writes survivors back exactly as stored, and like native content rewrites releases the message's `blob_ref` rows for hashes it no longer names, deleting a blob only when unreferenced. - Decided by stored representation, not version: no refs → unchanged; refs without recognisable blob tables → `unsupported_storage` (distinct from `conflict`). - Docs: `SESSION_PRUNING_RPC.md`, `SESSION_HISTORY_NAVIGATION.md`, `PANEL_EXTENSIONS.md`. ## Validation - `media-blob.test.ts` (projection, placeholders, bounds, preview, prune, shared blob retained, unsupported storage, assets, navigation). - New native check `scripts/test-session-media-blob-native.mjs` (run by `test-session-pruning-native.mjs`): 2.2.0 with media_blob passes; 2.1.4 passes; red on unmodified dev. - Server and UI typecheck; all session-pruning, route, proxy and panel-asset tests. |
||
|
|
4e113583c8
|
fix(missions): surface a failed first recurring admission at once (#1171)
Follow-up to the #1158 review (pre-existing P2). ## Problem When the first native admission of a due recurring passage threw, the runner returned `unknown`, the due adapter reported `pending` and the clock treated the wake as successful: warning cleared, sleep up to 1 h. The passage looked silently `starting` for that hour. ## Change - `native-recurrence-due.ts`: new wake result `admission-unknown` for that case. - `native-service-clock.ts`: after its reread, while the passage is still pending and unadmitted, that result is a failed wake: the existing redacted `admission-failed` warning is published at once and the existing 30 s backoff retries the same pending identities. Nothing archived, no cursor advance, no new identity. - `MISSIONS_RECURRING.md` describes it. ## Validation - `recurring-day.e2e` test M: injected admission refusal and failing observation read (new fixture `failQuery`): warning on the first wake, no start before 30 s, exactly one admission with the original identities at 30 s, survives restart. Red without the fix. - Server typecheck; all missions tests; `test-recurring-simple-native.mjs ABCD` on isolated OpenCode 2.2.0 and 2.1.4. |
||
|
|
4d8db216b8
|
fix(missions): remove the 20-mission project limit (#1170)
Fixes #1167. ## Problem A project refused its 21st mission with `Project mission limit reached`, counting finished missions too. Long sessions whose coordinator starts missions itself hit it and could only continue after deleting missions by hand. The limit only bounded refresh cost. ## Change - Creation (panel, `mission_prepare`, the coordinator's `mission_inspect` start) no longer counts missions; the snapshot lists every mission. - The `mission-limit` code, its no-effect receipt path and `missions.control.creation.limit` (10 locales) are removed; `request-conflict` stays the only no-effect creation refusal. - Refresh cost stays bounded: activity observation reads at most 160 actors per refresh (the former 8 × 20), active missions first, then the most recently updated; the rest read as unknown. - Unchanged: 8 actors and 96 tasks per mission, the 2,000-entry project journal (follow-up), the 20 most recent settled cleanups. - `dev-docs/MISSIONS.md` states the current bounds. Help never documented the limit; nothing to update there. ## Validation - `control.test`: 25 missions created and listed. `activity.test`: 31 missions / 241 actors → exactly 160 session reads, the oldest active mission observed. - Creation-fence forged/mismatched no-effect receipt matrix kept on `request-conflict`; damaged-storage refusal kept. - Server and UI typecheck; all server missions, routes and native missions tests; UI mission and i18n unit tests. |
||
|
|
444991d352
|
fix(events): reconcile a recipient instead of dropping an unroutable event (#1165)
When the event relay could not resolve whether a workspace owns an
event's location (the lookup failed twice, for example while the shared
OpenCode connection was being replaced), it logged a warning, cached
"not owner" for two seconds and dropped the event while the stream stayed
connected. A question form or any other update for that workspace could
then stay missing until an unrelated refresh.
The relay now treats such a failure like a lost connection for that
recipient alone: the failed lookup is evicted from the cache, and that
instance receives an `error` status ("event routing failed") followed by
`connected`, which runs the client's existing reconnect reconciliation.
Other workspaces keep receiving the event, and the shared subscription is
not failed, so one workspace's lookup cannot resync every instance.
Pulses are leading and trailing per instance, at most every 5 s and
doubling to 60 s while failures persist; pending pulses are cleared on
reconnect, workspace stop or error and shutdown, and skipped when the
stream is not connected or the workspace is no longer listed.
Repeated pulses exposed that TrailingResyncCoordinator chained every
request, so a slow pass accumulated one full reconciliation per request.
It now keeps one active pass per instance plus a dirty bit: requests
during a pass share one trailing pass, and each returned promise settles
after a pass that began after its request. Passes still start on a
microtask, so callers keep their timing.
A relay regression covers a persistently failing recipient next to a
healthy owner (immediate pulse, trailing pulse, doubling backoff), and a
coordinator regression covers eight requests during an active pass; both
fail without their change. The server workspace and event suites, the
CI UI unit-test set and both typechecks pass.
|
||
|
|
5a49ac36b2
|
feat(history): rank whole-project transcript search, keep conversation search chronological (#1164)
## Summary PR 2 of 2 after #1161. The transcript search window's **project scope** uses the ranked engine; the **conversation scope** keeps its literal, chronological, paginated search and code path. - **Project scope:** - One request returns the best 20 results in relevance order: rare words weigh more, an exact phrase ranks first, and each conversation appears once before repeats. There is no next-page traversal. - Every query term is highlighted in the excerpts, in the preview and in the destination conversation when a result is opened. - Compact notes report a partial scan, excluded folders, or a runtime without ranked search. - **Route:** `POST /api/workspaces/:id/session-history/ranked` calls only the plugin's `historyRanked` RPC. - It applies the same ownership, provenance, deletion, connection and cancellation fences as the literal history route, through scope helpers shared in `session-history-scope.ts`. - It validates counts and every contributing location before publishing and strips provenance. - It records an unsupported method per connection. - Disjoint worktree roots are interleaved by native rank before session diversity is applied. - UI strings and help pages are updated in all ten locales. ## Validation - Server and UI typechecks pass. - Routes 13/13, covering ownership rejection, fences, invalid provenance and the unsupported state. - Store tests 6/6. - Chromium: - history 2/2; - search-highlight 12, plus 1 optional benchmark skipped; - session-search 1/1. The browser cases cover ranked order, highlighting, opening a result and the unchanged conversation scope. Oversized files touched: `message-section.tsx` (~1,816 lines), `message-block.tsx` (~1,775), `api-client.ts` (~833). |
||
|
|
7a206a9297
|
feat(history): rank past-session search and expose it to agents as session_search (#1161)
## Summary Agents can now find past discussions themselves. The bundled session-pruning plugin registers a Code Mode tool, `codenomad.session_search`, ranked by relevance over the calling session's own project history. This is PR 1 of 2: server/plugin only; the search window keeps its current behaviour until PR 2 wires the same engine. - **Tool scope:** the caller's directory tree, native workspace and project (plus `global`) history, never other projects. The caller's location is checked before and after the read, and excerpts are flagged as untrusted transcript data. The tool follows backend presence like the plugin's RPCs. - **Ranking:** up to six Unicode terms; a precise SQL prefilter over rowid chunks (superset of the JS judge: ASCII folding, Unicode case classes, literal `\u` escape fallback) yields per-term IDF weights. The best candidates are judged one bounded body at a time on word boundaries, with an exact-phrase bonus, prose before tool payloads, recency as tie-break and one hit per session before repeats. Optional inclusive UTC `since`/`until`. Candidate, session, judge and time caps are reported. - **Shared limits:** scope SQL, readable-part extraction, the 16 MiB message bound and 40 ms slices move to `history-content.ts` and are shared with the literal search. - **For PR 2:** a versioned `historyRanked` RPC returns the same hit fields plus per-session provenance, so the broker can validate ownership as for `history`. Existing RPC methods, HTTP routes and UI are unchanged; the generic RPC proxy stays closed. Inspired by `@rizal72/oc-session-search`. ## Validation - Session-pruning tests 98/98, including the package regression; native plugin bundle 1/1; server typecheck. - Regressions: Unicode terms and accented case variants, weights, phrase bonus, diversity, dates, prefilter precision (multi-line and accented messages lacking a term are not candidates), a rare old match outranking 600+ newer frequent ones, scope isolation, cancellation, caps, skipped rows, tool registration and presence disposal. - Isolated native history fixture on OpenCode 2.2.0: 23 checks, including a ranked query (3 hits across 2 sessions). No shared daemon or user database was used. |
||
|
|
a0fd004283
|
chore(missions): refer to the minimum OpenCode without its version number (#1159)
Comments and two test names said `as of the 2.1.0 minimum` while the supported minimum is 2.1.2 (`MINIMUM_OPENCODE_VERSION` in `opencode/runtime-support.ts`). The number copied into comments goes stale whenever the minimum moves; they now say `the minimum supported OpenCode` and keep describing the native behavior they explain. Unchanged: comments citing a version for a version-specific bug or contract (`runtime-support.ts`, CLI 2.0.11 discovery, pre-2.0.4 contracts, 2.0.26 observations) and test assertions of the minimum value. Comment and test-name changes only (10 files). Server typecheck and the touched missions tests pass. |
||
|
|
10ae3f9031
|
fix(missions): stop reading the session_pending table OpenCode 2.2.0 dropped (#1158)
## Problem On OpenCode **2.2.0**, recurring Missions never run: a due daily passage creates its coordinator session and stays `starting`, with no start message and no model call. The latest-runtime CI job (`tests-runtime-contracts (latest)`, `scripts/test-recurring-simple-native.mjs`, stage "A daily useful work") fails on 2.2.0 for every PR; it passes on 2.1.4. Reproduced locally with isolated runtimes. ## Cause - OpenCode 2.2.0 drops the `session_pending` table (`core/src/database/migration/20261025154125_drop_session_pending.ts`, upstream #59020 "drop unused session_pending table"). - `native-passage-observation.ts` still counted rows there, so every passage observation threw `no such table: session_pending`: admission failed right after session creation (`native-recurrence-admission.ts`), and the never-started fallback failed on the same read, leaving the passage silently `starting`. - Nothing has written that table since the native session inbox replaced it (before 2.0), so the count was always 0 on 2.x. Queued input lives in `session_inbox`, which the observation already counts. ## Change - Drop the `session_pending` count and the `pending` observation field; settlement and passage transport stop testing it. Active execution, inbox, running tools, suspension and failed turns still gate starts and settlement. - No version check: the consumed contract is the inbox. - Test fixtures no longer create a fake `session_pending` table, so a reintroduced read fails the unit tests. ## Validation - Server typecheck; all `packages/server/src/opencode/missions` tests. - `scripts/test-recurring-simple-native.mjs <cli> ABCD` passes against isolated OpenCode **2.2.0** and **2.1.4**. |
||
|
|
339a0844ca
|
fix(opencode): stop an emptied project experimental block from capping subagent depth (#1153)
## Problem
A mission asked to nest subagents three levels deep stopped at the first
level: every nested call returned `Subagent depth limit reached (1).
Increase "experimental.subagent_depth"…`, although the global OpenCode
config sets `experimental.subagent_depth: 5`. The project file
`.opencode/opencode.jsonc` contained only `"experimental": {}`.
## Cause
- OpenCode reads `Config.latest(entries, "experimental")?.subagent_depth
?? 1` (`core/src/tool/plugin/subagent.ts:129`): the **whole**
`experimental` object of the highest-priority document that has one,
unmerged (`core/src/config.ts:24-27`). An empty project block therefore
hides the global depth.
- CodeNomad's depth editor reset (**Inherit**) removed only
`subagent_depth`, leaving that empty block behind.
- The editor computed the effective depth as the last document defining
`subagent_depth`, so it showed the global 5 while OpenCode applied 1.
## Change
- **Inherit** also removes a project `experimental` object it leaves
empty; foreign experimental keys, comments and formatting are kept.
- The effective depth follows OpenCode's read; a project block without
the depth reports the native default (unknown when the contract declares
none) instead of the hidden global value.
- `dev-docs/MISSIONS.md` describes both. Help already says that an empty
field inherits, which is now true in every case.
## Validation
- `subagent-depth-settings.test.ts`: shadowing read, Inherit keeps
foreign experimental keys, an empty project block reported as hiding the
global depth and removed by Inherit after an override.
- Server typecheck.
- `scripts/test-subagent-depth-settings-native.mjs` against an isolated
OpenCode 2.1.4.
|
||
|
|
f1a205a350
|
fix(missions): keep the coordinator recovery nudge's opening sentence (#1151)
#1150 extended the coordinator recovery text for resumed missions by replacing the first sentence's period with "; if you had concluded it, the user has resumed it". The native Missions contract (scripts/test-missions-native.mjs) recognizes the delivered nudge by the exact sentence "Recover coordination of existing mission <id>.", so the runtime-contracts job (minimum and latest) failed on dev at "explicit targeted coordinator recovery" even though the nudge was admitted and sent. The opening sentence is restored and the resume note becomes its own sentence, with the same meaning for the coordinator. A unit test in recovery-input.test.ts now pins the opening sentence the native contract matches, so a wording change fails without the native runtime. Validation: recovery-input and mission-resume tests; locally with an isolated OpenCode v2.1.4, scripts/test-missions-native.mjs, test-missions-native-trajectory.mjs and test-recurring-simple-native.mjs pass; server typecheck. |
||
|
|
048cd65183
|
fix(missions): one status palette, no agent-decided failure, resumable missions (#1150)
fix(missions): one status palette, no agent-decided failure, resumable missions (#1150) #1147 reported a red chevron beside a running mission because another, older mission had failed. Missions mixed two colour systems: rows showed session-style attention when there was some and otherwise a lifecycle colour (blue running, green completed forever, red failed), the collapsed chevron added "failed", and the task tree had its own palette. Red meant both "your answer is expected" and "this ended badly", a failed mission offered no action, and a one-time coordinator could end a mission as failed on its own. Colour now follows the session list everywhere (mission and schedule rows, the selected mark, task icons, the chevron) and shows only what needs attention now: - red: an answer is requested, counted like Needs you (declared actors and observed family; schedule rows without a passage read count the live passage's conversation and loaded subsessions); - yellow, pulsing: observed work; - orange: interrupted or paused, with an action to continue (an interrupted schedule, or a running one whose latest run failed); - green: a completed mission this window has not shown yet; - everything else is neutral: outcomes (done, failed, blocked, waiting, retired) are status text and icon shapes. missionMark/scheduleMark in mission-picker-model.ts compute one mark per entry. Like a collapsed session parent, the chevron colours only while the list is collapsed, from the other entries (paused is not counted). "Seen" works like an idle session: mission-view-state keeps the completed missions this window has shown under its own layout key and signal, so recording one never wakes project-view readers (an early write through the project view re-ran session-view effects and aborted an in-flight outline read). The first read counts missions already completed as seen; a completion is marked when selected while the panel is visible in a focused window (createDocumentAttention, now shared with SessionView). One-time coordinators no longer decide a failure: the plugin's MissionControl sets completedFinalOnly, and the refusal, coordinator context and tool description tell the coordinator to ask the human through the native question tool. Only the human stops a mission. Recurring passages keep their own control and completed|failed. Interrupted missions get an action: - A running mission's coordinator owes its final report: observed idle with an empty inbox and no coordinator notification still being delivered (native returns excluded), it projects idle-without-report; when nothing else runs the mission reads Interrupted and coordinator recovery replaces Pause. - A failed mission reads as interrupted unless stopped since, and the same recovery resumes it: under the mutation lock it refuses with already-member when one of its sessions joined another active mission (as at creation), otherwise appends mission.reopened at the exact revision and sends the ordinary recovery nudge at the new revision. The reducer resumes only a failure, drops its summary and lets a later Stop end it; a later finish uses a revision-bound event identity. The unwired authority policy accepts the same resume. dev-docs/MISSIONS.md records the rules; the Missions help page gains "Colours and interrupted missions" in all ten languages, and new strings are added in every locale. Validation: mission-resume.test.ts (completed-only refusal, passage-style failed finishes, resume once with conflicts and no second nudge, shared-session refusal, reducer reopen/Stop rules); activity.test.ts (owed final report, queued inbox, pending and native-return reports, non-running missions); mission-picker-model unit tests; the new mission-status-marks browser test (marks, collapsed chevron, unseen completion surviving reload, recovery replacing Pause, neutral done and blocked tasks, failed mission resume, recurring requests including independent actors and a zero selected count, quiet stopped/paused schedules), red on dev; updated mission-control, mission-briefing and mission-recovery tests. All Missions server tests, the full browser suite (four shards), help checks, UI and root typechecks pass. |
||
|
|
13450749b8
|
feat(preview): open loopback conversation links in the web preview (#1119)
When the frontend is reached from a browser, an agent-printed `http://localhost:3000` link points at the browser's machine, not the backend host that runs the dev server, so clicking it opens a dead tab. Many development apps also break inside the opaque preview, where `localStorage`, `sessionStorage` and `document.cookie` throw. A plain left click on an explicit http(s) loopback link (`localhost`, a literal `127.x.x.x`, `[::1]`, `0.0.0.0`) in session Markdown now opens it in that session's web preview. Middle and modifier clicks, external links and Markdown rendered without a session keep the browser default. Interception waits until the server meta is known and stays off where previews are unavailable, such as Remote Control while the host keeps web previews off. Intercepted links without an author title get a localized tooltip naming the Ctrl/⌘-click and middle-click escape hatch. The preview runtime bridge gives opaque documents whose storage access throws one stable in-memory `localStorage`, `sessionStorage` and `document.cookie` object each for the current page load, with property access and enumeration, `Max-Age` taking precedence over `Expires`, and detection by reading alone, so working storage (a per-token preview host opened on its own) stays native. The help in all ten locales and the preview paragraph of architecture.md describe both. Validation: the loopback predicate unit test, the runtime-bridge shim tests, the Markdown loopback-link browser test, the server preview, proxy and Remote Control suites, both typechecks and the help checks pass; the interception and the shim were checked in Chromium against the real preview proxy and Remote Control surface. |
||
|
|
1e515921e3
|
fix(sidecars): refuse opaque-origin WebSocket upgrades (#1135)
The HTTP preHandler refuses `Origin: null` everywhere except preview capabilities, so an opaque frame (a sandboxed preview iframe, a CSP-sandboxed preview document or a popup it opens) cannot reach `/sidecars/*`. WebSocket upgrades never pass through that preHandler, and the SideCar upgrade handler only checked the session: an authenticated handshake with `Origin: null` was proxied to the SideCar. Current preview contexts could not exploit it: in real Chromium their handshakes to the application carry no session cookie. The SideCar upgrade handler now refuses `Origin: null` with 403 before authentication, like the HTTP routes, so SideCar access does not rest on the browser's cookie policy alone. Preview upgrades keep accepting opaque origins, since previews are their own capabilities. Validation: the SideCar proxy test adds an authenticated `Origin: null` handshake, which got 101 without the fix and now gets 403; the server routes, proxy, preview, SideCar and Remote Control suites and the server typecheck pass. |