Repository navigation
Harden Windows per-loop terminal layout cleanup (kill safety, restore retry, sweep, data safety) - #682
Merged
Conversation
coneilen
force-pushed
the
coneilen-harden-layout-cleanup-safety
branch
4 times, most recently
from
October 10, 2026 08:13
7d92918 to
fbdbb5a
Compare
Only end a shell session this shell provably made (graphcode- plus a lowercase uuid, no node's id, no other layout's pane) and a fresh zmx ls shows running; refuse and log anything else instead of killing it. Retry saved shell tabs after a failed listing and on reopening the open loop. Require two agreeing non-empty snapshots, and layouts older than this shell, before sweeping deleted loops. Keep unusable layouts as .bad, write layouts atomically with a per-writer temporary file. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens <coneilen@microsoft.com>
Name shape is not provenance: record each tab and split session the shell mints in shell-sessions beside the layouts and end only recorded, live sessions. A layout scan that is cut short or hits an unreadable file now refuses the kill instead of assuming nothing claims the session, and a corrupt layout that cannot be set aside fails the open instead of being overwritten. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens <coneilen@microsoft.com>
… unreadable scans The layout root is shared by every GraphCode shell of the user, so a record must name the loop it was minted for and only that loop's retirement may use it. New Tab and Split save the layout claim before the record is written and the session started. A scan that could not read every layout now defers the cleanup (layout kept) instead of setting it aside. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens <coneilen@microsoft.com>
…t observes it The attach stand-in copied the loop layout on every attach, which could hold the file open while the shell renamed a new layout over it and made a later save fail intermittently. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens <coneilen@microsoft.com>
coneilen
force-pushed
the
coneilen-harden-layout-cleanup-safety
branch
from
October 10, 2026 08:32
fbdbb5a to
e2aec1b
Compare
3 of 5 tasks
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
Hardens the per-loop terminal layout cleanup that #674 added, after independent reviews found it could end sessions it did not own and could lose or skip saved tabs. Windows shell only (
graphcode-windows/src); nothing under macOS orGraphcodeKitchanges.Every review finding was checked against the code first and was real:
retireLoopkilled everyagent:falsepane with no validation and no listing; a failedzmx lsdropped shell tabs without any retry andscopeToLoopreturned early for the open loop; the first-snapshot sweep acted on a single, possibly empty or partial snapshot and was only tested by callingendDeletedLoopsdirectly; and a corrupt or unknown-schema layout was overwritten by the next save.Changes
graphcode-plus a lowercase 8-4-4-4-12 uuid, so agent sessions (uppercase uuid), foreign prefixes and unprefixed names are excluded; (2) a GraphCode shell recorded it in the shared support root for that loop: a marker file named for the session undershell-sessionsbeside the layouts, whose content is the loop it was minted for, and which only that loop's retirement (or closing that loop's pane) accepts. New Tab and Split save the layout that claims the new session first, then write the record, then start the session, and, if a step fails, undo the in-memory claim, save the layout again without it and remove the record (the compensating save is best effort; see the limits below). The record is removed once the kill exits 0 or the session is found already gone; (3) it is not any graph node's id (case-insensitive, including ids nested in a composite's stored subgraph) and not the open loop's id; (4) neither the open layout nor any other saved layout names it, and the scan of the other layouts completed (a scan cut short by the 1024-file cap, an unreadable directory or file, or a file that is not a layout is "unknown" and refuses; unknown is retryable, so the layout is kept as it was for the next sweep rather than set aside); (5) a fresh successfulzmx lsshows it running at kill time. Anything else is refused and logged, never killed; a layout that named a permanently refused entry is renamed.refused, not deleted. Duplicate names are ended once. The same gate covers closing a tab or split. A harness'sGRAPHCODE_SHELL_SESSION_PREFIXids have the shape under that exact prefix only.C:/againstC:\) is not swept. Loop ids match case-insensitively. If the listing fails, the layout is kept for the next sweep..bad(never over an earlier one) and ends nothing. If it cannot be set aside (for example every.badname is taken), the load fails and nothing is written over it: opening that loop reports "Unable to open selected loop", a project-level rebind fails, and at startup persistence stays off. A layout that is merely unreadable (locked) is left alone and the open fails rather than overwriting it. Layout saves use a per-writer temporary name,syncbefore the rename, and delete the temporary file on failure. Legacy adoption no longer proceeds if its marker file cannot be written.What is and is not proven
The ownership record is a file some GraphCode shell wrote; it is not cryptographic provenance and it does not identify which shell wrote it. The layout directory is shared by every GraphCode shell of the user: the workspace reservation (
WorkspaceReservation, App.zig) is per workspace folder, not per support directory, so two shells of different workspaces can run against the same layout root at once. Because of that, a record names the loop it was minted for and only that loop's retirement can use it, so shell B retiring a stale layout of a different loop cannot end a session shell A has just recorded for its own loop. The remaining limits: a hand-created record for a foreign session, with the right loop name, in the layout root would still make that session killable (after passing every other check); and two shells operating on the same loop's layout at once are not coordinated beyond the claim-before-start order.Upgrade effect: sessions from tabs created by builds before this change have no record, so deleting their loop or closing their tab will not end them. They are left running (a leak) rather than guessed at.
What each test proves
New or changed tests (App root unless noted; they use the fake
zmx.cmdfixture, real winghostty child surfaces and real child processes):attachrecords what it found when it started (the loop layout file and the record), and the record names the loop. This test passed before the reordering fix (the pre-fix order raced too fast for the fake attach to observe), so the claim-before-start change itself has no RED run; only the record-names-the-loop part failed before..badname taken, the corrupt layout's bytes and the earlier.badfile are unchanged, the loop is not opened and the status says so. RED before the fix.zmx killargv is exactly one session. RED in the first push (before any production change); it now also needs the record.zmx lsend nothing and keep the layout; a later good listing ends the session. RED in the first push.onFrameWithAccessibilityPublishpath. RED in the first push.ZmxSession.zig(2 tests: name shape and harness prefix) andWorkspaceLayout.zig(6 tests: set-aside naming, load-error classification, claim scan including the cap and unreadable files, ownership records, atomic save): unit tests of new functions, so their only "RED" would be a compile error; none was run.Not tested, and limits
zmxis a.cmdstand-in and daemon snapshots are hand-built frames. Behavior against a realzmx lsformat, a real daemon restart, or hardware was not exercised.zmx lson the UI thread (bounded at 2s). A wedged zmx can stall it for that long.make testandmake checkare macOS targets and were not run.zig fmt --checkfails on these files before and after this change, so it was not used.Test plan
RED: zig test src\App.zig (App shell flags) --test-filter "workspace layout" with the latest review-round tests against the previous push's production code -> 23 passed, 3 failed (a session another loop's record names was killed: expected 0, found 1; record content empty where the loop name was expected; unrelated unreadable layout did not defer)
GREEN: zig test src\App.zig (App shell flags) --test-filter "workspace layout" with the fix -> All 26 tests passed
REGRESSION: zig test for App 1077/1077, TerminalSurface 249/249, WorkspaceLayout 17/17, ZmxSession 5/5, LoopLaunchWait 15/15 -> all passed
Earlier rounds' RED runs: the first push's (8 passed, 10 failed: kill argv expected 1, found 7; an empty snapshot killed both sessions; shell tab restore timed out; .bad file not found) was against the unfixed production code of the original review; the second round's 19 passed, 4 failed covered the missing ownership record, the scan cap and the set-aside overwrite. All runs used the pinned Zig 0.15.2 from Tools\windows\bootstrap.ps1 and a process-only scratch profile (USERPROFILE, GRAPHCODE_SUPPORT_DIR, LOCALAPPDATA, APPDATA, TEMP, TMP). The unit-test groups above are GREEN-only.
Tools\windows\Tests\WindowsShell.Tests.ps1was not run end to end; the Zig roots were run directly with the same flags it uses.Checklist
git commit -s) per the DCOmake test) (macOS target; the Windows Zig roots above were run instead)make check) (macOS target; not run)