feat(serve+sdk): add GET /session/:id/stats + /export (#4514 T2.5+T2.6)#4515
feat(serve+sdk): add GET /session/:id/stats + /export (#4514 T2.5+T2.6)#4515doudouOUC wants to merge 6 commits into
Conversation
Two read-only session routes that reuse the `collectSessionData` +
`normalizeSessionData` SSOT the `/stats` and `/export` slash commands
already drive, so daemon surfaces and TUI surfaces agree for any
session state flushed to disk.
- `GET /session/:id/stats` returns the metadata envelope (promptCount,
totalTokens, context utilization, files touched).
- `GET /session/:id/export?format=md|html|json|jsonl` streams the
formatter body verbatim with the matching `Content-Type` and
`Content-Disposition` headers so a browser `<a download>` works
out of the box.
Both routes go through the ACP child via new `qwen/status/session/{stats,export}`
ext-methods, live-session required (mirrors the `/context` posture).
Capability tags `session_stats` (always-on) and `session_export`
(advertises `modes: ['md','html','json','jsonl']` for client feature
detection).
SDK helpers: `DaemonClient.sessionStats` / `sessionExport`,
`DaemonSessionClient.stats()` / `export()`. The export helper reads
the response as text and parses both legacy `filename="..."` and
RFC 5987 `filename*=utf-8''...` Content-Disposition shapes.
Tests cover the bridge ext-method label + shape, the HTTP route
including format validation (unknown / repeated `?format` → 400) +
404 mapping, the SDK fetch paths including the filename fallback,
and the public surface re-exports.
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
📋 Review SummaryThis PR implements two new read-only session routes ( 🔍 General Feedback
🎯 Specific Feedback🟡 High
🟢 Medium
🔵 Low
✅ Highlights
🔧 Action Items
|
There was a problem hiding this comment.
Pull request overview
Adds two read-only per-session daemon routes—GET /session/:id/stats and GET /session/:id/export—wired through new ACP ext-methods and surfaced via the TypeScript SDK, with accompanying capability tags and tests to keep the HTTP/ACP/SDK layers in sync.
Changes:
- Add daemon HTTP routes for session stats and session export (format-dispatched, with content headers for browser downloads).
- Add ACP bridge + agent ext-method implementations for
qwen/status/session/{stats,export}plus new capability tags (session_stats,session_exportwithmodes). - Add SDK client/session helpers and expand unit tests + protocol/user docs.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/cli/src/serve/server.ts | Adds GET /session/:id/stats and GET /session/:id/export HTTP routes. |
| packages/cli/src/serve/server.test.ts | Adds route tests (happy paths + validation + missing-session mappings) and capability ordering pin. |
| packages/cli/src/serve/capabilities.ts | Registers session_stats and session_export capabilities (with modes). |
| packages/cli/src/acp-integration/acpAgent.ts | Implements ACP-side ext-method handlers to compute stats and render exports from persisted JSONL. |
| packages/acp-bridge/src/status.ts | Adds ext-method labels and wire types for stats/export + export format enum. |
| packages/acp-bridge/src/bridgeTypes.ts | Extends HttpAcpBridge interface with getSessionStats/getSessionExport. |
| packages/acp-bridge/src/bridge.ts | Implements bridge forwarding for new status ext-methods (incl. export payload). |
| packages/acp-bridge/src/bridge.test.ts | Adds ext-method forwarding tests and unknown-session rejection tests. |
| packages/sdk-typescript/src/daemon/DaemonClient.ts | Adds sessionStats and sessionExport, plus Content-Disposition filename parsing. |
| packages/sdk-typescript/src/daemon/DaemonSessionClient.ts | Adds session-bound convenience wrappers stats() and export(). |
| packages/sdk-typescript/src/daemon/types.ts | Introduces DaemonSessionStats, DaemonSessionExport, and export format constants/types. |
| packages/sdk-typescript/src/daemon/index.ts | Re-exports new daemon types/constants from the daemon entry. |
| packages/sdk-typescript/src/index.ts | Re-exports new daemon types/constants from the public SDK barrel. |
| packages/sdk-typescript/test/unit/DaemonClient.test.ts | Adds SDK unit tests for stats/export URL encoding, header parsing, and error mapping. |
| packages/sdk-typescript/test/unit/DaemonSessionClient.test.ts | Extends session wrapper tests to cover session.stats() and session.export(). |
| packages/sdk-typescript/test/unit/daemon-public-surface.test.ts | Pins DAEMON_SESSION_EXPORT_FORMATS on the public entry surface. |
| docs/users/qwen-serve.md | Documents the new session stats/export routes at a high level. |
| docs/developers/qwen-serve-protocol.md | Documents the new protocol routes, capability tags, params, and response semantics. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Critical fixes surfaced by deep review:
1. `sendBridgeError` now maps JSON-RPC error codes from the ACP child
to HTTP statuses (-32700/-32600/-32602 → 400, -32601 → 404). The
ACP SDK forwards agent-side `RequestError` as a plain
`{code, message, data}` object (not an Error instance), so the
`instanceof` chain in `sendBridgeError` was missing it and
`loadNormalizedSessionData`'s `RequestError.invalidParams` was
silently falling to 500 instead of the documented 400. Tested with
both `-32602 → 400` and `-32601 → 404` cases on the new routes.
2. `/session/:id/export` now validates the bridge response shape
before stamping headers: contentType / filename / body must be
strings and the echoed `format` must match the requested one.
Mismatches are 500'd with a `bridge returned malformed export
envelope` operator breadcrumb instead of throwing a setHeader
TypeError with a stack body. The filename is also CRLF/quote-
stripped before being interpolated into the `Content-Disposition`
header so a future filename builder that incorporates user input
can't inject extra response headers.
3. Comment + doc corrections — the previous wording claimed the
daemon `/stats` route shared a SSOT with the TUI `/stats` slash
command. It doesn't: the slash command reads in-memory
`uiTelemetryService` counters while the daemon route runs
`collectSessionData` + `normalizeSessionData` on the persisted
JSONL (same pipeline as the `/export` slash command). Also
corrected: `chatRecordingService.appendRecord` is async via a
serialized write chain (not sync), the suggested filename is
`qwen-code-export-<ts>.<ext>` from `generateExportFilename`
(not `qwen-session-<id>-<ts>.<ext>`, which is only the SDK-side
fallback when the response header is missing), and the route's
default format is `md` for SDK-probe ergonomics (not "parity with
the /export slash command" — that defaults to html).
4. New regression tests (6 added):
- bearer-auth required on `/session/:id/stats` + `/export`
- JSON-RPC `-32602` from the agent maps to HTTP 400
- JSON-RPC `-32601` from the agent maps to HTTP 404
- malformed bridge response → 500 + stderr breadcrumb
- CRLF in filename gets sanitized out of Content-Disposition
- SDK + bridge export format arrays pinned in lockstep (drift fence)
Counts: 417 → 423 passing tests across the four touched packages.
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
…k-error disambiguation
Three findings from the second deep-review pass:
1. `jsonRpcCodeToHttpStatus` now returns `{status, code}` rather than
just status, and `sendBridgeError` echoes the per-code identifier
to the response body. The previous fold-in hardcoded
`body.code: 'invalid_params'` for every mapped JSON-RPC code, so
a `-32601 → 404` (method not found) was returning
`{code: 'invalid_params'}` — SDK clients branching on the code
field for "fix your params" UX would have misfired on a route-
dropped 404. New mapping: -32700 → `parse_error`, -32600 →
`invalid_request`, -32602 → `invalid_params`, -32601 →
`method_not_found`. `-32603` Internal Error still falls through
to the 500 + operator-stderr path.
2. `loadNormalizedSessionData` now `fs.stat`s the JSONL path before
calling `SessionService.loadSession` so the disk error modes stay
distinguishable. Previously every undefined result collapsed into
the misleading "no persisted records yet" 400, which the fold-in's
JSON-RPC mapping made *strictly worse* by also dropping the
operator-stderr breadcrumb — a daemon with a permission-flipped
chats dir would silently lie to every `/stats` caller. New
contract:
- ENOENT → invalidParams → HTTP 400 with actionable message
- EACCES / EIO / ENOTDIR → real Error → 500 + stderr breadcrumb
- File exists but loadSession returns undefined (project hash
mismatch, corrupt JSONL) → distinct 500 + stderr breadcrumb
3. Five new regression tests:
- `-32601` body.code is `method_not_found` (not `invalid_params`)
- `-32602` body.code is `invalid_params`
- `-32700` Parse error → 400 + `parse_error`
- `-32600` Invalid Request → 400 + `invalid_request`
- `-32603` Internal Error stays 500 (negative pin against future
broadening that would silently swallow internal errors as 4xx)
- Format-echo mismatch in `/export` shape validator → 500
Counts: 423 → 428 passing tests.
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
… + doc accuracy Three real findings from the GitHub Copilot review pass on PR #4515: 1. `DaemonClient.sessionExport` fallback filename now sanitizes the sessionId before interpolating it. The qwen daemon's `SESSION_ID_RE` only accepts `[0-9a-fA-F-]`, so this is theoretical against a real qwen serve deployment — but the SDK can in principle talk to any non-qwen daemon that re-uses the wire shape, and a session id with a literal `/` would otherwise produce `qwen-session-with/slash.md` that breaks `Blob` / `a.download` and could traverse upward in legacy file pickers. New `sanitizeSessionIdForFilename` collapses non-`[A-Za-z0-9._-]` characters to `_`. 2. `DaemonSessionExport` JSDoc fixed: previously said `DaemonClient.exportSession` (the method is actually `sessionExport`); also softened the filename pattern claim from a fixed shape to "daemon-selected" so SDK consumers treat it as opaque rather than parsing. 3. `docs/users/qwen-serve.md` archived-session note fixed: an unknown-to-daemon session id returns `404` (the bridge rejects before consulting disk), `400 invalid_params` is reserved for live sessions that have not yet flushed any records. The previous wording conflated the two cases. Copilot also flagged the Content-Disposition CRLF risk and the filename pattern in the protocol doc — both were already addressed in fold-in `bb9224c11` (CRLF sanitizer + protocol doc rewrite); no code change needed there, just bot replies. Counts: 428 → 429 passing tests. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
|
Thanks — for the record: the four numbered Action Items in this summary are all based on a partial view of the diff and have been addressed in the commits already on the branch.
(Line-level comments from the Copilot review pass were addressed in this thread above.) |
`/simplify` pass found three over-justified comment blocks (~14-line JSDoc on a 1-line replace, 19-line JSDoc on DaemonSessionExport, 7-line test comment paraphrasing the commit message). Trimmed each to the non-obvious WHY: helper now points at the near-twin in `packages/core/src/agents/agent-transcript.ts` so a future `@qwen-code/utils` refactor has a breadcrumb. Behavior unchanged; 132/132 sdk tests still pass. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Closing this for now after re-triaging #4514. The base stats/export behavior is already reachable through the existing slash-command passthrough path via The dedicated HTTP/SDK routes in this PR are still valid as optional structured API convenience, but they are no longer classified as missing daemon capability. Let's keep the backlog focused on gaps that cannot be represented through This can be reopened later if a client explicitly needs structured |
Summary
Closes Tier-2 rows T2.5 and T2.6 in the daemon backlog (issue #4514). Two read-only session routes that reuse the
collectSessionData+normalizeSessionDataSSOT the TUI/statsand/exportslash commands already drive — so daemon surfaces and the CLI agree for any state flushed to disk.GET /session/:id/stats— per-session aggregate metrics (promptCount, totalTokens, files touched, context utilization).GET /session/:id/export?format=md|html|json|jsonl— streams the formatter body verbatim with the matchingContent-Type+Content-Dispositionso a browser<a download>works out of the box.Both routes route through the ACP child via new
qwen/status/session/{stats,export}ext-methods (read-only namespace, mirrors/context's posture). Capability tagssession_stats(always-on) andsession_export(advertisesmodes: ['md','html','json','jsonl']so SDK clients can feature-detect supported formatters).SDK helpers
DaemonClient.sessionStats/sessionExportplus the session-boundsession.stats()/session.export(format). The export helper reads the response as text and parses both legacyfilename="..."and RFC 5987filename*=utf-8''...Content-Disposition shapes.Test plan
npm --workspaces typecheckcleaneslintclean for all touched filespackages/acp-bridge/src/bridge.test.ts— 4 new ext-method tests (forwards label + shape, rejects unknown session for both routes)packages/cli/src/serve/server.test.ts— 6 new HTTP route tests (200 happy path × 2, missing-session 404 × 2, unknownformat400, repeatedformat400) +EXPECTED_STAGE1_FEATURESupdatedpackages/sdk-typescript/test/unit/DaemonClient.test.ts— 5 new fetch-path tests covering URL encoding, header passthrough, RFC 5987 filename decode, derived-filename fallback, and non-2xx error mappingpackages/sdk-typescript/test/unit/DaemonSessionClient.test.ts— session-scoped wrapper test extended forsession.stats()+session.export()packages/sdk-typescript/test/unit/daemon-public-surface.test.ts— pinsDAEMON_SESSION_EXPORT_FORMATSat the public entryLinked
/recap) — followed the same bridge → ext-method → SDK → docs shapedaemon_mode_b_main @ 81b46c2(matches issue tracking(serve): daemon capability gaps & prioritized backlog (post v0.16-alpha) #4514 snapshot)🤖 Generated with Qwen Code