Repository navigation
Description
Problem
The Workspace service's idle janitor only ever sees workspaces that the current process has connected to. After a restart, persisted workspace rows with live bindings are invisible to it — nothing at boot reconciles them — so their backing resources are never idle-suspended (and the promised ~23h safety snapshot rotation does not exist at all) until someone happens to provision/connect that exact workspace ID again.
The code itself promises this in a later PR that has not landed:
packages/core/src/workspace.ts:358-359 (at main a6bc4dca06faa872d83f1b65b00c4f7cd6dbd6a5):
// TODO(workspace-plan): add the boot janitor and ~23h safety snapshot rotation in a later PR.
// TODO(workspace-plan): make cold wake interruptible with a re-pin loop against janitor races.
Evidence (verified this run, pinned reads at a6bc4dca)
- The janitor loop (
workspace.ts:190-222, repeated onSchedule.spaced(pollInterval ?? 1 minute)at:222) iterates only[...connections.entries()](:194) — the in-memoryconnectionsmap, which is empty at process start and only gains entries viaopen()on provision/connect. - Every
WorkspaceTableaccess in the tree is by-ID (find:96,saveBinding:105, updates:184/:211, create select:228-230, insert:244-247, delete:347); there is no scan of persisted rows anywhere, and no boot-time reconciliation pass in the layer. grep -rn "janitor" packages/ --include="*.ts"returns only the two TODO lines;grep -rn "snapshot" packages/core/src/workspace/ packages/core/src/environment/returns nothing — the safety snapshot rotation is entirely unimplemented, not just unscheduled.packages/core/test/workspace.test.ts(33 tests) pins the in-process lifecycle — including "persists the workspace lifecycle and reconnects after idle suspension" (:330) with a fake driver recordingsuspendForIdle— but has no boot/restart scenario: no test builds the layer over pre-existing persisted rows (grep for boot/restart/reconcile in the test file: no matches).
Why it matters
Workspaces back real (potentially billed / resource-holding) driver resources. A server restart — deploy, crash, upgrade — silently orphans every persisted, bound workspace that nobody reconnects to: it is never suspended for idleness, never snapshotted, and its row's last_used_at goes stale with no process responsible for it. The longer the deployment runs between restarts, the more this looks like a slow resource leak that only manual destroy calls can clean up. The TODO's second line compounds it: cold wake is not interruptible against janitor races, so a boot janitor added naively could race wakes — the two TODOs need to be designed together.
Acceptance criteria
- On layer startup (boot), the Workspace service scans persisted
WorkspaceTablerows and reconciles them: rows with a binding whoselast_used_atis beyond the idle threshold are driven through the samesuspendForIdlepath the poll loop uses (respecting the per-workspace lock and the deliberate racing-spawn semantics documented atworkspace.ts:206); rows with no binding are left untouched. - Boot reconciliation is safe against a concurrent provision/connect of the same workspace (no double-suspend, no suspend of an active connection) — the cold-wake re-pin concern in the
:359TODO is addressed or explicitly documented as out of scope with reasoning. - The ~23h safety snapshot rotation from the
:358TODO is either implemented (driver capability + rotation cadence + retention) or split into its own follow-up issue with the TODO updated to point at it — the TODO must not silently survive another "later PR". - New tests in
packages/core/test/workspace.test.tsstyle (fake driver, persisted rows seeded before layer construction): (a) a stale bound row is suspended at boot without any connect; (b) a fresh bound row is left alone; (c) a boot scan racing a connect does not suspend the active workspace. - Existing workspace tests still pass.
Dedup / related
gh search issuesfor "janitor", "boot janitor", "safety snapshot", "workspace-plan": no matches in any state. "workspace idle suspend" returns only fix(console): docs, share and enterprise proxy routes fetch upstream with no timeout — a stalled upstream hangs the console request forever #1110 (server fs endpoints auth, unrelated) and Migrate the fleet to OpenCode 2 #374 (local-first epic — possible parent, but it names no janitor work; link as related if the epic owner wants it attached natively).- Filed by new-issue-creator-opencode, 2026-11-04 run; verification in a detached laptop probe worktree at
dc3613c185plus pinnedgh apireads ata6bc4dca(the comparedc3613c185...a6bc4dcatouches onlysession/runner/step.ts+ its test, so the workspace evidence is unchanged at current main).
Activity
developing-today commentedon Nov 4, 2026
OPENCODE-254
dezren39 commentedon Nov 4, 2026
🤖 Agent note · filed
dezren39 commentedon Nov 4, 2026
agent-note:v1
agent: repo-queue-worker-shift-opencode (lane opencode-a)
task: 9524
state: done (triaged)
Triage CONFIRMED on fresh main (workspace evidence unchanged at a6bc4dc): workspace.ts:358 TODO verbatim; the janitor loop (:190-222) iterates only the in-memory connections map (:194, empty at boot) and suspendForIdle (:205) hangs off a live connection; every WorkspaceTable access is by-ID; no boot scan exists; the safety snapshot rotation is entirely unimplemented (no snapshot code in workspace/ or environment/). workspace.test.ts pins the in-process lifecycle but has no boot/restart scenario.
Follow-up: fix task 9526 enqueued (packet hidden_files/1622-fix-packet-9524.json) covering boot reconciliation through the poll loop's suspend semantics, boot/connect race safety, the (a)(b)(c) test shapes from this issue's acceptance, and splitting the snapshot rotation into its own issue with the TODO repointed.
3 remaining items
dezren39 commentedon Nov 4, 2026
Parked by Lane A (task 9526): another session holds the work — its 13:06Z pickup note and implementation commit
37312eabfeon branchqueue/9526-1622-workspace-boot-janitor(workspace.ts + workspace.test.ts, pushed 13:12Z) — but no PR is open yet and the queue claim had lapsed when this lane claimed the task. Parked 30 minutes naming that branch as the conflicting artifact; no parallel work was done. Resume brief on task 9526: shepherd the owner's PR if one appears; otherwise salvage the existing branch (test it, open the PR from it) rather than reimplementing.Merge pull request #1631 from developing-today-forks/queue/9526-1622-…