Skip to content

Surface sessions that have no tasks directory - #29

Open
dgruhin-hrizn wants to merge 3 commits into
L1AD:mainfrom
dgruhin-hrizn:fix/missing-sessions
Open

Surface sessions that have no tasks directory#29
dgruhin-hrizn wants to merge 3 commits into
L1AD:mainfrom
dgruhin-hrizn:fix/missing-sessions

Conversation

@dgruhin-hrizn

@dgruhin-hrizn dgruhin-hrizn commented Aug 19, 2026

Copy link
Copy Markdown

Fixes #18.

The bug

/api/sessions enumerates only TASKS_DIR. The comment above that loop reads:

// First, add sessions that have tasks directories

…but the second pass it implies was never written. Claude Code only creates a tasks directory once a session actually writes a todo, so any ordinary conversation that never used one is unreachable through any endpoint — no limit, filter, or sort will surface it.

On my machine that's 30 of 37 sessions invisible.

A strong hint this was always intended: fetchTasks already handles the 404 for a missing tasks directory, with a comment reading "Session has no tasks directory yet." Clicking one of these sessions already worked — there was simply no way to reach it.

The fix

Server — after the existing loop, add sessions known only from the transcripts in PROJECTS_DIR, with taskCount: 0 and hasTasks: false.

modifiedAt comes from the transcript's mtime rather than a directory's. Note the existing task-less fallback uses directory mtime, which Claude Code bumps when it removes its .lock at session end — so that timestamp reflects teardown rather than activity. I didn't repeat that here, and haven't changed the existing fallback in this PR.

Client — these render No tasks instead of a 0/0 progress bar, which otherwise reads as "nothing done yet" rather than "never had any".

Interactions, all checked

  • "Active Only" is unchanged. It requires pending > 0 || inProgress > 0, and these sessions are all zero, so they were already excluded.
  • Auto-archive absorbs most of them. Most are older than the 7-day threshold, so they land in the collapsed Archived disclosure rather than the main list.
  • The default limit of 20 was the real risk, and it needed more than the obvious fix. Applied naively, 30 new sessions compete for those slots and the default view drops from 7 task-bearing sessions to 6 — a regression for anyone who only cares about sessions with tasks.

So the new "With Tasks" filter is applied server-side, before the limit, rather than client-side after it:

?limit=20              → 20 shown, 6 with tasks
?limit=20&withTasks=1  →  7 shown, 7 with tasks   ← exactly the old view

Filtering after limiting would quietly turn "20 most recent" into "however many of the 20 most recent happen to match". filterBySessions now refetches rather than only re-rendering, since that filter is no longer purely client-side; the client-side filter is kept as a no-op safety net so the option still behaves against an older server.

This is non-breaking by default

"With Tasks" is the default, so the out-of-the-box sidebar is byte-for-byte the view you have today — the 30 newly-reachable sessions are one click away rather than in your face. On my machine they outnumber the rest 30:7, and as a default they bury the thing you opened the app to look at.

Only the default changes. sessionFilter is written to localStorage solely by filterBySessions — i.e. only when a user picks something — so anyone who has explicitly chosen "All Sessions" keeps it. Verified both: a first run with no stored value lands on with-tasks with no implicit write, and a stored 'all' still loads as 'all'.

If you'd rather ship it the other way round, flipping the default is a one-line change.

Note on ordering

If you take #28 as well, that one keeps its short-id aliases in a separate cache specifically so this second pass — which iterates Object.entries(metadata) — doesn't manufacture phantom duplicate sessions from alias keys. They're order-independent either way.

/api/sessions enumerates only TASKS_DIR. The comment above that loop says
"First, add sessions that have tasks directories" -- but the second pass
it implies was never written, so a session that never wrote a todo is
unreachable through any endpoint. Claude Code only creates a tasks
directory once a session actually writes one, so this hides ordinary
conversations.

On this machine that is 30 of 37 sessions invisible. Reported in L1AD#18.

Strong hint the second pass was always intended: fetchTasks already
handles the 404 for a missing tasks directory, with a comment reading
"Session has no tasks directory yet." Clicking one of these sessions
already worked -- there was just no way to reach it.

Server: after the existing loop, add sessions known only from the
transcripts in PROJECTS_DIR, with taskCount 0 and hasTasks false.
modifiedAt comes from the transcript's mtime rather than a directory's.
The existing task-less fallback uses directory mtime, which Claude Code
bumps when it removes its .lock at session end -- that timestamp reflects
teardown rather than activity, so it is not repeated here.

Client: these render "No tasks" instead of a 0/0 progress bar, which
otherwise reads as "nothing done yet" rather than "never had any".

Two interactions worth stating:

The "Active Only" filter needs pending > 0 || inProgress > 0, and these
sessions are all zero, so they were already excluded -- that filter is
unchanged. Most are also older than the 7-day threshold, so they land in
the collapsed Archived disclosure rather than the main list.

The default limit of 20 is the real risk. Applied naively, 30 new
sessions compete for those slots and the default view drops from 7
task-bearing sessions to 6 -- a regression for anyone who only cares
about sessions with tasks. So the new "With Tasks" option is applied
server-side, before the limit, rather than client-side after it:
?limit=20&withTasks=1 returns 7 of 7. Filtering after limiting would
quietly turn "20 most recent" into "however many of the 20 most recent
happen to match".

filterBySessions now refetches rather than only re-rendering, since that
filter is no longer purely client-side. The client-side filter is kept as
a no-op safety net so the option still behaves against an older server.
Surfacing task-less sessions made them eligible for the boot auto-select,
which picks sessions[0]. Since that list is sorted newest-first and a
conversation that never wrote a todo is often the most recent thing you
did, the app frequently opened to an empty board -- and then 404'd
/api/sessions/:id on every SSE refresh, because there is no tasks
directory to read.

The 404 is handled and harmless, but an empty board is a poor landing
view and the repeated request is noise.

Picks the newest session with hasTasks !== false, falling back to
sessions[0] so behaviour is unchanged when nothing has tasks. Task-less
sessions remain listed and selectable; they are just not the default.

(cherry picked from commit ac6ac43)
Surfacing sessions that never wrote a todo is right -- they were
unreachable -- but they outnumber the rest roughly 4:1 here (30 of 37), so
as a default they bury the thing people open the app to look at.

Defaulting to "With Tasks" means the out-of-the-box sidebar is exactly
what it was before those sessions became visible, and seeing everything is
one click away rather than the other way round.

Only the default changes. sessionFilter is written to localStorage solely
by filterBySessions, i.e. only when the user picks something, so anyone
who has explicitly chosen "All Sessions" keeps it. Verified both paths: a
first run with no stored value lands on with-tasks showing 7 sessions and
no implicit write, and a stored 'all' still loads as 'all' showing 20.

Also adds the matching empty-state hint, which previously had no branch
for this filter and would have shown a bare "No sessions".

(cherry picked from commit 0832851)
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.

Missing Claude Code Session Records, Unable to Retrieve

1 participant