Skip to content

fix(core): list sessions across every project row of the same worktree - #59946

Open
argszero wants to merge 2 commits into
anomalyco:devfrom
argszero:worktree-session-list
Open

argszero wants to merge 2 commits into
anomalyco:devfrom
argszero:worktree-session-list

Conversation

@argszero

@argszero argszero commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes #59371

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

session list resolves one project for the current directory and lists sessions for that project id. A worktree can own more than one project row — the row's identity includes the worktree's git fingerprint, so re-creating a remote, or opening the folder before and after that, leaves a second row behind (the report and #57985 both arrive this way). When that happens, sessions recorded under the other row are invisible: the caller is in the same folder, but the query only asks about one of the folder's rows.

packages/core/src/session/store.ts changes the project-scoped branch of list to cover every project row that shares the worktree, instead of only the row it was handed. The worktree is what a caller means by "this project"; the rows are an implementation detail of how that worktree was recorded.

Sessions therefore reappear for anyone already in this state, not only for worktrees created after the fix. Rows on a different worktree stay out of the result, and the exact-directory filter, the cursor and the returned session rows are unchanged.

A second commit keeps the global project row out of the match. That row's worktree is whichever commitless repository resolved to it first rather than the folder its sessions belong to, so a folder that had just gained its first commit listed sessions from unrelated commitless folders; a global caller has no folder identity to expand either, so it stays scoped to its own row. Its test fails without the fix and passes with it.

Part B of the report — /api/location returning the service's start directory rather than the caller's cwd — is a separate defect in a different layer and is deliberately not part of this change.

How did you verify your code works?

Red before green, at the layer the defect lives in. packages/core/test/session-store.test.ts adds a case with two project rows on one worktree and one on a different worktree, each with a session:

  • On dev (bc175f675c) without the change, listing with the newer project's id returns only that project's session; the other row's session does not appear.
  • With the change, both sessions appear, and the session on the different worktree still does not.
  • Reverting only store.ts after committing makes the new test fail again.

Suite, in packages/core:

  • Pristine dev: 6812 pass, 43 skip, 86 fail, 1 error.
  • With this change: 6813 pass, 86 fail — the extra pass is the new test; the 86 failures are pre-existing in this environment (plugin watcher timeouts, plugin resolution, an ESM reload) and identical on both trees.
  • tsc -b in packages/core exits 0; prettier --check and oxlint are clean on the two changed files.

Screenshots / recordings

No UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Related: #57985, #50979

@opencodejr opencodejr Bot left a comment

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.

This brings back the hidden sessions in the remote-added case. I ran the issue's reproduction from source on the base and with this PR: before, only the post-remote session is listed; with the PR, both are. The new store test passes and tsc -b is clean. The public API is unchanged.

It does introduce a leak, though (see inline). Matching every project row that shares a worktree also pulls in the global row, whose worktree is just the first commitless git repo that resolved to it. Repro: git init two folders a/ and b/ without committing, create a session in each, then make the first commit in a/. session list in a/ now also shows the session from b/. On the base it shows only a's session. Any directory that was once an empty repo would end up listing every global session from anywhere.

On the approach: the codebase already has sessions follow a directory's new identity. The Worktree.Event.Resolved handler in session/projector.ts moves sessions from previous to the new project ID. The remote-added case slips through because previous comes from an in-memory cache, and #58668 fixes it at that layer. Matching rows at query time instead leaves sessions whose projectID differs from the project they're listed under. It also leaves other readers that filter by one project ID (e.g. session/stats.ts) out of step with the list. Fixing it in the migration step, or at least never merging the global row, seems more robust.

Related: #57985, #50979

Comment thread packages/core/src/session/store.ts Outdated
const rows = yield* db
.select({ id: ProjectTable.id })
.from(ProjectTable)
.where(eq(ProjectTable.worktree, row.worktree))

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.

Matching on worktree alone also merges the global row. Its worktree is whatever commitless git repo first resolved to global (it is never re-pointed while that folder exists). Once that folder gets a first commit, its real project shares the worktree with global, and this list returns every global session, including ones in unrelated directories. I reproduced this with two uncommitted repos a/ and b/: after committing in a/, session list in a/ shows b's session. At minimum, global (and probably any row whose sessions aren't all under this worktree) shouldn't be merged here.

@argszero

Copy link
Copy Markdown
Contributor Author

You are right about the leak, and I reproduced it before changing anything: with global and a real project row sharing a worktree, listing for the real project returned the global session as well. Fixed in 874420a485, plus a regression test that fails without the fix.

The exclusion is at both ends, because the global row is the same trap in two directions: its worktree is whichever commitless repository resolved to it first rather than the folder its sessions belong to, so it must not be matched from outside, and a global caller has no folder identity to expand, so it stays scoped to its own row.

On query-time matching versus the migration step: I agree the projector is the right place for sessions that move, and #58668 covers that case. What it does not cover is rows that already diverged before it lands — the sessions filed under the old row are already recorded with the old projectID, and until something re-files them the folder still cannot see them. This PR is the read-side answer for those; if #58668 merges first and re-files them, this becomes redundant rather than wrong, and I would rather it be redundant than have the sessions unreachable in the meantime.

session/stats.ts filtering by a single project id is a real inconsistency, and I have deliberately not widened this change to it: list was the reported defect, and stats has the same narrow view today that list had. Say the word if you would rather both move in one change and I will fold it in.

@opencodejr opencodejr Bot left a comment

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.

The global row leak is fixed. I re-ran the case with two commitless repos and a first commit in one: session list there now shows only that folder's sessions, the same as on the base. The remote-added reproduction still lists both sessions. The store tests and tsc -b pass, and the public API is unchanged.

Nothing new to flag in this commit.

A worktree's project identity changes with its git fingerprint, so adding a
remote leaves a second project row and every earlier session keeps the old
project_id. A project-scoped session list matched one row and hid the rest.

Filter on all project rows that share the worktree instead.

Refs anomalyco#59371
Matching every project row that shares a worktree also matched the global
row, whose worktree is whichever commitless repository resolved to it
first. A folder that had just gained its first commit therefore listed
sessions from unrelated commitless folders. A global caller has no folder
identity to expand either, so it stays scoped to its own row.

@opencodejr opencodejr Bot left a comment

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.

This push is a rebase onto the latest dev; the change itself is the same as before. I re-ran the remote-added reproduction on the new base and with this PR. The base still hides the earlier session, and the PR lists both. The global row case is still fixed. The store tests and tsc -b pass, and the public API is unchanged.

Nothing new to flag.

@matt8s

matt8s commented Nov 4, 2026

Copy link
Copy Markdown

The shared-worktree expansion also preserves the existing filters and cursor ordering in synthetic integration coverage. A test-only follow-up based on this PR is available at matt8s@4b0e87c. It covers parent selection, archived status, workspace, subpath, search, two-page traversal across identities, exact-directory exclusion and the reverse global-project query.

The test blob matches the successful Linux integration validation: full Core and CLI suites, canonical checks and official-compiler checks for both changed packages. Ordinary top-level list defaults and child access are unchanged. No duplicate implementation PR was opened.

@argszero

argszero commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @matt8s — I did not just read your commit (4b0e87cc2c, test(core): preserve filters across shared project identities), I ran it against this branch.

Applied on top of our head 7f38866e04 it passes unchanged: 6 pass / 0 fail in packages/core/test/session-store.test.ts.

The check that matters more is the reverse one. Reverting only the source change from this PR and keeping your test in place gives 4 pass / 2 fail:

  • (fail) SessionStore > lists sessions from every project row that shares the same worktree
  • (fail) SessionStore > preserves filters and pagination across shared-worktree identities

The second one fails on the pagination assertion — Expected ["ses_filter_old"], Received []. Without the fix, the second page of a shared-worktree identity resolves to the wrong project row, so the cursor returns nothing. Your test therefore does exercise this change rather than passing on the old behaviour, which is exactly what a regression test for it should do.

Your commit touches only the test file, so it changes no behaviour on its own and nothing in this PR needs to change for it to go in.

For anyone reproducing this later: the commit object is not reachable by SHA from the fork (couldn't find remote ref 4b0e87cc2c), so the verification used the raw file contents at that commit rather than a fetched ref.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sessions: folder-scoped list hides sessions when a worktree owns duplicate project rows

2 participants