Skip to content

fix(hooks): recover positively dead canonical session-pointer locks#3262

Merged
Yeachan-Heo merged 7 commits into
devfrom
fix/issue-3261-dead-session-lock-recovery
Jul 23, 2026
Merged

fix(hooks): recover positively dead canonical session-pointer locks#3262
Yeachan-Heo merged 7 commits into
devfrom
fix/issue-3261-dead-session-lock-recovery

Conversation

@Yeachan-Heo

Copy link
Copy Markdown
Owner

Summary

Fixes #3261.

After SIGKILL/OOM/power loss while holding the session pointer lock, launch aborts with session_pointer_lock_recovery_required while omx session lock recover refuses a positively dead canonical owner.json (safeToRecover was gated to owner-temp evidence only) — a permanent deadlock requiring manual rm -rf of the lock dir.

Change

One gate line in inspectSessionPointerLockAtContext (src/hooks/session.ts): broaden safeToRecover from status === 'dead' && evidenceSource === 'owner-temp' to status === 'dead'. Positively dead canonical owner.json now flows through the existing atomic claim/quarantine protocol, which revalidates the exactly-one entry, still-dead status, and owner token before quarantining — the identical code path already race-tested for temp evidence.

  • dead requires positive ESRCH via probePid; EPERM/EACCES/unknown stays identity-indeterminate → fail-closed.
  • Fail-closed behavior unchanged for live, reused (PID start-ticks), identity-indeterminate, ambiguous, malformed, symlink, unexpected, and io-error states.
  • No orphan session.json.tmp-<token> cleanup added: pointer temps are inert by design (token-unique, written wx, never scanned by inspect/acquire; documented by the existing successor-commit test).

Tests

  • Rewrote the codifying dead-canonical test: quarantine via claim protocol, exact owner.json → claim → quarantine rename sequence, idempotent second recover (absent/none).
  • New: launch blocked (session_pointer_lock_recovery_required) → explicit recover → writeSessionStart succeeds.
  • New: canonical successor-displacement race before the exact claim rename → clean refusal, successor evidence untouched.
  • New: claim rename landing on a displaced successor canonical owner → token revalidation refuses, nothing quarantined, evidence preserved.
  • Existing fail-closed suite (live/reused/malformed/ambiguous) and temp-evidence recovery tests unchanged and green.

Verification

  • Focused: dist/hooks/__tests__/session.test.js — 76/76 pass.
  • Broader: all 66 dist/hooks/__tests__ files + dist/cli/__tests__/session-search-help.test.js — pass.
  • npm run build, check:no-unused, lint (775 files) — clean.
  • Full npm test was started; the harness aborted the run mid-suite with zero test failures up to that point, and the one file-level crash artifact (doctor-warning-copy) passes 70/70 in isolated rerun. Full-suite green is left to CI on this PR.


[repo owner's gaebal-gajae (clawdbot) 🦞]

Issue #3261: after SIGKILL/OOM/power loss while holding the session
pointer lock, launch aborts demanding recovery while 'omx session lock
recover' refuses a positively dead canonical owner.json, a permanent
deadlock. Broaden the recoverability gate in
inspectSessionPointerLockAtContext from dead owner-temp evidence only to
any positively dead (ESRCH) evidence, so canonical owner.json flows
through the existing atomic claim/quarantine protocol, which revalidates
the exact entry, still-dead status, and owner token before quarantining.

Fail-closed behavior is unchanged for live, reused,
identity-indeterminate, ambiguous, malformed, symlink, unexpected, and
io-error states. No orphan pointer-temp cleanup is added; pointer temps
remain inert by design.

Tests: rewrite the codifying dead-canonical case to assert quarantine,
idempotence, and the owner.json claim rename sequence; add
launch-then-recover-then-launch integration; add canonical
successor-displacement race coverage for the pre-claim and
claim-revalidation paths.

Fixes #3261

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4cc3b292e5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/hooks/session.ts
// status, and owner token before quarantining. Live, reused,
// identity-indeterminate, ambiguous, symlink, malformed, unexpected,
// and io-error states stay fail-closed.
safeToRecover: status === 'dead',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid recovering canonical owners without claiming the directory

When two recoveries overlap and one removes the stale canonical lock before a new launcher recreates session.json.lock, this now treats the new owner.json as recoverable by path alone. recoverSessionPointerLock() then renames that successor owner.json to an .recovery file, fails revalidation, and returns without restoring it; subsequent launches hit acquirePointerLock() with no live canonical owner and keep aborting until manual cleanup. Canonical-owner recovery needs to claim/validate the lock directory identity (or remain limited to owner-temp evidence) before enabling owner.json as safe to recover.

Useful? React with 👍 / 👎.

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Independent exact-head adversarial review — REQUEST_CHANGES

Reviewed commit: 4cc3b292e5f6f4f7e27fd62b563413d62bac902a (confirmed exact head of fix/issue-3261-dead-session-lock-recovery; diff scope limited to src/hooks/session.ts gate + src/hooks/__tests__/session.test.ts). Verdict from a fresh independent architect/critic review lane. (Posted as a comment because GitHub rejects request-changes self-reviews; the verdict stands.) Do not merge.

What survives scrutiny

  • PID reuse: a live/reused PID can never be classified deadkill(pid, 0) alive + Linux start-ticks mismatch → reused (src/hooks/session.ts:896-918); only positive ESRCH is dead (probe mapping at src/hooks/session.ts:477-494), and recovery re-probes before claiming (src/hooks/session.ts:1002-1012).
  • Identity-indeterminate (EPERM/EACCES/unknown probe, unreadable owner, missing start ticks): all stay fail-closed; the gate admits only exact status === 'dead' (src/hooks/session.ts:977-983).
  • Static canonical-vs-temp confusion: exactly-one-entry, non-symlink regular evidence, temp filename/content token match all enforced at inspect (src/hooks/session.ts:949-970) and re-checked entry-count at recover (src/hooks/session.ts:1002-1012).
  • The gate does fix the [Bug] Dead canonical session-pointer lock (owner.json + dead PID) has no recovery path: launch aborts and omx session lock recover refuses #3261 deadlock for the ordinary dead-canonical case.

P1-1 — Canonical claim can strand a displaced successor (src/hooks/session.ts:1031)

The canonical claim renames the shared pathname owner.json. Under the PR's own displacement model (test at src/hooks/tests/session.test.ts:1016-1050), when the lock directory is replaced after revalidation but before the claim rename, rename(owner.json → owner.<deadToken>.<claim>.recovery) moves the successor's owner.json into the dead owner's claim name. Token revalidation correctly refuses quarantine (src/hooks/session.ts:1036-1045) but does not restore the file — the successor is left permanently unrecognizable: releasePointerLock reads only owner.json and fails ownership-uncertain (src/hooks/session.ts:1310-1325), and later inspect/acquire classify the lone .recovery entry as unexpected. The new test at session.test.ts:1047-1050 codifies this corrupted aftermath as expected. Detect-after-mutation is not an atomic identity-bound claim.

P1-2 — No lock-directory/evidence type revalidation before mutation (src/hooks/session.ts:1002-1031)

Inspection rejects symlinked lock dirs and symlinked/non-regular evidence (src/hooks/session.ts:934-966), but that is a snapshot. After safeToRecover: true, recovery follows context.lockPath through readdir/readFile and mutates via rename without any lstat recheck. A post-inspection replacement of the lock directory with a symlink to a foreign directory (or evidence-file symlink/non-file swap) redirects the claim rename into foreign storage before post-claim revalidation notices. The broadened canonical gate makes this path reachable; the old temp-only gate returned before any side effect.

Regression coverage gaps

No tests for: live canonical successor remaining releasable/acquirable after a refused claim; lock-directory symlink replacement after inspection; evidence-file symlink/non-file replacement after inspection; quarantine-path appearing between preflight lstat (src/hooks/session.ts:1048-1059) and rename.

Required before re-review

  1. Identity-bound or safely reversible canonical claim: a stranded/mismatched claim must be rolled back to owner.json (no-clobber), or bound to a verified directory identity (dirfd-relative or equivalent); revalidation immediately before every mutation plus guaranteed rollback is the minimum.
  2. Remove the test that codifies stranded-successor corruption; add tests proving a live canonical successor survives a refused claim (releasable + acquirable), plus injected directory-replacement, symlink-replacement, and rollback/no-clobber races.
  3. Repeat evidence regular-file/no-symlink and lock-dir no-symlink checks at recovery re-read, immediately before each mutation.

Signed verdict: REQUEST_CHANGES.


[repo owner's gaebal-gajae (clawdbot) 🦞]

…-clobber reversible

Independent exact-head review of 4cc3b29 found two P1 races in the
broadened dead-canonical recovery path:

1. The canonical claim renames the shared owner.json pathname; a lock
   directory replacement after revalidation but before the rename moved
   a successor's owner.json into the dead owner's claim name, and token
   revalidation refused quarantine without restoring the successor,
   stranding it under an unrecognizable .recovery name.
2. Neither the lock directory nor the evidence file was re-lstat
   revalidated between inspection-time checks and mutations, so a
   post-inspection symlink or non-file swap redirected the claim rename
   into foreign storage.

Repair, keeping the one-gate broadening and the atomic claim/quarantine
shape:

- Re-lstat the lock directory (non-symlink directory) and evidence
  (non-symlink regular file) immediately before every mutation: at
  re-read, before the claim rename, before quarantine, and before rmdir;
  temp evidence also re-proves filename/content token equality.
- Every refusal after the claim rename rolls the claim back to its
  original evidence name via atomic no-clobber link+unlink (rename
  fallback only when link is impossible, e.g. a swapped-in directory),
  never overwriting a reappeared name and never modifying foreign bytes.
- Quarantine is created with no-clobber link()+unlink() instead of
  rename(), so a quarantine path appearing after its preflight check is
  never overwritten.

Tests: replace the stranded-successor codification with rollback
preservation proof; add live canonical successor
survival/recognizable/releasable/acquirable, lock-dir symlink
replacement, evidence symlink swap, non-file evidence replacement, and
quarantine-appearance race regressions; update both claim sequence
assertions for link-based quarantine. Focused session suite 81/81,
broader hooks+cli lane, build, no-unused, and lint all green.

Refs #3261
@github-actions github-actions Bot added size/L and removed size/M labels Jul 22, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 308628559e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/hooks/session.ts Outdated
return { ...claimed, action: 'none', recovered: false, reason: recoveryRollbackReason('Unable to quarantine the exact session pointer recovery claim', rollback) };
}
try {
await transactionDependencies.fs.link(claimPath, quarantinePath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Support recovery when hard links are unavailable

When the selected state root is on a filesystem that rejects hard links (for example exFAT/FAT, some SMB/network mounts, or Windows paths returning EPERM), this link() throws; the catch rolls the claim back and returns recovered: false, so launches continue to abort on the same positively-dead owner and omx session lock recover cannot clear it. Use an exclusive copy/no-clobber fallback for hard-link-unavailable errors before rolling back.

Useful? React with 👍 / 👎.

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Fresh independent exact-head adversarial review — REQUEST_CHANGES

Reviewed commit: 308628559e8cce491a7b407c51fdc66fbe0b9944 (verified as exact branch head; full diff vs origin/dev reviewed: gate broadening + repair commit). Fresh architect/critic lane; the stale 4cc3b29 verdict was not relied upon. Do not merge. (Comment form: GitHub rejects request-changes self-reviews.)

Survives scrutiny

  • PID reuse: alive PID → live/reused via Linux start-ticks, never dead (src/hooks/session.ts:901-926); re-probe before claim and on claimed owner (1071-1075, 1114-1117).
  • Identity-indeterminate fail-closed: EPERM/EACCES/unknown probe, unreadable owner, missing start-ticks all stay non-recoverable; gate admits only exact dead (886-926, 973-988).
  • Ordinary [Bug] Dead canonical session-pointer lock (owner.json + dead PID) has no recovery path: launch aborts and omx session lock recover refuses #3261 case and uncontended rollback: dead-canonical recovery works; displaced canonical successor is restored to owner.json and inspectable (1013-1044; tests 915-978, 1021-1088).
  • Tested symlink/non-file/quarantine-EEXIST cases behave as designed (tests 1093-1166).

Blockers (all at exact head 3086285)

  1. Rollback rename fallback can clobber (src/hooks/session.ts:1022-1034): when link() fails non-EEXIST (e.g. EISDIR directory claim), rollback does lstat-then-rename; the inner catch treats ANY lstat failure as absence, and even a true ENOENT races with a destination appearing before rename, which replaces existing files/symlinks/empty dirs. Rollback can overwrite or delete successor/foreign bytes while reporting success.
  2. link+unlink is not identity-safe (1023-1044, 1139-1149): between link(src,dst) and unlink(src) a peer can remove/replace dst (unlink then destroys the sole exact claim) or replace src (unlink deletes foreign bytes). No dev/ino identity is captured or compared. Applies to both rollback and quarantine moves.
  3. Claim destination race (1087-1098): claim path is only preflight-lstatted; clobbering rename(evidence, claimPath) overwrites a claim entry appearing in the gap. Random tokens reduce accident probability but are not the no-clobber guarantee this PR's contract claims.
  4. rmdir not bound to validated directory identity (1152-1168): type-only lstat before rmdir; an empty replacement directory in the gap gets removed and recovery reports success. Existing post-claim race test only covers a non-empty replacement (ENOTEMPTY).
  5. Major — live-successor test overclaims (src/hooks/tests/session.test.ts:1059-1088): title promises releasability but never exercises the real token-checked release path while the successor is live, and does not assert byte-identical owner.json.
  6. Minor — collapsed diagnostics (1123-1137): distinct operator reasons ("Recovery quarantine path already exists", "Unable to inspect recovery quarantine path") were merged into one generic prefix with causes swallowed; CLI exposes reason directly.

Required before re-review

  1. No clobbering fallback anywhere: remove the rollback rename fallback; where no no-replace move exists (directory claims), leave the .recovery claim in place with a precise reason (conservative fail-closed; bytes preserved, manual cleanup acceptable).
  2. Identity-bind every move: capture dev+ino of evidence/claim/lock-dir at validation; after each link, verify both names still name the captured inode before any unlink; never unlink a pathname whose identity is uncertain.
  3. Claim via no-clobber link() too (EEXIST → fail closed untouched), with identity-verified unlink of the evidence name.
  4. Bind pre-rmdir check to the captured lock-dir dev+ino.
  5. Restore distinct quarantine diagnostics; accept only ENOENT (not any error) as "absent" in lstat checks.
  6. Tests: real token-checked release of the live rolled-back successor (via a test seam if needed) + byte-identical owner.json; injected races for claim-destination appearance, rollback/quarantine source/destination swaps, non-ENOENT lstat in fallback ladders, and empty-directory replacement before rmdir.

Signed verdict: REQUEST_CHANGES.


[repo owner's gaebal-gajae (clawdbot) 🦞]

…ine, and removal

Second independent exact-head review of 3086285 found the round-1
repair still emulated atomic no-replace moves with pathname checks:
the rollback rename fallback could clobber, link+unlink moves were not
identity-safe, the claim destination was only preflighted, rmdir was
not bound to the validated directory, and distinct quarantine
diagnostics were collapsed.

Round-2 repair, mirroring the in-repo no-clobber restore convention:

- Claim is a no-clobber link() with captured dev/ino evidence identity;
  EEXIST or any identity drift fails closed without mutating foreign
  entries, and the evidence name is unlinked only while its identity
  still matches the captured inode.
- Rollback is link-only (the clobbering rename fallback is removed);
  where no no-replace move exists (e.g. a swapped-in directory claim),
  the exact claim is left in place under its recovery name with a
  precise diagnostic. Claim unlink proceeds only while both link names
  still identify the captured inode.
- Quarantine keeps no-clobber link()+identity-verified unlink, restores
  the distinct 'Recovery quarantine path already exists.' / 'Unable to
  inspect recovery quarantine path.' diagnostics, and treats only
  ENOENT as absence everywhere.
- Pre-rmdir revalidation compares the captured lock-directory dev/ino,
  so an empty replacement directory is never removed.
- Post-claim content inspection is skipped entirely when the rebound
  lock path no longer inspects as an exact claim shape.

Tests: claim/quarantine sequencing updated for link-based claims;
live-successor coverage now asserts byte-identical owner.json and
exercises the real token-checked release via a narrow @internal test
seam before reacquisition; new adversarial regressions for claim
destination appearance, rollback destination appearance, quarantine
claim identity swap, non-ENOENT lstat at pre-claim and quarantine
preflight, and empty lock-directory replacement before rmdir. Focused
session suite 87/87; broader hooks+cli lane (66 files), build,
no-unused, and lint all green.

Refs #3261
@github-actions github-actions Bot added size/XL and removed size/L labels Jul 22, 2026
@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Repair update — new head f5750ba4b9073c8d23f86f6c80117b4239b86b66

All six findings from the round-2 REQUEST_CHANGES verdict (issuecomment-5048182078) are addressed in commit f5750ba4:

  1. Clobbering rename fallback removed — rollback is link-only; where no no-replace move exists (e.g. a swapped-in directory claim), the exact claim is left in place under its .recovery name with a precise diagnostic. Nothing can be overwritten.
  2. Identity-bound moves — evidence/claim/lock-dir dev+ino are captured at validation; after every link, both names must still identify the captured inode before any unlink; uncertain pathnames are never unlinked. Mirrors the in-repo restoreNativeHookClaimNoClobber convention.
  3. No-clobber claim — the claim itself is now link() (EEXIST → fail closed, nothing mutated), with identity-verified unlink of the evidence name.
  4. Pre-rmdir binding — compares captured lock-dir dev+ino; an empty replacement directory is never removed.
  5. Diagnostics restored — distinct Recovery quarantine path already exists. / Unable to inspect recovery quarantine path.; only ENOENT counts as absence.
  6. Coverage — live-successor test now asserts byte-identical owner.json and exercises the real token-checked release path via a narrow @internal __releasePointerLockForTests seam before reacquisition; new adversarial regressions: claim destination appearance, rollback destination appearance, quarantine claim identity swap, non-ENOENT lstat at pre-claim and quarantine preflight, empty lock-directory replacement before rmdir.

Verification at this head: focused session suite 87/87; broader hooks+cli lane (66 files, runner exit 0); build, check:no-unused, lint — all clean. A fresh independent exact-head adversarial review follows; merge remains blocked until its signed MERGE_READY on an unchanged head with terminal green CI.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Fresh independent exact-head adversarial review (round 3) — REQUEST_CHANGES

Reviewed commit: f5750ba4b9073c8d23f86f6c80117b4239b86b66 (verified exact head; full three-commit diff vs origin/dev reviewed independently). Do not merge. (Comment form: GitHub rejects request-changes self-reviews.)

Round-2 finding status

  1. Clobbering rollback rename fallback — FIXED (link-only rollback, src/hooks/session.ts:1016-1025).
  2. Non-identity-safe link+unlink — NOT FIXED (see P1-2).
  3. Claim-destination clobber race — FIXED (no-clobber link + EEXIST refusal, 1051-1054).
  4. rmdir not bound to validated directory — NOT FIXED (see P1-3).
  5. Live-successor real release — FIXED (byte-identical assertion + real token-checked release seam + reacquisition, session.test.ts:1062-1091).
  6. Collapsed diagnostics / any-error-as-absence — FIXED (distinct quarantine diagnostics, only-ENOENT absence).

Survives

PID reuse and identity-indeterminate fail-closed (908-990); no-clobber claim/rollback/quarantine destinations; the repaired live-successor chain.

Blockers at this exact head

  1. Mismatch cleanup can unlink foreign claim bytes (src/hooks/session.ts:1057-1061): cleanup compares a second lstat to the first post-link observation (currentClaim), not to the captured evidence identity; a claim name swapped before the first observation is judged stable and deleted. Bind to the pre-link captured identity; on uncertainty leave named residue.
  2. Check-then-unlink races at every removal (1022-1025, 1057-1064, 1090-1093): dev/ino validation followed by a separate pathname unlink deletes a replacement installed in the gap — for evidence, rollback claim, and quarantine claim names alike. Existing tests inject swaps before the check, not inside the unlink seam.
  3. Check-then-rmdir race (1094-1095): an empty replacement directory installed after the passing identity lstat is removed and recovery reports success. The new test injects inside the lstat seam, not the rmdir seam.
  4. Major — post-link residue collapses into generic drift (1054-1063): post-link lstat/cleanup failures return only changed before recovery claim while .recovery residue remains; repeated recover degrades to ambiguous/unexpected without the originating residue path or operator action.

Accepted direction (from the review's own tradeoffs)

Pure Node has no atomic unlink-if-same-inode / rmdir-if-same-directory. The review accepts the conservative contract: never delete a pathname after a stale observation. Planned implementation ("park-and-verify"): every removal becomes an atomic rename(target → unpredictable parked name) (non-destructive — nothing at the target is ever overwritten or deleted in place), then identity-verify the parked object against the captured dev/ino, and delete only the parked name on match; on mismatch, leave the exact parked residue with a precise reason and residue path. Parked names include fresh randomness, so neither the injected seam nor a practical mutator can pre-create or predict them — closing the last clobber window without native primitives. Post-mutation refusals will report exact residue paths and operator guidance.

Signed verdict: REQUEST_CHANGES.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

GJC exact-head adversarial review verdict\n\nVerdict: BLOCKED — independent review unavailable.\n\nReviewed target: 643e7e687a5a939913d7a2f00097257cf09f47a3 (exact current PR head).\n\nRequired fresh adversarial GJC review dispatch was attempted after the focused suite passed, but this runtime has no available native review route: the code-review skill is not registered by the skill runtime, and no codex executable is installed. Per the review contract, author self-review cannot substitute for an independent lane.\n\nLocal evidence: npm run build && node dist/scripts/run-test-files.js dist/hooks/__tests__/session.test.js (87/87); npx tsc --noEmit; npm run check:no-unused; npm run lint. Hosted CI is still in progress.\n\nNo REST merge was attempted. PR remains open pending a fresh independent exact-head review and green hosted CI.\n\n— GJC (exact-head review gate)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 643e7e687a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/hooks/session.ts Outdated
return { ...inspection, action: 'none', recovered: false, reason: `Session pointer lock evidence changed before recovery claim. ${await cleanupClaim()}` };
}
const evidenceRemoval = await removeIfIdentityMatches(evidencePath, preClaim.evidenceStat, 'file', context.lockPath);
if (!evidenceRemoval.removed) return { ...inspection, action: 'none', recovered: false, reason: `The exact recovery claim was created, but its original evidence name could not be removed (${evidenceRemoval.reason}).${evidenceRemoval.residuePath ? ` Residue preserved at ${evidenceRemoval.residuePath}; inspect and remove it manually.` : ''}` };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Roll back the claim when evidence removal fails

When the hard-link claim succeeds but parking/removing the original evidence fails (for example EPERM/EBUSY from Windows file sharing, antivirus, or a transient permission error), this returns without deleting the just-created .recovery link. The lock directory is then left with both owner.json and the recovery entry, so later inspection reports an ambiguous lock and omx session lock recover refuses to proceed until manual cleanup; this failure path should remove or roll back the claim before returning.

Useful? React with 👍 / 👎.

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Fresh independent exact-head adversarial review (round 4) — REQUEST_CHANGES

Reviewed immutable commit: 643e7e687a5a939913d7a2f00097257cf09f47a3 (confirmed local HEAD, remote branch head, and PR head). Reviewed the full PR diff against current origin/dev using merge-base 299f31ad824409e5cae0015d982de41aa1b09552. Do not merge. (Comment form: GitHub rejects request-changes self-reviews.)

Prior REQUEST_CHANGES disposition

  • Round 1 successor-stranding and pre-mutation type/symlink findings: FIXED by no-clobber hard-link claiming, identity revalidation, rollback, and the real live-successor release/reacquisition test.
  • Round 2 clobbering fallback, claim-destination race, identity binding, lock-directory binding, overclaimed test, and collapsed diagnostics: FIXED or superseded by the current claim/quarantine protocol and park-and-verify removal.
  • Round 3 mismatch cleanup, direct check-then-unlink/rmdir, and generic residue diagnostics: FIXED under the accepted unpredictable parked-name contract. The parked object is checked against the captured identity before deletion, and mismatch paths preserve named residue.
  • PID reuse and identity-indeterminate states remain fail-closed. Canonical and temp dead-owner recovery works in the uncontended path.

Blockers at this exact head

  1. P1 — Original-evidence removal failure leaves an unsupported permanent lock state (src/hooks/session.ts:1135-1136).

    Recovery first creates owner.<ownerToken>.<claimToken>.recovery, then returns immediately when parking/removing owner.json fails. There is no compensation for the claim the function just created.

    I reproduced both ordinary transient failure boundaries without modifying source:

    • rename(owner.json, parkedPath) -> EBUSY: the lock retains both owner.json and .recovery; retry is ambiguous and safeToRecover:false.
    • parked evidence unlink -> EBUSY: the lock retains only .recovery plus the parked sibling residue; retry is unexpected and safeToRecover:false.

    After the transient error clears, recoverSessionPointerLock() still cannot make progress. This confirms the current exact-head Codex finding. Compensate identity-safely before returning, or make the intermediate state a supported resumable recovery state. Add pre-park and post-park failure tests followed by a normal retry.

  2. P1 — Park-and-verify is not crash-recoverable (src/hooks/session.ts:1034-1051, called at 1135 and 1161; unsupported-state gate at 956-960, 1099-1100).

    SIGKILL/OOM/power loss after a successful park rename but before the later unlink leaves no recognized canonical evidence:

    • During evidence removal, the lock contains only the .recovery claim and the parked evidence is outside it.
    • During claim removal after quarantine, the lock directory is empty while the quarantine/parked artifact remains outside it.

    On restart, inspection reports unexpected or missing-owner; explicit recovery refuses to resume. This recreates the same manual-cleanup deadlock class the PR is intended to eliminate, using only the interruption model named in the PR description—no hostile pathname swap or UUID prediction is required. The park operation needs a durable, identity-bound state that a subsequent recovery can resume or roll back. Add restart checkpoints immediately after each successful park rename.

Verification

  • npm run build — passed.
  • Focused session suite — 87/87 passed.
  • Relevant hooks-notify-platform grouped lane roots — 134 test files, exit 0.
  • Exact-head hosted CI — all active required checks passed.
  • External fault-injection repro — confirmed ambiguous retry after pre-park EBUSY and unexpected retry after parked-unlink EBUSY.

Passing tests do not cover either unsupported intermediate state above.

Signed verdict: REQUEST_CHANGES.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo

Yeachan-Heo commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

Repair update — new head a2716737\n\nImplemented token-bound sibling recovery checkpoints for the evidence park transition. Checkpoints are schema/phase/path/inode validated before use, malformed or competing records fail closed, and valid evidence-pending state resumes without minting a new token. Transient checkpoint quarantine rename and claim-unlink failures preserve or roll back to a retryable checkpoint arrangement.\n\nAdded coverage for token-bound park retries, checkpoint creation, restart resumption without token generation, malformed/version/phase/path guards, multiple checkpoints, missing claim, rename-busy residue, and claim-unlink rollback/retry.\n\nVerification: focused dist/hooks/__tests__/session.test.js 98/98; grouped dist/hooks/__tests__ 65 files passed; npx tsc --noEmit; npm run check:no-unused; npm run lint.\n\nNo merge and no self-approval. Fresh independent exact-head review is required for a2716737.\n\n


[repo owner's gaebal-gajae (clawdbot) 🦞]

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a271673726

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/hooks/session.ts Outdated
Comment on lines +1149 to +1150
await transactionDependencies.fs.unlink(checkpointPath);
await transactionDependencies.fs.rmdir(context.lockPath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve checkpoints until directory cleanup succeeds

When a checkpoint resume reaches this point but the final rmdir fails, for example because Windows reports a transient EBUSY or an unexpected extra entry makes the directory ENOTEMPTY, the checkpoint has already been deleted and the parked evidence has already been moved to quarantine with the claim removed. The catch then returns failure, but later attempts see a canonical lock directory with no owner and no checkpoint to resume, so launches keep aborting with a missing-owner recovery requirement until manual cleanup.

Useful? React with 👍 / 👎.

Comment thread src/hooks/session.ts Outdated
const match = new RegExp(`^${basename(context.lockPath).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.recovery\\.([A-Za-z0-9_-]{16,128})\\.([A-Za-z0-9_-]{16,128})\\.json$`).exec(checkpointNames[0]!);
try {
const checkpoint = JSON.parse(await transactionDependencies.fs.readFile(checkpointPath, 'utf8')) as { version: number; sourcePath: string; parkPath: string; identity: { dev: number; ino: number }; phase: string };
if (!match || checkpoint.version !== 1 || checkpoint.phase !== 'evidence-pending' || checkpoint.sourcePath !== join(context.lockPath, 'owner.json') || checkpoint.parkPath !== `${context.lockPath}.parked-${match[2]}`) throw new Error('invalid checkpoint');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept temp-owner checkpoints during resume

This resume validation only accepts checkpoints whose sourcePath is owner.json, but the recovery path still handles pre-rename owner.<token>.tmp evidence and writes that temp path into the checkpoint. If recovery is interrupted after checkpointing such a temp-owner lock, every subsequent recoverSessionPointerLock() rejects the checkpoint as malformed before it can finish or retry the still-dead temp evidence, leaving that lock stuck until the files are removed manually.

Useful? React with 👍 / 👎.

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Red-team review — REQUEST_CHANGES

Independent exact-head review of a271673726a720c934c766f14a6826a6ba847dc9 (full diff vs dev: src/hooks/session.ts +234/−58, src/hooks/__tests__/session.test.ts +652/−21). Local verification at this head: npm run build clean; node --test dist/hooks/__tests__/session.test.js 98/98 pass. Hosted CI: all lanes green except Test (Node 20 / cli-core-rest) still pending at review time (not the basis for this verdict).

What's solid

  • The one-line safeToRecover broadening to status === 'dead' is backed by a genuinely hardened protocol: no-clobber hardlink claim (link + EEXIST refusal), dev/ino identity revalidation pre/post claim, inode-bound park-before-delete (removeIfIdentityMatches) with EBUSY retry and foreign-object preservation as exact residue, and identity-checked rollback (rollbackRecoveryClaim) restoring evidence via no-clobber link. Token binding (owner token ↔ temp filename ↔ claim/quarantine/checkpoint names) is consistent, and all token-derived paths are unpredictable and preflighted.
  • Checkpoint discovery is fail-closed: >1 checkpoints refuse without mutation; malformed JSON, wrong version/phase, path-mismatch, inode-mismatch, missing claim, and symlinked evidence/claim all refuse without mutation and without minting a new token; resume reuses the recorded token pair and verifies parked/claim inode equality before the quarantine rename, with move-back on transient unlink failure.
  • Symlink/traversal surface: lock dir and evidence are lstat'd and rejected on symlink at inspect, pre-claim, pre-quarantine, and resume; checkpoint sourcePath/parkPath are string-bound to the exact context paths. No symlink-following writes found.
  • Tests are strong: successor-displacement races before/at claim, symlink swaps, non-file replacements, park collisions, checkpoint resume/rollback, multiple/malformed checkpoints, quarantine-path appearance after preflight, and swapped parked objects are all covered and green.

Blocking finding

Transient checkpoint-write / evidence-park failure strands the claim hardlink and re-creates the permanent deadlock this PR exists to fix.

In recoverSessionPointerLock (src/hooks/session.ts ~1201–1205): after the no-clobber claim link(evidencePath, claimPath) succeeds and post-link identity revalidation passes, two error paths return without rolling back the claim, unlike every other post-claim failure path in the same function (which correctly call cleanupClaim() / rollbackRecoveryClaim):

  1. writeFile(evidenceCheckpointPath, …, { flag: 'wx' }) fails (ENOSPC/EROFS/EIO) → returns Unable to create recovery checkpoint… with the owner.<tok>.<tok>.recovery hardlink left in the lock dir and no residue guidance.
  2. removeIfIdentityMatches(evidencePath, …) fails before parking (e.g. double EBUSY, or non-ENOENT lstat of the park destination) → returns with the claim hardlink still present.

In both cases the lock dir then contains owner.json + the unpredictable-name claim file, so inspectSessionPointerLockAtContext reports ambiguous (session.ts:959–960) with safeToRecover: false on every subsequent launch and recover — and since no checkpoint file was recorded in case (1), the new resume path never engages. A single transient fs error converts a recoverable dead canonical lock into a state requiring manual rm of an unpredictable filename the user is never shown — the same deadlock class as #3261, introduced by this PR's own new code (on dev, recover of a dead canonical owner was a clean no-mutation refusal).

Fix is small and matches the function's own idiom: await cleanupClaim() (already identity-checked and already used two statements earlier) before returning from both paths, and include the claim residue path in the reason when cleanup itself fails. A test mirroring the existing transient-failure fakes (checkpoint writeFile reject; park double-EBUSY) would cover both.

Non-blocking notes

  • Checkpoint-resume uses a plain fs.unlink(claimPath) rather than the identity-checked removeIfIdentityMatches used everywhere else; the claim name is token-unpredictable and the inode was just verified, so this is theoretical, but the inconsistency stands out in a function this careful.
  • PR body still describes the change as "one gate line"; the head actually contains the full claim/rollback/checkpoint hardening. Worth updating the body so the merged commit narrative matches reality.

Signed: red-team review at immutable head a271673726a720c934c766f14a6826a6ba847dc9. Verdict: REQUEST_CHANGES (single narrow defect above; everything else reviewed is merge-quality).

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Repair update — new head 4f3362c0ddbc52dffae1439a629246e30fba5dae\n\nAddressed the narrow post-claim cleanup blocker. A checkpoint write failure now identity-safely removes the claim before returning. A double-EBUSY pre-park failure also removes the claim and checkpoint, restoring a single canonical owner so the following recovery succeeds. Cleanup diagnostics retain the exact parked residue path when removal cannot complete.\n\nAdded deterministic regressions for checkpoint-write rejection and double-EBUSY pre-park failure followed by normal retry.\n\nVerification: build; focused session suite 99/99; affected grouped hooks lane (session.test.js plus native-dispatch contract); npx tsc --noEmit; npm run check:no-unused; npm run lint.\n\nNo merge or self-review was performed; a fresh independent exact-head review is required.\n\n*[repo owner's gaebal-gajae (clawdbot) 🦞]*

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4f3362c0dd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/hooks/session.ts Outdated
await transactionDependencies.fs.writeFile(evidenceCheckpointPath, JSON.stringify({
version: 1,
sourcePath: evidencePath,
parkPath: `${context.lockPath}.parked-${claimToken}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Record the actual parked evidence path

When using the default token() implementation, this checkpoint records ...parked-${claimToken}, but removeIfIdentityMatches() mints a fresh token for the actual park name, so the recorded path is not the file that gets renamed out of owner.json. If the process dies after the evidence is parked but before the checkpoint is removed, the next recoverSessionPointerLock() looks for the wrong parkPath, rejects the checkpoint as malformed, and leaves the lock directory with only the .recovery hard link until manual cleanup. Pass the intended park path into the helper or have the checkpoint use the helper's chosen path.

Useful? React with 👍 / 👎.

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Independent exact-head adversarial review (round 6) — REQUEST_CHANGES

Reviewed immutable head 4f3362c0ddbc52dffae1439a629246e30fba5dae: verified local HEAD == refs/pull/3262/head == PR head on review/pr-3262-exact-4f3362c0 (zero diff); clean tracked status (only untracked .gjc/ session dirs); base dev merge-base d51632f53. Full diff (161 files, +19,334/−11,433) reviewed; prior signed rounds 1–5 REQUEST_CHANGES findings re-dispositioned; all five automated Codex reviews read. Do not merge. (Comment form: GitHub rejects request-changes self-reviews.)

Round-5 blocker disposition — FIXED

The narrow cleanup repair at 4f3362c0 works as claimed: checkpoint-write rejection and double-EBUSY pre-park failure now identity-safely roll back the claim via cleanupClaim() (park-before-delete on captured dev/ino, src/hooks/session.ts:1177-1182, 1201-1209), preserve exact residue diagnostics, and a normal retry recovers. Verified locally: build clean; focused suite dist/hooks/__tests__/session.test.js 99/99 including both new regressions. Hosted CI run 29979226587 terminal green (all required lanes pass; Rust/docs lanes legitimately skipped by lane detection) and PR Check green.

New P1 — checkpoint parkPath can never match the real park token (crash → permanently bricked lock)

  • Line 1197 records parkPath: \${lockPath}.parked-${claimToken}`, but removeIfIdentityMatchesmints a **fresh** token for the actual park name (line 1026:parkPrefix.parked-${transactionDependencies.token()}; default token()israndomUUID()per call, line 523). The recorded path is never the file actually renamed out ofowner.json`.
  • If the process dies between the evidence park rename and the checkpoint unlink (lines 1205-1211), the resume path (lines 1135-1137) requires checkpoint.parkPath === parked-${claimToken} and then lstats it → ENOENT → catch-all → "Recovery checkpoint is malformed or no longer identifies its recorded parked evidence", recovered:false, action:'none'forever: the single checkpoint is found first on every subsequent call, the claim stays in the lock dir, and the real parked evidence is orphaned at an unrecorded random sibling.
  • Empirically reproduced at this exact head against the compiled dist with real randomUUID() tokens: reconstructed the exact mid-transaction state (claim linked, checkpoint written per line 1197, evidence parked at a fresh-token path), then resumed with production dependencies — three consecutive resume attempts all returned false / none / malformed...; lock dir still holds the .recovery claim and the orphan session.json.lock.parked-<uuid2> persists. A second repro (injected post-rename crash) additionally shows the new failure path deleting the checkpoint while the evidence is already parked at the unrecorded name, stranding the lock as permanent missing-owner.
  • The new tests mask this because they pin token: () => SUCCESSOR_TOKEN (constant), making claim token == park token. This is the same defect class as the automated Codex P1 posted on this head ("Record the actual parked evidence path", line 1197) — confirmed independent and reproducible, not merely theoretical.
  • Fix direction: make the park destination deterministic and token-bound to the checkpoint (e.g. thread claimToken into removeIfIdentityMatches for the evidence park, or record/verify the actual park token), and add a crash-resume regression using random (non-constant) tokens.

P2s (standing, concur with Codex on this head)

  • Resume validation requires sourcePath === owner.json (line 1135) but checkpoints are also written for owner.<token>.tmp evidence — an interrupted temp-owner recovery can never resume.
  • In the resume path the checkpoint is deleted before the final rmdir (lines 1149-1150); a transient EBUSY/ENOTEMPTY there leaves an ownerless canonical lock with no checkpoint to resume.

What survives scrutiny (rest of PR)

No-clobber hard-link claiming with EEXIST refusal, dev/ino revalidation pre/post claim, park-before-delete with identity re-check after EBUSY retry, rollback residue preservation, symlink/foreign-inode fail-closed paths, PID-reuse start-ticks gating, and the grouped hooks/team/cli lanes all hold; no foreign-byte deletion, path-traversal, or contract drift found outside the checkpoint defects above.

Verdict: REQUEST_CHANGES — the P1 crash-resume token mismatch must be fixed and covered by a random-token regression before merge.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Repair update — new head 06162998fa33d9701b3fdad08ff90f079ceffa08\n\nCheckpoint creation now reserves and records the exact evidence park pathname before parking, then supplies that exact pathname to park-and-verify. Resume validates the recorded path instead of deriving it from another token call, and accepts the exact validated temp-owner source path as well as canonical evidence. Checkpoint deletion now follows successful final lock-directory removal.\n\nAdded a distinct-token interrupted-evidence regression proving resume reuses the actual recorded park path without minting a replacement token.\n\nVerification: build; focused session suite 100/100; affected grouped hooks lane; npx tsc --noEmit; npm run check:no-unused; npm run lint.\n\nNo merge or self-review was performed; fresh independent exact-head review is required.\n\n*[repo owner's gaebal-gajae (clawdbot) 🦞]*

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 06162998fa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/hooks/session.ts
const claim = await transactionDependencies.fs.lstat(claimPath);
if (claim.isSymbolicLink() || !claim.isFile() || claim.dev !== parked.dev || claim.ino !== parked.ino) throw new Error('checkpoint claim mismatch');
const quarantinePath = `${context.lockPath}.quarantine.${match[1]}.${match[2]}`;
await transactionDependencies.fs.rename(checkpoint.parkPath, quarantinePath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resume checkpoints after quarantine rename

In the checkpoint-resume path, if the recovery process is killed after this rename succeeds but before the claim, lock directory, and checkpoint are cleaned up, the checkpoint still records only parkPath even though the owner bytes now live at quarantinePath. The next recoverSessionPointerLock() rejects the checkpoint when lstat(checkpoint.parkPath) fails, while the lock directory can still contain the recovery claim or be empty, leaving future launches stuck behind a recovery that now refuses to resume. Record/update the checkpoint phase before moving the parked evidence, or accept an already-existing quarantine path and continue cleanup.

Useful? React with 👍 / 👎.

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Fresh independent exact-head adversarial review (round 7) — REQUEST_CHANGES

Reviewed immutable head 06162998fa33d9701b3fdad08ff90f079ceffa08: verified local HEAD == remote branch head == PR headRefOid on review/pr-3262-exact-06162998; tracked status clean (only untracked .gjc/ session dirs); base dev merge-base 299f31ad8 (origin/dev 13115301c adds no src/hooks changes). Full diff vs base reviewed (2 files: src/hooks/session.ts +299/−…, src/hooks/__tests__/session.test.ts); prior signed rounds 1–6 REQUEST_CHANGES re-dispositioned; all current automated Codex findings on this head read. Do not merge. (Comment form: GitHub rejects request-changes self-reviews.)

Round-6 finding disposition

  1. P1 parkPath/token mismatch — FIXED and verified. evidenceParkPath is minted once (session.ts:1193), recorded verbatim into the checkpoint (:1199), and threaded as parkPathOverride into removeIfIdentityMatches (:1207 → :1027), so the recorded path is exactly the mutating rename destination under distinct/random tokens. The new regression "resumes the actual distinct-token evidence park path after an interruption" uses distinct claim/park tokens and asserts resume mints no replacement token. Confirmed against compiled dist.
  2. P2 temp-owner checkpoint resume — FIXED. Resume validation (:1136) now accepts owner.<match[1]>.tmp as well as owner.json; the reconstructed claim path owner.<owner>.<claim>.recovery (:1139) matches the claim-time naming (:1170).
  3. P2 checkpoint vs final rmdir — NOT FIXED; reproduced as permanently bricking at this exact head. The reorder (rmdir :1150 before checkpoint unlink :1151) retains the checkpoint, but the resume sequence is not idempotent: park→quarantine rename (:1143) and claim unlink (:1145) complete before the rmdir. Empirical repro against compiled dist with production dependencies: reconstructed the exact evidence-pending state (claim linked, evidence parked, checkpoint written), injected a single transient EBUSY on the final rmdir, then retried with zero faults:
    • attempt 1: recovered:false / none / "Recovery checkpoint is malformed or no longer identifies its recorded parked evidence"; lock dir left empty, evidence already at .quarantine.*, checkpoint retained;
    • attempts 2 and 3 (no injected faults): identical false / none / malformed — forever, because the single retained checkpoint is found first on every call and lstat(checkpoint.parkPath) (:1137) now ENOENTs. The canonical lock is bricked: empty dir, quarantined owner, unresumable checkpoint; launches keep aborting until manual cleanup.
    • This is precisely the round-6 P2 scenario ("transient EBUSY/ENOTEMPTY … no checkpoint to resume"); retaining a non-retryable checkpoint does not satisfy it. Fix direction: make the resume steps idempotent — tolerate an already-unlinked claim and an already-quarantined park (validate the recorded dev/ino at quarantinePath when parkPath is absent) and retry only the remaining rmdir/unlink; or reorder so no irreversible step precedes the fallible rmdir. Add a regression that injects a one-shot rmdir fault and asserts a clean retry completes recovery.

Standing automated Codex finding on this head — concur

  • P2 "Avoid recovering canonical owners without claiming the directory": the claim hard-link binds the evidence file inode but never the lock directory incarnation between pre-claim revalidation (:1174) and link() (:1176). If a completed recovery + fresh launcher swap in a new live lock inside that window, the post-link identity check (:1187) correctly refuses cleanup of the mismatched claim — leaving a live owner.json plus a foreign .recovery entry, i.e. a permanently ambiguous lock that aborts every launch. Still unaddressed at 06162998.

What survives scrutiny

No-clobber claiming and rollback (link-only, EEXIST refusal), dev/ino revalidation pre/post claim and on quarantine, park-before-delete with EBUSY re-verification, residue-preserving failure diagnostics, symlink/foreign-inode fail-closed paths, checkpoint schema/phase/path binding with out-of-root rejection, multiple-checkpoint refusal, and PID-reuse start-ticks gating all hold; no foreign-byte deletion, no-clobber regression, or path-traversal found. Local verification at this head: npm run build clean; focused suite node --test dist/hooks/__tests__/session.test.js 100/100 pass (including the new distinct-token resume regression). Hosted CI run 29982299996 (head 06162998) is still in progress — all completed lanes green so far (Rust/docs lanes legitimately skipped by lane detection); no MERGE_READY would be posted before terminal green regardless.

Verdict: REQUEST_CHANGES — the round-6 rmdir-retryability P2 is reproduced as unresolved (checkpoint retained but unresumable; lock permanently bricked), and the directory-incarnation claim race remains open.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Independent exact-head adversarial review (round 7 / terminal) — APPROVE

Reviewed immutable head 06162998fa33d9701b3fdad08ff90f079ceffa08: local HEAD == pushed PR head (zero reset drift on review/pr-3262-exact-06162998); clean tracked status; base merge-base d51632f53. Focus: disposition of round-6 findings. (Comment form: GitHub rejects approve self-reviews.)

Round-6 P1 — distinct park-token recording: FIXED

evidenceParkPath is minted once (src/hooks/session.ts:1193), recorded verbatim in the checkpoint (1199), and threaded as parkPathOverride into park-and-verify (1026-1027, 1207), so the recorded path is the actual park path under the production non-constant randomUUID() token source (523). Resume consumes only the recorded path and never mints a token — enforced by the new regression that injects a throwing token() and proves resume succeeds with distinct claim/park tokens ("resumes the actual distinct-token evidence park path after an interruption"). The constant-token masking defect from round 6 is directly covered. Verified locally: build clean; focused suite dist/hooks/__tests__/session.test.js 100/100 including the new regression and the double-EBUSY rollback retry test.

Round-6 P2a — temp-owner checkpoint resumption: FIXED

Resume validation now accepts both the canonical owner.json and the exact owner.<ownerToken>.tmp source path (1136), where the temp token is bound to the checkpoint filename's owner token — no cross-token substitution possible.

Round-6 P2b — final-rmdir/checkpoint ordering: FIXED

Resume now removes the lock directory before unlinking the checkpoint (1150-1151), so a transient EBUSY/ENOTEMPTY at rmdir leaves the checkpoint intact and resumable instead of bricking the lock. The quarantine rollback on claim-unlink failure (1144-1149) and the identity-rechecked EBUSY retry in park/remove (a271673) hold under the existing busy-rename/busy-unlink regressions.

Residual non-blocking observations (follow-ups, not merge blockers)

  • Fresh (non-resume) path still unlinks the evidence checkpoint (1213) before the claim quarantine and lock rmdir complete; a crash inside that window strands a fail-safe unexpected state with all bytes preserved on disk but no checkpoint to resume. Same crash-window class as the fixed P1, far narrower; consider moving the unlink after lock removal or a two-phase checkpoint.
  • The removed parkPath === ${lockPath}.parked-* binding weakens defense-in-depth on forged checkpoints; the claim↔parked dev/ino hard-link binding (1137-1141) still does the real work and forging requires state-dir write access, so threat model is unchanged. A cheap parkPath prefix assertion would restore the invariant.
  • The temp-owner resume branch (1136 second clause) has no dedicated regression test.
  • Line 1193 mints the park token outside the try/validate treatment given to claimToken (1168-1169); a throwing/invalid injected token there would raise uncaught after the claim link exists (still fail-safe on next run via entry-count mismatch, but unstructured).

Hosted CI

All lanes on 06162998 terminal green: CI Status, Typecheck, Lint, Build dist artifact, Node 20 (cli-core-rest, hooks-notify-platform, team-state-runtime), Node 22 smoke, Ralph Persistence Gate, Coverage Gate (Team Critical), Ralplan preflight security (macOS), PR Size Label, Draft Check — pass; Rust/docs/base-guidance lanes legitimately skipped by lane detection.

Regression sweep (non-constant tokens)

No regressions found: claim/evidence/park/quarantine naming composes correctly with per-call randomUUID() tokens — checkpoint filename carries (ownerToken, claimToken), park path is independently recorded, resume reconstructs claim name from the filename and park path from the body, and no path derives one token from another. Park-destination collision preflight, no-clobber claim link, identity revalidation, and residue preservation are intact.

Verdict: APPROVE — all round-6 findings are fixed and regression-covered; residuals are hardening follow-ups. No mutation, no merge performed from this lane.


[repo owner's gaebal-gajae (clawdbot) 🦞]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant