feat(serve): query a single session's status by id - #5857
Conversation
Add a daemon HTTP endpoint, GET /session/:id/status, that returns the live status summary for one session by its id — the same per-item shape that the workspace session list produces (sessionId, workspaceCwd, createdAt, displayName, clientCount, hasActivePrompt). It answers 200 with the summary when the daemon holds a live session with that id, and 404 when the id is unknown. Previously the only way to read a session's live state was the full paginated workspace session list, forcing a caller that already holds a session id to fetch every page and filter client-side just to answer "is this session still running?". A by-id lookup is the natural primitive for polling a single known session's hasActivePrompt / clientCount — for example, a client UI that disables controls or shows a "task in progress" hint while a specific session is running. The data already exists on the bridge, so this adds one accessor (getSessionSummary) sharing the same summary builder as the list path, one route, unit tests, and a docs note. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@qwen-code /triage |
|
Thanks for the PR! Template looks good ✓ On direction: this directly addresses issue #5855 — a real pain point for client UIs that need to poll a single session's On approach: the scope is tight and focused — a shared 中文说明感谢贡献! 模板完整 ✓ 方向:直接解决 issue #5855——客户端 UI 需要轮询单个 session 的 方案:范围紧凑聚焦——共享 — Qwen Code · qwen3.7-max |
Code ReviewIndependent proposal matched the PR's approach exactly: extract a No correctness bugs, security holes, or regressions found. The No AGENTS.md violations. No over-abstraction, no duplication, no code in the wrong package. Real-Scenario TestingBuilt the bundle from the PR branch and started Daemon log confirming both routes: Both paths behave exactly as described: 404 with Unit Tests中文说明代码审查独立提案与 PR 方案完全一致:提取 未发现正确性 bug、安全漏洞或回归。 无 AGENTS.md 违规。无过度抽象、无重复、无错放位置的代码。 真实场景测试从 PR 分支构建 bundle,在 4199 端口启动
daemon 日志确认两条路由均正常处理。 单测全部通过:bridge 298 个、server 528 个。 — Qwen Code · qwen3.7-max |
|
This is one of those PRs where everything lines up. The motivation is clear (issue #5855 — client UIs shouldn't have to paginate through every session just to check one), the implementation is minimal (shared helper + bridge accessor + route + tests + docs), and the live-daemon test confirms both paths work exactly as described. My independent proposal was identical to the PR's approach — there's really only one sensible way to add this: DRY the summary builder, expose it by ID on the bridge, add a GET route. The PR doesn't over-engineer, doesn't carry unrelated changes, and follows every existing pattern in the codebase. The Live testing confirmed: 404 returns the session ID in the error body (useful for debugging), 200 returns the correct live summary with Approving. ✅ 中文说明这是一个各方面都对齐的 PR。动机清晰(issue #5855——客户端 UI 不该为了查一个 session 而翻页拉取全部),实现最小化(共享帮助函数 + bridge 访问器 + 路由 + 测试 + 文档),真实 daemon 测试确认两条路径都按描述工作。 我的独立提案与 PR 方案完全一致——这个问题本质上只有一种合理的实现方式:去重 summary 构建器、按 id 暴露到 bridge、添加 GET 路由。PR 没有过度工程化,没有夹带无关改动,遵循代码库中所有既有模式。 真实测试确认:404 在错误体中返回 session ID(便于调试),200 返回正确的实时 summary, 批准 ✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
Local runtime verification — PR #5857Verdict: functionally verified. I drove the new Method & environment
1. Live daemon E2E (the headline)Real daemon, real bridge, real socket — Dynamic fields track real state:
So the motivating use case — "poll one known session's run-state without scanning the whole list" — works exactly as described: 2. Unit tests, typecheck, revert-proof (merged tree)
The shared-helper refactor ( 3. Non-blocking note — "same shape as the list" is only true at the bridge layerThe PR description and the docs say the response is "the same item shape that But the HTTP
The two fields that matter for the stated use case ( Verified on the real esbuild bundle via a live 🇨🇳 中文说明(点击展开)本地运行验证 — PR #5857结论:功能验证通过。 在真实的 方法与环境
1. 真实 daemon E2E(核心证据)
2. 单测 / 类型检查 / 可证伪(合并树)
3. 非阻塞说明 ——"与列表结构一致"只在 bridge 层成立
用例真正关心的 (以上均在真实 esbuild 产物 + 真实 |
| `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 |
There was a problem hiding this comment.
[Suggestion] The docs state this returns "the same item shape that GET /workspace/:id/sessions lists", but the list endpoint enriches updatedAt from SessionService persisted data (file mtime) via listWorkspaceSessionsForResponse(). The new route calls bridge.getSessionSummary() directly, which never populates updatedAt — SessionEntry has no such field.
Clients relying on this "same shape" contract will see updatedAt missing 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:
app.get('/session/:id/status', async (req, res) => {
const sessionId = requireSessionId(req, res);
if (sessionId === null) return;
try {
const live = bridge.getSessionSummary(sessionId);
const existing = await sessionService.getSession(sessionId);
res.status(200).json({
...live,
updatedAt: existing?.mtime
? new Date(existing.mtime).toISOString()
: undefined,
});
} catch (err) {
sendBridgeError(res, err, { route: 'GET /session/:id/status', sessionId });
}
});— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Fixed in e029794 — reworded to describe the live bridge summary and spelled out that the list endpoint enriches createdAt (persisted first-prompt time), adds updatedAt, and derives displayName, whereas /status returns the live createdAt, omits updatedAt, and only returns displayName when set. No longer claims byte-parity.
chiga0
left a comment
There was a problem hiding this comment.
Overview
Final Verdict: Approve — Small, clean additive change. Adds a single by-id session status lookup that reuses the existing summary builder, follows established route patterns, and has good test coverage including a cross-check against the list path.
Findings Summary
- Critical/Major: 0 items
- Minor: 0 items
- Nit: 0 items
Key Observations
The design is sound — extracting toSessionSummary as a shared builder ensures the single-item and list shapes can't drift. The route implementation (requireSessionId → bridge.getSessionSummary → sendBridgeError on SessionNotFoundError) mirrors the existing session-scoped GET routes exactly.
The bridge test that cross-checks getSessionSummary(id) against listWorkspaceSessions().find(s => s.sessionId === id) is a nice guard against future divergence between the two paths.
Additional Audit Coverage
- Synchronous bridge method:
getSessionSummaryis synchronous (map lookup), unlike async methods (spawnOrAttach,load). This is correct — no I/O involved, just aMap.get()call. - Error mapping:
SessionNotFoundErroris correctly mapped to 404 viasendBridgeError. The 404 response body echoes the session id, which is useful for debugging. displayNameoptionality:toSessionSummarypassesentry.displayName(possibly undefined) directly — matchesBridgeSessionSummary'sdisplayName?: stringtype. Consistent with list path.- Route ordering: The new route is placed before
GET /session/:id/context, which is fine — Express matches routes in order and/statusdoesn't conflict with any other session-scoped path segment. - No SDK/ACP wiring: PR description correctly scopes this out — the daemon route is the primitive. SDK helper can be added in a follow-up if needed.
This review was generated by QoderWork AI
The session-status docs said the response is "the same item shape that
GET /workspace/:id/sessions lists". That parity only holds at the bridge
layer; the HTTP list endpoint enriches each item with persisted
session-store data, so for the same live session the two routes diverge
on createdAt (persisted first-prompt time vs live spawn time), updatedAt
(present only on the list), and displayName (derived from the stored
title/prompt vs the live session's own, usually unset). Reword to
describe /status as the raw live-session view, spell out those
differences, and fix the 404 note to match the actual { error, sessionId }
body (no code field).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@wenshao Thanks for the thorough live-daemon verification — and you're right about the shape claim. The bridge-layer parity ( I kept the behavior as-is (the route intentionally returns the raw live view) and fixed the docs + PR description instead, per your suggestion:
Pushed in e029794. 中文说明感谢细致的真实 daemon 验证——结构一致性的说法确实需要修正。bridge 层是完全一致的( 行为保持不变(该路由有意返回原始实时视图),按你的建议改的是文档与 PR 描述:
已在 e029794 推送。 |
| } | ||
| }); | ||
|
|
||
| app.get('/session/:id/status', (req, res) => { |
There was a problem hiding this comment.
[Suggestion] This adds a new public route, but the route side tables are not updated with it. /capabilities.features still has no tag for this endpoint, so clients cannot feature-detect it the way they do for neighboring session routes such as session_context, session_tasks, session_stats, and session_lsp. resolveDaemonTelemetryRoute also has no GET /session/:id/status matcher, so polling traffic and failures for this endpoint will not get normalized telemetry attribution.
Please add an always-on capability tag such as session_status, update the expected feature/protocol docs, and add a telemetry matcher for GET /session/:id/status that carries the session id.
— GPT-5 Codex via Qwen Code /review
There was a problem hiding this comment.
Added the always-on session_status capability tag in e4104ef — registry, registered-features test, and the protocol feature list / capability→route map / capability-versioning reference. On the telemetry matcher: I left resolveDaemonTelemetryRoute alone for sibling-consistency — none of the GET status routes (context/stats/tasks/lsp/supported-commands) are in it; adding only /status would make it the lone read-only GET route with telemetry attribution. Happy to add matchers for all of them as a separate change if you'd prefer.
| 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 |
There was a problem hiding this comment.
[Suggestion] This says unknown sessions return 404 session_not_found, but the shared SessionNotFoundError mapper currently responds with only { error, sessionId }; it does not include a code field. A client following this doc could key off a non-existent error code and mishandle the main negative response for this route.
Please either document the actual response shape or intentionally add and test a code: 'session_not_found' mapping for this route contract.
— GPT-5 Codex via Qwen Code /review
There was a problem hiding this comment.
Fixed in e029794 — the docs now describe the actual { error, sessionId } body (no code field), matching the shared SessionNotFoundError mapper used by every other session route.
chiga0
left a comment
There was a problem hiding this comment.
Code Review Overview (AI Generated)
PR: #5857 feat(serve): query a single session's status by id
Type: New Feature
Change size: +145/-9 across 6 files
Findings Summary
- Critical/Major: 0 items
- Minor: 1 item (enrichment suggestion)
- Nit: 0 items
Key Observations
This is a clean, well-scoped PR. The getSessionSummary accessor reuses the same toSessionSummary builder as listWorkspaceSessions, preventing shape drift. The route follows the existing pattern (requireSessionId → bridge call → sendBridgeError). Tests cover both happy path and 404. Good work.
However, I believe this endpoint has significant potential to provide richer status information that would be far more useful for remote clients (web-shell, IDE extensions, etc.). See the enrichment suggestion below.
Cross-Validation
| Finding | Source | My Assessment |
|---|---|---|
| PR template format | qwen-code-ci-bot | Non-blocking format nit, not a code issue |
No other automated findings. My independent review found no bugs, security issues, or correctness problems.
Minor: Consider enriching the status response with turn-phase information
Currently GET /session/:id/status returns the same BridgeSessionSummary shape as the list endpoint — essentially { sessionId, workspaceCwd, createdAt, displayName, clientCount, hasActivePrompt }. While this is correct and DRY, a dedicated /status endpoint has the opportunity to provide much richer per-session state that remote clients (web-shell, IDE plugins) desperately need but cannot cheaply obtain.
The core gap: hasActivePrompt is a boolean, but clients need to know what phase the session is in — is the model thinking? Generating text? Executing a tool? Waiting for permission approval? This information is already available on the daemon side:
-
EventBus ring buffer — the per-session
EventBusalready holds the most recent events. Scanning the last few events can derive the current phase:- Last event is
session_updatewithagent_thought_chunk→ thinking - Last event is
session_updatewithagent_message_chunk→ generating - Last event is
session_updatewithtool_call/tool_call_updatewith non-terminal status → tool_execution (with tool name available) - Last event is
permission_request→ awaiting_permission - No active prompt → idle
- Last event is
-
SessionEntry fields already available:
activePromptOriginatorClientId,pendingPermissionIds.size,modelRoundtripInFlight,events.lastEventId,events.subscriberCount.
Suggested enriched response shape (additive, backward-compatible):
interface SessionStatusResponse extends BridgeSessionSummary {
// Current turn phase (derived from recent EventBus events)
phase?: 'idle' | 'thinking' | 'generating' | 'tool_execution' | 'awaiting_permission' | 'streaming';
// Currently executing tool (when phase === 'tool_execution')
activeTool?: {
toolCallId: string;
toolName: string;
status: string;
};
// Aggregate counts from EventBus ring
pendingPermissionCount?: number;
subscriberCount?: number;
lastEventId?: number;
}Implementation sketch: Add a peekRecentPhase(eventBus: EventBus) helper that scans the last ~20 events in the ring backwards, finds the most recent session_update event, checks its sessionUpdate sub-type, and returns the phase. This is O(k) where k is the scan depth (small constant), and reads from an already-in-memory ring buffer — negligible cost.
Why this matters: Without phase information, a polling client can only show "busy" vs "idle". With it, the client can show "Thinking…", "Writing response…", "Running Bash: npm test…", "Waiting for permission…" — dramatically better UX for remote/headless clients.
This could be a follow-up PR, but since the /status endpoint is being introduced here, it's worth considering whether to include richer data from the start to avoid versioning the response shape later.
This review was generated by QoderWork AI
chiga0
left a comment
There was a problem hiding this comment.
Requesting changes on the current /session/:id/status shape.
The route name says “status”, but the implementation currently returns only the same sparse live-session list summary shape: sessionId, workspaceCwd, createdAt, optional displayName, clientCount, and hasActivePrompt. That is not enough for the main status use case. A caller still cannot tell whether the session is waiting for the model, streaming thought, streaming assistant text, running a tool, waiting on permission, cancelling, or recovering from an error. In practice every UI/operator would still need to subscribe to SSE and reconstruct activity from events, which defeats the purpose of a lightweight status endpoint.
This looks feasible to support from the existing code rather than speculative. The bridge already has the relevant live signals:
sendPrompt()owns prompt start/end and currently flipsentry.promptActive.BridgeClient.sessionUpdate()already seesagent_thought_chunk,agent_message_chunk,tool_call,tool_call_update,plan, and other update kinds before publishing to the session event bus.requestPermission()publishespermission_requestand trackspendingPermissionIds.turn_complete,turn_error, andprompt_cancelledare already emitted as explicit terminal/cancel frames.SessionEntryalready caches model/mode state viacurrentModelId/currentApprovalMode.
So I think /status should either be renamed to something narrower like /summary / /live-summary, or preferably be expanded now into a real live activity status. A minimal additive shape could be something like:
{
sessionId: string;
workspaceCwd: string;
createdAt: string;
clientCount: number;
hasActivePrompt: boolean;
phase: 'idle' | 'queued' | 'thinking' | 'responding' | 'tool_calling' | 'awaiting_permission' | 'cancelling' | 'errored';
activePrompt?: {
promptId?: string;
originatorClientId?: string;
startedAt: string;
lastActivityAt: string;
};
activeTools?: Array<{
toolCallId: string;
name?: string;
title?: string;
status?: string;
startedAt?: string;
lastUpdatedAt?: string;
}>;
pendingPermissions?: Array<{
requestId: string;
toolName?: string;
issuedAt: string;
}>;
lastEventId?: number;
currentModelId?: string;
currentApprovalMode?: string;
}The implementation does not need to expose raw tool args or full event payloads; a redacted summary is enough. The important part is that the daemon owns the runtime phase and active tool/permission summary, so clients do not each have to infer it differently.
Suggested implementation path:
- Add a dedicated
BridgeSessionStatustype instead of reusingBridgeSessionSummary. - Extend
SessionEntrywith a smallruntimeStatusobject updated at the same publish sites that already emit prompt/session/permission/turn events. - Keep
hasActivePromptfor backward compatibility, but derivephaseand active summaries from the same state. - Add bridge tests that drive
sendPrompt,agent_thought_chunk,agent_message_chunk,tool_call/tool_call_update,permission_request,turn_complete,turn_error, and cancel, asserting the phase transitions. - Update the serve route/docs to describe
/statusas the richer live activity endpoint.
With the current shape, this PR would likely bake in a misleading /status contract and require another immediate API expansion for real clients.
Generated by GPT-5
|
@qwen-code /triage |
| }); | ||
|
|
||
| describe('GET /session/:id/status', () => { | ||
| it('200 with the live session summary', async () => { |
There was a problem hiding this comment.
[Suggestion] This 200 test only covers the displayName-present case. The PR documents a specific /status contract — "returns displayName only when one is set on the live session" (docs/users/qwen-serve.md) — but nothing asserts the key is absent from the HTTP body when unset.
That omission relies entirely on res.json() dropping the undefined-valued key that toSessionSummary always sets (bridge.ts: displayName: entry.displayName). A future "consistency" change to that helper (e.g. entry.displayName ?? '') would silently break the documented shape with the whole suite still green. A sibling test pins the contract:
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
There was a problem hiding this comment.
Added your suggested test in 842d722 — a 200 case with a no-displayName summary asserting 'displayName' in res.body === false, so a future change to the shared summary builder can't silently break the documented shape. Thanks!
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
…ability The new single-session status route had no entry in the capability registry, so clients couldn't feature-detect it the way they pre-flight the sibling read-only session routes (session_context, session_tasks, session_stats, session_lsp, …). Add an always-on `session_status` tag, mirror it in the registered-features test, and document it in the protocol feature list, the capability→route map, and the capability versioning reference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks all for the careful review. Pushed two follow-up commits ( Addressed
One push-back — telemetry matcher (@wenshao server.ts:3327): I left On enriching 中文说明感谢各位细致 review。已推送两个后续提交( 已处理
一处不改 —— telemetry matcher(@wenshao server.ts:3327):我有意没动 关于把 |
The docs state the route returns displayName only when the live session has one, but no test asserted the key is absent from the HTTP body in that case — it relied implicitly on res.json() dropping the undefined-valued key. Add a sibling 200 test with a summary that has no displayName and assert the key is not present, so a future change to the shared summary builder can't silently break the documented shape. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Follow-up on my earlier request-changes review after re-checking compatibility and the latest discussion. I agree the richer live activity shape can be handled as an additive follow-up, especially now that #5863 tracks it explicitly. Since this endpoint is new and the response is JSON, adding fields later such as The remaining blocker I see is CI, not the API shape: Ubuntu currently fails in Generated by GPT-5 |
doudouOUC
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅ Downgraded from Approve to Comment: CI failing (Test (ubuntu-latest, Node 22.x)).
— qwen3.7-max via Qwen Code /review
The capabilities-envelope integration test pins the full caps.features list returned by a live daemon, so adding the session_status capability tag to the registry made the live list diverge from the test's hardcoded baseline (CI: expected 65, received 66). Add session_status to that baseline in the same position the registry emits it (after session_lsp), and to the session-lifecycle capability-tag reference for completeness. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
chiga0
left a comment
There was a problem hiding this comment.
Re-Review at HEAD 21cda4c
5 new commits since last review. All actionable feedback addressed.
Re-Review Status Table
| Previous Finding | Status | Evidence |
|---|---|---|
| Docs accuracy (live vs list shape) | ✓ Fixed | e029794 — reworded to "live bridge summary", spelled out differences from list endpoint (createdAt source, updatedAt omission, displayName conditional) |
| 404 response body docs | ✓ Fixed | e029794 — docs now describe actual { error, sessionId } body |
| Capability advertisement missing | ✓ Fixed | e4104ef — session_status added to SERVE_CAPABILITY_REGISTRY, EXPECTED_STAGE1_FEATURES test, protocol docs, and capability→route map. Author correctly left resolveDaemonTelemetryRoute alone for sibling consistency (no GET status routes in it). |
displayName omission untested |
✓ Fixed | 842d722 — new test asserts 'displayName' in res.body === false when summary has no displayName |
| Enrichment: phase information | — Deferred | Broader scope (requires EventBus scanning logic), appropriate as follow-up. Current shape is well-documented and the capability tag lets clients gate on availability. |
Additional changes
7eff417— Integration capabilities baseline updated withsession_status21cda4c— Merge from main
No new issues. The PR is clean and well-documented.
Verdict: Approve
This review was generated by QoderWork AI
chiga0
left a comment
There was a problem hiding this comment.
Re-Review at HEAD 21cda4c
Re-Review Status Table
| Previous Finding | Status | Evidence |
|---|---|---|
| Docs: live vs list shape 描述不准确 | ✓ Fixed | e029794 — 文档已重写,明确区分 live bridge summary 与 list endpoint 的差异 |
| Docs: 404 响应体描述不准 | ✓ Fixed | e029794 — 已更新为 { error, sessionId } |
缺少 session_status capability 广告 |
✓ Fixed | e4104ef — 已注册到 SERVE_CAPABILITY_REGISTRY、测试基线、协议文档 |
| 缺少 displayName 缺失场景测试 | ✓ Fixed | 842d722 — 新增 test 断言 'displayName' in res.body === false |
| Enrichment: phase 信息 (thinking/generating/tool_execution) | — Deferred | 合理的后续优化,不阻塞本 PR |
All actionable feedback has been addressed. The code is clean, tests are comprehensive, and the implementation is well-scoped.
This review was generated by QoderWork AI
doudouOUC
left a comment
There was a problem hiding this comment.
Re-review at 762c94b6 (merge commit only, no new author commits). All 828 tests pass, build/typecheck/lint clean.
[Suggestion] Batch 404 test missing /session/:id/status — packages/cli/src/serve/server.test.ts:4671
The batch test exercising /session/missing/{context,supported-commands,stats,tasks,lsp} was not updated to include /session/missing/status. While the PR has its own dedicated 404 test, the batch test serves as a regression safety net for consistent error handling across all read-only session routes.
Suggested fix: Add { method: 'get', path: '/session/missing/status' } to the batch test alongside the existing routes.
| } | ||
| }); | ||
|
|
||
| app.get('/session/:id/status', (req, res) => { |
There was a problem hiding this comment.
[Suggestion] This returns raw bridge data while GET /workspace/:id/sessions enriches with persisted fields (createdAt from item.startTime, updatedAt from item.mtime, displayName from item.customTitle || item.prompt). This divergence is well-documented but no test pins it. A future refactor could silently harmonize or break the divergence with no test catching it.
Suggested fix: Add a server-level test that sets up a session with persisted metadata and asserts the two endpoints return different createdAt / displayName values for the same session.
— qwen3.7-max via Qwen Code /review
What this PR does
Adds a daemon HTTP endpoint that returns the live status of a single session looked up by its id — session id, workspace cwd, creation time, optional display name, attached client count, and whether a prompt is currently active. The route answers 200 with that summary when the daemon is hosting a live session with the given id, and 404 (body
{ error, sessionId }) when the id is unknown to the daemon. This is the raw live-session view from the bridge: the two fields the use case depends on —clientCountandhasActivePrompt— match the corresponding entry in the workspace session list, but the routes are not byte-identical, because the list endpoint enriches each item with persisted session-store data (itscreatedAtis the persisted first-prompt time, plus anupdatedAtand a deriveddisplayName) while/statusreports the live session's owncreatedAt, omitsupdatedAt, and returnsdisplayNameonly when one is set on the live session.Why it's needed
Until now the only way to read a session's live state was the full, paginated workspace session list. A caller that already holds a session id and only wants to know "is this one session still running, and how many clients are attached?" had to fetch every page and filter on the client side — wasteful and racy as the session list grows. A direct by-id lookup is the natural primitive for that question. The motivating case is a client UI that drives a session and wants to reflect its run state in the surrounding controls — for example disabling buttons that must not be clicked mid-run, or showing a "task in progress" hint — by polling that one session's active-prompt flag until its task completes.
The status data already lives on the bridge, so the change is small: a single by-id accessor that shares the same summary builder as the list path, the new route, unit tests on both layers, and a documentation note.
Reviewer Test Plan
How to verify
Start a daemon (
qwen serve), open a session, and request its status by id:Expected: 200 with the summary object for a live session id; while a prompt is running on that session,
hasActivePromptreadstrue;clientCounttracks attached clients. Requesting an id the daemon does not host returns 404 with the session id echoed in the error body.Unit coverage: the bridge accessor returns the summary for a known id and throws for an unknown one; the route returns 200 with the summary and 404 when the session is unknown. Run
cd packages/acp-bridge && npx vitest run src/bridge.test.tsandcd packages/cli && npx vitest run src/serve/server.test.ts.Evidence (Before & After)
N/A — daemon HTTP API addition, no user-visible TUI change.
Tested on
Environment (optional)
Unit tests via vitest; both affected packages typecheck and lint clean.
Risk & Scope
Linked Issues
Closes #5855
中文说明
这个 PR 做了什么
新增一个 daemon HTTP 接口,按 session id 查询单个 session 的实时状态——session id、工作区 cwd、创建时间、可选的显示名、已连接客户端数,以及当前是否有正在执行的 prompt。当 daemon 持有该 id 对应的存活 session 时返回 200 及该 summary,id 未知时返回 404(响应体
{ error, sessionId })。这是来自 bridge 的原始实时视图:用例真正依赖的两个字段clientCount、hasActivePrompt与工作区 session 列表中对应项一致,但两个接口并非逐字节相同——列表接口会用持久化 session store 数据对每项做富化(其createdAt是持久化的首个 prompt 时间,另含updatedAt和派生的displayName),而/status返回存活 session 自身的createdAt、不含updatedAt,且仅在存活 session 设置了显示名时才返回displayName。为什么需要
此前读取 session 实时状态的唯一方式是拉取完整的、分页的工作区 session 列表。一个已经握着 session id、只想知道"这个 session 还在跑吗、连了几个客户端"的调用方,必须翻完所有分页再在客户端自己过滤——列表一大就低效且有竞态。按 id 直接单查才是这个问题的自然原语。典型场景是一个驱动 session 的客户端 UI,希望据此在周边控件上反映运行状态——比如禁用运行期间不可点击的按钮、或显示"任务进行中"提示——通过轮询该 session 的 active-prompt 标志,直到其任务完成。
状态数据本来就在 bridge 上,所以改动很小:一个共用列表路径同一 summary 构建逻辑的按 id 访问器、新路由、两层的单测,以及一段文档说明。
验证方式
启动 daemon(
qwen serve),开一个 session,按 id 请求其状态:对存活的 session id 返回 200 及 summary;prompt 运行期间hasActivePrompt为true,clientCount跟踪已连接客户端;请求 daemon 未持有的 id 返回 404 并在错误体中回显该 session id。单测覆盖 bridge 访问器与路由的命中/未命中两条路径。风险与范围
关联 issue:Closes #5855