Skip to content

(sidebar): hide sessions launched through the Agent SDK - #486

Merged
devsuitup merged 9 commits into
devsuitup:mainfrom
paulo-jay:hide-sdk-sessions
Oct 7, 2026
Merged

devsuitup merged 9 commits into
devsuitup:mainfrom
paulo-jay:hide-sdk-sessions

Conversation

@paulo-jay

Copy link
Copy Markdown

Problem

Programs that drive Claude through the Agent SDK write ordinary top-level transcripts in the project folder. Examples are a Python review tool that starts one session per batch of files, the brain-runner's claude -p episodes, and strap's developers. These transcripts have isSidechain: false, no subagents/ directory and no record pointing back at the session that started them. In internal/runner they buried the real sessions under dozens of "Security review of …" rows.

The only reliable marker is the entrypoint field the CLI writes on every record: cli when typed in a terminal, sdk-cli / sdk-py / sdk-ts when launched through the SDK. On one machine that is about 4,500 SDK transcripts against about 540 interactive ones.

Change

  • readSessionFile keeps the entrypoint of the first type: 'user' record, and it is stored in a new session_cache.entrypoint column. The column is added through schema reconciliation with mustReindex, so the cache is rebuilt once.
  • buildProjectsFromCache leaves sdk-* rows out of the project list while the new global hideSdkSessions setting is on (the default). The setting has a toggle in Global Settings; changing it reloads the project list.
  • Scheduled runs stay visible. Their first user turn is pre-seeded by createScheduleSession without an entrypoint, and only the later claude --resume -p records say sdk-cli.
  • Rows stay in session_cache, session_metrics and FTS, so the heatmap and token totals still count that activity.

Documentation: .ai/contexts/session-cache.md ("SDK-launched sessions"), docs/settings.md and docs/session-browser.md.

Tests

  • New test/hide-sdk-sessions.test.js covers how the entrypoint is read, a scheduled run, the hidden-by-default case and the setting turned off.
  • task check: 0 errors, 0 failing tests.

https://claude.ai/code/session_0121XfXy4y1Fwt3wJsD4v82q

pjay added 4 commits October 7, 2026 14:29
Programs driving Claude through the Agent SDK (headless claude -p runs,
review tools spawning one session per file batch) write top-level
transcripts with no link to the session that started them, so they
flooded project groups as unrelated sessions. Keep the entrypoint of the
first user turn in session_cache and leave sdk-* rows out of the project
list while the new hideSdkSessions setting (default on) is set.
Scheduled runs stay visible: their first turn is pre-seeded without an
entrypoint.

Claude-Session: https://claude.ai/code/session_0121XfXy4y1Fwt3wJsD4v82q
Subagents of a hidden SDK session fell into Orphan subagents and kept
SDK-only projects listed. A session resumed from a terminal stayed hidden
because only its first turn counted, and the header-only refresh never saw
the later turn: count any cli turn, and re-read cached SDK rows in full.

Claude-Session: https://claude.ai/code/session_0121XfXy4y1Fwt3wJsD4v82q
A 13 MB live SDK transcript re-read in full costs ~250 ms of main thread
per watcher flush; above 2 MB keep the header-only refresh.

Claude-Session: https://claude.ai/code/session_0121XfXy4y1Fwt3wJsD4v82q
@paulo-jay

Copy link
Copy Markdown
Author

@devsuitup ready for review: CI is green and an internal review loop converged with no remaining findings (its fixes are in 5394ae8 and 843f633).

@devsuitup devsuitup left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Review at 843f633. The goal is clear and the filter is in the right place (the rows stay in session_cache, session_metrics and FTS, so the heatmap and token totals keep counting that activity). The branch is level with main, and 224 tests in 22 related files pass locally on Node 24. Four points block; each was traced by the reviewer in the shipped code, and I re-read the two I could check directly.

Blocking

  1. An SDK session the user has open disappears from under them (session-cache.js, buildProjectsFromCache, the hiddenSdkIds skip at ~481). Turning the setting on (or on the first rebuild) removes an open SDK session from the sidebar and grid listings, and a cold restore treats a saved SDK session as unavailable. A session that is open in a terminal, or in the saved working set, must stay listed. Test: open an sdk-* session, then check that it is still listed with the setting on.
  2. An interactive continuation of an SDK session never makes the parent visible (session-cache.js ~447). A resumed transcript whose later records say cli, or a compaction mirror, keeps the parent hidden even after a rebuild. The first-user record alone decides, so a session the user continued by hand can be lost. Decide on the whole transcript (or treat a later cli record as interactive) and test it.
  3. The schema migration clears the cache before the single-instance lock (db.js ~313 and ~345, main.js ~3278). Adding the entrypoint column sets mustReindex, which runs DELETE FROM session_cache and DELETE FROM cache_meta when db.js is loaded, before requestSingleInstanceLock. A second launch that is refused still empties the database of the running instance, and the running version does not know the new column. The fileMtime migration already works that way, but a new column should not repeat it: take the lock before opening the database, or make the column added without a wipe (a NULL entrypoint means "unknown, rescan lazily").
  4. A failure before the first scan result leaves zero cached sessions (db.js ~345). After the wipe there is no previous cache to fall back to, so an interruption or an error during the scan shows an empty app until the next successful scan. Keep the old rows until the new scan has written its results, or rebuild into the same rows.

Non-blocking

  • A resumed SDK transcript above 2 MB stays hidden until a full rebuild (session-cache.js ~247).
  • A project that holds only SDK sessions keeps its header in the sidebar, empty (session-cache.js ~579).
  • A non-string entrypoint in a record may make the bind fail (db.js ~561); validate it to a string or null before storing.

Not run: Linux Node 20/22 with c8, a large-cache benchmark of the one-time reindex, lint, fresh CI. CI on this head is not approved yet; I will approve it once the blocking points are fixed.

pjay added 2 commits October 7, 2026 15:36
… wipe

Review of devsuitup#486: an open or saved SDK session vanished from the list, a
continuation typed in a compaction mirror kept its parent hidden, and the
entrypoint column wiped session_cache from db.js, before the single-instance
lock. The column now lands as NULL (visible) and a chunked backfill fills it
from each transcript's head; a cheap entrypoint scan replaces the full
re-read of cached SDK rows; an SDK-only project no longer keeps an empty
header; non-string entrypoints are stored as none.

Claude-Session: https://claude.ai/code/session_0121XfXy4y1Fwt3wJsD4v82q
An SDK prompt is written first as a queue-operation line that can exceed
the 256 KB head on its own, so the backfill left ~190 SDK sessions visible.
Read the head in chunks until the first user turn.

Claude-Session: https://claude.ai/code/session_0121XfXy4y1Fwt3wJsD4v82q
@paulo-jay

Copy link
Copy Markdown
Author

@devsuitup thanks for the review. Every point is addressed in 732628f and 7f1dd0d. CI is green on 7f1dd0d, and an internal re-review run against your list found nothing left.

Blocking

  1. An open SDK session disappeared. hiddenSdkSessionIds (session-cache.js) keeps every session open in a terminal (activeSessions, not exited) and every session in global.openWorkingSet. They stay listed, reach sessionMap, and restore normally. Test: "an SDK session open in a terminal or in the saved working set stays listed".
  2. An interactive continuation left the parent hidden. entrypoint becomes cli as soon as any user record says cli. A parent whose compaction mirror is not SDK stays visible. refreshFolder no longer re-reads the file in full: a cheap readSessionEntrypoint scan picks up a terminal turn appended to a cached SDK row. That scan reads the whole file up to 2 MB, and its last 256 KB beyond that, so N1 is covered too. Tests: "…typed into from a terminal…", "…continued from a terminal in a compaction mirror…", "refreshFolder notices a terminal turn…", "…tail of a large SDK transcript".
  3. and 4. The cache wipe at migration. The entrypoint column is now added without mustReindex, so nothing is wiped before the single-instance lock and there is no window with an empty cache. NULL means not read yet and is treated as visible. backfillEntrypoints() fills those rows lazily from get-projects, 200 rows per setImmediate tick, reading only each transcript's head. Measured over 5,040 local transcripts: about 4.4 s in total, 17.7 ms for the slowest file, and no disagreement with the full read on what is hidden. Test: "adding the entrypoint column keeps the cached sessions", which seeds a pre-column database, loads db.js and checks the row is kept with a NULL entrypoint.

Non-blocking

  • A resumed transcript over 2 MB stayed hidden: the tail scan covers it (see 2).
  • An empty header for an SDK-only project: fixed. Test: "a project holding only SDK sessions gets no empty header".
  • A non-string entrypoint: stored as '' (none) when read, and bound as typeof === 'string' ? value : null.

The context doc (.ai/contexts/session-cache.md, "SDK-launched sessions"), docs/session-browser.md and the CHANGELOG entry are updated to match.

@devsuitup devsuitup left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Review at 7f1dd0d. The last commit (the first user turn past an oversized prompt) only touches read-session-file.js and its test, so the findings below were traced at 732628f and still hold: session-cache.js and public/app.js are unchanged by it. Thanks for the rework: the entrypoint column is now added without a wipe, a pure SDK transcript stays hidden while a later interactive turn makes the parent visible, and the failure case no longer leaves an empty cache. 287 tests in 32 related files pass locally on Node 24. Two points still block; the first is the part of the earlier blocker that remains.

Blocking

  1. A partial cold restore overwrites the saved working set before pending SDK sessions are indexed (session-cache.js hiddenSdkSessionIds ~477, public/app.js persistWorkingSet ~194). The exemption reads openWorkingSet from the saved settings. persistWorkingSet rewrites that list with only the open sessions and the skipped entries, so a saved session the restore has not reached yet is dropped from it. Persisting once during a cold restore (any click or close) therefore removes the exemption of every still-pending SDK session: it becomes hidden, and the next restore cannot find it. Fix: keep the saved entries that are not yet resolved when persisting (the same shape of problem as the one in #441), or take the exemption from the live state instead of the saved list. Test: persist while a saved SDK entry is still unindexed, then check that it is still exempt.
  2. Attaching a hidden SDK session after the list was built does not refresh the sidebar (session-cache.js ~478, public/app.js openSession ~1365). activeSessions is read when the list is built; opening an SDK session afterwards (a resume from search, a deep link, a trigger) adds no reload, so the attached terminal stays absent from the sidebar and can be left out when the grid is rebuilt. Fix: reload the projects, or notify the renderer, when a session that the filter hides becomes active. Test: hide it, open it, and check that the next list contains it.

Non-blocking

  • Files above 2 MiB miss terminal turns outside the final 256 KiB, including turns already inside the inspected head (read-session-file.js ~643); worth a follow-up.
  • Deleting the backfill invocation leaves test/get-projects-cold-start-reconcile.test.js green, and no test combines the restore with the open timing.
  • test/hide-sdk-sessions.test.js carries rationale comments beyond the one-line pointer allowed in code.

Not run: Electron-based migration and native DB tests, a large-cache benchmark, Linux Node 20/22 with c8, lint, fresh CI. CI on this head is not approved yet; I will approve it once the blocking points are fixed.

…e opened

Second review of devsuitup#486: a persist during a cold restore rewrote
openWorkingSet without the entries not reached yet, dropping their SDK
exemption; keep the planner's pending entries and the ones awaiting the
restore toast. Opening a hidden SDK session after the list was built left
it out of the sidebar; notify the renderer when it becomes active. Scan the
head of large SDK transcripts as well as their tail, and pin the backfill
call in get-projects with a test.

Claude-Session: https://claude.ai/code/session_0121XfXy4y1Fwt3wJsD4v82q
@paulo-jay

Copy link
Copy Markdown
Author

@devsuitup thanks. Both blocking points from the review at 7f1dd0d are addressed in 89e4604. CI is green, and an internal re-review of that commit found nothing.

Blocking

  1. A persist during a cold restore dropped the pending entries. persistWorkingSet now re-adds pendingRestoreEntries(): the planner's unresolved entries (restorePlanner.pending(), new in restore-plan.js) and the ask-mode candidates while the restore toast is unanswered (restoreAwaitingConsent, cleared on Restore and on Dismiss). Each goes back at its saved index, and open sessions are filtered out, so a saved SDK session keeps its exemption until the restore reaches it. Test: test/restore-pending-persist.test.js, which runs the shipped persistWorkingSet through loadAppFunctions. It persists twice while a saved entry is not indexed yet, then checks the toast case and its dismissal.
  2. Attaching a hidden SDK session left the sidebar stale. revealIfSdkSession(sessionId) (session-cache.js) runs right after activeSessions.set at both sites in main.js: the local PTY open (not for plain terminals) and the remote attach. When the cached row is sdk-*, it sends projects-changed, so the renderer rebuilds a list that now keeps the session. Test: the list holds only the interactive session, the SDK session is opened, a single projects-changed is sent, and the next list holds both.

Non-blocking

  • Files over 2 MiB: readSessionEntrypoint now scans both the first and the last 256 KiB. A terminal turn in the middle of such a file is still only seen by the next full read, which the context doc says.
  • The backfill call: a new test in get-projects-cold-start-reconcile.test.js asserts ['backfill', 'build'] on a cold and on a warm cache, so removing the call fails it.
  • Comments: the header of test/hide-sdk-sessions.test.js is now the one-line pointer only.

@devsuitup devsuitup left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Review at 89e4604. Thanks: pending restore entries are now kept when the working set is persisted, and opening a hidden SDK session notifies the renderer (the reviewer checked the wiring and the existing assertions were preserved, not weakened). 199 tests in 27 related files pass locally on Node 24, and there is no conflict with main. One point still blocks, and it sits in the gap the new pendingRestoreEntries leaves.

Blocking

  1. A restore candidate is in neither list while its check is pending (public/app.js, persistWorkingSet ~189, pendingRestoreEntries ~202, runRestore). An entry leaves restorePlanner.pending() when it is handed to runRestore, but it is only in openSessions or skippedWorkingSetEntries after liveElsewhereMany and the open have finished. If a persist runs in that window (or a second restore overlaps the first), the entry is dropped from openWorkingSet. An SDK candidate then loses its exemption, a filtered reload hides it, and the restore silently skips it. The reviewer reproduced it with the shipped functions. Fix: keep a set of the entries dispatched to runRestore and not yet decided, and include it in held; clear each entry when it is opened or skipped. Test: persist during the liveElsewhereMany await, and with two overlapping restores, then check that the SDK entry is still stored.

Non-blocking

  • Transcripts above 2 MiB still miss interactive turns between the sampled head and tail (read-session-file.js ~661).
  • Deleting the production reveal calls leaves the helper test green (test/hide-sdk-sessions.test.js ~217): assert the call through the opening path.

Not run: Electron migration and native DB tests, Linux Node 20/22 with c8, lint, fresh CI. CI on this head is not approved yet; I will approve it once this point is fixed.

pjay added 2 commits October 7, 2026 16:26
Third review of devsuitup#486: an entry handed to runRestore left the planner's
pending list but only reached openSessions or the skipped list once its
live-elsewhere check and open were done, so a persist in between (or an
overlapping restore) dropped it. Track those entries in restoreInFlight.
Also read large SDK transcripts in full during the one-time backfill, and
pin the production reveal calls with a test.

Claude-Session: https://claude.ai/code/session_0121XfXy4y1Fwt3wJsD4v82q
An entry opened by hand or deleted during the live-elsewhere check left
restoreInFlight through no path, so a session the user then closed was put
back into the working set on every persist. Also read main.js with LF line
endings in the reveal wiring test, which failed on Windows checkouts.

Claude-Session: https://claude.ai/code/session_0121XfXy4y1Fwt3wJsD4v82q
@paulo-jay

Copy link
Copy Markdown
Author

@devsuitup thanks. The blocking point from the review at 89e4604 is addressed in 3dd7fbc and 2b46e28. CI is green on 2b46e28, Windows included, and an internal re-review found nothing left.

Blocking: a restore candidate was in no list while its check was pending.
runRestore now records each candidate it dispatches in restoreInFlight before awaiting liveElsewhereMany, and pendingRestoreEntries includes that set, so persistWorkingSet keeps the entry. The entry leaves the set once it is decided:

  • it was opened or skipped (a finally around openSession);
  • it was opened by hand in the meantime, or is no longer in sessionMap (the early-continue branch);
  • the run failed part-way (an outer finally clears every entry of the run).

So a session the user opened during the check and then closed is not written back. In test/restore-pending-persist.test.js, which runs the shipped functions:

  • a persist during the liveElsewhereMany await keeps the entry, and it is opened afterwards;
  • two overlapping restores keep it;
  • a candidate opened by hand during the check, then closed, is not kept;
  • a restore that throws part-way leaves nothing in flight.

Non-blocking

  • Transcripts above 2 MiB: backfillEntrypoints, which runs once per row, now reads them in full (readSessionEntrypoint(path, { full: true })). Only the live refreshFolder path samples the head and the tail, because it runs at every watcher flush and the turn just typed is at the end. A test pins both behaviors on a terminal turn in the middle of the file.
  • The reveal calls: a new test runs the shipped registerRemoteAttachSession and checks the reveal happens after activeSessions holds the session. It also checks the open-terminal handler source, where the reveal follows activeSessions.set. Deleting either call fails it. Its first version broke on the Windows CRLF checkout; fixed in 2b46e28.

@devsuitup devsuitup left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Review at 2b46e28. The blocker of 89e4604 is fixed: restore candidates are kept in the stored set while they are being opened, and cleared on every exit of runRestore. The reviewer reproduced the earlier scenario with the shipped functions and it no longer drops the entry; the new regression assertions fail against 89e4604. Existing presence and refresh assertions were preserved, the changelog still ends (#486), and there is no conflict with main. 186 tests in the related files pass locally on Node 24.

Non-blocking

  • With two direct restores of the same id, the first rejection deletes the second call's tracking entry and a persist then drops it (public/app.js ~251). The planner does not dispatch an id twice and the presence helper catches IPC errors, so I found no production route; keying the entries by call rather than by id would close it.
  • Deleting the outer cleanup in runRestore leaves the six tests in test/restore-pending-persist.test.js green: the failure test has one candidate and the overlap test releases both checks together. A case where the first call rejects while the second is pending would pin it.
  • The full backfill can scan up to 200 large transcripts synchronously before yielding (session-cache.js ~457), and it now detects interactive turns beyond 2 MiB. Startup latency was not measured; yielding between files would keep it bounded.

Not run: Electron migration and native DB tests, Linux Node 20/22 with c8, lint, a large-cache benchmark, fresh CI.

@devsuitup
devsuitup merged commit f3d22c8 into devsuitup:main Oct 7, 2026
12 checks passed
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.

2 participants