Repository navigation
feat(serve): query a single session's status by id #5857
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
44e1f71
e029794
e4104ef
842d722
7eff417
21cda4c
762c94b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -86,9 +86,24 @@ The daemon also exposes read-only runtime snapshots for client UIs and | |
| operators: `GET /daemon/status`, `GET /workspace/mcp`, | ||
| `GET /workspace/skills`, `GET /workspace/providers`, `GET /workspace/env`, | ||
| `GET /workspace/preflight`, | ||
| `GET /session/:id/context`, `GET /session/:id/supported-commands`, and | ||
| `GET /session/:id/status`, `GET /session/:id/context`, | ||
| `GET /session/:id/supported-commands`, and | ||
| `GET /session/:id/tasks`, and `GET /session/:id/lsp`. | ||
|
|
||
| `GET /session/:id/status` returns the live status summary for a single | ||
| session — the same item shape that `GET /workspace/:id/sessions` lists | ||
| (`sessionId`, `workspaceCwd`, `createdAt`, `displayName?`, `clientCount`, | ||
| `hasActivePrompt`). It answers `200` with the summary when the daemon holds a | ||
| live session with that id, and `404 session_not_found` otherwise. Use it to | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] This says unknown sessions return Please either document the actual response shape or intentionally add and test a — GPT-5 Codex via Qwen Code /review
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in e029794 — the docs now describe the actual |
||
| poll whether one known session is still running (`hasActivePrompt`) or how many | ||
| clients are attached (`clientCount`) without fetching and scanning the whole | ||
| paginated session list: | ||
|
|
||
| ```bash | ||
| curl http://127.0.0.1:4170/session/$SESSION_ID/status | ||
| # → {"sessionId":"…","workspaceCwd":"…","createdAt":"…","clientCount":1,"hasActivePrompt":false} | ||
| ``` | ||
|
|
||
| `GET /session/:id/lsp` returns structured per-session LSP status. Start the | ||
| daemon with `--experimental-lsp` to enable LSP in spawned agent sessions; | ||
| otherwise the route returns `enabled: false` with no servers. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -381,6 +381,7 @@ interface FakeBridgeOpts { | |
| context?: BridgeClientRequestContext, | ||
| ) => boolean; | ||
| listImpl?: (workspaceCwd: string) => BridgeSessionSummary[]; | ||
| summaryImpl?: (sessionId: string) => BridgeSessionSummary; | ||
| workspaceMcpImpl?: () => Promise<ServeWorkspaceMcpStatus>; | ||
| workspaceMcpToolsImpl?: ( | ||
| serverName: string, | ||
|
|
@@ -557,6 +558,7 @@ interface FakeBridge extends AcpSessionBridge { | |
| context?: BridgeClientRequestContext; | ||
| }>; | ||
| listCalls: string[]; | ||
| summaryCalls: string[]; | ||
| workspaceMcpCalls: number; | ||
| workspaceMcpToolsCalls: string[]; | ||
| workspaceSkillsCalls: number; | ||
|
|
@@ -682,6 +684,7 @@ function fakeBridge(opts: FakeBridgeOpts = {}): FakeBridge { | |
| const permissionVotes: FakeBridge['permissionVotes'] = []; | ||
| const sessionPermissionVotes: FakeBridge['sessionPermissionVotes'] = []; | ||
| const listCalls: string[] = []; | ||
| const summaryCalls: string[] = []; | ||
| let workspaceMcpCalls = 0; | ||
| const workspaceMcpToolsCalls: string[] = []; | ||
| let workspaceSkillsCalls = 0; | ||
|
|
@@ -738,6 +741,11 @@ function fakeBridge(opts: FakeBridgeOpts = {}): FakeBridge { | |
| const respondImpl = opts.respondImpl ?? (() => true); | ||
| const sessionRespondImpl = opts.sessionRespondImpl ?? (() => true); | ||
| const listImpl = opts.listImpl ?? (() => []); | ||
| const summaryImpl = | ||
| opts.summaryImpl ?? | ||
| ((sessionId: string): BridgeSessionSummary => { | ||
| throw new SessionNotFoundError(sessionId); | ||
| }); | ||
| const workspaceMcpImpl = | ||
| opts.workspaceMcpImpl ?? | ||
| (async () => ({ | ||
|
|
@@ -1074,6 +1082,7 @@ function fakeBridge(opts: FakeBridgeOpts = {}): FakeBridge { | |
| permissionVotes, | ||
| sessionPermissionVotes, | ||
| listCalls, | ||
| summaryCalls, | ||
| workspaceMcpToolsCalls, | ||
| extensionEvents, | ||
| sessionContextCalls, | ||
|
|
@@ -1215,6 +1224,10 @@ function fakeBridge(opts: FakeBridgeOpts = {}): FakeBridge { | |
| listCalls.push(workspaceCwd); | ||
| return listImpl(workspaceCwd); | ||
| }, | ||
| getSessionSummary(sessionId) { | ||
| summaryCalls.push(sessionId); | ||
| return summaryImpl(sessionId); | ||
| }, | ||
| async getWorkspaceMcpStatus() { | ||
| workspaceMcpCalls += 1; | ||
| return workspaceMcpImpl(); | ||
|
|
@@ -6367,6 +6380,51 @@ describe('createServeApp', () => { | |
| }); | ||
| }); | ||
|
|
||
| describe('GET /session/:id/status', () => { | ||
| it('200 with the live session summary', async () => { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] This 200 test only covers the That omission relies entirely on it('200 omits displayName when the live session has none', async () => {
const summary: BridgeSessionSummary = {
sessionId: 's-2',
workspaceCwd: WS_BOUND,
createdAt: '2026-05-17T12:00:00.000Z',
clientCount: 0,
hasActivePrompt: false,
};
const bridge = fakeBridge({ summaryImpl: () => summary });
const app = createServeApp(
{ ...baseOpts, workspace: WS_BOUND },
undefined,
{ bridge },
);
const res = await request(app)
.get('/session/s-2/status')
.set('Host', `127.0.0.1:${baseOpts.port}`);
expect(res.status).toBe(200);
expect('displayName' in res.body).toBe(false);
});— claude-opus-4-8[1m] via Qwen Code /qreview
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added your suggested test in 842d722 — a 200 case with a no- |
||
| const summary: BridgeSessionSummary = { | ||
| sessionId: 's-1', | ||
| workspaceCwd: WS_BOUND, | ||
| createdAt: '2026-05-17T12:00:00.000Z', | ||
| displayName: 'demo', | ||
| clientCount: 2, | ||
| hasActivePrompt: true, | ||
| }; | ||
| const bridge = fakeBridge({ summaryImpl: () => summary }); | ||
| const app = createServeApp( | ||
| { ...baseOpts, workspace: WS_BOUND }, | ||
| undefined, | ||
| { bridge }, | ||
| ); | ||
|
|
||
| const res = await request(app) | ||
| .get('/session/s-1/status') | ||
| .set('Host', `127.0.0.1:${baseOpts.port}`); | ||
|
|
||
| expect(res.status).toBe(200); | ||
| expect(res.body).toEqual(summary); | ||
| expect(bridge.summaryCalls).toEqual(['s-1']); | ||
| }); | ||
|
|
||
| it('404 when the session id is unknown to the daemon', async () => { | ||
| // fakeBridge's default getSessionSummary throws SessionNotFoundError. | ||
| const bridge = fakeBridge(); | ||
| const app = createServeApp( | ||
| { ...baseOpts, workspace: WS_BOUND }, | ||
| undefined, | ||
| { bridge }, | ||
| ); | ||
|
|
||
| const res = await request(app) | ||
| .get('/session/ghost/status') | ||
| .set('Host', `127.0.0.1:${baseOpts.port}`); | ||
|
|
||
| expect(res.status).toBe(404); | ||
| expect(res.body.sessionId).toBe('ghost'); | ||
| expect(bridge.summaryCalls).toEqual(['ghost']); | ||
| }); | ||
| }); | ||
|
|
||
| describe('POST /session/:id/model', () => { | ||
| it('200 with the agent response on success', async () => { | ||
| const bridge = fakeBridge({ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1074,6 +1074,7 @@ function advertisedMaxPendingPromptsPerSession( | |
| * - `POST /session/:id/load` | ||
| * - `POST /session/:id/resume` | ||
| * - `GET /workspace/:id/sessions` | ||
| * - `GET /session/:id/status` | ||
| * - `GET /session/:id/context` | ||
| * - `GET /session/:id/supported-commands` | ||
| * - `GET /session/:id/tasks` | ||
|
|
@@ -3323,6 +3324,19 @@ export function createServeApp( | |
| } | ||
| }); | ||
|
|
||
| app.get('/session/:id/status', (req, res) => { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] This adds a new public route, but the route side tables are not updated with it. Please add an always-on capability tag such as — GPT-5 Codex via Qwen Code /review
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added the always-on
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] This returns raw bridge data while Suggested fix: Add a server-level test that sets up a session with persisted metadata and asserts the two endpoints return different — qwen3.7-max via Qwen Code /review |
||
| const sessionId = requireSessionId(req, res); | ||
| if (sessionId === null) return; | ||
| try { | ||
| res.status(200).json(bridge.getSessionSummary(sessionId)); | ||
| } catch (err) { | ||
| sendBridgeError(res, err, { | ||
| route: 'GET /session/:id/status', | ||
| sessionId, | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| app.get('/session/:id/context', async (req, res) => { | ||
| const sessionId = requireSessionId(req, res); | ||
| if (sessionId === null) return; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion] The docs state this returns "the same item shape that
GET /workspace/:id/sessionslists", but the list endpoint enrichesupdatedAtfromSessionServicepersisted data (file mtime) vialistWorkspaceSessionsForResponse(). The new route callsbridge.getSessionSummary()directly, which never populatesupdatedAt—SessionEntryhas no such field.Clients relying on this "same shape" contract will see
updatedAtmissing from the single-item response while it is always present in the list response.Either update the docs to describe the subset (e.g., "a live-only summary with the same core fields"), or enrich the response in the route handler to match the list endpoint:
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in e029794 — reworded to describe the live bridge summary and spelled out that the list endpoint enriches
createdAt(persisted first-prompt time), addsupdatedAt, and derivesdisplayName, whereas/statusreturns the livecreatedAt, omitsupdatedAt, and only returnsdisplayNamewhen set. No longer claims byte-parity.