Repository navigation
feat(core): record when a session's latest execution started - #59910
Conversation
There was a problem hiding this comment.
The change works as described. I ran OpenCode from source with the simulated model and read the session back from /api/session. On the base, time has no busy field. With this PR, time.busy is set between created and idle, and time.updated is unchanged. The related core and client tests pass. Client and schema type-check cleanly; core's errors match the base.
Public API: one addition, an optional time.busy (epoch millis) on Session.Info. It appears in the HTTP API and OpenAPI spec, the generated client types and @opencode/schema. Storage gets a new nullable session_v2.time_busy column with a migration, and the export/import format carries the new field. All of this is backwards compatible: existing clients just ignore the field, and older exports import without it.
One concern, on the projector's "keep the original start" condition (inline): it is tied to time_idle ordering instead of the execution claim, so it can leave a stale start on child sessions whose claim is released without a terminal event.
| and( | ||
| eq(SessionTable.id, event.data.sessionID), | ||
| // A turn resumed after shutdown or a crash never reached idle, so it keeps its original start. | ||
| or(isNull(SessionTable.time_busy), gte(SessionTable.time_idle, SessionTable.time_busy)), |
There was a problem hiding this comment.
This treats "time_idle is older than time_busy" as "this is a resumed turn", but that only matches the claim rule if every claimed turn ends with a non-shutdown terminal event. SessionRestart.resumeSuspendedSessions → store.releaseChildClaims(children) clears the claims of child sessions that won't be recovered, and no terminal event is published for them. Their last event was Interrupted { reason: "shutdown" }, which doesn't set time_idle. If such a session runs again later, this guard keeps the old time_busy, so both now - busy and idle - busy report the time since the abandoned turn. The PR description says it follows the same rule as the execution claim. To match that, the reset could key off whether the session still holds a claim (time_suspended IS NULL at start, i.e. a fresh claim) instead of idle >= busy, or the claim release could clear or adjust time_busy.
There was a problem hiding this comment.
The new commits fix my earlier point: every execution start now resets time.busy, including a resume after a restart, and the migration backfill is gone. Child sessions can no longer keep a stale start time. As the description says, a resumed turn doesn't count time spent before the crash or shutdown. I re-ran it from source with the simulated model: time.busy falls between created and idle, and updated is unchanged. The projector, migration and import/export tests pass, and core's type errors match the base.
Public API is the same as before: one addition, an optional time.busy on Session.Info, in the HTTP API and OpenAPI spec, the SDK types and the export format. It is backwards compatible.
No further concerns.
51bf7bd to
37edb65
Compare
There was a problem hiding this comment.
The new push is a rebase onto the latest dev. The migration was renamed to run after the new schedule migration, and the generated types now match the experimental import route. My earlier point stays fixed: every execution start resets time.busy. I re-ran it from source with the simulated model: time.busy falls between created and idle, and updated is unchanged. The core projector, migration and import/export tests and the client solid-data tests pass.
Public API is unchanged since my last review: one addition, an optional time.busy on Session.Info, in the HTTP API and OpenAPI spec, the SDK types and the export format. It is backwards compatible.
No further concerns.
37edb65 to
d12631e
Compare
There was a problem hiding this comment.
The new push is a rebase onto the latest dev. The migration is now ordered after the new session_part migration, and the change itself is the same as before. My earlier point stays fixed. I re-ran it from source with the simulated model: time.busy falls between created and idle, and updated is unchanged. The core projector, migration and import/export tests and the client solid-data tests pass.
Public API is unchanged since my last review: one addition, an optional time.busy on Session.Info, in the HTTP API and OpenAPI spec, the SDK types and the export format. It is backwards compatible.
No further concerns.
…ct (#1209) ## Summary - OpenCode **2.2.3** becomes both the **technical minimum** and the **recommended** runtime. The minimum moves because upstream [#60258](anomalyco/opencode#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](anomalyco/opencode#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](anomalyco/opencode#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](anomalyco/opencode#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](anomalyco/opencode#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](anomalyco/opencode#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](anomalyco/opencode#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](anomalyco/opencode#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](anomalyco/opencode#60480)), and progress is throttled to at most one update per 100 ms ([#60120](anomalyco/opencode#60120)). Programs see the same failure text as direct calls ([#60109](anomalyco/opencode#60109)), `fetch` in Code Mode is now subject to `webfetch` permissions ([#59954](anomalyco/opencode#59954)), `for…in` visits inherited keys ([#59933](anomalyco/opencode#59933)) and `null` survives text encoding ([#57639](anomalyco/opencode#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](anomalyco/opencode#60102)). Shell permission checks moved fully off tree-sitter and now check redirects, PowerShell method calls and dynamic directory changes ([#60332](anomalyco/opencode#60332), [#60376](anomalyco/opencode#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](anomalyco/opencode#60322)), and a persistent terminal with a missing cwd is rejected ([#60300](anomalyco/opencode#60300)). Forked and imported sessions store media as blobs when `media_blob` is on ([#60118](anomalyco/opencode#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](anomalyco/opencode#58782)). Plugin-supplied model variants are preserved ([#60097](anomalyco/opencode#60097)), and the dynamic provider npm fallback runs after external SDK hooks ([#60380](anomalyco/opencode#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](anomalyco/opencode#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](anomalyco/opencode#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](anomalyco/opencode#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](anomalyco/opencode#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](anomalyco/opencode#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.
Adds
time.busytoSession.Info: when the session's most recent execution started. With the existingtime.idle(when it ended), clients can show how long a session has been working without relying ontime.updated, which moves on every streamed part.now - time.busy(while it runs,time.idlestill holds the previous execution's end, sobusy > idle, oridleis absent)time.idle - time.busyDetails
session.execution.startedinto a newsession_v2.time_busycolumn. It doesn't touchtime_updated, so session ordering is unchanged.session.execution.startedresets it, including a resume after shutdown or a crash, so working time never includes server downtime.busyand clamps it toidle, the same wayviewedis handled.session.execution.started(terminals already do this), sotime.busyis current while a turn runs.Testing
packages/core:session-projector,session-create(import/export),v1-migration,database-migration,session-view,session-owned,session-executionpackages/client:solid-data,solid-pending-statusRequested by: @Brendonovich (Brendan via Slack)
Added by the opencode bot (AI-generated).
Public API: adds optional
time.busyto Session.Info in the HTTP API, OpenAPI spec and SDK types (backwards compatible)