Repository navigation
fix(core): keep locations alive while sessions are executing (24h cap) - #57274
Merged
Merged
Conversation
LocationActivity only refreshed a location's deadline on durable session events. A session blocked in one long tool call (a shell waiting on a build) or on an unanswered question emits none, so after the 60 minute TTL the sweep interrupted it with reason "inactivity" and evicted its services. Count active execution as activity instead: every sweep refreshes the deadline of each location an active session belongs to, and only locations without active owners expire. The deadline restarts when the session settles.
Active executions keep their location alive, but only while a durable session event has arrived within maxActiveHold (default 24 hours). A location held that long in silence falls back to #47629's behaviour: interrupt its sessions with reason "inactivity", await settlement, then evict.
1 task
pascalandr
added a commit
to NeuralNomadsAI/CodeNomad
that referenced
this pull request
Oct 29, 2026
## 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](anomalyco/opencode#58997)). - `GET /api/agent` can now answer 503 while plugins settle ([#59082](anomalyco/opencode#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](anomalyco/opencode#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](anomalyco/opencode#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](anomalyco/opencode#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](anomalyco/opencode#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](anomalyco/opencode#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](anomalyco/opencode#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](anomalyco/opencode#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](anomalyco/opencode#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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue for this PR
No issue filed. Found on Kit's Studio server running OpenCode Beta.
Type of change
What does this PR do?
Symptom: subagents get cancelled after about 60 minutes when they're blocked in a single
shellcall (for example, waiting on a long build). The parent session gets no notice. The execution ends asInterrupted { reason: "inactivity" }, and the server log shows the location being torn down:One server logged 270 of these lines in a single day. At least 11 subagents were killed mid-work this way.
Root cause:
LocationActivityonly refreshes a location's deadline on durableSessionEvents. A session that's blocked in one long tool call, or waiting on an unanswered question, emits none. After the 60 minute TTL, the sweep sees the location as expired. It callsexecution.interrupt(id, { reason: "inactivity", awaitSettlement: true })for every active session there and then invalidates the location. That ordering came from #47629, which fixed the detached-graph bug from #47626 by interrupting.Fix: count active execution as activity, up to a safety cap. On each sweep, refresh the deadline of every cached location that has an active session (
execution.active→sessions.get→ location key). Locations with no active session expire after the usual 60 minutes.Safety cap (
maxActiveHold, default 24 hours): each entry tracks its last durable session event (activeAt) separately from its keep-alive deadline (expiresAt). Keep-alive never pushes the deadline pastactiveAt + maxActiveHold. A location that active sessions have held for 24 hours with no durable event falls back to #47629's behaviour: interrupt its sessions (reason: "inactivity",awaitSettlement), wait for them to settle, re-check for work admitted during cleanup, then evict. Any durable event resets the clock, so a long session that's still emitting events never hits the cap.Form.Servicethe waiting tool holds. That's the case fix(core): retain locations while sessions are executing #47626 was about, now handled by retention instead of interruption.Tradeoff: before this change, any session silent for 60 minutes was interrupted. Now a silent session keeps its location (and its MCP, browser and form services) for up to 24 hours. That's more memory and processes held for stuck sessions, in exchange for no longer killing ordinary long tool calls and questions left unanswered. The cap keeps #47629's guarantee that nothing lives forever. A truly hung session is still interrupted and its location evicted, just after 24 silent hours instead of 1.
Other idle sweepers checked (none changed):
Workspaceidle suspension (workspace.ts, 20 min) already skips connections withactive > 0(each spawn holds a ref), so it isn't affected by this bug.model-transportIDLE_TIMEOUT(30 min) is a provider WebSocket read timeout, not a sweep over executions.Formcache keeps pending forms with an infinite TTL.Project.sweeparchives projects with no recent activity. It doesn't touch executions.browser-idle.test.ts). They're now retained while a session is active.How did you verify your code works?
packages/core/test/location-activity.test.tsuses real execution ownership, location caching and forms, onTestClock:Form.Serviceis the same instance the tools hold, and answering completes each session. After the TTL the location is evicted.reason: "inactivity", its forms are cancelled, slow cleanup still owns the graph, and then it's evicted. Work admitted during cleanup keeps the location, as before.browser-idleandlocation-layer.The 7 failures are existing
ShellTool/real-shellcompound-syntax tests (tool-shell.test.ts,shell-scan/*). They fail the same way on the unmodified base on this macOS machine and are unrelated to this change.Screenshots / recordings
Not a UI change.
Checklist