Conversation
…ghosts at startup Empty session files (0 messages, only bootstrap entries + session_state:active) accumulated in ~/.prime/agent/sessions/ because closeSessionOnce() short-circuited isEmptyDraftSession via closeKeepsResumeEntry: for shutdown/update reasons the condition was always false, so deleteSessionFile() was never called. Core fix: replace the closeKeepsResumeEntry gate on isEmptyDraftSession with isActiveSessionBusy, mirroring the detach-time discard path (isDiscardableDraft). An empty draft is now deleted on ALL close reasons — but only when the session is truly idle (not streaming, not compacting, no unfinished actions, no running subagents). Safety nets for existing ghosts and orphaned leases: - sweepStaleSessionLeases(): at daemon startup, remove session-lease directories whose owner PID is no longer alive - sweepGhostSessionFiles(): at daemon startup, remove session files that contain only bootstrap entries + session_state (no messages or user content) - Both are best-effort, fire-and-forget, and never block startup
…files
Remove session_state from the _persist bypass so that
appendSessionState("active") in addRuntime() no longer writes a
session file to disk before any assistant message arrives. The file
is created only when the first assistant message triggers _rewriteFile,
which writes all accumulated entries (including the deferred
session_state) at once.
This addresses the root cause that PR PrimeIntellect-ai#1079's cleanup-only approach
missed: ghost sessions were still being born, then living for ~90 min
in active state with no way to delete them.
Keep session_info in the bypass so explicit /name renames persist
immediately.
Added two tests verifying:
- appendSessionState(active) does NOT create a file before an assistant
message
- After the first assistant message, the file IS created and INCLUDES
the session_state entry
|
Hi, thanks for taking the time to contribute to Prime Agent! Since open sourcing the project, we’ve received far more pull requests than we can responsibly review and validate. Prime Agent runs directly on users’ machines, so we need to be deliberate about which changes we accept and how they are reviewed. Rather than leave a large backlog that we cannot meaningfully work through, we’re closing the current PR queue and moving to a discussion-first contribution process. We have established new contribution guidelines to help us continue iterating on Prime Agent and better manage contributions from the community. Going forward, we won’t review unsolicited pull requests. Instead, please start with a GitHub Discussion. We’ll identify recurring bugs and feature requests, create Issues for work we want to pursue, and invite pull requests from maintainers or vouched contributors when implementation is ready. Please read the full process documented in our contribution guidelines. While we’re closing this backlog, we’re still reviewing it at a high level to identify recurring bugs, useful ideas, and important problems that we should address ourselves. Thanks again for the time you put into this! |
Root cause (two parts): a session-lease directory left behind by a crashed or killed process is never reclaimed, so the daemon continues to resolve the corresponding session as active. That blocks deletion from the agents view: Ctrl+X on the resulting '(no messages)' row either does nothing or reports 'Session became active; stop it before deleting'. Separately, an empty draft session file (bootstrap entries only, never sent a message) has no cleanup path once its owning process is gone. Adds two swept startup passes, run once by the top-level daemon supervisor (never a per-session worker), fire-and-forget so a sweep failure never blocks socket binding: - sweepStaleSessionLeases (session-lease.ts): removes every session-leases/*.lock directory whose recorded owner process is dead, reusing the existing isLeaseOwnerAlive/reclaimStaleLease machinery already in this file. Safe by construction -- a live process can always re-acquire a lease that gets swept from under it. - sweepGhostSessionFiles + isEmptyDraftSessionFile (session-file-actions.ts): deletes a session .jsonl whose entries are all bootstrap/state types and which has no live lease, via the existing trash-first deleteSessionFile path. This ports only the verified-safe half of a change bisected from an earlier upstream PR (PrimeIntellect-ai#1079, never merged): a companion change there also excluded the session_state entry type from persistence, which broke legitimate passive RLM child discovery (a passive child shares the exact same on-disk shape as a ghost draft -- bootstrap entries plus session_state, no messages -- and is distinguished only by having a live lease). That companion change is deliberately NOT replicated here; hasLiveSessionLease is the explicit safety check that keeps passive children untouched. 18 new regression tests across session-lease.test.ts (5 for the sweep, 3 for the new hasLiveSessionLease helper) and a new session-file-actions-ghost-sweep.test.ts (10 tests, including the exact passive-RLM-child protection case). Verified two tests fail when the lease-liveness check is removed, reproducing precisely the regression class the upstream companion change introduced -- proving these tests catch that specific failure mode, not just a generic one. Full daemon-mode.test.ts (198 tests, this branch predates the cwd-resume fix's 2 additional tests) plus the two new files: 224 tests, zero regressions. tsgo --noEmit clean across all three touched files. Build + boot gate pass.
Root cause (two parts): a session-lease directory left behind by a crashed or killed process is never reclaimed, so the daemon continues to resolve the corresponding session as active. That blocks deletion from the agents view: Ctrl+X on the resulting '(no messages)' row either does nothing or reports 'Session became active; stop it before deleting'. Separately, an empty draft session file (bootstrap entries only, never sent a message) has no cleanup path once its owning process is gone. Adds two swept startup passes, run once by the top-level daemon supervisor (never a per-session worker), fire-and-forget so a sweep failure never blocks socket binding: - sweepStaleSessionLeases (session-lease.ts): removes every session-leases/*.lock directory whose recorded owner process is dead, reusing the existing isLeaseOwnerAlive/reclaimStaleLease machinery already in this file. Safe by construction -- a live process can always re-acquire a lease that gets swept from under it. - sweepGhostSessionFiles + isEmptyDraftSessionFile (session-file-actions.ts): deletes a session .jsonl whose entries are all bootstrap/state types and which has no live lease, via the existing trash-first deleteSessionFile path. This ports only the verified-safe half of a change bisected from an earlier upstream PR (PrimeIntellect-ai#1079, never merged): a companion change there also excluded the session_state entry type from persistence, which broke legitimate passive RLM child discovery (a passive child shares the exact same on-disk shape as a ghost draft -- bootstrap entries plus session_state, no messages -- and is distinguished only by having a live lease). That companion change is deliberately NOT replicated here; hasLiveSessionLease is the explicit safety check that keeps passive children untouched. 18 new regression tests across session-lease.test.ts (5 for the sweep, 3 for the new hasLiveSessionLease helper) and a new session-file-actions-ghost-sweep.test.ts (10 tests, including the exact passive-RLM-child protection case). Verified two tests fail when the lease-liveness check is removed, reproducing precisely the regression class the upstream companion change introduced -- proving these tests catch that specific failure mode, not just a generic one. Full daemon-mode.test.ts (198 tests, this branch predates the cwd-resume fix's 2 additional tests) plus the two new files: 224 tests, zero regressions. tsgo --noEmit clean across all three touched files. Build + boot gate pass.
Root cause (two parts): a session-lease directory left behind by a crashed or killed process is never reclaimed, so the daemon continues to resolve the corresponding session as active. That blocks deletion from the agents view: Ctrl+X on the resulting '(no messages)' row either does nothing or reports 'Session became active; stop it before deleting'. Separately, an empty draft session file (bootstrap entries only, never sent a message) has no cleanup path once its owning process is gone. Adds two swept startup passes, run once by the top-level daemon supervisor (never a per-session worker), fire-and-forget so a sweep failure never blocks socket binding: - sweepStaleSessionLeases (session-lease.ts): removes every session-leases/*.lock directory whose recorded owner process is dead, reusing the existing isLeaseOwnerAlive/reclaimStaleLease machinery already in this file. Safe by construction -- a live process can always re-acquire a lease that gets swept from under it. - sweepGhostSessionFiles + isEmptyDraftSessionFile (session-file-actions.ts): deletes a session .jsonl whose entries are all bootstrap/state types and which has no live lease, via the existing trash-first deleteSessionFile path. This ports only the verified-safe half of a change bisected from an earlier upstream PR (PrimeIntellect-ai#1079, never merged): a companion change there also excluded the session_state entry type from persistence, which broke legitimate passive RLM child discovery (a passive child shares the exact same on-disk shape as a ghost draft -- bootstrap entries plus session_state, no messages -- and is distinguished only by having a live lease). That companion change is deliberately NOT replicated here; hasLiveSessionLease is the explicit safety check that keeps passive children untouched. 18 new regression tests across session-lease.test.ts (5 for the sweep, 3 for the new hasLiveSessionLease helper) and a new session-file-actions-ghost-sweep.test.ts (10 tests, including the exact passive-RLM-child protection case). Verified two tests fail when the lease-liveness check is removed, reproducing precisely the regression class the upstream companion change introduced -- proving these tests catch that specific failure mode, not just a generic one. Full daemon-mode.test.ts (198 tests, this branch predates the cwd-resume fix's 2 additional tests) plus the two new files: 224 tests, zero regressions. tsgo --noEmit clean across all three touched files. Build + boot gate pass.
Root cause (two parts): a session-lease directory left behind by a crashed or killed process is never reclaimed, so the daemon continues to resolve the corresponding session as active. That blocks deletion from the agents view: Ctrl+X on the resulting '(no messages)' row either does nothing or reports 'Session became active; stop it before deleting'. Separately, an empty draft session file (bootstrap entries only, never sent a message) has no cleanup path once its owning process is gone. Adds two swept startup passes, run once by the top-level daemon supervisor (never a per-session worker), fire-and-forget so a sweep failure never blocks socket binding: - sweepStaleSessionLeases (session-lease.ts): removes every session-leases/*.lock directory whose recorded owner process is dead, reusing the existing isLeaseOwnerAlive/reclaimStaleLease machinery already in this file. Safe by construction -- a live process can always re-acquire a lease that gets swept from under it. - sweepGhostSessionFiles + isEmptyDraftSessionFile (session-file-actions.ts): deletes a session .jsonl whose entries are all bootstrap/state types and which has no live lease, via the existing trash-first deleteSessionFile path. This ports only the verified-safe half of a change bisected from an earlier upstream PR (PrimeIntellect-ai#1079, never merged): a companion change there also excluded the session_state entry type from persistence, which broke legitimate passive RLM child discovery (a passive child shares the exact same on-disk shape as a ghost draft -- bootstrap entries plus session_state, no messages -- and is distinguished only by having a live lease). That companion change is deliberately NOT replicated here; hasLiveSessionLease is the explicit safety check that keeps passive children untouched. 18 new regression tests across session-lease.test.ts (5 for the sweep, 3 for the new hasLiveSessionLease helper) and a new session-file-actions-ghost-sweep.test.ts (10 tests, including the exact passive-RLM-child protection case). Verified two tests fail when the lease-liveness check is removed, reproducing precisely the regression class the upstream companion change introduced -- proving these tests catch that specific failure mode, not just a generic one. Full daemon-mode.test.ts (198 tests, this branch predates the cwd-resume fix's 2 additional tests) plus the two new files: 224 tests, zero regressions. tsgo --noEmit clean across all three touched files. Build + boot gate pass.
Root cause (two parts): a session-lease directory left behind by a crashed or killed process is never reclaimed, so the daemon continues to resolve the corresponding session as active. That blocks deletion from the agents view: Ctrl+X on the resulting '(no messages)' row either does nothing or reports 'Session became active; stop it before deleting'. Separately, an empty draft session file (bootstrap entries only, never sent a message) has no cleanup path once its owning process is gone. Adds two swept startup passes, run once by the top-level daemon supervisor (never a per-session worker), fire-and-forget so a sweep failure never blocks socket binding: - sweepStaleSessionLeases (session-lease.ts): removes every session-leases/*.lock directory whose recorded owner process is dead, reusing the existing isLeaseOwnerAlive/reclaimStaleLease machinery already in this file. Safe by construction -- a live process can always re-acquire a lease that gets swept from under it. - sweepGhostSessionFiles + isEmptyDraftSessionFile (session-file-actions.ts): deletes a session .jsonl whose entries are all bootstrap/state types and which has no live lease, via the existing trash-first deleteSessionFile path. This ports only the verified-safe half of a change bisected from an earlier upstream PR (PrimeIntellect-ai#1079, never merged): a companion change there also excluded the session_state entry type from persistence, which broke legitimate passive RLM child discovery (a passive child shares the exact same on-disk shape as a ghost draft -- bootstrap entries plus session_state, no messages -- and is distinguished only by having a live lease). That companion change is deliberately NOT replicated here; hasLiveSessionLease is the explicit safety check that keeps passive children untouched. 18 new regression tests across session-lease.test.ts (5 for the sweep, 3 for the new hasLiveSessionLease helper) and a new session-file-actions-ghost-sweep.test.ts (10 tests, including the exact passive-RLM-child protection case). Verified two tests fail when the lease-liveness check is removed, reproducing precisely the regression class the upstream companion change introduced -- proving these tests catch that specific failure mode, not just a generic one. Full daemon-mode.test.ts (198 tests, this branch predates the cwd-resume fix's 2 additional tests) plus the two new files: 224 tests, zero regressions. tsgo --noEmit clean across all three touched files. Build + boot gate pass.
Closes #1078
Problem
Empty session files (ghost sessions) accumulate in
~/.prime/agent/sessions/after every daemon shutdown or update restart. These files contain zero messages — only the 4 bootstrap entries plus a daemon-writtensession_state:active(~768 bytes). Orphaned session-lease directories also accumulate when a daemon exits without releasing its lease. Neither is ever cleaned up.Full diagnosis and evidence in #1078.
Root cause
Two layers:
1. Ghost creation (now fixed)
SessionManager._persist()allowedsession_stateentries to bypass the "no assistant message → don't write to disk" guard:When
addRuntime()callsappendSessionState({ status: "active" }),_persistwrote the file immediately — before any user message. This is the ghost's birth.session_infobypass is kept so explicit/namerenames persist immediately. The file is now created only when the first assistant message triggers_rewriteFile, which writes all accumulated entries (including the deferredsession_state) at once.2. Ghost persistence (original PR #1079 fix)
closeSessionOnce()gatedisEmptyDraftSessiononcloseKeepsResumeEntry(reason):For shutdown/update,
keepsResumeEntryistrue→isEmptyDraftSessionis alwaysfalse→deleteSessionFile()is never called. The empty draft stays on disk permanently.Fix
Root cause prevention (commit
df9959a)Remove
session_statefrom the_persistbypass so ghost files are never created in the first place.Cleanup at close (commit
153b291)Replace the
keepsResumeEntrygate withisActiveSessionBusy— mirroring the existingisDiscardableDraft()in the detach-time discard path:Empty drafts are now deleted on all close reasons, but only when the session is truly idle (not streaming, not compacting, no unfinished actions, no running subagents).
keepsResumeEntrystill gates archiving and error propagation unchanged.Safety nets
sweepStaleSessionLeases(agentDir)insession-lease.tssweepGhostSessionFiles(sessionDir)insession-file-actions.tssweepStaleStartupState()indaemon-mode.tsstart()Consumer safety (verified — root cause fix is safe)
All 5 places that read
state.statusfrom disk were verified safe after removingsession_statefrom the_persistbypass:inactiveLifecycleForSessionstatus === "archived"isPersistedCronJobRunnablestatus !== "active"restoreRlmHeartbeatSessionparentInfo.state?.statuscreateRuntime→"active"re-writtenarchiveSession"archived"deactivatePendingAgent"archived"existsSync(sessionFile)before openingTests (9 new, all green)
session-manager-flush.test.tsappendSessionState(active)does NOT create a file before assistant messagesession-manager-flush.test.tssession_stateentrydaemon-mode.test.tsdaemon-mode.test.tsdaemon-mode.test.tshasRunningRlmChildrensession-lease.test.tssession-lease.test.tssession-artifacts-delete.test.tssession-artifacts-delete.test.tsChecklist
tsgo --noEmit) passes