Skip to content

feat(serve+sdk): add GET /session/:id/stats + /export (#4514 T2.5+T2.6)#4515

Closed
doudouOUC wants to merge 6 commits into
daemon_mode_b_mainfrom
feat/daemon-session-stats-export
Closed

feat(serve+sdk): add GET /session/:id/stats + /export (#4514 T2.5+T2.6)#4515
doudouOUC wants to merge 6 commits into
daemon_mode_b_mainfrom
feat/daemon-session-stats-export

Conversation

@doudouOUC

Copy link
Copy Markdown
Collaborator

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 + normalizeSessionData SSOT the TUI /stats and /export slash 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 matching Content-Type + Content-Disposition so 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 tags session_stats (always-on) and session_export (advertises modes: ['md','html','json','jsonl'] so SDK clients can feature-detect supported formatters).

SDK helpers DaemonClient.sessionStats / sessionExport plus the session-bound session.stats() / session.export(format). The export helper reads the response as text and parses both legacy filename="..." and RFC 5987 filename*=utf-8''... Content-Disposition shapes.

Test plan

  • npm --workspaces typecheck clean
  • eslint clean for all touched files
  • packages/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, unknown format 400, repeated format 400) + EXPECTED_STAGE1_FEATURES updated
  • packages/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 mapping
  • packages/sdk-typescript/test/unit/DaemonSessionClient.test.ts — session-scoped wrapper test extended for session.stats() + session.export()
  • packages/sdk-typescript/test/unit/daemon-public-surface.test.ts — pins DAEMON_SESSION_EXPORT_FORMATS at the public entry
  • Combined run across all four packages: 417 / 417 passing

Linked

🤖 Generated with Qwen Code

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)
@github-actions

Copy link
Copy Markdown
Contributor

📋 Review Summary

This PR implements two new read-only session routes (GET /session/:id/stats and GET /session/:id/export) that surface per-session aggregate metrics and conversation export functionality. The implementation correctly reuses the existing collectSessionData + normalizeSessionData pipeline from the TUI /stats and /export slash commands, ensuring consistency between daemon and CLI surfaces. The changes span the ACP bridge extension methods, HTTP routes, SDK TypeScript client, and documentation.

🔍 General Feedback

  • Strong architectural consistency: The decision to reuse collectSessionData and normalizeSessionData as the SSOT for session data is excellent — this ensures daemon and TUI outputs remain byte-identical for the same session state.
  • Good capability tagging: The session_stats (always-on) and session_export (with modes array for feature detection) capability tags follow the established pattern from /context and /supported-commands.
  • Comprehensive test coverage: 15 new tests across 4 test files covering HTTP routes, bridge ext-methods, and SDK client helpers.
  • RFC 5987 compliance: The parseAttachmentFilename helper correctly handles both legacy filename="..." and RFC 5987 filename*=utf-8''... Content-Disposition formats.
  • Good documentation: Protocol docs clearly describe response shapes, error cases, and pre-flight requirements.

🎯 Specific Feedback

🟡 High

  • Missing ext-method registration: The diff shows SERVE_STATUS_EXT_METHODS in packages/acp-bridge/src/status.ts only lists sessionContext and sessionSupportedCommands (lines 96-97), but the PR description mentions new ext-methods qwen/status/session/stats and qwen/status/session/export. These need to be added to the SERVE_STATUS_EXT_METHODS constant for the bridge to route these calls correctly.

  • Missing server route handlers: The grep searches of packages/cli/src/serve/server.ts did not find the actual route handlers for /session/:id/stats and /session/:id/export. Based on the pattern of existing routes like /session/:id/context (lines 1056-1073), these should be added as app.get() handlers that call through to the bridge. Please verify these routes are actually registered.

🟢 Medium

  • Export format validation: The /export route should validate the format query parameter against the advertised caps.features.session_export.modes before forwarding to the ACP child. The docs mention "Anything else → 400" but the implementation should explicitly check this at the route level for defense in depth.

  • Session load state check: The protocol docs state "Requires a live session... For archived sessions, call POST /session/:id/load first." Consider adding a check that returns a clearer error (perhaps 400 with session_not_loaded) when the session exists but isn't currently loaded, rather than letting the ACP call fail with a generic error.

🔵 Low

  • Filename fallback consistency: In DaemonClient.sessionExport, the fallback filename uses qwen-session-${sessionId}.${format}. Consider extracting this to a shared constant alongside SERVE_SESSION_EXPORT_FORMATS for consistency if the server side uses the same pattern.

  • Error message localization: The parseAttachmentFilename function has a comment "Fall through to the legacy form below" — consider adding a brief comment explaining why RFC 5987 decoding might fail (e.g., malformed encoding) for future maintainers.

  • Test coverage gap: The daemon-public-surface.test.ts test pins DAEMON_SESSION_EXPORT_FORMATS but there's no corresponding test that the enum values match the server-side SERVE_SESSION_EXPORT_FORMATS. Consider adding a drift detection test similar to the DAEMON_APPROVAL_MODES test mentioned in the types.ts comments.

✅ Highlights

  • Excellent SSOT pattern: Reusing collectSessionData + normalizeSessionData ensures daemon and TUI outputs are byte-identical — this is the right architectural choice for maintaining consistency.
  • RFC 5987 support: The parseAttachmentFilename helper correctly handles both legacy and RFC 5987 Content-Disposition formats, showing attention to detail in HTTP standards compliance.
  • Comprehensive SDK helpers: The DaemonClient.sessionStats/sessionExport and session-scoped session.stats()/session.export() wrappers provide a clean, ergonomic API for SDK consumers.
  • Good error handling: The tests cover URL encoding (with/slashwith%2Fslash), header passthrough, RFC 5987 filename decoding, derived filename fallback, and non-2xx error mapping.
  • Clear capability advertising: The session_export capability tag includes modes: ['md','html','json','jsonl'] so SDK clients can feature-detect supported formats before requesting them from older daemons.

🔧 Action Items

  1. Verify ext-method registration: Ensure qwen/status/session/stats and qwen/status/session/export are added to SERVE_STATUS_EXT_METHODS in packages/acp-bridge/src/status.ts.

  2. Verify route handlers: Confirm the HTTP route handlers for /session/:id/stats and /session/:id/export are registered in packages/cli/src/serve/server.ts.

  3. Consider format validation: Add explicit format validation at the route level against advertised capability modes.

  4. Add drift detection test: Consider adding a test to verify DAEMON_SESSION_EXPORT_FORMATS matches the server-side enum.

Copilot AI 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.

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_export with modes).
  • 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.

Comment thread packages/cli/src/serve/server.ts
Comment thread packages/sdk-typescript/src/daemon/DaemonClient.ts Outdated
Comment thread packages/sdk-typescript/src/daemon/types.ts
Comment thread docs/developers/qwen-serve-protocol.md
Comment thread docs/users/qwen-serve.md Outdated
doudouOUC added 3 commits May 26, 2026 01:54
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)
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

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.

  1. "Missing ext-method registration"SERVE_STATUS_EXT_METHODS does add sessionStats + sessionExport in the initial commit (packages/acp-bridge/src/status.ts lines ~99-115).
  2. "Missing server route handlers"app.get('/session/:id/stats', ...) and app.get('/session/:id/export', ...) are both registered in the initial commit (packages/cli/src/serve/server.ts around lines 1210 / 1239).
  3. "Format validation" — the route validates format against SERVE_SESSION_EXPORT_FORMATS before forwarding, and the ACP child re-validates it as defense in depth. Tests cover unknown / repeated formats → 400.
  4. "Drift detection test" — added in fold-in bb9224c11: daemon-public-surface.test.ts imports SERVE_SESSION_EXPORT_FORMATS from @qwen-code/acp-bridge/status and asserts lockstep with DAEMON_SESSION_EXPORT_FORMATS.

(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)
Comment thread packages/sdk-typescript/src/daemon/DaemonSessionClient.ts Outdated
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Closing this for now after re-triaging #4514. The base stats/export behavior is already reachable through the existing slash-command passthrough path via POST /session/:id/prompt (/stats, /export md|html|json|jsonl).

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 /prompt or that have a concrete downstream need for typed JSON/download semantics.

This can be reopened later if a client explicitly needs structured GET /session/:id/stats or browser-download-friendly GET /session/:id/export.

@doudouOUC doudouOUC closed this May 26, 2026
xaelistic pushed a commit to xaelistic/qwen-code that referenced this pull request Jun 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants