Skip to content

fix(session): auto-adopt across git common dirs on commit - #1777

Open
suhaanthayyil wants to merge 33 commits into
mainfrom
fix/1439-auto-adopt-cross-common-dir
Open

fix(session): auto-adopt across git common dirs on commit#1777
suhaanthayyil wants to merge 33 commits into
mainfrom
fix/1439-auto-adopt-cross-common-dir

Conversation

@suhaanthayyil

@suhaanthayyil suhaanthayyil commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

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 an Entire-Checkpoint trailer 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-sessions is empty, so the trailer is silently skipped even though Entire is installed. Manual entire session adopt (#1472) already exists; this wires a safe automatic path on commit.

How

  • Maintain a best-effort live-session registry under the user cache whenever an ACTIVE session is saved (cleared on end/clear/tombstone).
  • On prepare-commit-msg, if the current repo is enabled and has no local ACTIVE session, discover exactly one recent cross-common-dir candidate via:
    1. live-session registry — reaches non-sibling worktrees under unrelated parents (covers Agent sessions should adopt enabled repos when a long session switches git common dirs #1439's own repro: a session in …/entire.io/.worktrees/… committing in an unrelated /private/tmp/… checkout), then
    2. immediate sibling repos under the parent directory (microservices / seeded repro).
  • Require a non-boilerplate FilesTouched∩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.
  • The adopt is split across the two commit hooks: prepare-commit-msg registers the adopted session in the target (so the trailer lands) and stamps a PendingSourceRetire marker, but leaves the source ACTIVE; post-commit completes 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.
  • Tradeoff: this is direction-1 handoff (adopt/retire), not a full joint multi-repo session model (issue direction 3). Coordinated A+B commits in one turn still need a future fan-out model.

Testing

  • Live repro (before): A trailer present; B message unchanged / no trailer; B entire-sessions empty.
  • Live repro (after, same script with /tmp/entire-1439):
    • A: Entire-Checkpoint: …
    • B: Entire-Checkpoint: … and session state present under B.
  • Edges: no FilesTouched overlap → no adopt; two overlapping sibling sources → no adopt.
  • Unit/regression: TestAutoAdopt_*, TestLiveRegistry_*, existing TestSessionAdopt_*.
  • Mutation: early-return no-op in tryAutoAdoptCrossCommonDirSessionTestAutoAdopt_PrepareCommitMsg_ViaLiveRegistry FAIL; restore → PASS.
  • Audit: two clean passes after requiring overlap (blocks owner-only steal) + wrapcheck/lint fixes.
  • mise run fmt && mise run lint green; go test ./cmd/entire/cli/session ./cmd/entire/cli green on committed tree.

Review follow-ups (trail 875)

Three architectural review findings addressed on top of the initial change:

  • Deferred source retire (HIGH): the destructive source-side tombstone moved from prepare-commit-msg to post-commit (finalizePendingSourceRetires), so an aborted commit no longer retires the source with no commit. Idempotent, panic-guarded, time-bounded.
  • Non-sibling registry reach (HIGH): dropped the parent-dir proximity gate from the registry discovery path so Agent sessions should adopt enabled repos when a long session switches git common dirs #1439's own cross-parent repro actually gets a trailer; steal-safety now rests on owner + non-boilerplate-overlap + uniqueness. Removed the now-unused autoAdoptSiblingProximity helper.
  • Integration coverage (MED): added integration_test/auto_adopt_integration_test.go driving the real entire hooks git post-commit binary after a real git 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).

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.
Copilot AI review requested due to automatic review settings July 16, 2026 14:50
@suhaanthayyil
suhaanthayyil requested a review from a team as a code owner July 16, 2026 14:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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-msg auto-adopt logic that filters candidates by recency and required FilesTouched ↔ staged-file overlap, falling back to scanning sibling repos when the registry is inconclusive.
  • Extends session adopt internals 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.

Comment thread cmd/entire/cli/session/live_registry.go
Comment thread cmd/entire/cli/hooks_git_cmd.go
suhaanthayyil and others added 25 commits July 21, 2026 19:40
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>
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.
suhaanthayyil and others added 7 commits July 31, 2026 15:09
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Agent sessions should adopt enabled repos when a long session switches git common dirs

2 participants