Skip to content

Stop stale ACTIVE sessions from inheriting another session's committed files#1118

Open
Soph wants to merge 2 commits intomainfrom
soph/checkpoint-session-bug
Open

Stop stale ACTIVE sessions from inheriting another session's committed files#1118
Soph wants to merge 2 commits intomainfrom
soph/checkpoint-session-bug

Conversation

@Soph
Copy link
Copy Markdown
Collaborator

@Soph Soph commented May 5, 2026

https://entire.io/gh/entireio/cli/trails/301

Summary

A recent ACTIVE session that did no real work (e.g. a Codex shell where the user typed exit hours ago — phase still ACTIVE, LastInteractionTime within 24h, no FilesTouched, transcript only contains a startup banner) was being condensed onto a commit authored entirely by a different ACTIVE session and inheriting that commit's file list via filterFilesTouched's evidence-of-work fallback.

The trigger was a hole in shouldCondenseWithOverlapCheck: the read-only-skip clause only fired when sessionsWithCommittedFiles > 0. When the working session's state.FilesTouched on disk happened to be empty at gate time (e.g. a Stop hook hadn't fired yet for an in-flight turn), sessionsWithCommittedFiles dropped to 0 and the stale session slipped through.

The fix adds two evidence-of-work signals checked alongside the existing condition:

  • hasTranscriptGrowth — live transcript file size is greater than the size captured at the last condensation (state.CheckpointTranscriptSize). Plain os.Stat, agent-agnostic, so it works for any agent that writes a transcript file (including Vogon, which doesn't implement TranscriptAnalyzer).
  • hasShadowContributionsstate.StepCount > 0, i.e. SaveStep ran since the last condensation. A session whose Stop hook fired and populated the shadow branch must still condense even if its state.FilesTouched got cleared by a subsequent flow.

A session with empty FilesTouched is now skipped if either another session claims the commit or this session has no transcript growth and no shadow contributions.

Test plan

  • new regression test TestPostCommit_StaleActiveSession_DoesNotInheritOtherSessionFiles (added skipped in c2dd73a, unskipped here) — passes
  • existing TestReadOnlySession_ActiveDuringCommit_NotCondensed and TestReadOnlySession_ActiveAcrossMultipleCommits — pass
  • vogon canary E2E (58 tests) — all pass
  • full strategy + integration suites — pass
  • mise run fmt && mise run lint — clean

🤖 Generated with Claude Code


Note

Medium Risk
Changes PostCommit condensation gating for ACTIVE sessions by adding transcript-growth and shadow-contribution heuristics, which can alter which sessions get condensed and what metadata is recorded in multi-session scenarios.

Overview
Prevents stale but still ACTIVE sessions with no real work from being condensed onto another session’s commit (and inheriting its committed files) by tightening shouldCondenseWithOverlapCheck to require evidence of work when FilesTouched is empty.

Adds two new signals to the PostCommit handler—hasTranscriptGrowth (live transcript file size grew past CheckpointTranscriptSize) and hasShadowContributions (StepCount > 0)—and uses them to skip no-op/read-only ACTIVE sessions while still allowing sessions with shadow-branch work to condense.

Introduces a regression test (TestPostCommit_StaleActiveSession_DoesNotInheritOtherSessionFiles) that reproduces the multi-session inheritance bug and asserts the stale session is skipped or does not claim another session’s files_touched in committed metadata.

Reviewed by Cursor Bugbot for commit dcd95e2. Configure here.

Soph and others added 2 commits May 5, 2026 12:06
Reproduces the multi-session condensation bug observed in the wild
where a still-ACTIVE session that did no real work (e.g. a Codex shell
where the user typed "exit" hours ago) inherits another session's
committed files via filterFilesTouched's evidence-of-work fallback.

Trigger: when sessionsWithCommittedFiles == 0 at gate-check time (no
session's state.FilesTouched on disk overlaps with the committed set
— happens when the working session's tool-use hooks haven't merged
file modifications back to state, or condensation cleared it before
the next SaveStep), shouldCondenseWithOverlapCheck's read-only-skip
clause does not fire and the stale session falls through to condense.
extractSessionDataFromLiveTranscript then yields a non-empty
Transcript with empty FilesTouched, sessionHasEvidenceOfWork returns
true on the transcript-non-empty branch, and the fallback assigns the
committed files to the stale session.

Test is t.Skip()'d so CI stays green; remove the Skip with the fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: b696119f3de4
Stops a stale-but-still-ACTIVE session (no FilesTouched on disk, no
transcript growth since the last condensation, no shadow-branch
contributions) from being condensed onto another session's commit.

Previously, the read-only-skip in shouldCondenseWithOverlapCheck only
fired when sessionsWithCommittedFiles > 0 — so when the working
session's state.FilesTouched on disk happened to be empty at gate
time (e.g. a Stop hook hadn't fired yet for an in-flight turn),
sessionsWithCommittedFiles dropped to 0 and a stale Codex shell that
the user had typed "exit" into hours earlier slipped through and
inherited the committed files via filterFilesTouched's evidence-of-
work fallback.

The new gate adds two more signals — checked alongside the existing
sessionsWithCommittedFiles condition:

  - hasTranscriptGrowth: live transcript file size > the size captured
    at the last condensation (state.CheckpointTranscriptSize). Uses a
    plain os.Stat so it works for any agent that writes a transcript,
    not only those that implement TranscriptAnalyzer (e.g. Vogon in
    the canary suite).
  - hasShadowContributions: state.StepCount > 0, i.e. SaveStep ran
    since the last condensation. A session whose Stop hook fired and
    populated the shadow branch must still condense even if its
    state.FilesTouched got cleared by a subsequent flow.

A session with empty FilesTouched is now skipped if EITHER another
session claims the commit OR this session has no transcript growth
and no shadow contributions. Removes the t.Skip from the regression
test added in c2dd73a.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 6ef1d3b4ed00
@Soph Soph requested a review from a team as a code owner May 5, 2026 11:53
Copilot AI review requested due to automatic review settings May 5, 2026 11:53
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 a multi-session PostCommit condensation bug where a stale-but-still-ACTIVE session (with empty FilesTouched) could be condensed onto another session’s commit and incorrectly inherit the other session’s committed file list via filterFilesTouched’s fallback logic. The change adds additional “evidence of work” signals (live transcript growth and shadow-branch contributions) to prevent stale/read-only ACTIVE sessions from being condensed inappropriately, and adds a regression test reproducing the reported scenario.

Changes:

  • Add transcript-growth and shadow-contribution signals to the ACTIVE-session condensation gate in shouldCondenseWithOverlapCheck.
  • Probe live transcript file size during PostCommit session processing to support agent-agnostic “did work this window?” detection.
  • Add a regression test ensuring a stale ACTIVE session does not inherit another session’s committed files.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
cmd/entire/cli/strategy/phase_postcommit_test.go Adds regression coverage that reproduces the stale ACTIVE session inheritance issue and verifies the stale session is skipped or does not claim files.
cmd/entire/cli/strategy/manual_commit_hooks.go Updates PostCommit condensation gating logic to require evidence of work for empty-FilesTouched ACTIVE sessions; introduces liveTranscriptGrew and threads new signals into the handler.

Comment on lines +664 to 667
hasTranscriptGrowth bool // true when state's live transcript advanced past CheckpointTranscriptStart
hasShadowContributions bool // true when SaveStep ran in this checkpoint window (state.StepCount > 0)
filesTouchedBefore []string
sessionsWithCommittedFiles int // number of processable sessions that have tracked files
Comment on lines +880 to +900
// liveTranscriptGrew reports whether the session's live transcript file has
// grown beyond the size captured at the last condensation
// (CheckpointTranscriptSize). When the session has never been condensed
// (CheckpointTranscriptSize == 0), any non-empty transcript counts as
// growth — the agent has produced *some* content this checkpoint window.
//
// Agent-agnostic: relies only on file size, so it works for agents without
// a TranscriptAnalyzer implementation. Returns false on stat errors or
// when no transcript path is recorded.
func liveTranscriptGrew(state *SessionState) bool {
if state == nil || state.TranscriptPath == "" {
return false
}
info, err := os.Stat(state.TranscriptPath)
if err != nil {
return false
}
if state.CheckpointTranscriptSize > 0 {
return info.Size() > state.CheckpointTranscriptSize
}
return info.Size() > 0
Comment on lines +880 to +901
// liveTranscriptGrew reports whether the session's live transcript file has
// grown beyond the size captured at the last condensation
// (CheckpointTranscriptSize). When the session has never been condensed
// (CheckpointTranscriptSize == 0), any non-empty transcript counts as
// growth — the agent has produced *some* content this checkpoint window.
//
// Agent-agnostic: relies only on file size, so it works for agents without
// a TranscriptAnalyzer implementation. Returns false on stat errors or
// when no transcript path is recorded.
func liveTranscriptGrew(state *SessionState) bool {
if state == nil || state.TranscriptPath == "" {
return false
}
info, err := os.Stat(state.TranscriptPath)
if err != nil {
return false
}
if state.CheckpointTranscriptSize > 0 {
return info.Size() > state.CheckpointTranscriptSize
}
return info.Size() > 0
}
Comment on lines 750 to +788
@@ -759,22 +762,42 @@ func (h *postCommitActionHandler) shouldCondenseWithOverlapCheck(isActive bool,
// (added trailer). The overlap check is only meaningful when we need
// heuristic evidence that a commit was related to the session.
//
// Exception: when another session's tracked files overlap with the
// committed files, skip this ACTIVE session if it has no tracked files
// itself. This prevents read-only sessions (e.g., codex exec from tools
// like summarize) from being condensed when a different session's commit
// triggers PostCommit. When no other session claims the committed files,
// the ACTIVE session is assumed to own the commit.
// Exception: skip ACTIVE sessions that have no evidence of work in this
// checkpoint window — neither tracked files nor transcript growth past
// CheckpointTranscriptStart. This catches read-only sessions (e.g.,
// codex exec from summarize tooling) and stale-but-still-ACTIVE sessions
// (e.g., a Codex shell where the user typed "exit" hours ago but the
// process is still alive). Without this check, those sessions would be
// condensed onto another session's commit and inherit its committed
// files via filterFilesTouched's evidence-of-work fallback.
//
// We check LastInteractionTime to avoid condensing stale ACTIVE sessions
// (agent killed without Stop hook) into every subsequent commit. A stale
// session has no recent interaction and falls through to the overlap check.
// We check LastInteractionTime to avoid condensing very stale ACTIVE
// sessions (agent killed without Stop hook) into every subsequent
// commit. A very stale session has no recent interaction and falls
// through to the overlap check.
if isActive && isRecentInteraction(lastInteraction) {
if h.sessionsWithCommittedFiles > 0 && len(h.filesTouchedBefore) == 0 {
logging.Debug(h.ctx, "post-commit: skipping read-only ACTIVE session (no tracked files, other sessions claim committed files)",
slog.Int("sessions_with_committed_files", h.sessionsWithCommittedFiles),
)
return false
if len(h.filesTouchedBefore) == 0 {
// No tracked files for this session. Skip when EITHER:
// (a) another session's tracked files overlap with the
// committed set — that session "owns" the commit, and
// this one is read-only relative to it (e.g. codex exec
// summarize triggered alongside the real edit session);
// (b) this session has no evidence of work in the current
// checkpoint window — no transcript growth past
// CheckpointTranscriptStart and no SaveStep contributions
// (StepCount == 0). This catches stale-but-still-ACTIVE
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.

2 participants