fix(tui): bind side-copy cleanup to process lifetime - #3783
Conversation
3cb1c74 to
ca943ea
Compare
jackwener
left a comment
There was a problem hiding this comment.
APPROVED at ca943ea5574b5d4ed1dd164ed954c1476a12fff1: no P0-P2 findings; one non-blocking P3 follow-up.
Finding
[P3][reasonable crash/recovery path] Retire unreferenced owner lease files left by an unclean TUI exit.
acquireProcessLifetimeOwner() eagerly creates and locks a UUID .lease file before any Session-copy record necessarily references it. If the process is killed before it writes a copy lease—or after its last copy record is removed—the OS releases the lock but the file remains. Recovery discovers owner references only through workflow_quote_companion_cleanup; it never enumerates the owners directory, so a reference with zero database rows is never claimed or retired. A real-process probe killed such an owner, started and gracefully closed a successor, and the dead owner's file still remained as the sole entry. This leaks one inode per qualifying crash indefinitely. Either acquire lazily or, during recovery, enumerate only strictly valid versioned lease filenames and claim/retire those that have no database references; an active owner remains protected because its native lock cannot be claimed. Add a zero-record crash regression. This is storage litter rather than a Session-safety failure, so it is non-blocking.
Evidence and gates
- Reviewed head/base:
ca943ea5574b5d4ed1dd164ed954c1476a12fff1/e28ce6f515471d59b9063dd8a951db82e58f8fea. - New records are classified by
ownerLifetimeRefbefore the legacy PID branch, so PID reuse cannot affect their liveness decision. Older PID-only records retain the existing fallback. - Real-process tests prove a live owner cannot be claimed, the lock becomes claimable after SIGKILL, and only one concurrent recovery claimant wins. A separate same-process dual-FD probe against
fs-native-extensions1.5.0 returnedtrue / false / false(first lock / second lock / retry after closing the second FD), confirming that closing an unrelated FD does not release the held lease. The dependency usesflockon macOS,F_OFD_SETLKon Linux, andLockFileExon Windows. - Reference parsing reconstructs paths only from a strict UUID capture;
O_NOFOLLOWplus regular-file and dev/inode checks rejects symlink/path replacement, and lock-inspection failures retain the Session copy. - Exact-head hosted
test,audit,owner (macos-latest), andowner (windows-latest)checks all completed successfully. - Current
mainwas19b47632be61b9116ae25d67e286f6a7878d6645; the synthetic merge was clean (tree016a0d014332a60b1fde19db5a0e8f69f65b2137), completed the fullbuild:test, and passed 90/90 focused owner/cleanup/Host/conversation-copy tests. This includes the newmainconversation-copy changes from #3806.
This approval does not merge the PR.
There was a problem hiding this comment.
Reviewed the production change. No P0 or P1 — the approval stands and this is ready to merge as far as I am concerned. Three comments inline: one P2 and two P3, none blocking.
What holds up well:
openStableNativeLockFilepairsO_NOFOLLOWwith a two-waydev/inocomparison between the handle and the path, so the lease cannot be swapped underneath the lock.- The
lock-v1:prefix plus the regex inownerPathmeans a reference read back out of SQLite cannot escapeowners/. - The unlink-before-release ordering in
close()is correct, and the comment explaining it is accurate. I worked the reverse order through: releasing first lets a second claimant lock the still-named inode, and after the unlink a third can create and lock a fresh one, giving two live claimants for one reference. Unlinking first closes that. - Extracting
native-file-lock.tsand rewritingprocess-lifetime-file-update-lock.tsonto it is the right seam — one lock primitive rather than a second copy. apps/desktop/src/main/runtime-host-boot.ts:791builds aSessionCopyCleanupAuthoritywithout aprocessLifetimeOwner, which I checked and is fine: itsworkspaceRootis the Host workspace, not<clientDataRoot>/tui-session-copies/<rootId>, so the two authorities never read each other's records.
One observation, not a graded finding: acquireProcessLifetimeOwner is now awaited unconditionally in createRuntimeHostTuiContext, so a failure to create or lock the lease aborts TUI startup for users who never open a side conversation. fs-native-extensions was already a hard dependency of the CLI startup path via runtime-host-service-manager.ts, so this is not a new dependency risk, but it is a newly unconditional one against clientDataRoot. Failing closed is a defensible choice and I could not point to a reachable filesystem where it breaks, so I am recording it rather than asking for a change.
AI use: drafted with Claude Code (Opus 5); I verified every claim in it against the source at ca943ea55 before posting.
Use an OS-held native file lock as the process-incarnation identity for temporary Session-copy cleanup. Preserve PID-only recovery for leases written by older clients and hold one recovery claim across Host-side removal. Generated-by: Codex
Generated-by: Codex
ca943ea to
ffa5690
Compare
|
Addressed the approved review's orphan-lease follow-up and all three inline findings in Validation: root build; affected owner/cleanup tests 21/21; full storage suite 969 passed and 16 skipped; CLI tests 459/459; lint; format; ASF headers; |
Astro-Han
left a comment
There was a problem hiding this comment.
I reviewed this head and found no blocking issues.
Owner lock correctly uses incarnation-aware UUID + lifetime reference grouping under single claim; cleanup/cancel states not mis-reclaimed; PID fallback retained for legacy leases. Hosted test/audit/owner SUCCESS.
No P0-P3.
简体中文
该头无阻断。Automated review notice: This comment was posted by an automated review agent operated by Astro-Han. It is not an independent human review and does not replace one.
Summary
Fixes #3782
Bind temporary Session-copy cleanup ownership to an OS-held process-lifetime file lock instead of relying on PID liveness alone.
The TUI now persists an opaque lifetime reference alongside the legacy PID, groups leases belonging to one process incarnation, and holds one recovery claim throughout Host-side removal. Lock inspection failures fail closed, and records written by older clients continue to use the existing PID fallback.
Verification
npm --workspace @maka/storage run buildnpm --workspace @maka/storage run typechecknpm --workspace maka-agent run buildnpm run lintnpm run format:checknpm run check:asf-headersgit diff --checkReview focus
The Runtime Host remains the authority that removes Sessions, SQLite remains the durable cleanup-intent store, and the native lock answers only whether a particular TUI process incarnation is still alive. No heartbeat, timer, new database table, or platform-specific process command is introduced.
AI use
Select exactly one:
Tool(s) and scope: Codex designed, implemented, tested, and reviewed the process-lifetime cleanup ownership change.
Checklist
Does this PR entail a change in behavior?