Skip to content

feat(daemon): add session tasks snapshot endpoint#4578

Merged
doudouOUC merged 1 commit into
QwenLM:daemon_mode_b_mainfrom
doudouOUC:codex/daemon-tasks-snapshot
May 28, 2026
Merged

feat(daemon): add session tasks snapshot endpoint#4578
doudouOUC merged 1 commit into
QwenLM:daemon_mode_b_mainfrom
doudouOUC:codex/daemon-tasks-snapshot

Conversation

@doudouOUC

Copy link
Copy Markdown
Collaborator

Summary

  • What changed: added a read-only daemon session task snapshot API (GET /session/:id/tasks) backed by ACP status extMethod qwen/status/session/tasks, SDK helpers, and web-shell /tasks local handling.
  • Why it changed: web-shell needs to inspect background tasks while a prompt is streaming without sending another ACP prompt or waiting behind the prompt queue.
  • Reviewer focus: whitelist task serialization, bridge status path bypassing prompt FIFO, and web-shell /tasks interception before generic slash forwarding.

Validation

  • Commands run:
    cd packages/acp-bridge && npx vitest run src/bridge.test.ts
    cd packages/cli && npx vitest run src/serve/server.test.ts src/acp-integration/acpAgent.test.ts
    cd packages/sdk-typescript && npx vitest run test/unit/DaemonClient.test.ts test/unit/DaemonSessionClient.test.ts
    npx vitest run packages/web-shell/client/utils/tasksCommand.test.ts --environment node --coverage=false --config /dev/null
    cd packages/web-shell && npm run build
    npm run build && npm run typecheck
  • Prompts / inputs used: N/A.
  • Expected result: /tasks in web-shell fetches a local snapshot while streaming and does not call sendPrompt or enqueue.
  • Observed result: unit coverage verifies the bridge status path bypasses the prompt queue, ACP serializes sanitized sorted task snapshots, HTTP/SDK routes use /session/:id/tasks, and web-shell handler dispatches local status output.
  • Quickest reviewer verification path: run the targeted vitest commands above, then manually start web-shell against a daemon and submit /tasks while a long prompt is streaming.
  • Evidence: targeted tests and full build/typecheck passed locally. npm run build && npm run typecheck printed existing vscode companion lint warnings but exited 0.

Scope / Risk

  • Main risk or tradeoff: V1 is snapshot-only; task stop, output tailing, and live SSE updates are intentionally not included.
  • Not covered / not validated: no browser screenshot/video for the web-shell status output.
  • Breaking changes / migration notes: none; additive route, capability, SDK methods, and types.

Testing Matrix

🍏 🪟 🐧
npm run ⚠️ ⚠️
npx ⚠️ ⚠️
Docker N/A N/A N/A
Podman N/A N/A N/A
Seatbelt N/A N/A N/A

Testing matrix notes:

  • Tested on macOS in the local worktree. Windows/Linux not run.

Linked Issues / Bugs

  • None.

Copilot AI review requested due to automatic review settings May 27, 2026 16:47
@github-actions

Copy link
Copy Markdown
Contributor

📋 Review Summary

This PR implements a read-only daemon session task snapshot API (GET /session/:id/tasks) that allows web-shell to inspect background tasks while a prompt is streaming without interfering with the prompt queue. The implementation spans the ACP bridge, CLI server, SDK TypeScript client, and web-shell UI. Overall, the code is well-structured with good type safety, comprehensive test coverage, and follows existing project conventions. The design correctly separates concerns between layers and maintains the read-only, out-of-band nature of the endpoint.

🔍 General Feedback

  • Positive architectural decisions: The implementation correctly keeps this as a read-only snapshot endpoint that bypasses the prompt FIFO queue, which is the core design goal. The separation between SERVE_STATUS_EXT_METHODS and SERVE_CONTROL_EXT_METHODS is a good pattern for distinguishing read-only vs mutation operations.
  • Type safety: Excellent use of discriminated unions for task types (agent, shell, monitor) with exhaustive type checking via the never pattern in switch statements.
  • Test coverage: Comprehensive unit tests across all layers (bridge, SDK, web-shell) with good edge case coverage.
  • Documentation: Protocol documentation in qwen-serve-protocol.md clearly specifies the new endpoint and its constraints.
  • Forward compatibility: Schema version (v: 1) and forward-compatible type patterns (e.g., string & {} for scope fields) follow the project's versioning conventions.

🎯 Specific Feedback

🟡 High

  • File: packages/web-shell/client/utils/tasksCommand.ts:17-27 - The sanitizeTaskText function removes control characters but preserves ANSI escape sequences partially (it removes codes 0x7f-0x9f but ANSI color codes use \u001b[ sequences which start with 0x1b). The test at line 42 in tasksCommand.test.ts shows npm test[31m instead of the full escape sequence, suggesting the sanitization may not be fully effective. Consider using a more robust ANSI stripping library or a complete regex-based approach. Suggested fix: Use a regex like /\x1b\[[0-9;]*m/g to strip ANSI escape sequences completely, or leverage an existing library like ansi-regex that's already in the dependency tree.

🟢 Medium

  • File: packages/cli/src/acp-integration/session/tasksSnapshot.ts:30-38 - The runtimeMs calculation uses Math.max(0, ...) which is defensive but the endTime ?? now pattern could produce misleading results for tasks that haven't been updated recently. If a task's endTime is never set but the task is no longer actually running, the runtime will keep growing. Suggested improvement: Add a comment explaining why this is acceptable (e.g., "Tasks are expected to have endTime set when completed; this is a defensive fallback for in-flight snapshots").

  • File: packages/acp-bridge/src/status.ts:437-443 - The ServeSessionMonitorTaskStatus interface has ownerAgentId?: string as optional, which is good for forward compatibility, but there's no corresponding field in ServeSessionAgentTaskStatus to track child monitors. This asymmetry is probably intentional (the parent knows about children, not vice versa), but a brief comment would help future maintainers.

  • File: packages/sdk-typescript/src/daemon/types.ts:815-820 - The DaemonSessionTasksStatus interface uses v: 1 literal instead of typeof STATUS_SCHEMA_VERSION like the bridge-side types. This creates a potential drift risk if the schema version is bumped in one place but not the other. Suggested fix: Import and use STATUS_SCHEMA_VERSION from the bridge package, or define the schema version constant in a shared location.

🔵 Low

  • File: packages/cli/src/serve/server.ts:1430-1447 - The new /session/:id/tasks route handler follows the existing error handling pattern well. Consider extracting the session ID validation pattern (if (!sessionId) { res.status(400).json(...) }) into a shared helper since it's repeated across multiple route handlers. This is a minor consistency improvement, not a blocker.

  • File: packages/web-shell/client/utils/tasksCommand.ts:107-117 - The error handling in handleTasksSlashCommand uses reportError(error, 'Failed to load tasks') but doesn't provide context about which session failed. Since this function doesn't have the session ID in its input, consider adding it for better debugging. Suggested enhancement: Add sessionId: string to the input parameter object.

  • File: packages/web-shell/client/utils/tasksCommand.ts:91-96 - The output file path display uses fixed indentation ( output:) which may not align well for task IDs of varying lengths. This is a UI polish issue, not a functional problem.

  • File: docs/developers/qwen-serve-protocol.md:899-907 - The protocol documentation shows an example with only an agent task type. Consider adding examples of shell and monitor task types to make the full schema clearer for API consumers.

✅ Highlights

  • Excellent type design: The discriminated union pattern for task types with exhaustive checking (const _exhaustive: never = task) is textbook TypeScript best practice.
  • Clean separation of concerns: The buildSessionTasksStatus function in tasksSnapshot.ts cleanly separates serialization logic from the HTTP layer, making it easy to test and reuse.
  • Comprehensive test coverage: Tests cover empty task lists, control character sanitization, command dispatch, and the full SDK integration path. The test at bridge.test.ts verifying the status path bypasses the prompt queue is particularly important for the core design goal.
  • Security-conscious: The whitelist approach to task serialization (explicitly naming fields rather than spreading objects) prevents accidental exposure of internal state. The comment at line 885 in qwen-serve-protocol.md explicitly calls out what is NOT exposed: "controllers, timers, offsets, pending messages, and raw registry objects are never exposed."
  • Good error handling: Consistent use of sendBridgeError helper and proper error propagation through all layers.

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 an out-of-band, read-only “session tasks snapshot” surface to the daemon stack so UIs (notably web-shell) can inspect background tasks during an actively streaming prompt without going through the prompt FIFO.

Changes:

  • Introduces GET /session/:id/tasks in qwen serve, backed by ACP status extMethod qwen/status/session/tasks.
  • Extends the TypeScript SDK with task snapshot types plus DaemonClient.sessionTasks() / DaemonSessionClient.tasks().
  • Adds a web-shell /tasks local slash-command handler that fetches and prints the snapshot, plus unit coverage.

Reviewed changes

Copilot reviewed 26 out of 27 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/web-shell/client/utils/tasksCommand.ts Adds local /tasks formatter + handler that fetches session task snapshots via SDK.
packages/web-shell/client/utils/tasksCommand.test.ts Adds unit tests for snapshot formatting and local command handling.
packages/web-shell/client/hooks/useDaemonSession.ts Exposes actions.getTasks() that delegates to session.tasks().
packages/web-shell/client/App.tsx Intercepts /tasks before generic slash forwarding/enqueueing.
packages/sdk-typescript/test/unit/DaemonSessionClient.test.ts Verifies DaemonSessionClient.tasks() hits /session/:id/tasks.
packages/sdk-typescript/test/unit/DaemonClient.test.ts Verifies DaemonClient.sessionTasks() URL-encodes session ids and sends client id header.
packages/sdk-typescript/src/index.ts Re-exports new daemon task snapshot types from the SDK root.
packages/sdk-typescript/src/daemon/types.ts Defines daemon-side session task snapshot wire types.
packages/sdk-typescript/src/daemon/index.ts Re-exports daemon task snapshot types.
packages/sdk-typescript/src/daemon/DaemonSessionClient.ts Adds tasks() convenience method on session client.
packages/sdk-typescript/src/daemon/DaemonClient.ts Adds sessionTasks() method mapping to GET /session/:id/tasks.
packages/cli/src/serve/server.ts Implements GET /session/:id/tasks route calling the bridge snapshot method.
packages/cli/src/serve/server.test.ts Covers the new route and 404 mapping for missing sessions.
packages/cli/src/serve/index.ts Re-exports serve task snapshot types.
packages/cli/src/serve/capabilities.ts Adds session_tasks capability tag.
packages/cli/src/acp-integration/session/tasksSnapshot.ts Serializes whitelisted task registries into a stable snapshot shape.
packages/cli/src/acp-integration/acpAgent.ts Adds ACP extMethod handler for qwen/status/session/tasks.
packages/cli/src/acp-integration/acpAgent.test.ts Ensures serialization is sanitized/whitelisted and does not leak internal fields.
packages/acp-bridge/src/status.ts Adds shared serve protocol status types for session task snapshots + ext method constant.
packages/acp-bridge/src/bridgeTypes.ts Extends HttpAcpBridge with getSessionTasksStatus().
packages/acp-bridge/src/bridge.ts Implements getSessionTasksStatus() via requestSessionStatus(...).
packages/acp-bridge/src/bridge.test.ts Ensures tasks status requests bypass the prompt queue and handle missing sessions.
packages/acp-bridge/README.md Documents the new /session/:id/tasks route as part of the lifted status slice.
integration-tests/cli/qwen-serve-routes.test.ts Updates capabilities envelope expectations to include session_tasks.
docs/users/qwen-serve.md Documents the new read-only session tasks snapshot route.
docs/developers/qwen-serve-protocol.md Documents capability tag + wire shape and semantics for GET /session/:id/tasks.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/web-shell/client/utils/tasksCommand.test.ts
Comment thread packages/web-shell/client/utils/tasksCommand.ts
Add a read-only daemon session task snapshot status method and HTTP route so clients can inspect background tasks without sending a prompt.

Expose the snapshot through the TypeScript SDK and intercept /tasks in web-shell before generic slash-command forwarding.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC doudouOUC force-pushed the codex/daemon-tasks-snapshot branch from ac1b37b to da3dbc3 Compare May 27, 2026 16:56
@doudouOUC doudouOUC requested a review from wenshao May 27, 2026 16:57
@doudouOUC doudouOUC self-assigned this May 27, 2026
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Fix summary (58e1629)

Comment Author Action
Vacuous sendPrompt/enqueue assertions in test Copilot Fixed — removed unused mocks and vacuous assertions
Unused promptBlocked parameter Copilot Fixed — removed from function signature, call site, and tests
Review summary (general feedback) github-actions[bot] Acknowledged — all suggestions are nits/polish, not blocking


expect(handled).toBe(false);
});
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] handleTasksSlashCommand error path is untested. When getTasks() rejects, the .catch handler at tasksCommand.ts:117-119 calls reportError(error, 'Failed to load tasks') — but no test exercises this branch.

Consider adding a test case:

it('reports an error when getTasks rejects', async () => {
  const error = new Error('network down');
  const getTasks = vi.fn().mockRejectedValue(error);
  const dispatch = vi.fn();
  const reportError = vi.fn();

  const handled = handleTasksSlashCommand({
    cmd: 'tasks',
    promptBlocked: true,
    getTasks,
    dispatch,
    reportError,
  });

  expect(handled).toBe(true);
  await vi.waitFor(() => expect(reportError).toHaveBeenCalledTimes(1));
  expect(reportError).toHaveBeenCalledWith(error, 'Failed to load tasks');
  expect(dispatch).not.toHaveBeenCalled();
});

— qwen3.7-max via Qwen Code /review

expect(text).not.toContain('\u001b');
});
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] formatTaskStatus is only tested for kind: 'shell' with status: 'running'. The agent kind has distinct branches (failed with error/'unknown error' fallback, paused + resumeBlockedReason, default passthrough). The monitor kind has completed with/without error, failed, and default with event-count pluralization (1 event vs N events). None of these are exercised.

Consider adding test cases covering at least agent with failed, agent with paused+resumeBlockedReason, and monitor with running (to verify event pluralization):

it('renders agent and monitor task kinds with status-specific formatting', () => {
  const text = formatTasksSnapshot(
    makeSnapshot([
      {
        kind: 'agent',
        id: 'a-1',
        label: 'reviewer: check',
        description: 'check',
        status: 'failed',
        startTime: 1_000,
        runtimeMs: 500,
        isBackgrounded: true,
        error: 'timeout',
      },
      {
        kind: 'monitor',
        id: 'm-1',
        label: 'tail logs',
        description: 'tail logs',
        status: 'running',
        startTime: 1_000,
        runtimeMs: 2_000,
        command: 'tail -f',
        eventCount: 1,
        lastEventTime: 2_000,
        droppedLines: 0,
      },
    ]),
  );
  expect(text).toContain('failed: timeout');
  expect(text).toContain('running (1 event)');
});

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented May 28, 2026

Copy link
Copy Markdown
Collaborator

Maintainer Verification Report

环境: macOS Darwin 25.4.0, Node.js v22.17.0
分支: codex/daemon-tasks-snapshot @ da3dbc31
测试时间: 2026-05-28


Build

Package 结果
packages/acp-bridge ✅ 编译成功
packages/cli ✅ 编译成功
packages/sdk-typescript ✅ 编译成功 (10.8s)
packages/web-shell ✅ 编译成功 (vite build + lib + tsc)

Unit Tests

测试文件 结果
acp-bridge/src/bridge.test.ts 199/199 passed
cli/src/serve/server.test.ts + acpAgent.test.ts 394/394 passed
sdk/DaemonClient.test.ts + DaemonSessionClient.test.ts 153/153 passed
web-shell/client/utils/tasksCommand.test.ts 4/4 passed
SDK 全量回归 (14 test files) 667/667 passed

E2E: GET /session/:id/tasks (使用本地构建 + tmux)

通过 tmux 启动本地构建的 qwen serve --port 4171,进行完整 E2E 验证:

1. Capabilities 验证

✅ features 列表包含 "session_tasks"

2. 空状态查询

✅ GET /session/:id/tasks → {"v":1, "sessionId":"...", "now":..., "tasks":[]}

3. Background Shell Task 可见性 (核心场景)

发送 prompt 启动 sleep 30 && echo done 后台任务,在 prompt 完成后连续轮询 /tasks 端点:

✅ Poll 1: tasks=1 [shell] bg_70ab6685: sleep 30 && echo done status=running
✅ Poll 2: tasks=1 [shell] bg_70ab6685: status=running
✅ Poll 3: tasks=1 [shell] bg_70ab6685: status=running
...
✅ 完成后: status=completed, runtimeMs=30064, exitCode=0

关键验证: /tasks 端点在无 active prompt 时成功返回,绕过了 prompt FIFO 队列 (PR 核心目标)

4. 完整字段序列化

✅ {
  "kind": "shell", "id": "bg_70ab6685",
  "label": "sleep 30 && echo done",
  "status": "completed",
  "startTime": 1779930714150,
  "runtimeMs": 30064,
  "outputFile": "/Users/.../.qwen/tmp/.../shell-bg_70ab6685.output",
  "command": "sleep 30 && echo done",
  "cwd": "/Users/.../qwen-code-x2",
  "endTime": 1779930744214,
  "pid": 60492,
  "exitCode": 0
}

5. SDK Client 验证

✅ DaemonClient.sessionTasks(sessionId) → v=1, tasks=1, [shell] bg_70ab6685 status=completed

6. Edge Cases

场景 结果
不存在的 session ID ✅ HTTP 404, "No session with id ..."
无认证 token ✅ HTTP 401, "Unauthorized"
已关闭的 session ✅ HTTP 404

结论

本 PR 功能验证通过,可以 merge。

核心目标 — web-shell 在 streaming 期间通过独立的 status 路径查询 background tasks snapshot,绕过 prompt FIFO — 已验证正常工作。涉及的 4 个包 (acp-bridge, cli, sdk, web-shell) 全部构建通过,750+ 单元测试全部通过,HTTP 端点/SDK/认证/边界情况均按预期工作。

@chiga0 chiga0 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

PR Classification: New Feature (feat)
Scope reviewed: Full stack — tasksSnapshot serialization, acpAgent extMethod, bridge status path, server HTTP route, SDK types + client methods, web-shell /tasks command, all tests.

Overall Assessment

Clean, well-structured PR that follows existing status endpoint patterns precisely. Key design decisions are sound:

  • FIFO bypass: Using requestSessionStatus (extMethod) instead of going through the prompt queue is the correct approach — clients need to inspect tasks while a prompt is streaming. The bridge test explicitly verifies this.
  • Whitelist serialization: tasksSnapshot.ts explicitly selects fields rather than spreading raw registry entries. Internal fields (abortController, pendingMessages, outputOffset, idleTimer) are properly excluded. Verified by acpAgent.test.ts assertions.
  • Terminal injection defense: sanitizeTaskText strips control characters (0x00-0x1F except HT/LF/CR, 0x7F-0x9F) from the formatted output — prevents ANSI escape injection in web-shell terminal.
  • Exhaustive switch: formatTaskStatus uses const _exhaustive: never = task for compile-time exhaustiveness checking.
  • Testable now: buildSessionTasksStatus accepts now = Date.now() parameter — enables deterministic runtimeMs assertions in tests.

Test Coverage

Comprehensive across all layers: bridge (FIFO bypass + SessionNotFoundError), server (200/404 paths), acpAgent (all 3 task kinds + serialization whitelist), tasksCommand (formatting, sanitization, slash command dispatch).

Findings (1 item)

# Severity File Issue
1 Nit tasksCommand.ts:100 promptBlocked in handleTasksSlashCommand input interface is declared but never read

LGTM overall — the design, implementation, and tests are solid.

This review was generated by QoderWork AI


export function handleTasksSlashCommand(input: {
cmd: string;
promptBlocked: boolean;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Nit] promptBlocked parameter declared but unused

The handleTasksSlashCommand input interface declares promptBlocked: boolean, and App.tsx passes it, but the function body never reads it.

Since /tasks is intentionally designed to work regardless of prompt state (it bypasses the FIFO via extMethod), this parameter is currently dead code. Consider either:

  • Removing it from the interface and the call site in App.tsx to avoid confusion
  • Adding a comment like // reserved for future prompt-aware routing if it's intentionally kept for forward compatibility

This review was generated by QoderWork AI

@chiga0 chiga0 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Clean implementation with comprehensive tests.

  • FIFO bypass via extMethod is the correct design choice
  • Whitelist serialization properly excludes internal fields (abortController, pendingMessages, outputOffset, idleTimer)
  • sanitizeTaskText provides terminal injection defense
  • Test coverage across all layers (bridge, server, acpAgent, tasksCommand)

One minor nit: promptBlocked parameter in handleTasksSlashCommand is declared but unused — non-blocking.

This review was generated by QoderWork AI

@doudouOUC doudouOUC merged commit 4701e6b into QwenLM:daemon_mode_b_main May 28, 2026
11 checks passed
@doudouOUC doudouOUC deleted the codex/daemon-tasks-snapshot branch May 28, 2026 06:47
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.

4 participants