Commit graph

431 commits

Author SHA1 Message Date
Pascal André
2b57a67df2
feat(missions): add byte-bounded panel page reads (#1246)
Some checks are pending
Publish help / build (push) Waiting to run
Publish help / deploy (push) Blocked by required conditions
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.
2026-11-01 03:39:14 +01:00
Pascal André
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.
2026-11-01 01:58:10 +01:00
Pascal André
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.
2026-11-01 01:47:20 +01:00
Pascal André
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).
2026-10-31 22:29:05 +01:00
Pascal André
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
2026-10-31 21:55:48 +01:00
Pascal André
da8607d49e
feat(missions): send compact mission maps to the panel (#1237)
Some checks are pending
Publish help / build (push) Waiting to run
Publish help / deploy (push) Blocked by required conditions
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.
2026-10-31 19:22:10 +01:00
Pascal André
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)
2026-10-31 18:43:49 +01:00
Pascal André
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.
2026-10-31 14:56:03 +01:00
Pascal André
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).
2026-10-31 09:46:04 +01:00
Pascal André
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`.
2026-10-31 07:49:59 +01:00
Pascal André
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.
2026-10-31 06:27:51 +01:00
Pascal André
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.
2026-10-31 06:21:12 +01:00
Pascal André
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.
2026-10-31 04:50:52 +01:00
Pascal André
c17752e9b7
feat(missions): route human questions through the coordinator and record who decided (#1227)
Some checks are pending
Publish help / build (push) Waiting to run
Publish help / deploy (push) Blocked by required conditions
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.
2026-10-31 03:19:24 +01:00
Pascal André
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`
`605820669`.

**Headline:** neither release changes a contract CodeNomad consumes.
2.2.4 adds opt-in **artifacts** (seven `/api/experimental/artifacts`
operations, four tools, a plugin hook), **renamable persistent
terminals**, and moves background-job recovery markers from `kv` into a
new **`job` table**. 2.2.5 is a bug-fix release; its one schema change
is that `compaction.buffer` accepts a percentage such as `"20%"`. The
image tool contract adopted in #1209 (`experimental_image_generate`, on
by default) is unchanged through 2.2.5, and still nothing writes
`session_part`.

- **Authenticated OpenAPI:** 134 → **138** → 138 paths, 162 → **169** →
169 operations, schemas 351 → 362 → 363.
- **Added operations (2.2.4):** `GET
/api/experimental/artifacts/providers`, `GET|POST
/api/experimental/artifacts`, `GET|PATCH|DELETE
/api/experimental/artifacts/{artifactID}`, `GET
/api/experimental/artifacts/{artifactID}/view/*`
([#60351](https://github.com/anomalyco/opencode/pull/60351)). None
removed in either release.
- **Changed operations (2.2.4):** `GET /api/event`, where the `types`
filter enum gains `persistent-pty.updated` and `artifact.updated` (94 →
96 types, none removed). `PUT /api/experimental/persistent-pty/{ptyID}`
can also rename (`title`; `null` clears it), `size` becomes optional,
and a 501 `PtyFeatureUnsupportedError` is returned while an older
terminal service can't rename
([#60329](https://github.com/anomalyco/opencode/pull/60329)).
  - **Changed operations (2.2.5):** none.
- **Consumed schemas:** same family selector as the 2.2.3 review, 97
schemas at every version.
- 2.2.3 → 2.2.4: 95 byte-identical. The two that change,
`PersistentPty.Info` (+ `userTitle`, nullable, required) and
`PersistentPty.UpdateInput` (`size` optional, + `title`), are both
additive.
    - 2.2.4 → 2.2.5: all 97 byte-identical.
- `V2EventEncoded`, `Session.*`, `Form.*`, `Permission.*`, `Prompt*`,
`Event.*`, `Location.*`, PTY/Shell are byte-identical from 2.2.3 to
2.2.5.
- **`Config.InfoEncoded`:** 2.2.4 adds `experimental.artifacts`
(boolean, opt-in). 2.2.5 turns `compaction.buffer` into
`Config.Compaction.Percent | integer`
([#60647](https://github.com/anomalyco/opencode/pull/60647)).
`experimental.image_generate`, `experimental.media_blob` and
`experimental.schedules` are unchanged.
- **Admission:** every predicate in `compatibility/negotiate.ts`
(unchanged on origin/dev) holds on 2.2.3, 2.2.4 and 2.2.5.
`contractProfile` lists only `2.2.3` as reviewed, so 2.2.4/2.2.5 are
admitted through authenticated contract recognition.
- **Instance proxy:** the allowlist is unchanged on origin/dev. No added
or changed route is proxied: artifacts and persistent PTYs are outside
it. The proxied `GET /api/config` returns the additive config fields
above.
- **Published declarations** (2.2.3 → 2.2.5):
- **Client:** 185 → **192** → 192 methods. 2.2.4 adds
`experimental.artifacts.{providers,list,create,get,update,delete,view}`,
the artifact and `PersistentPtyUpdated` types, and
`PtyFeatureUnsupportedError`. It removes
`createPtyClient`/`PtyConnectInput` from `@opencode/client/solid/pty`,
which CodeNomad doesn't import. 2.2.5 adds `ConfigCompactionPercent`.
- **Schema:** 2.2.4 adds the `artifact` module and
`PersistentPty.Updated`; 2.2.5 adds `config/compaction` `Percent`.
- **Plugin:** 2.2.4 adds `effect/artifact` and `promise/artifact` and
`ctx.experimental.artifact` (`transform`, `reload`). 2.2.5 has no export
changes.
- **Typecheck:** the server and UI typecheck with **0 errors** against
the 2.2.4 and 2.2.5 client/schema/plugin/effect declarations. The pinned
2.1.0 control is also clean.
- **Host modules:** the bundles' host imports are identical to the 2.2.3
review. A probe plugin in an isolated **2.2.4** and **2.2.5** daemon
resolves every module and binding. `runtime-modules.ts` is unchanged
from v2.2.3 to v2.2.5.
- **Host Effect:** stays **4.0.6** (`bun.lock` sha512 identical;
published `dependencies.effect` 4.0.6).
- **Persistent PTY binary:** the 2.2.4 and 2.2.5 CLIs ship
`@opencode/pty` **0.4.0** (string `opencode-pty 0.4.0` in the Windows
binary; 2.2.3: `0.3.3`). `process-daemon-v0.4.0` adds only terminal
renaming ([#60324](https://github.com/anomalyco/opencode/pull/60324)):
an additive `rename` request and `features: ["rename"]` with an
unchanged `PROTOCOL_VERSION`. CodeNomad only lists persistent PTYs
(worktree deletion guard) and reads `status`/`cwd`.
- **Private native services:** the keys (`LocationServiceMap`, `Form`,
`Permission`, `Location`, `Job`, `Database`, `Session`, `Bus`, `Shell`)
and the members CodeNomad calls are unchanged. The changes are additive:
`Job.cancelSession` (2.2.4), `Permission.allows` (2.2.5; never asks,
used by `edit` to decide whether to quote file lines), and
`Session.remove` now calls `jobs.cancelSession`. The Missions recurrence
clock calls `Job.get/start/cancel`. Its Job metadata carries no
`sessionID`, so deleting a session doesn't cancel it.
- **Storage:** one migration, `20261030054400_job` (2.2.4,
[#60491](https://github.com/anomalyco/opencode/pull/60491)). It creates
`job`, copies every `kv` row `job.background/%` into it and deletes
those `kv` rows. 2.2.5 adds none.
- CodeNomad reads only its own `kv` keys (Missions, pruning receipts and
claims), never `job.background/*`.
- Native migration 2.2.3 → 2.2.4 passes. Every 2.2.4/2.2.5 fixture DB
inspected holds the `job` table, **0** `job.background/` rows and no
leftover `schedule:leader` lease.
- **`session_part`:** still **not written**. `history.ts`/`projector.ts`
are unchanged from v2.2.3 to v2.2.5, the projector still only rewrites
existing part rows, and every inspected 2.2.4/2.2.5 fixture DB has **0**
rows. The forward risk from the 2.2.3 review stands (Recommendations).
- **Behavior changes checked** (plain-language list in [What users will
notice](#what-users-will-notice)):
- **Image tool** (#1209 contract): `image-generate.ts` is unchanged from
v2.2.3 to v2.2.5. 2.2.4 refactors restart recovery into one
`restartMessage` but keeps `metadata.source:
"experimental_image_generate"`, `jobID`, `state`, `path` and the `<image
id state>` notice text. Design companion, activation, content and image
settings pass natively on 2.2.4.
- **Schedules** (2.2.4,
[#60478](https://github.com/anomalyco/opencode/pull/60478), then
`da87c475a7`): the loop now runs in every server process, including
`opencode serve`, and elects a leader through a `kv` lease
`schedule:leader` (claimed at boot, released when there are no active
schedules). Firing stays opt-in (`experimental.schedules === true`).
CodeNomad uses neither OpenCode schedules nor that key; Missions
recurrence uses its own storage and the Job service.
- **Sessions:** deleting a session now cancels its running background
jobs (shell, image, subagent) and drops their markers (2.2.4). That is
the intended upstream cleanup; CodeNomad's session deletions
(blank-session cleanup, temporary workspaces) get it automatically.
- **Events:** `persistent-pty.updated` and `artifact.updated` reach
CodeNomad's shared event relay. The relay routes generically
(`instance-events.ts`), the 2.1.0 client's SSE reader doesn't validate
types, and the UI handles neither type. The native relay suite passes on
2.2.4.
- **Tools** (2.2.5): a model call to `bash` runs as `shell` with shell
permissions, and the result carries a note
([#60687](https://github.com/anomalyco/opencode/pull/60687)). CodeNomad
already groups and renders `bash` and `shell` alike
(`message-part-grouping.ts:48`, `tool-call/utils.ts`). `read`/`edit`
failure texts are richer
([#60688](https://github.com/anomalyco/opencode/pull/60688),
[#60689](https://github.com/anomalyco/opencode/pull/60689)).
- **Compaction** (2.2.5): the prompt is capped at the context window
when a catalog's input limit is larger
([#60660](https://github.com/anomalyco/opencode/pull/60660)), and
`compaction.buffer` may be `"N%"`. CodeNomad never reads or writes
`compaction.buffer`. LiteLLM context errors now trigger compaction
([#60692](https://github.com/anomalyco/opencode/pull/60692)).
- **Reasoning replay** (2.2.5,
[#60498](https://github.com/anomalyco/opencode/pull/60498)): reasoning
is no longer re-sent as plain text after a model switch, and interrupted
or errored reasoning is dropped from the request. This changes provider
requests only; stored transcripts and `Session.Message.*` are
byte-identical.
- **MCP** (2.2.5): stdio messages over 10 MiB no longer disconnect a
server ([#60694](https://github.com/anomalyco/opencode/pull/60694)), and
an idle Location no longer restarts its MCP servers right after shutting
down ([#60668](https://github.com/anomalyco/opencode/pull/60668)).
- **Providers:** a Console-supplied provider can now be overridden by
user, project and remote config (2.2.4). The Claude Agent provider only
replaces Claude Code's `/login` hint with `claude auth login` in its
errors ([#60592](https://github.com/anomalyco/opencode/pull/60592)); the
guided setup
([#60603](https://github.com/anomalyco/opencode/pull/60603)) is OpenCode
Desktop only. The provider ID (`Provider.ID.claudeAgent`) and its use of
Claude Code's own login are unchanged, so CodeNomad's `claude-agent`
quota reader (#1214) is unaffected.
- **Service lifecycle:** the CLI now adds `127.0.0.1,localhost,::1` to
`NO_PROXY`/`no_proxy` for itself and its children
([#60441](https://github.com/anomalyco/opencode/pull/60441)), so
loopback traffic from daemon-hosted code, CodeNomad's plugins included,
bypasses environment proxies. CodeNomad sets no proxy variables.
- **Code Mode, Forms, permission prompts, media blobs:** no source
changes from v2.2.3 to v2.2.5 (`packages/codemode` only bumps its
version). The Code Mode run-to-completion regression passes on 2.2.4: `{
total: 2000, beforeAwait: 1000, count: 1000 }`.
- **`@opencode/ai`:** one `ai` object for Promise and Effect
([#60516](https://github.com/anomalyco/opencode/pull/60516); left out of
the changelog by
[#60631](https://github.com/anomalyco/opencode/pull/60631)). CodeNomad
doesn't import `@opencode/ai`.
- Desktop/TUI-only changes (desktop terminals on persistent PTYs,
base-branch and worktree shortcuts, quote sending, flat inputs, palette
ordering) don't affect CodeNomad. One indirect effect follows.
- **Worktree deletion:** OpenCode Desktop terminals now live as
persistent PTYs that survive restarts
([#60346](https://github.com/anomalyco/opencode/pull/60346)).
CodeNomad's guard (`routes/worktrees.ts`, "Running persistent PTY blocks
deletion: <id>") will therefore refuse more often for users who also
open terminals in OpenCode Desktop. The behavior is correct, but the
message names the PTY ID rather than the terminal's title.

**Verdict:**

- The **2.1.0 client/plugin pins can stay**. Bumping them would bring
only artifacts, PTY renaming and a config type, none of which CodeNomad
consumes.
- The **2.2.3 minimum has no reason to move**. No consumed HTTP, plugin,
private-service or storage contract broke in 2.2.4 or 2.2.5.
- **Recommend 2.2.5.** It is admitted today through contract
recognition. Recommending it is CodeNomad-side bookkeeping:
`RECOMMENDED_OPENCODE_VERSION` (`runtime-support.ts`), the
reviewed-version check in `contractProfile`
(`compatibility/runtime.ts`), the version texts in docs and help, and
CI's qualification target. This is subject to the full native suite on
the exact 2.2.5 CLI, run separately.
- **No CodeNomad bug found** in 2.2.3 → 2.2.5. Risks: the `session_part`
forward risk (unchanged) and the more frequent worktree-deletion refusal
(UX).

**Not verified here:**
- the full native suite on 2.2.5: only a host-module probe ran natively
on 2.2.5, and everything else for 2.2.5 is source and OpenAPI review
(Appendix I);
- the 2.2.4 recurring Missions journey (`test-recurring-simple-native`
ABCDGELFWQN), which was interrupted by a server restart;
- the `kv` → `job` migration with a real pending background marker: only
the empty case ran, and upstream `job.test.ts` covers the copy;
- whether a `bash` call is stored as `bash` or `shell` (source suggests
the model's name is kept; CodeNomad renders both);
- artifacts, Claude Agent and MCP stdio framing at runtime (source
only);
- macOS/Linux.

---

## What users will notice

**OpenCode 2.2.4**
- **Persistent desktop terminals:** OpenCode Desktop terminals belong to
the session, keep their scrollback across server restarts, stay in sync
with the TUI and other windows, and can be renamed. Renamed terminals
keep their names.
- **Artifacts (opt-in):** with `"experimental": { "artifacts": true }`,
agents can publish, update and serve HTML or static artifacts
(`artifact_create`, `artifact_read`, `artifact_update`,
`artifact_delete`) through a built-in local provider, which stores files
under OpenCode's data folder, or a plugin-provided one.
- **Schedules:** enabled schedules also fire under `opencode serve`, and
only once when several OpenCode processes share a database. They stay
off unless `experimental.schedules` is `true`.
- **Claude Agent:** OpenCode Desktop guides installation, `claude auth
login` and connection checking. Error messages now say `claude auth
login` instead of `/login`.
- **Config:** user, project and remote configuration can override
Console-supplied provider names, limits, variants, settings and headers.
- **Networking:** requests to the local OpenCode service no longer go
through `HTTP(S)_PROXY`.
- **Sessions:** deleting a session also stops its running background
work, such as shells, image generations and subagents.
- **Desktop/TUI:** base-branch shortcut, simpler location popover,
faster Markdown streaming and various fixes.

**OpenCode 2.2.5**
- **Compaction:** `"compaction": { "buffer": "20%" }` keeps a share of
each model's context free; token counts still work. Compaction no longer
overfills models whose catalog input limit exceeds their context window,
and LiteLLM context errors trigger compaction instead of stopping.
- **Tools:** a model calling `bash` gets `shell` (with shell
permissions) instead of an error. `read` names the missing folder, and
`edit` shows the closest lines when `oldString` isn't found.
- **Reasoning:** switching models or resuming an interrupted reply no
longer resends reasoning as plain text, which some providers rejected.
- **MCP:** large stdio messages (over 10 MiB) no longer disconnect a
server, and idle locations don't immediately restart their MCP servers.
- **Desktop/TUI:** worktree and base-branch shortcuts, a shell-mode
toggle, sending a single quote from its popup, and MCP servers removed
from the connect menu.

## Follow-ups (outside this PR)

- Hardening for `session_part` (independent of this upgrade; 2.2.5 still
writes no rows): make the direct `session_message.data.content` readers
and the pruning transaction fail safe when `session_part` has rows for
the target messages.
- Optional UX: when the worktree deletion guard finds a running
persistent PTY, name it by `userTitle ?? title` (2.2.4+) and say it may
be an OpenCode Desktop terminal.
- Any future reader of `compaction.buffer` must accept `"N%"` as well as
a token count.

## Isolated Windows qualification

Base: dev `605820669`; Node 25.2.1 (local; CI runs the pinned 24.20.0);
exact official CLI **2.2.5**; locked client/plugin **2.1.0**;
`CODENOMAD_NATIVE_FIXTURE_PARENT` outside the user profile.

- **Native suites on 2.2.5, all passing:** Code Mode jobs, plugin
runtime, automation, session pruning (history, navigation, media blobs,
proxy, session images), Location/worktrees/event relay, permission
receipts, Git init/degraded, Missions (contract, family control, native,
trajectory) and all 11 recurring journeys, sessions (environment, fork,
aside, interrupt, lifecycle, blank cleanup, compaction isolation, prompt
skills), providers (usage, accounts, account selection), service usage,
web search settings, plugin packages, output limit, Promise-tool
failure, subagent depth, temporary workspaces, Design
companion/activation/content, image settings, history migrations from
2.1.2/2.2.3/2.2.4, shared install of the 2.2.3 minimum.
- **Installer suites after the bump:** upgrade (installed `opencode
v2.2.5`) and legacy replacement (target `2.2.5`) install the new
recommendation and pass.
- **Builds and checks after the change:** server build, native plugin
builds and root typecheck clean; server **2,840 passed, 9 skipped, 1
cancelled** (`index.test.ts` shutdown test timed out under full-suite
load; **4/4** alone); runtime-support/transport/update **35/35**;
browser `opencode-setup` **34/34**; ten-locale help **31/31**.
2026-10-31 01:46:05 +01:00
Pascal André
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.
2026-10-31 00:33:06 +01:00
Pascal André
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.
2026-10-30 23:31:29 +01:00
Pascal André
6a280aa15b
fix(design): keep a retried image provable after a failed attempt under the same name (#1210)
Some checks are pending
Publish help / build (push) Waiting to run
Publish help / deploy (push) Blocked by required conditions
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.
2026-10-30 18:35:34 +01:00
Pascal André
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 `f39642bc4`; Node 25.2.1 (local; CI runs the pinned 24.20.0);
exact official CLI **2.2.3**; locked client/plugin **2.1.0**;
`CODENOMAD_NATIVE_FIXTURE_PARENT` outside the user profile for the
Design fixtures.

- **Before the image fix (recommendation-only change):** every native
suite passed on 2.2.3 except the Design companion end to end, which
passes on 2.2.1 and times out on 2.2.2/2.2.3 at "series 1 generated and
read" (`No tool named "image_generate" is currently available`).
- **After this change, on 2.2.3:** Design companion (3 files, 2 series,
all proven, lineage), Design activation (image tool offered to Design
and Build, default on), Design content, image settings (default on,
Off→On hot reload), session pruning including session images/media, Code
Mode jobs `{ total: 2000, beforeAwait: 1000, count: 1000 }`.
- **Full native set on 2.2.3 (before the image fix, unaffected areas):**
plugin runtime, automation, Location/worktrees/event relay, permission
receipts, Git init/degraded, Missions (contract, family control, native,
trajectory) and all 11 recurring journeys, sessions (environment, fork,
aside, interrupt, lifecycle, blank cleanup, compaction, prompt skills,
providers, service usage, web search, plugin packages, output limit,
Promise-tool failure, subagent depth, temporary workspaces), migrations
from 2.1.2/2.2.1/2.2.2, install/upgrade/legacy replacement.
- **Units and checks after the change:** root and server typecheck
clean; server **2,825 passed, 9 skipped, 1 cancelled** (`index.test.ts`
shutdown test timed out under full-suite load; **4/4** alone); gallery
107/107; Design 23/23; opencode-update 62 passed / 1 skipped; touched UI
units 8/8; browser `opencode-setup` + `websearch-settings` 38/38; native
plugin build 2/2; ten-locale help 31/31.
2026-10-30 15:45:45 +01:00
Pascal André
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).
2026-10-30 09:25:48 +01:00
Pascal André
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.
2026-10-30 07:30:13 +01:00
Pascal André
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.
2026-10-30 06:35:33 +01:00
Pascal André
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).
2026-10-30 05:45:54 +01:00
Pascal André
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`
2026-10-30 05:18:38 +01:00
Pascal André
f3246e27c3
fix(settings): keep Store images once working in projects with their own experimental block (#1200)
Some checks are pending
Publish help / build (push) Waiting to run
Publish help / deploy (push) Blocked by required conditions
## 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`
2026-10-30 03:04:15 +01:00
Pascal André
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.
2026-10-29 16:47:47 +01:00
Pascal André
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.
2026-10-29 16:45:07 +01:00
Pascal André
93ee926ab5
chore(release): prepare CodeNomad 1.0.1 versions (#1189)
## Summary

Prepare **CodeNomad 1.0.1** version metadata on dev `ca514ed2e`.

- Root and all five workspace versions and the lockfile move to
**1.0.1** (only the version fields change in the lock).
- Tauri configuration, Cargo manifest and Cargo lock entry move to
**1.0.1**.
- The separately published remote UI keeps its **1.0.0** minimum server:
a patch release gives no reason to refuse 1.0.0 servers.
- `scripts/test-release-workspace-version.mjs` exercises the 1.0.1
stable and preview family.

## Validation

- `node scripts/test-release-workspace-version.mjs` passes: committed
version alignment, stable `1.0.1`, preview
`1.0.1-dev-20261029-ca514ed2`, offline clean installation and local
remote-tunnel linking.
- `git diff --check` passes.

## Publication boundary

This PR creates no release or tag. Stable publication requires the
separate dev → `main` release PR.
2026-10-29 15:34:47 +01:00
Pascal André
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 `157766206`; Node **25.2.1** (local; CI runs the pinned
24.20.0); exact official CLI **2.2.1**; unchanged locked client/plugin
**2.1.0**.

- **Build and checks:** production server and native plugin builds
(pruning, automation, Missions), root typecheck.
- **Native runtime:** Code Mode job ordering (`{ total: 2000,
beforeAwait: 1000, count: 1000 }` on 2.2.1), plugin runtime, automation;
complete pruning, history, proxy, navigation and media; Location,
worktree families/management and event relay; permission receipts; Git
init/degraded.
- **Missions:** contract, family control, native and trajectory; **all
11 recurring Mission journeys** (`ABCDGELFWQN`).
- **Sessions:** environment, fork, aside, interrupt, lifecycle, blank
cleanup (`--conditions=browser`, as in CI), compaction isolation, prompt
skills, provider usage/accounts/selection, service usage, web search,
plugin packages, output limit, Promise-tool failure, subagent depth,
temporary workspaces.
- **Migrations:** history from **2.1.0, 2.1.2, 2.1.4 and 2.2.0** to
2.2.1.
- **Installation:** fresh install, native upgrade and legacy replacement
fixtures.
- **Units after the recommendation change:**
compatibility/transport/update **35/35**; full server **2,628 passed, 8
skipped, 1 cancelled** (`index.test.ts` shutdown test timed out at 5 s
under full-suite load and passes **4/4** alone); ten-locale help checks
pass.

In CI, `latest` resolves to 2.2.1: the compatibility job runs the native
suites against the minimum and latest on Windows, and the migration and
Git-degraded certification runs them on Ubuntu, Windows and macOS. WSL,
packaged desktop, installers/updater end-to-end and real mobile devices
keep their separate release gates.
2026-10-29 15:04:29 +01:00
bluelovers
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).
2026-10-29 09:49:43 +01:00
Pascal André
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 `681b85486`
(pre-existing, investigated separately).
2026-10-28 20:16:17 +01:00
Pascal André
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 681b85486`)

| Metric | Before | After |
|---|---:|---:|
| Messages scanned | 19,001–22,001, oldest first | 16,384 each run,
newest first |
| Scan duration | 1,503–1,550 ms | 650–739 ms |
| First-row latency | 50–76 ms | 0.8–1.8 ms |
| Longest synchronous step | 70–82 ms | 1.4–1.9 ms |
| Rows fully decoded | 30,000–34,500 | 3,276 |

## Validation

- Superset property checks against the exact mask: 12,547 cases,
including Unicode case crossings, escapes and surrogate pairs.
- Deterministic coverage and streaming latency regressions.
- Cursor tampering, binding and excluded appends; store lifecycle.
- Session-pruning and history routes 155/155; native bundle 1/1; UI unit
44/44.
- Browser: history 3/3; search-highlight 12, plus 1 optional benchmark
skipped.
- Help 9/9; both typechecks.
- Isolated native history fixture on OpenCode 2.2.0: 26 checks.

Oversized file touched: `message-section.tsx` (~1,823 lines).
2026-10-28 19:26:40 +01:00
Pascal André
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.
2026-10-28 15:44:19 +01:00
Pascal André
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).
2026-10-28 14:35:25 +01:00
Pascal André
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.
2026-10-28 13:56:25 +01:00
Pascal André
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.
2026-10-28 12:27:05 +01:00
Pascal André
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.
2026-10-28 11:59:23 +01:00
Pascal André
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.
2026-10-28 10:41:25 +01:00
Pascal André
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.
2026-10-28 10:37:25 +01:00
Pascal André
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.
2026-10-28 10:13:37 +01:00
Pascal André
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.
2026-10-27 21:55:12 +01:00
Pascal André
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).
2026-10-27 21:43:48 +01:00
Pascal André
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.
2026-10-27 19:41:50 +01:00
Pascal André
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.
2026-10-27 15:19:07 +01:00
Pascal André
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**.
2026-10-27 15:03:48 +01:00
Pascal André
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.
2026-10-27 13:03:36 +01:00
Pascal André
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.
2026-10-27 06:15:44 +01:00
Pascal André
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.
2026-10-27 05:05:26 +01:00
heunghingwan
13450749b8
feat(preview): open loopback conversation links in the web preview (#1119)
Some checks are pending
Publish help / build (push) Waiting to run
Publish help / deploy (push) Blocked by required conditions
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.
2026-10-25 15:46:39 +01:00
Pascal André
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.
2026-10-25 10:50:16 +01:00