Skip to content

feat(coding-agent): sweep empty ghost sessions when opening the agents view - #1561

Closed
snimu wants to merge 2 commits into
mainfrom
feat/agents-view-empty-session-sweep
Closed

snimu wants to merge 2 commits into
mainfrom
feat/agents-view-empty-session-sweep

Conversation

@snimu

@snimu snimu commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

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-written session_state — no messages, no user content. A file is skipped when it is:

The 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 sessions

All 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

  • Adds a sweep_empty_sessions daemon command that scans a session directory for .jsonl files containing only bootstrap entries (no user messages) and deletes them.
  • Sessions are protected from deletion if they are open in the daemon, held by a live lease, or bound to an incomplete scheduled job; stale leases from dead processes are reclaimed in place.
  • On entering the agents view, AgentsViewMode requests the daemon to sweep ghost sessions before loading the saved-session catalog; sweep failures are swallowed so catalog load still proceeds.
  • Bumps the daemon protocol schema to revision 17 with a compatibility gate on minSchemaRevision 17 for the new command.

Macroscope summarized cb9cc61.

…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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

❌ 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e0d1759. Configure here.

}
reclaimStaleLease(directory);
return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e0d1759. Configure here.

// Older daemons without the command or transient failures: just load the catalog.
}
await this.refreshSavedSessions();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e0d1759. Configure here.

(sessionPath) =>
this.findActiveSessionByFile(sessionPath) !== undefined ||
hasLiveSessionLease(sessionPath, this.agentDir) ||
hasJobsForFile(sessionPath),

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.

🟡 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, 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.

🚀 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)) {

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.

🟡 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.

Suggested change
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[]> {

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.

🟡 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());

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.

🟡 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));

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.

🟠 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);

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.

🟠 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

@snimu snimu closed this Aug 19, 2026
@kevinjosethomas
kevinjosethomas deleted the feat/agents-view-empty-session-sweep branch September 8, 2026 20:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant