You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
session.list ignores location[directory] and returns sessions from every project #57635
GET /api/session silently ignores the location[directory] query parameter and returns the newest 50 sessions across every project. Every other location-scoped endpoint on the same server uses that shape (location.get, command.list, session.command, session.skill), so it is the natural parameter for a client to send. session.list instead expects a flat directory field, and the unknown location[...] key is dropped without an error, so the request silently degrades to an unscoped list.
Because DELETE /api/session/:id is not location scoped either, "list the sessions in this directory and delete them" deletes sessions from other projects.
Steps to reproduce
Two projects, one shared data directory (isolated so nothing else interferes):
Start a server with cwd /tmp/rp/a and create a session:
Start a server with cwd /tmp/rp/b on the same XDG_DATA_HOME and create a session the same way.
List on either server:
GET /api/session?location[directory]=/tmp/rp/a -> "B", "A" (both projects)
GET /api/session?directory=/tmp/rp/a -> "A" (flat field filters correctly)
GET /api/session -> "B", "A"
With a location scoping parameter, the list is limited to that directory, or the server rejects the unknown parameter instead of ignoring it.
A delete issued for a location does not silently affect a session that belongs to another project.
Actual
location[directory] is ignored by session.list; the response is the global newest-50 list across all projects.
session.remove has no location middleware, so it deletes any session by id regardless of the caller's location.
Impact
This is not theoretical. A cleanup script ran GET /api/session?location[directory]=<scratch dir> and then DELETE /api/session/:id for each returned id, expecting to remove its own scratch sessions. It deleted 49 sessions spanning 7 projects in one pass, including the session that was running the script. All 49 were recovered from the SQLite freelist, but that only worked because the pages had not been reused yet.
Pointers
packages/protocol/src/groups/session.ts: SessionsQuery inherits Session.ListFilter.fields (directory, project, subpath as flat fields) and has no location parameter, unlike command.list and location.get.
session.remove is declared without .middleware(sessionLocationMiddleware), while session.command and session.skill have it.
Thanks for the detailed write-up, and sorry that cleanup script took out sessions from other projects. Glad you got them back.
As the API is defined right now, the server is doing what it was built to do:
GET /api/session documents flat directory / project / subpath filters (SessionsQuery in packages/protocol/src/groups/session.ts). location[directory] isn't one of its parameters, so it's dropped like any other unknown query key, and the list comes back unfiltered. As you found, ?directory= filters correctly.
DELETE /api/session/:id deletes a session by its ID, which is unique across all projects. Adding sessionLocationMiddleware wouldn't scope it to your location: that middleware works out the location from the session being acted on, not from the request.
The naming really is inconsistent with location.get, command.list and the other location-scoped endpoints, and it's easy to get wrong in a way that loses data. So I'm recording this as a request to harden the API. That could mean having session.list accept location[directory] or reject unknown query parameters instead of ignoring them, and optionally letting deletes check a stated location. Maintainers will consider it.
Until then, ?directory=<path> on session.list scopes the list correctly. You can also check each returned session's location before deleting it.
Related: #56197 (a closed PR that changed how the session list filters work).
A bounded compatibility proposal: accept the explicitly supplied location[directory] as an alias for the existing list directory filter, while keeping unfiltered listing and ID-only deletion available. Two details in the current implementation need explicit decisions before coding:
Pagination must preserve the asserted scope.Session.list takes its filters from the cursor when one is supplied, ignoring filters alongside it. Simply mapping the alias to directory would therefore still allow a request carrying location[directory]=A and an unscoped/B cursor to return other locations. I suggest mapping the alias into the existing directory filter on the first page and returning InvalidRequestError when the alias and flat directory disagree, or for a cursor whose stored directory differs from the explicitly asserted directory. Cursor-only pagination would retain its current behavior. The existing directory-filtered pagination test is a useful compatibility baseline.
A delete guard is a precondition on the target, not implicit process scoping. Keep omission of location as today's ID-only operation. If an explicit location is accepted on DELETE, compare it with the target session before interruption, transport closure, or deletion; a mismatch must have no side effects. Session.remove recursively removes children, so checking only the target must not be documented as guaranteeing that every descendant has that directory. I would preserve the existing cascade contract in this slice; subtree isolation would need its own agreed semantics.
Unlike the closed #56197, this would not add restrictions to existing directory/project/subpath combinations; the new consistency check applies only when the explicit location alias is used.
This keeps the existing generated client's flat list parameters and ID-only remove compatible, rather than changing omitted-location calls to mean the server's cwd or header. The protocol/OpenAPI and regenerated client should expose the alias and delete precondition consistently. Acceptance checks would cover alias/flat equivalence, conflicting selectors, cursor-scope mismatch, unchanged cursor-only pagination, and a mismatched delete leaving the target and children untouched.
This is source-level analysis, not a reproduction of the reported recovery incident. Would this scope be suitable for design approval?
Activity
opencode-agent commentedon Oct 21, 2026
Thanks for the detailed write-up, and sorry that cleanup script took out sessions from other projects. Glad you got them back.
As the API is defined right now, the server is doing what it was built to do:
GET /api/sessiondocuments flatdirectory/project/subpathfilters (SessionsQueryinpackages/protocol/src/groups/session.ts).location[directory]isn't one of its parameters, so it's dropped like any other unknown query key, and the list comes back unfiltered. As you found,?directory=filters correctly.DELETE /api/session/:iddeletes a session by its ID, which is unique across all projects. AddingsessionLocationMiddlewarewouldn't scope it to your location: that middleware works out the location from the session being acted on, not from the request.The naming really is inconsistent with
location.get,command.listand the other location-scoped endpoints, and it's easy to get wrong in a way that loses data. So I'm recording this as a request to harden the API. That could mean havingsession.listacceptlocation[directory]or reject unknown query parameters instead of ignoring them, and optionally letting deletes check a stated location. Maintainers will consider it.Until then,
?directory=<path>onsession.listscopes the list correctly. You can also check each returned session'slocationbefore deleting it.Related: #56197 (a closed PR that changed how the session list filters work).
Dante-dan commentedon Oct 21, 2026
A bounded compatibility proposal: accept the explicitly supplied
location[directory]as an alias for the existing listdirectoryfilter, while keeping unfiltered listing and ID-only deletion available. Two details in the current implementation need explicit decisions before coding:Session.listtakes its filters from the cursor when one is supplied, ignoring filters alongside it. Simply mapping the alias todirectorywould therefore still allow a request carryinglocation[directory]=Aand an unscoped/B cursor to return other locations. I suggest mapping the alias into the existing directory filter on the first page and returningInvalidRequestErrorwhen the alias and flatdirectorydisagree, or for a cursor whose stored directory differs from the explicitly asserted directory. Cursor-only pagination would retain its current behavior. The existing directory-filtered pagination test is a useful compatibility baseline.locationas today's ID-only operation. If an explicit location is accepted on DELETE, compare it with the target session before interruption, transport closure, or deletion; a mismatch must have no side effects.Session.removerecursively removes children, so checking only the target must not be documented as guaranteeing that every descendant has that directory. I would preserve the existing cascade contract in this slice; subtree isolation would need its own agreed semantics.Unlike the closed #56197, this would not add restrictions to existing
directory/project/subpathcombinations; the new consistency check applies only when the explicit location alias is used.This keeps the existing generated client's flat list parameters and ID-only remove compatible, rather than changing omitted-location calls to mean the server's cwd or header. The protocol/OpenAPI and regenerated client should expose the alias and delete precondition consistently. Acceptance checks would cover alias/flat equivalence, conflicting selectors, cursor-scope mismatch, unchanged cursor-only pagination, and a mismatched delete leaving the target and children untouched.
This is source-level analysis, not a reproduction of the reported recovery incident. Would this scope be suitable for design approval?