Repository navigation
fix(artifacts): resolve a linked state root so a relocated Codewhale home keeps working - #6947
Merged
Hmbown merged 6 commits intoOct 10, 2026
Conversation
… keep working When the Codewhale state directory is reached through a junction (the standard way to keep ~/.codewhale on another volume on Windows), the Windows WorkspaceFile opener refuses the root's own reparse component, so every session-artifact write failed with "Fleet artifact path must stay within the workspace". Compact fails outright (the pre-compaction checkpoint write propagates its error), and spillover, background-shell evidence and the workflow journal degrade on the same path. Open the state root through a new open_resolved_root: the root is resolved once, and the existing link-refusing walk then runs, so links below the root are still refused. The Unix opener has always resolved the root; this restores the same behaviour on Windows for the application's own state directory. Caller-supplied workspace roots keep the strict opener. Test: fleet::files::windows_root_link_tests::linked_state_root_resolves_once_and_keeps_the_inner_rule
The Unix opener has always resolved the root inside open_confined, so
open_resolved_root forwards to open there instead of resolving a second time;
the Windows variant keeps the one resolve it needs. The added method took
crates/tui/src/fleet/files.rs from one path_canonicalize site to two, which the
blocking-calls ratchet refuses, so the file's recorded budget is raised to two
with that reason: this is a synchronous, confined file-I/O layer whose callers
already accept the file's blocking work, and the new site lives beside the five
std_fs sites the budget already records.
Verified against the checker's own counting logic: files.rs counts
{thread_sleep: 1, std_fs: 5, path_canonicalize: 2} against a budget of
{1, 6, 2}.
Adversarial review of this PR (SpikeBot 003, finding F1) showed the fix covered writes only. resolve_session_artifact and image_evidence_summary opened through the strict opener, so on a machine whose state root is a junction the writer succeeded and the reader still failed: the compact checkpoint was written and could not be read back through GET /sessions/<id>/artifacts/<artifact_id>, or through the turn-ref route that shares the same resolver. runtime_api::workspace gains open_confined_file_resolved and the two session artifact reads move to it, so a reader follows its writer. The two remaining open_confined_file callers keep the strict opener on purpose: the root in turn_artifacts.rs is canonical_workspace(thread_workspace) and the root in workspace_file_read is the caller's workspace, neither of which is the application's own state directory. session_artifact_absolute_path also documents what it is (finding F3): the recorded spelling for receipts and display, not a path to open, because it keeps the state root as spelled.
Hmbown
approved these changes
Oct 9, 2026
1 task done
Hmbown
enabled auto-merge (squash)
October 9, 2026 21:50
Resolves the one conflict, scripts/check-blocking-calls-budget.json: main re-keyed and tightened the budget; this keeps main's version and applies the PR's single change (crates/tui/src/fleet/files.rs path_canonicalize 1 -> 2). No other file differs from main apart from the PR's own changes. Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Hmbown
pushed a commit
that referenced
this pull request
Oct 11, 2026
…isconnect - The CHANGELOG 0.10.2 section gains Added (`/connect` and `/disconnect`, #6950) and Fixed lines for the slices on this branch: - git 2.43 /undo, shell cancel, the 30 s background notice and the todo_write receipt; - goal usage, pet live rows and crisp sprites, #6931 and /constitution; - the fixes that already landed on main for #6953, #6954, #6955, #6923, #6947 and #6945, plus the Auto-Review guardian budget, skills, MCP reconnect, the pet owner idle exit, tool headers, /jobs and the one-row gap. - Contributors: @LIghtJUNction for #6950, #6947 added to @SparkofSpike, and #6952 added to @Lstarsky0. - crates/tui/CHANGELOG.md was regenerated with scripts/sync-changelog.sh; the tui crate include_str!s it. - docs/features.toml: the plugin-oauth-providers summary names /disconnect next to /connect (111 characters, under the 120 limit). Evidence: ./scripts/release/check-versions.sh --require-dated-release prints "Version state OK: workspace=0.10.2, npm=0.10.2, npm-binary=0.10.2, lockfile in sync" and "Feature release-note receipts OK: 8 linked issue reference(s) checked in v0.10.1..HEAD". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BsXVvReYca2UxzimQtj7Mj
Hmbown
pushed a commit
that referenced
this pull request
Oct 11, 2026
…ibutor page CI on 054b122 failed "Lint & Type Check": web/lib/public-copy.test.ts:167 compares the 0.10.2 CHANGELOG contributor handles with the website credit arrays and the docs/CONTRIBUTORS.md band. The previous commit (49bd217) added @LIghtJUNction (#6950) to the CHANGELOG. The website still listed them under UNRELEASED_CONTRIBUTORS, which RELEASE_CONTRIBUTORS does not include. - web/lib/release-credits.ts moves @LIghtJUNction from UNRELEASED_CONTRIBUTORS to RELEASE_CONTRIBUTORS, as its own comment describes for the release that carries the work. UNRELEASED_CONTRIBUTORS is now empty. - The docs/CONTRIBUTORS.md v0.10.2 band gains the #6950 line, and adds #6947 to SparkofSpike's entry and #6952 to Lstarsky0's, matching the CHANGELOG. Evidence: a python3 re-implementation of the test's three handle extractions gives the same 8 sorted handles for the CHANGELOG, the website arrays and the doc band (@BX166 @LIghtJUNction @Lstarsky0 @SparkofSpike @asto18089 @dajiaohuang @gaord @jayanthvee). `python3 scripts/check-contributor-credit.py`: "Every contributor in the window is credited on all three surfaces." The vitest run itself is CI's job on this PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BsXVvReYca2UxzimQtj7Mj
SparkofSpike
added a commit
to SparkofSpike/CodeWhale
that referenced
this pull request
Oct 27, 2026
A user who relocates application state to another volume cannot save a session at all. Every save fails with the red "cannot save session" notice and the log records persistence write failed what=session:<id> error_kind=InvalidInput error=Fleet artifact path must stay within the workspace `SessionManager::save_session_inner` hydrates approval receipts before it serializes anything, and `ApprovalReceiptStore::log_file` opened `<sessions>/<id>/approval_receipts.jsonl` through the strict `WorkspaceFile::open`. On Windows that opener walks the root component by component and refuses the junction that is the state root, so the save died before producing a document. Storing session receipt data went from plain file I/O (0.10.2) to the confined opener, which is the right direction, but the state root is a root the user owns and may relocate. `open_resolved_root` exists for exactly that (codewhale-hq#6947): it resolves the root once and then runs the same link-refusing walk below it. These call sites now use it: - approval receipt log (the one that blocked saving) - session archive artifact reads and the two directory checks - clipboard image sink, screencapture attachment sink - pet habitat store, descriptor and lifetime lock No boundary is widened: a link below the state root is still refused, and `WorkspaceFile::open` stays the opener for caller-supplied workspace roots. Verified with cargo check -p codewhale-tui (exit 0) and cargo fmt. The 0.10.2 binary with the same junction layout reproduces the failure, so this is not a regression from an unreleased branch.
This was referenced Oct 27, 2026
Hmbown
added a commit
that referenced
this pull request
Oct 27, 2026
…7097) * fix(tui): resolve the state root before opening session-side state A user who relocates application state to another volume cannot save a session at all. Every save fails with the red "cannot save session" notice and the log records persistence write failed what=session:<id> error_kind=InvalidInput error=Fleet artifact path must stay within the workspace `SessionManager::save_session_inner` hydrates approval receipts before it serializes anything, and `ApprovalReceiptStore::log_file` opened `<sessions>/<id>/approval_receipts.jsonl` through the strict `WorkspaceFile::open`. On Windows that opener walks the root component by component and refuses the junction that is the state root, so the save died before producing a document. Storing session receipt data went from plain file I/O (0.10.2) to the confined opener, which is the right direction, but the state root is a root the user owns and may relocate. `open_resolved_root` exists for exactly that (#6947): it resolves the root once and then runs the same link-refusing walk below it. These call sites now use it: - approval receipt log (the one that blocked saving) - session archive artifact reads and the two directory checks - clipboard image sink, screencapture attachment sink - pet habitat store, descriptor and lifetime lock No boundary is widened: a link below the state root is still refused, and `WorkspaceFile::open` stays the opener for caller-supplied workspace roots. Verified with cargo check -p codewhale-tui (exit 0) and cargo fmt. The 0.10.2 binary with the same junction layout reproduces the failure, so this is not a regression from an unreleased branch. * fix(tui): keep the strict root rule on the workspace fallback The review of the previous commit found that two of the converted call sites do not always hold the state root. `clipboard_images_dir_for_home` and `screenshot_stabilization_dir` fall back to a directory under the workspace when no home directory resolves, so `dir.parent()` is then a caller-supplied workspace root — and resolving it would have widened the rule that a link at the workspace root is refused. Both sites now pick the opener from the root they actually hold: the resolved opener only for `<home>/.codewhale`, the strict opener otherwise. The normal path (a home directory exists) keeps the junction fix; the narrow fallback keeps the boundary it had. --------- Co-authored-by: Hunter Bown <hmbown@gmail.com>
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.
Summary
On Windows, when the Codewhale state directory is reached through a junction —
the standard way to keep
%USERPROFILE%\.codewhaleon another volume — everysession-artifact write fails with:
/compactfails outright, because it persists a pre-compaction checkpoint asits first step (
compact_messages_safepropagates that write's error with?).Spillover saving, background-shell evidence, tool-media storage and the
workflow journal degrade on the same path, and the Runtime API cannot read an
artifact back.
Cause
The strict opener in
crates/tui/src/fleet/files.rs(Windows) walks everycomponent of the root path and refuses any reparse point — including the
components of the state root itself.
resolve_state_dir("sessions")returns
%USERPROFILE%\.codewhale\sessionsin the original spelling; when.codewhaleis a junction,WindowsDirectory::open_innerrefuses that verycomponent and
invalid_path()reports the Fleet-artifact message.v0.10.0 resolved the root first (
workspace.canonicalize()), and the Unixopener still does; the v0.10.1 Windows rewrite dropped that resolution for
every caller. The state root is the application's own directory, not a
caller-supplied workspace: a user who relocates it behind a link is placing
their state, not widening a trust boundary.
Changes
WorkspaceFile::open_resolved_root(new): resolves the root once(
canonicalize) and then runs the existing link-refusing walk. It exists forthe application's own state directory, which the user may have relocated
behind a link; a caller-supplied workspace root must keep using
open. Thedoc comment says so on every platform. On Unix it forwards to
open, whoseconfined opener already resolves the root — no second resolve and no new
blocking-call site.
crates/tui/src/artifacts.rs:open_session_relativeopens throughopen_resolved_root.crates/tui/src/runtime_api/:workspace.rsgainsopen_confined_file_resolved, and the two session-artifact read routes(
resolve_session_artifact,image_evidence_summary) move to it. Withoutthis the writer succeeded and the reader still failed on a relocated root:
the compact checkpoint was written and could not be read back through
GET /sessions/<id>/artifacts/<artifact_id>or the turn-ref route thatshares the same resolver. The two remaining
open_confined_filecallerskeep the strict opener on purpose — their roots are
canonical_workspace(thread_workspace)and the caller's workspace, neitherof which is the application's state directory.
the state directory, and
open— used by Fleet workspaces, delivery files,the LSP root and the Runtime API workspace reads — is unchanged.
session_artifact_absolute_pathdocuments what it is: the recordedspelling for receipts and display, not a path to open, because it keeps the
state root as spelled.
scripts/check-blocking-calls-budget.json:crates/tui/src/fleet/files.rsmoves from one
path_canonicalizesite to two, with the reason below. TheUnix variant adds no site.
Evidence
Reproduced before the fix on 0.10.1 (Windows). A component-by-component probe
of both spellings shows the exact split:
New Windows regression test
(
fleet::files::windows_root_link_tests::linked_state_root_resolves_once_and_keeps_the_inner_rule)creates a real junction and pins both directions: the strict opener must keep
refusing a linked root,
open_resolved_rootmust write through to the realtarget, and a link below the resolved root must still be refused.
Not run here: the repository's own gates. The change is Windows-only in
effect; the pushed branch is the design under review, and CI is the gate.
Blocking-call budget
The file's recorded
path_canonicalizebudget is raised from one to two. Thisis a synchronous, confined file-I/O layer whose callers already accept the
file's blocking work, and the new site sits beside the five
std_fssites thebudget already records. An adversarial review of the change independently
re-verified the count with the checker's own logic: base has one site, the new
method adds one, and the inline test module contributes none.
Type of Change
Related Issues
No-Issue: observed in an operator session on 0.10.1 (Windows); the state
directory relocated behind a junction makes every session-artifact write fail.