You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Stop re-deriving the devpod workspace id. Persist the id dl actually created and address devpod by that, then reconcile the workspaces already orphaned.
Re-scoped. This ticket previously proposed giving run_devpod an explicit cwd. That is retracted — see "Retracted premises" at the bottom. The cause below is observed on the host, not hypothesised.
Root cause
PR #81 (fix/64-workspace-id-parse-boundary — commits 5db5c42, 3875246) changed both the workspace id and the clone-directory leaf from <branch> to <repo-slug>-<branch-slug>-<8-char syllable suffix>. It migrated dl's own metadata and left devpod's records pointing at the old paths.
The metadata record kept its original created_at (2026-03-01T18:39:40, matching devpod's creationTimestamp) while carrying the new id and path — the same record, rewritten in place. devpod's record was never touched.
Why the failure looks like a missing hook
The old devlaunch/main checkout is gone. On recreate, devpod finds its recorded source folder absent and reconstitutes it from cached workspace_result.json, which contains exactly:
That is byte-for-byte the shape of the lone devlaunch/main/.devcontainer/devcontainer.json left on disk. devpod then chdirs into that folder and runs the hook — but init-host.sh is repo content, not devcontainer config, so it was never restored. Absent → the reported error. Fresh create works because it clones to the new path and registers a workspace whose source matches.
Scale
36 of 39 devpod workspaces on the reporting host have a recorded source folder that is missing (35) or a config-only stub (1). Affects bencher, wayfinder, python_template, rockerc, lifetime-foc-rig, scratch_env, devlaunch. Only three are healthy — all created after the scheme change.
1. Follow the record, don't re-derive (the durable fix)
WorktreeInfo.devpod_workspace_id already exists (worktree/models.py:71) and is never assigned anywhere — grep it: one declaration, zero writers. Had dl stored the devpod workspace id it created and used that to address devpod, changing the derivation would have been harmless.
This is the same lesson the codebase already learned for the path. From remove_workspace_by_id's own docstring:
Following the record matters because the derivation has changed: every workspace created before the current id scheme has a bare branch name as its clone-directory leaf. Re-deriving the leaf here looked for a directory that never existed […] The stored path makes old and new workspaces both removable with no migration.
Exactly that, for the id. Persist devpod_workspace_id when a workspace is created, and have every devpod-addressing path (up, ssh, stop, rm, recreate/restart/reset) use the stored id, falling back to derivation only when there is no record.
2. Reconcile what is already orphaned
A derivation fix does not repair existing state — the 36 broken records predate it. Needs a one-time reconciliation that, per devpod workspace whose recorded source is missing or has no .git:
if a metadata record for the same (owner, repo, branch) points at a healthy clone, adopt it: rewrite devpod's source.localFolder and populate devpod_workspace_id;
if no healthy clone exists, say so and let the user drop the workspace — do not silently delete.
Decide deliberately whether this is dl migrate, dl prune --repair, or automatic on startup, and whether writing ~/.devpod/.../workspace.json directly is acceptable versus driving it through devpod CLI. Containers bind-mount the dead source path, so any repaired workspace needs a rebuild regardless — the reconciliation should say that rather than implying the container is reusable.
Must be idempotent, and must cope with metadata that has already been migrated to the new scheme while devpod's has not (the current host state).
3. Version skew
Installed dl on the reporting host is 0.0.9, which still computes branch-name paths, while a main-based run already rewrote metadata.json to the new scheme. Note explicitly what happens when an older dl meets migrated metadata. PR #86 (Release 0.0.10) is what ships PR #81 — the migration should land before or with it, not after.
Done when
A test asserts devpod_workspace_id is persisted on create and that devpod-addressing paths use the stored id in preference to a derived one.
A test asserts a workspace created under the old id/path scheme is still addressable after the derivation changes — i.e. the regression that caused this cannot recur. Simulate the scheme change; don't hardcode today's suffix.
A test covers reconciliation: orphaned record + healthy clone → adopted; orphaned record + no clone → reported, not deleted; running it twice changes nothing.
pixi run ci green.
Verification honesty
The root cause above is observed from on-host state (devpod list, metadata.json, workspace.json, workspace_result.json) — not inferred. What cannot be verified in a session without a Docker daemon is that a repaired workspace then recreates successfully end to end. That last step is the human's, on the host: dl blooop/devlaunch recreate.
Retracted premises
Recorded so nobody re-treads them:
cwd on the devpod spawn is not the fix. devpod chdirs into the workspace's recorded source folder before running initializeCommand, identically whether the workspace is addressed by path or by id. dl's working directory never reaches the hook. Verified by running devpod v0.26.1 for real against a throwaway project with the same bare relative hook — no Docker needed, since initializeCommand fires before any container exists.
The original ticket's supporting facts were read off a stale branch. At dl.py:1367-1370 on main, when the workspace already exists, workspace_spec is a bare workspace id, not a clone path, and custom_id is None. The proposed cwd=workspace_spec would have handed subprocess a nonexistent directory named after a workspace id — on exactly the three subcommands it targeted.
"The fast path never calls ensure_workspace" is also not the cause. It is true that if workspace_id in existing_ids skips clone_mgr.ensure_workspace(), but adding a clone check there would inspect the new path and find it healthy, while devpod keeps using the old one. No check about the clone catches a disagreement over where the workspace lives.
recreate/restart fail on a relative initializeCommand #87's "not a stale clone" was right for the wrong reason. It ruled this out on repo-content grounds — correct, there is no commit where the initializeCommand reference exists without the script. But the folder devpod runs in is not a checkout at all; it is a config-only stub devpod itself wrote from cache. Content-level reasoning could not see it.
Work
Stop re-deriving the devpod workspace id. Persist the id
dlactually created and address devpod by that, then reconcile the workspaces already orphaned.Root cause
PR #81 (
fix/64-workspace-id-parse-boundary— commits5db5c42,3875246) changed both the workspace id and the clone-directory leaf from<branch>to<repo-slug>-<branch-slug>-<8-char syllable suffix>. It migrateddl's own metadata and left devpod's records pointing at the old paths.For
devlaunch-mainon the reporting host:~/.cache/devlaunch/metadata.json…/repos/blooop/devlaunch/devlaunch-main-zovomoboinit-host.shpresent~/.devpod/contexts/default/workspaces/devlaunch-main/workspace.json…/repos/blooop/devlaunch/main.gitThe metadata record kept its original
created_at(2026-03-01T18:39:40, matching devpod'screationTimestamp) while carrying the new id and path — the same record, rewritten in place. devpod's record was never touched.Why the failure looks like a missing hook
The old
devlaunch/maincheckout is gone. On recreate, devpod finds its recorded source folder absent and reconstitutes it from cachedworkspace_result.json, which contains exactly:{"name":"devlaunch","initializeCommand":{"":[".devcontainer/claude-code/init-host.sh"]}}That is byte-for-byte the shape of the lone
devlaunch/main/.devcontainer/devcontainer.jsonleft on disk. devpod then chdirs into that folder and runs the hook — butinit-host.shis repo content, not devcontainer config, so it was never restored. Absent → the reported error. Fresh create works because it clones to the new path and registers a workspace whose source matches.Scale
36 of 39 devpod workspaces on the reporting host have a recorded source folder that is missing (35) or a config-only stub (1). Affects
bencher,wayfinder,python_template,rockerc,lifetime-foc-rig,scratch_env,devlaunch. Only three are healthy — all created after the scheme change.Diagnostic, for anyone reproducing:
The change
1. Follow the record, don't re-derive (the durable fix)
WorktreeInfo.devpod_workspace_idalready exists (worktree/models.py:71) and is never assigned anywhere — grep it: one declaration, zero writers. Haddlstored the devpod workspace id it created and used that to address devpod, changing the derivation would have been harmless.This is the same lesson the codebase already learned for the path. From
remove_workspace_by_id's own docstring:Exactly that, for the id. Persist
devpod_workspace_idwhen a workspace is created, and have every devpod-addressing path (up,ssh,stop,rm, recreate/restart/reset) use the stored id, falling back to derivation only when there is no record.2. Reconcile what is already orphaned
A derivation fix does not repair existing state — the 36 broken records predate it. Needs a one-time reconciliation that, per devpod workspace whose recorded source is missing or has no
.git:(owner, repo, branch)points at a healthy clone, adopt it: rewrite devpod'ssource.localFolderand populatedevpod_workspace_id;Decide deliberately whether this is
dl migrate,dl prune --repair, or automatic on startup, and whether writing~/.devpod/.../workspace.jsondirectly is acceptable versus driving it throughdevpodCLI. Containers bind-mount the dead source path, so any repaired workspace needs a rebuild regardless — the reconciliation should say that rather than implying the container is reusable.Must be idempotent, and must cope with metadata that has already been migrated to the new scheme while devpod's has not (the current host state).
3. Version skew
Installed
dlon the reporting host is 0.0.9, which still computes branch-name paths, while amain-based run already rewrotemetadata.jsonto the new scheme. Note explicitly what happens when an olderdlmeets migrated metadata. PR #86 (Release 0.0.10) is what ships PR #81 — the migration should land before or with it, not after.Done when
devpod_workspace_idis persisted on create and that devpod-addressing paths use the stored id in preference to a derived one.pixi run cigreen.Verification honesty
The root cause above is observed from on-host state (
devpod list,metadata.json,workspace.json,workspace_result.json) — not inferred. What cannot be verified in a session without a Docker daemon is that a repaired workspace then recreates successfully end to end. That last step is the human's, on the host:dl blooop/devlaunch recreate.Retracted premises
Recorded so nobody re-treads them:
cwdon the devpod spawn is not the fix. devpod chdirs into the workspace's recorded source folder before runninginitializeCommand, identically whether the workspace is addressed by path or by id.dl's working directory never reaches the hook. Verified by running devpod v0.26.1 for real against a throwaway project with the same bare relative hook — no Docker needed, sinceinitializeCommandfires before any container exists.dl.py:1367-1370onmain, when the workspace already exists,workspace_specis a bare workspace id, not a clone path, andcustom_idisNone. The proposedcwd=workspace_specwould have handed subprocess a nonexistent directory named after a workspace id — on exactly the three subcommands it targeted.ensure_workspace" is also not the cause. It is true thatif workspace_id in existing_idsskipsclone_mgr.ensure_workspace(), but adding a clone check there would inspect the new path and find it healthy, while devpod keeps using the old one. No check about the clone catches a disagreement over where the workspace lives.initializeCommandreference exists without the script. But the folder devpod runs in is not a checkout at all; it is a config-only stub devpod itself wrote from cache. Content-level reasoning could not see it.