Conversation
…s view Empty session files (bootstrap entries + session_state, no messages or user content) accumulate as undeletable "(no messages)" rows in the agents view, sometimes pinned by orphaned session leases whose owner process died (#1531). Add a sweep_empty_sessions daemon command that deletes such files from the session directory, skipping any that are open in the daemon, leased by a live process, or bound to scheduled jobs. Reading a lease now reclaims it on the spot when its owner is dead, so ghost rows stop resolving as active. The agents view fires the sweep once on entry before loading the saved-session catalog; failures never block the catalog, and older daemons without the command are tolerated.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e0d1759. Configure here.
| } | ||
| if (entry.type === "session") sawHeader = true; | ||
| } | ||
| return sawHeader; |
There was a problem hiding this comment.
Sweep deletes configured empty drafts
High Severity
isEmptySessionFile treats every model_change, thinking_level_change, and service_tier_change as disposable bootstrap. hasUserContent already treats extra changes after the creation prefix as user configuration, and close/detach keeps those drafts. After such a draft is closed, opening the agents view can still delete it.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit e0d1759. Configure here.
| } | ||
| reclaimStaleLease(directory); | ||
| return false; | ||
| } |
There was a problem hiding this comment.
Unguarded stale lease reclaim race
Medium Severity
hasLiveSessionLease reclaims a lease without withLeaseGuard, unlike acquireSessionLease. After a stale owner is observed, another process can install a live lease before reclaim runs, so the sweep can steal that lease and then delete the session file.
Reviewed by Cursor Bugbot for commit e0d1759. Configure here.
| // Older daemons without the command or transient failures: just load the catalog. | ||
| } | ||
| await this.refreshSavedSessions(); | ||
| } |
There was a problem hiding this comment.
Sweep ignores catalog session directory
Medium Severity
Entry-time sweep calls sweepDaemonEmptySessions without the catalog sessionDir. Saved-session listing uses getSavedSessionCatalogContext(), so a custom session directory is catalogued but not swept, while the daemon default directory may be cleaned instead.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit e0d1759. Configure here.
| (sessionPath) => | ||
| this.findActiveSessionByFile(sessionPath) !== undefined || | ||
| hasLiveSessionLease(sessionPath, this.agentDir) || | ||
| hasJobsForFile(sessionPath), |
There was a problem hiding this comment.
🟡 Medium daemon/daemon-mode.ts:4253
sweep_empty_sessions deletes settings-only drafts even when the user changed the model, thinking level, or service tier before sending a prompt. Because the predicate checks only for active sessions, leases, and jobs, it never applies SessionManager.hasUserContent()'s ordered bootstrap-prefix rule and can remove the user's selected configuration. Reuse hasUserContent() when deciding whether a file is safe to sweep.
- hasJobsForFile(sessionPath),
+ hasJobsForFile(sessionPath) ||
+ SessionManager.open(sessionPath).hasUserContent(),Also found in 1 other location(s)
packages/coding-agent/src/core/session-file-actions.ts:106
The predicate treats every
model_change,thinking_level_change, andservice_tier_changeas bootstrap state. Those entries are also appended when a user changes session settings before sending a first message, so a closed draft with a user-selected model/thinking/tier is classified empty and deleted. ExistingSessionManager.hasUserContent()only ignores the initial ordered creation prefix and treats later configuration entries as user content.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/modes/daemon/daemon-mode.ts around line 4253:
`sweep_empty_sessions` deletes settings-only drafts even when the user changed the model, thinking level, or service tier before sending a prompt. Because the predicate checks only for active sessions, leases, and jobs, it never applies `SessionManager.hasUserContent()`'s ordered bootstrap-prefix rule and can remove the user's selected configuration. Reuse `hasUserContent()` when deciding whether a file is safe to sweep.
Evidence trail:
packages/coding-agent/src/modes/daemon/daemon-mode.ts:4236-4254 @ e0d1759
packages/coding-agent/src/core/session-file-actions.ts:76-112,119-134 @ e0d1759
packages/coding-agent/src/core/session-manager.ts:1559-1583 @ e0d1759
packages/coding-agent/src/core/agent-session.ts:6553-6563 @ e0d1759
Also found in 1 other location(s):
- packages/coding-agent/src/core/session-file-actions.ts:106 -- The predicate treats every `model_change`, `thinking_level_change`, and `service_tier_change` as bootstrap state. Those entries are also appended when a user changes session settings before sending a first message, so a closed draft with a user-selected model/thinking/tier is classified empty and deleted. Existing `SessionManager.hasUserContent()` only ignores the initial ordered creation prefix and treats later configuration entries as user content.
| } catch { | ||
| return false; | ||
| } | ||
| if (typeof entry.type !== "string" || !EMPTY_SESSION_ENTRY_TYPES.has(entry.type)) { |
There was a problem hiding this comment.
🟡 Medium core/session-file-actions.ts:106
A valid JSONL line containing null throws a TypeError at entry.type, aborting sweepEmptySessionFiles instead of retaining that session file and continuing the sweep. Guard the parsed value before accessing entry.type.
| if (typeof entry.type !== "string" || !EMPTY_SESSION_ENTRY_TYPES.has(entry.type)) { | |
| if (!entry || typeof entry !== "object") return false; | |
| if (typeof entry.type !== "string" || !EMPTY_SESSION_ENTRY_TYPES.has(entry.type)) { |
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/core/session-file-actions.ts around line 106:
A valid JSONL line containing `null` throws a `TypeError` at `entry.type`, aborting `sweepEmptySessionFiles` instead of retaining that session file and continuing the sweep. Guard the parsed value before accessing `entry.type`.
Evidence trail:
packages/coding-agent/src/core/session-file-actions.ts:98-109, 119-130 (REVIEWED_COMMIT)
| return data.sessions.map(deserializeSavedSessionInfo); | ||
| } | ||
|
|
||
| export async function sweepDaemonEmptySessions(client: DaemonClient, sessionDir?: string): Promise<string[]> { |
There was a problem hiding this comment.
🟡 Medium daemon/saved-session-catalog.ts:48
sweepDaemonEmptySessions always fails through the public daemon path with Unknown daemon command, so Agents View's entry-time sweep never deletes ghost sessions. DaemonSupervisor rejects sweep_empty_sessions because it is missing from DAEMON_COMMAND_TYPES; add the command to that allowlist and route it to the worker.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/modes/daemon/saved-session-catalog.ts around line 48:
`sweepDaemonEmptySessions` always fails through the public daemon path with `Unknown daemon command`, so Agents View's entry-time sweep never deletes ghost sessions. `DaemonSupervisor` rejects `sweep_empty_sessions` because it is missing from `DAEMON_COMMAND_TYPES`; add the command to that allowlist and route it to the worker.
Evidence trail:
packages/coding-agent/src/modes/daemon/saved-session-catalog.ts:48-57 @ e0d1759; packages/coding-agent/src/modes/agents-view/agents-view-mode.ts:2183-2195 @ e0d1759; packages/coding-agent/src/modes/daemon/daemon-supervisor.ts:158-205,1276-1279 @ e0d1759; packages/coding-agent/src/modes/daemon/daemon-mode.ts:4236-4258 @ e0d1759
| */ | ||
| private async sweepEmptySessionsThenRefreshSaved(): Promise<void> { | ||
| try { | ||
| await sweepDaemonEmptySessions(this.requireClient()); |
There was a problem hiding this comment.
🟡 Medium agents-view/agents-view-mode.ts:2191
Opening Agents View with a custom sessionDir sweeps empty sessions from the daemon's default directory instead of the displayed directory, leaving its ghost sessions intact and potentially deleting unrelated drafts. Pass this.options.config.sessionDir to sweepDaemonEmptySessions so cleanup and catalog loading use the same directory.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/modes/agents-view/agents-view-mode.ts around line 2191:
Opening Agents View with a custom `sessionDir` sweeps empty sessions from the daemon's default directory instead of the displayed directory, leaving its ghost sessions intact and potentially deleting unrelated drafts. Pass `this.options.config.sessionDir` to `sweepDaemonEmptySessions` so cleanup and catalog loading use the same directory.
Evidence trail:
e0d17593eefa96a720af2f6e2033dac05f618344: packages/coding-agent/src/modes/agents-view/agents-view-mode.ts:1320-1326, 2183-2221
e0d17593eefa96a720af2f6e2033dac05f618344: packages/coding-agent/src/modes/daemon/saved-session-catalog.ts:48-57
e0d17593eefa96a720af2f6e2033dac05f618344: packages/coding-agent/src/modes/daemon/daemon-mode.ts:4236-4258
| * owner is dead is reclaimed on the spot so it stops blocking deletion. | ||
| */ | ||
| export function hasLiveSessionLease(sessionPath: string, agentDir: string): boolean { | ||
| const directory = leaseDirectory(agentDir, canonicalSessionPath(sessionPath)); |
There was a problem hiding this comment.
🟠 High core/session-lease.ts:237
hasLiveSessionLease can return false and remove a newly installed live lease, allowing sweep_empty_sessions to delete a session while another process is opening it. The owner check and reclaimStaleLease call are not protected by withLeaseGuard, so another process can replace the lease between them; perform the existence check, owner check, and reclamation under the lease guard.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/core/session-lease.ts around line 237:
`hasLiveSessionLease` can return `false` and remove a newly installed live lease, allowing `sweep_empty_sessions` to delete a session while another process is opening it. The owner check and `reclaimStaleLease` call are not protected by `withLeaseGuard`, so another process can replace the lease between them; perform the existence check, owner check, and reclamation under the lease guard.
Evidence trail:
packages/coding-agent/src/core/session-lease.ts:188-216, 218-246, 249-301 at REVIEWED_COMMIT; packages/coding-agent/src/modes/daemon/daemon-mode.ts:4236-4258 at REVIEWED_COMMIT; packages/coding-agent/src/core/session-file-actions.ts:114-134 at REVIEWED_COMMIT; git diff MERGE_BASE..REVIEWED_COMMIT -- packages/coding-agent/src/core/session-lease.ts
| if (!entry.endsWith(".jsonl")) continue; | ||
| const sessionPath = join(sessionDir, entry); | ||
| if (!isEmptySessionFile(sessionPath) || shouldSkip(sessionPath)) continue; | ||
| const result = await deleteSessionFile(sessionPath).catch(() => undefined); |
There was a problem hiding this comment.
🟠 High core/session-file-actions.ts:129
The sweep can delete a session that becomes active after shouldSkip(sessionPath) returns false, including its artifacts. The lease check and emptiness check are not coordinated with deleteSessionFile, which does not revalidate either condition before unlinking; recheck or atomically claim the file before deletion.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/core/session-file-actions.ts around line 129:
The sweep can delete a session that becomes active after `shouldSkip(sessionPath)` returns `false`, including its artifacts. The lease check and emptiness check are not coordinated with `deleteSessionFile`, which does not revalidate either condition before unlinking; recheck or atomically claim the file before deletion.
Evidence trail:
packages/coding-agent/src/core/session-file-actions.ts:64-73, 119-134 at e0d17593eefa96a720af2f6e2033dac05f618344; packages/coding-agent/src/modes/daemon/daemon-mode.ts:4236-4254 at e0d17593eefa96a720af2f6e2033dac05f618344; packages/coding-agent/src/core/session-lease.ts:236-246, 249-301 at e0d17593eefa96a720af2f6e2033dac05f618344; git diff MERGE_BASE..REVIEWED_COMMIT -- packages/coding-agent/src/core/session-file-actions.ts


What this does
Fixes the "(no messages)" ghost-row pollution from discussion #1531 by cleaning up empty sessions when the user enters the agents view. Sessions are cheap to recreate (ctrl+n /
/new), so abandoned empty drafts get deleted instead of accumulating.How it works
New daemon command
sweep_empty_sessions: deletes session files from the session directory that hold nothing but bootstrap entries (session,model_change,thinking_level_change,service_tier_change) and daemon-writtensession_state— no messages, no user content. A file is skipped when it is:findActiveSessionByFile)hasLiveSessionLease), which also unblocks the "Session became active; stop it before deleting" dead-lease failure mode from [Bug] Ghost sessions: empty '(no messages)' rows undeletable from agents view, orphaned leases never reclaimed, partial fix #1079 has a regression #1531The agents view fires the sweep once on entry, before loading the saved-session catalog. Best-effort: sweep failures (including older daemons without the command) never block the catalog. Protocol schema revision bumps to 17.
Deletion reuses
deleteSessionFile(trash-first, artifacts cleanup) — same path as manual Ctrl+X.Relation to #1079
Ships the spirit of that PR's safe half (bisected safe by the #1531 reporter): sweep + dead-lease reclaim. It deliberately does NOT touch
shouldPersistWithoutAssistant/persistence semantics — the half the reporter bisected as the regression (it broke passive-session discovery). Legitimate passive RLM children are protected here by the live-lease and open-in-daemon guards rather than by file shape.Tests
session-sweep.test.ts: empty-file detection (ghost / with-message / named / headerless / garbled), sweep with skip guard, lease liveness (absent / live / dead-owner reclaim)daemon-mode.test.ts: end-to-end command test — sweeps the ghost, keeps active, leased, job-bound, and real sessionsAll touched suites green (daemon-mode 199, session-lease/artifacts, agents-view suites 116); full typecheck + biome clean.
Linear: ENG-5326
Related: #1531, #1079
Note
Cursor Bugbot is generating a summary for commit e0d1759. Configure here.
Note
Sweep empty ghost sessions when opening the agents view
sweep_empty_sessionsdaemon command that scans a session directory for.jsonlfiles containing only bootstrap entries (no user messages) and deletes them.AgentsViewModerequests the daemon to sweep ghost sessions before loading the saved-session catalog; sweep failures are swallowed so catalog load still proceeds.minSchemaRevision 17for the new command.Macroscope summarized cb9cc61.