Stop stale ACTIVE sessions from inheriting another session's committed files#1118
Open
Stop stale ACTIVE sessions from inheriting another session's committed files#1118
Conversation
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
Contributor
There was a problem hiding this comment.
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 | |||
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.
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
exithours ago — phase still ACTIVE,LastInteractionTimewithin 24h, noFilesTouched, 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 viafilterFilesTouched's evidence-of-work fallback.The trigger was a hole in
shouldCondenseWithOverlapCheck: the read-only-skip clause only fired whensessionsWithCommittedFiles > 0. When the working session'sstate.FilesTouchedon disk happened to be empty at gate time (e.g. a Stop hook hadn't fired yet for an in-flight turn),sessionsWithCommittedFilesdropped to0and 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). Plainos.Stat, agent-agnostic, so it works for any agent that writes a transcript file (including Vogon, which doesn't implementTranscriptAnalyzer).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 itsstate.FilesTouchedgot cleared by a subsequent flow.A session with empty
FilesTouchedis now skipped if either another session claims the commit or this session has no transcript growth and no shadow contributions.Test plan
TestPostCommit_StaleActiveSession_DoesNotInheritOtherSessionFiles(added skipped in c2dd73a, unskipped here) — passesTestReadOnlySession_ActiveDuringCommit_NotCondensedandTestReadOnlySession_ActiveAcrossMultipleCommits— passmise 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
shouldCondenseWithOverlapCheckto require evidence of work whenFilesTouchedis empty.Adds two new signals to the PostCommit handler—
hasTranscriptGrowth(live transcript file size grew pastCheckpointTranscriptSize) andhasShadowContributions(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’sfiles_touchedin committed metadata.Reviewed by Cursor Bugbot for commit dcd95e2. Configure here.