fix(session): auto-adopt across git common dirs on commit - #1777
Open
suhaanthayyil wants to merge 33 commits into
Open
fix(session): auto-adopt across git common dirs on commit#1777suhaanthayyil wants to merge 33 commits into
suhaanthayyil wants to merge 33 commits into
Conversation
When prepare-commit-msg runs in an enabled repo with no local ACTIVE session, discover a unique recent cross-common-dir candidate (live registry or sibling scan) whose FilesTouched overlaps staged paths and adopt it so Entire-Checkpoint trailers are not silently dropped.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR addresses cross-repository agent session continuity by enabling prepare-commit-msg to automatically adopt a unique, recent session from another git common dir when the current repo has no local adoptable session state—so commits in the new repo can still receive an Entire-Checkpoint trailer.
Changes:
- Introduces a best-effort per-user “live session” registry in the user cache to enable fast cross-common-dir discovery without filesystem scans.
- Adds
prepare-commit-msgauto-adopt logic that filters candidates by recency and requiredFilesTouched↔ staged-file overlap, falling back to scanning sibling repos when the registry is inconclusive. - Extends
session adoptinternals with an option to skip transcript-path validation for auto-adopt, clearing invalid transcript paths instead of failing adoption.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| cmd/entire/cli/session/state.go | Registers/unregisters sessions in the live registry on save/clear. |
| cmd/entire/cli/session/live_registry.go | Implements the live-session registry stored under the user cache. |
| cmd/entire/cli/session/live_registry_test.go | Unit tests for registry behavior and Save/Clear integration. |
| cmd/entire/cli/session_auto_adopt.go | Implements hook-time cross-common-dir auto-adoption and candidate selection logic. |
| cmd/entire/cli/session_auto_adopt_test.go | Regression tests covering registry-based and sibling-scan adoption plus skip cases. |
| cmd/entire/cli/session_adopt.go | Adds SkipTranscriptValidation and transcript-path clearing for auto-adopt safety. |
| cmd/entire/cli/hooks_git_cmd.go | Invokes auto-adopt during prepare-commit-msg before trailer insertion. |
Require matching agent owner plus FilesTouched overlap so unrelated sibling repos cannot steal live sessions via common relative paths. Sweep stale live-registry entries on list, guard nil RegisterLiveSession, skip auto-adopt for merge/squash sources, and treat git as transient for owner resolution under prepare-commit-msg. Co-authored-by: Cursor <cursoragent@cursor.com>
Gate prepare-commit-msg auto-adopt on IsGitSequenceOperation and surface a warning when SkipTranscriptValidation clears an invalid transcript pointer. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
adoptFromExternalSessionStore is called with the raw hook ctx, but WithSessionStateLocks acquired its flock via the blocking syscall.Flock(LOCK_EX), which ignores context cancellation. A lock held by another process could block git commit indefinitely. Add flock.AcquireContext (unix + windows), a non-blocking try-lock that polls until ctx is done, and switch WithSessionStateLocks to use it so a deadline actually bounds lock acquisition. Wrap the auto-adopt call site with a 2s timeout (autoAdoptAdoptTimeout), matching the existing discovery timeouts; on timeout it logs and returns without adopting, never erroring back to the hook. Co-authored-by: Cursor <cursoragent@cursor.com>
Remove always-set-never-read autoAdoptCandidate.OwnerMatch/OverlapMatch and LiveSessionEntry.Owner/FilesTouched/AgentType (written by RegisterLiveSession but never consumed — registry candidates re-load source state). Drop the now-dead cloneOwner helper and its imports.
…window Add strategy.SkipsPrepareCommitMsg(ctx, source) as the single source of truth for the merge/squash/git-sequence-operation skip invariant, shared by ManualCommitStrategy.PrepareCommitMsg and the auto-adopt gate so they cannot drift; un-export IsGitSequenceOperation (it only covered the sequence-op third). Define adoptRecentWindow as session.LiveSessionMaxAge instead of a second independent 12h const tied only by a comment.
Read registry entries through the already-held os.Root (osroot.ReadFile) so a symlink planted in the cache dir cannot redirect the read off-tree. Add Debug logs when corrupt-entry and TTL sweeps delete files so the evidence is not removed silently, and document that liveSessionExpired treats a nil LastInteractionTime as expired (version-skew tripwire).
…ncancelable Move the package doc and acquirePollInterval into a new untagged flock.go so they are defined once instead of duplicated per-platform. Wrap every release in sync.Once (onceRelease) so a double release cannot call UnlockFileEx on an already-closed Windows handle. In AcquireContext, when ctx has no Done channel (Background/TODO — neither deadline nor cancel) fall back to the blocking Acquire instead of busy-polling forever; cancelable contexts keep the poll loop so cancellation is still observed.
Auto-adopt runs in prepare-commit-msg (swallowed stderr) and logged adopt failures at Debug, so a two-repos-active corruption could pass unnoticed. Log ordinary adopt failures at Warn; introduce adoptRollbackFailedError for the retire-failed-AND-rollback-failed case and log that at Error. Recover from any panic in the adopt path and log it at Error (never break git commit). Run the compensating rollback Save on context.WithoutCancel(ctx) so a canceled/timed-out caller context cannot turn a recoverable retire failure into both-repos corruption.
The live-session registry is keyed by session ID alone, so a cross-repo adopt's source retire (Save of the tombstoned source state -> UnregisterLiveSession) deleted the entry the target Save had written one statement earlier, erasing the adopted session from the registry. UnregisterLiveSession now takes the caller's common dir and is a no-op when the on-disk entry belongs to a different common dir; Save/Clear pass their store's common dir. Add TestLiveRegistry_CrossRepoRetireKeepsTargetEntry proving the target entry survives the source retire.
git diff --cached --name-only C-quotes non-ASCII paths (core.quotepath defaults on), so an agent working on a non-ASCII or spaced file never matched the UTF-8 paths in FilesTouched and the overlap check silently failed. Use -z and split on NUL. Add TestStagedFilesForAutoAdopt_NonASCIIAndSpacedPaths.
…queness test The sibling scan previously ran only when the registry returned zero candidates, so a single registry hit bypassed the len(candidates)!=1 ambiguity guard even when a second distinct candidate existed on disk — auto-adopt could then steal one of two concurrent sessions. Always run both discovery sources and union them (deduped by session ID) before the uniqueness check via unionAutoAdoptCandidates.
hasLocalActiveSession used isAdoptableSourceSession, which accepts Idle sessions of any age, so a single months-old Idle state file permanently disabled cross-common-dir auto-adopt for the whole repo. Use isRecentAdoptCandidate so only sessions inside adoptRecentWindow count, and add a Debug log on the fail-closed List-error branch.
TestAutoAdopt_SkipsDistantRegistryEntry used README.md, so the boilerplate overlap guard rejected first and proximity was never exercised — switch to a distinctive path (services/billing/handler.go) so proximity is the sole rejecting guard. Add owner-mismatch coverage with a distinctive path at both levels: a flow-level TestAutoAdopt_SkipsOwnerMismatchDistinctivePath and a candidateFromLoaded unit test with a matching-owner positive control that proves the owner guard (not overlap) is what rejects.
The transcript-loss warning writes to /dev/tty only best-effort: agent committers have no controlling terminal and Windows has no /dev/tty, so the durable record is the logging.Warn in .entire/logs. Fix the buildHookSpecs and writeAdoptUserWarning comments that claimed the tty write always surfaces / that manual adopt reaches this path (only auto-adopt sets SkipTranscriptValidation). Route the stderr fallback through a package-level adoptWarningWriter and switch TestClearInvalidAdoptTranscript_WarnsAndClears to override it instead of swapping the process-global os.Stderr.
Add an 'Automatic cross-common-dir adoption (#1439)' section to sessions-and-checkpoints.md covering the ~/.cache/entire/live-sessions/ registry, the destructive prepare-commit-msg adopt behavior, and the adoption-safety invariants (enabled+trailer, no recent local session, owner match, non-boilerplate overlap, sibling proximity, uniqueness). Note the live-sessions registry in the root CLAUDE.md test-isolation cache list so new harnesses isolate XDG_CACHE_HOME. Fix the code comment that referenced a non-existent feature doc.
…udget The per-step timeouts (target resolve + staged + registry + sibling + adopt) are worst-case and stack to ~7.5s, adding that much latency to a git commit on the miss path. Wrap the whole attempt in one 5s wall-clock budget so every step derives its timeout from it and the worst-case sum is bounded. Opt-out setting intentionally left to the author.
…-commit Cross-common-dir auto-adopt tombstoned the source session inside prepare-commit-msg, before the commit existed. An aborted commit (editor abort or empty-message strip) permanently retired the source's checkpointing with no commit to show for it. Split the adopt across the two hooks: prepare-commit-msg registers the adopted session in the target (so the checkpoint trailer lands) and stamps a PendingSourceRetire marker, but leaves the source ACTIVE; post-commit runs finalizePendingSourceRetires once the commit is a fact, tombstoning the source and clearing the marker. The retire is idempotent, panic-guarded, and time bounded. Manual 'entire session adopt' keeps its immediate retire. Entire-Checkpoint: 01KYWT59HPRCDP79WRB1Z6FB12
) Issue #1439's own repro is cross-parent — an agent session in .../entire.io/.worktrees/... committing in an unrelated /private/tmp/... checkout — but both discovery paths required parent-dir proximity, so the motivating case never got a checkpoint trailer. Drop the proximity gate from the registry discovery path (collectRegistryAutoAdoptCandidates); it now reaches any worktree, relying on the stronger owner + non-boilerplate-overlap + uniqueness guards to prevent cross-repo steals. The immediate-sibling scan stays inherently parent-scoped. Removes the now-unused autoAdoptSiblingProximity helper. Flips TestAutoAdopt_SkipsDistantRegistryEntry to TestAutoAdopt_AdoptsDistantRegistryEntry (asserts the non-sibling adopt + trailer landing) and adds TestAutoAdopt_SkipsDistantRegistryOwnerMismatch proving the owner guard still rejects non-siblings. Entire-Checkpoint: 01KYWTE0EVNGGTRD3YFKSF7472
Drive the real 'entire hooks git post-commit' binary after a real git commit to cover the finding #74a prepare/post-commit split end-to-end (the in-process unit tests only call the helpers directly): - PostCommitFinalizeRetiresSource: post-commit tombstones the deferred source and clears the marker. - ContendedSourceLockLeavesSourceActive: holding the source flock across the process boundary makes finalize give up within its timeout without corrupting state (source stays ACTIVE, marker retained for retry). - DisabledTargetDoesNotFinalize: a disabled target's hooks are inert. The owner-gated cross-repo discovery path stays unit-covered: proclive owner fingerprinting resolves a spawned hook's owner to the go-test process itself, which the external integration package cannot seed a match for. Entire-Checkpoint: 01KYWV34GRNR1AXPFNWWN8XD2D
…ent double-adopt The deferred cross-common-dir auto-adopt (DeferSourceRetire) registers the adopted session in the target and stamps PendingSourceRetire, but left the source session untouched at prepare time. Two targets that concurrently discover the same unique live source could both pass the exactly-one-candidate check, acquire the shared source lock in turn, and each register their own copy of the same SessionID — a double-adopt, because neither wrote anything the other would observe. The in-band tombstone the deferral removed had provided that cross-process mutual exclusion. Restore it without re-tombstoning at prepare time: under the same source-common- dir lock, stamp a non-destructive AdoptClaim (target common dir + timestamp) on the source. It does not end the session, so the source agent keeps checkpointing (preserving the deferral's abort-safety). A second concurrent adopt re-reads the source under the lock, observes a fresh claim by a different target, and refuses (sourceClaimedError -> auto-adopt skips). The candidate discovery filter drops a claimed source too. post-commit finalize's retire supersedes the claim; a claim older than adoptRecentWindow is ignored, so an abandoned claim from an aborted commit self-heals. A re-claim by the same target is idempotent. Entire-Checkpoint: 01KYX128X8JKV7FH75QR1FV8NW
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.
Trail: https://entire.io/gh/entireio/cli/trails/875
Summary
Fixes #1439
What
Auto-adopt a unique ACTIVE agent session from another git common dir during
prepare-commit-msg, so commits in an enabled repo B get anEntire-Checkpointtrailer when the live session state still lives under repo A.Why / how it helps
Long agent sessions often move across independently enabled repos. Hooks fire in B, but B's
.git/entire-sessionsis empty, so the trailer is silently skipped even though Entire is installed. Manualentire session adopt(#1472) already exists; this wires a safe automatic path on commit.How
prepare-commit-msg, if the current repo is enabled and has no local ACTIVE session, discover exactly one recent cross-common-dir candidate via:…/entire.io/.worktrees/…committing in an unrelated/private/tmp/…checkout), thenFilesTouched∩staged overlap and a process-owner match for every candidate (the registry path drops proximity and leans on these stronger guards to prevent cross-repo steals); ambiguous candidates skip.prepare-commit-msgregisters the adopted session in the target (so the trailer lands) and stamps aPendingSourceRetiremarker, but leaves the source ACTIVE;post-commitcompletes the destructive source-side retire once the commit is a fact. An aborted commit therefore never strands the source with its checkpointing retired and no commit.Testing
entire-sessionsempty./tmp/entire-1439):Entire-Checkpoint: …Entire-Checkpoint: …and session state present under B.TestAutoAdopt_*,TestLiveRegistry_*, existingTestSessionAdopt_*.tryAutoAdoptCrossCommonDirSession→TestAutoAdopt_PrepareCommitMsg_ViaLiveRegistryFAIL; restore → PASS.mise run fmt && mise run lintgreen;go test ./cmd/entire/cli/session ./cmd/entire/cligreen on committed tree.Review follow-ups (trail 875)
Three architectural review findings addressed on top of the initial change:
prepare-commit-msgtopost-commit(finalizePendingSourceRetires), so an aborted commit no longer retires the source with no commit. Idempotent, panic-guarded, time-bounded.autoAdoptSiblingProximityhelper.integration_test/auto_adopt_integration_test.godriving the realentire hooks git post-commitbinary after a realgit commit— post-commit finalize retires the source, a held source flock makes finalize give up without corruption, and a disabled target stays inert. Full integration suite green (440 passed).