feat(daemon): add explicit channel delivery - #7388
Conversation
Real IM E2E reportTested commit: Environment: isolated local daemon on macOS, real Qwen DingTalk application credentials, authenticated mutation routes, DingTalk Stream connection, one previously observed direct-message target. Credentials and target identifiers are intentionally omitted.
|
|
Thanks for the PR! Template looks good ✓ Problem: this is a feature addition backed by issue #7387 with clear acceptance criteria, labeled Direction: aligned. Channel delivery for daemon notifications, prompt finals, and scheduled finals sits squarely on the Size: this PR touches core paths ( Approach: the scope feels right for the stated goal. The three producers (notify, prompt, scheduled) genuinely share one worker/adapter boundary, and the PR explicitly avoids the premature durable retry outbox from #7153. The design follows existing patterns — the delivery IPC mirrors the webhook IPC contract, the worker supervisor correlation mirrors webhook task settlement, and the Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题:这是一个有 issue(#7387)支撑的功能新增,带有明确的验收标准,标记为 方向:对齐。Channel 投递覆盖 daemon 通知、Prompt 最终答案和定时任务最终答案,完全在 规模:本 PR 触及核心路径( 方案:范围对于既定目标是合理的。三个生产者(通知、Prompt、定时)确实共用一套 Worker/Adapter 底座,PR 明确避免了 #7153 中过早的持久化重试信箱。设计遵循现有模式。diff 中的每项改动对于既定目标都是必要的;没有顺手重构或无关改动。 进入代码审查 🔍 — Qwen Code · qwen3.8-max-preview Reviewed at |
🩺 serve daemon A/BBuilt the PR base vs this PR head
|
| field | PR base (before) | this PR (after) |
|---|---|---|
features[] |
— | "channel_delivery" |
— Qwen Code · serve A/B
Code ReviewIndependent proposal: For this feature, I would add a Findings — no critical blockers. The implementation is well-structured and follows project conventions:
sequenceDiagram
participant P1 as Caller
participant P2 as Daemon Route
participant P3 as BridgeClient
participant P4 as Session
participant P5 as Worker Supervisor
participant P6 as Channel Worker
participant P7 as ChannelBase Adapter
P1->>P2: POST /session/:id/prompt (with delivery)
P2->>P2: parse and validate delivery, strip from ACP payload
P2->>P3: sendPrompt with trusted channelDelivery context
P3->>P4: inject delivery metadata into _meta
P4->>P4: collect final text on end_turn (skip if tool calls)
P4->>P3: extMethod qwen/control/channel-delivery
P3->>P3: validate session, correlation, bounds
P3->>P5: onChannelDelivery handler (bound workspace)
P5->>P5: authorization store consume (one-shot or replay-protected)
P5->>P6: IPC channel_delivery message
P6->>P7: deliverProactive(target, text)
P7-->>P6: success or ChannelProactiveDeliveryError
P6-->>P5: IPC channel_delivery_result
P5-->>P3: delivered or failed
P3->>P3: publish sanitized channel_delivery_result event
P3-->>P4: result
Files changed (30 of 64 shown)
Real-Scenario TestingThis feature requires a running daemon with configured Channel workers and real IM credentials (DingTalk/Feishu Stream connection) for full delivery testing. Without these external dependencies, end-to-end message delivery cannot be exercised in this environment. The PR author reports real DingTalk E2E verification with provider acceptance observed for all three producers. Daemon smoke test (PR head Started the daemon from the PR build and verified the delivery surface: The daemon starts cleanly, advertises Unit test verification (all passed on PR head TypeScript typecheck ( 中文说明代码审查独立方案: 对于此功能,我会在 发现——无关键阻塞项。 实现结构良好,遵循项目规范:
真实场景测试此功能需要运行中的 daemon、已配置的 Channel Worker 和真实 IM 凭证。没有这些外部依赖,在此环境中无法进行端到端消息投递测试。PR 作者报告了真实钉钉 E2E 验证。 Daemon 冒烟测试: 从 PR 构建启动 daemon,验证 单元测试验证: 共 3,509 个测试通过,覆盖所有变更文件。TypeScript 类型检查通过。完整构建通过。CI 全绿。 — Qwen Code · qwen3.8-max-preview Reviewed at |
|
Confidence: 3/5 — clean review across every stage, but the Stage 0 maintainer-awareness escalation for a large cross-package feat (2,182+ production lines, 5 packages, core paths touched) needs a human sign-off. Going back to my independent proposal: the PR's approach matches it closely. The delivery IPC mirrors the webhook IPC contract, the worker supervisor correlation mirrors webhook task settlement, and The code is straightforward and well-organized. Security is handled properly — caller-supplied delivery metadata is stripped at the bridge boundary and only trusted context is injected. The authorization store enforces one-shot consumption for prompts and replay protection for scheduled tasks. Privacy is maintained — result events carry no text, target IDs, or credentials, and public errors are fixed strings. Error classification is correct — permanent vs transient, sanitized messages, proper HTTP status mapping. Backward compatibility is preserved — delivery is optional everywhere, and the missed-one-shot batch explicitly clears delivery. The final-only delivery gating (skip tool-call blocks, rollback on retry/fallback) is correctly implemented across all three prompt loops. All 3,509 unit tests pass across every changed file. TypeScript typecheck passes. Full build passes. CI is green at Since the last triage at The reason for 3/5 rather than 4–5/5 is purely policy: this is a large cross-package feat that touches core paths ( 中文说明置信度:3/5 — 每个阶段审查均干净,但 Stage 0 维护者关注升级(大型跨包 feat,2,182+ 生产行,5 个包,触及核心路径)需要人工签核。 回到我的独立方案:PR 的方案与之高度一致。投递 IPC 镜像 Webhook IPC 契约,Worker Supervisor 关联镜像 Webhook 任务结算, 代码直接且组织良好。安全处理得当——调用方提供的投递元数据在 bridge 边界被剥离,只注入受信任上下文。授权存储强制 Prompt 一次性消费和定时任务防重放。隐私得到维护——结果事件不携带文本、目标 ID 或凭证,公开错误为固定字符串。错误分类正确。向后兼容得到保留。仅最终答案投递门控(跳过工具调用块,重试/回退时回滚)在所有三个 Prompt 循环中正确实现。 所有 3,509 个单元测试通过。TypeScript 类型检查通过。完整构建通过。CI 全绿。Daemon 冒烟测试确认能力已广播、notify 路由强制认证、错误已脱敏。 自上次在 3/5 而非 4–5/5 的原因纯粹是策略:这是一个大型跨包 feat,触及核心路径,Stage 0 门控要求维护者关注。代码质量本身支持更高评分。转交维护者对投递契约和跨包范围进行架构签核。 — Qwen Code · qwen3.8-max-preview Reviewed at |
|
⏸️ Deferring to @wenshao @tanzhenxin @yiliang114 @LaZzyMan — fresh full review at |
|
🚫 Takeover not engaged: fork takeover requires the PR author to hold write access on this repository (author 中文说明🚫 未接管:fork 托管要求 PR 作者在本仓库持有 write 及以上权限(作者 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. 2 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here.
— qwen3.7-max via Qwen Code /review
|
@qwen-code /takeover |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. Not reviewed: reverse audit — an auditor ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and what the agent was actually asked is not what this skill certifies.
— qwen3.7-max via Qwen Code /review
|
🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 |
…error A proactive token fetch failure escaped sendProactiveChunk as a plain Error, bypassing ChannelProactiveDeliveryError classification. Wrap it as a transient typed error (preserving the cause) so downstream classification dispatches on it consistently. Also cover the scheduled-source branch of the bridge channel-delivery ext-method, which previously had no test coverage.
Review:
|
Review —
|
Local Build + Real Daemon Review Evidence for PR #7388Reviewer: wenshao (maintainer) Build & Typecheck
Targeted Unit Tests
Known macOS limitation (pre-existing, not introduced by this PR): 4 CLI test files ( Real Daemon E2E Verification (Built Bundle)Started the built
Key observations:
Screenshot EvidenceTerminal capture of the real daemon API responses (built bundle, isolated environment): Screenshot branch: Scope & Limitations of This Verification
Merge RecommendationFrom a local build + real daemon perspective, this PR is ready to merge. The public API contract is correctly implemented: capability advertisement, authentication gating, input validation, workspace-scoped routing, and sanitized error codes all match the design document. The 1,483 targeted unit tests pass, and the built bundle behaves correctly under real HTTP probing. The remaining unverified paths (real IM delivery) are covered by the author's evidence and CI. 中文报告PR #7388 本地构建 + 真实 Daemon 验证报告审查者: wenshao(维护者) 构建与类型检查
定向单元测试
已知 macOS 限制(已有问题,非本 PR 引入): 4 个 CLI 测试文件( 真实 Daemon 端到端验证(构建产物)使用构建后的
关键观察:
截图证据真实 daemon API 响应的终端截图(构建产物,隔离环境): 截图分支: 本次验证的范围与限制
合入建议从本地构建 + 真实 daemon 角度看,本 PR 可以合入。公开 API 契约实现正确:能力通告、鉴权门控、输入校验、workspace 作用域路由和脱敏错误码均与设计文档一致。1,483 条定向单元测试通过,构建产物在真实 HTTP 探测下行为正确。剩余未验证路径(真实 IM 投递)由作者证据和 CI 覆盖。 |
| return JSON.stringify([workspaceCwd, sessionId, id]); | ||
| } | ||
|
|
||
| function targetsEqual( |
There was a problem hiding this comment.
[P1] JSON.stringify([workspaceCwd, sessionId, id]) can produce key collisions if field values contain ","\" or "[". Use length-prefixed concatenation instead: ${workspaceCwd.length}:${workspaceCwd}/${sessionId.length}:${sessionId}/${id}`.
— qwen3.8-max-preview via Qwen Code /review
There was a problem hiding this comment.
复核当前 head 464baba 后,此项不成立,因此不修改实现。
源码证据:authorizationKey 的三个入参均为 string,key 是固定三元素字符串数组的 JSON 序列化。JSON 字符串会转义逗号、引号、反斜杠和方括号;JSON.parse(key) 可以无歧义还原原始三元组,因此这些字符不会改变数组元素边界。
额外执行证据:用 "", ,, ", [, ], \\, a, \n 组成全部 512 个三元组,得到 512 个唯一 key,逐个 JSON round-trip 均保持原值,collision 为 null。长度前缀方案不会提升这里的正确性,反而增加自定义编码逻辑。
| authorization.sessionId, | ||
| authorization.deliveryId, | ||
| ), | ||
| authorization.target, |
There was a problem hiding this comment.
[P1] #prompts Map entries that are never consumed (e.g. session crashes before consume) accumulate indefinitely — memory leak risk. Add TTL-based cleanup or batch revoke on session end.
— qwen3.8-max-preview via Qwen Code /review
There was a problem hiding this comment.
复核当前 head 464baba 后,评论给出的“session crash 后永久累积”路径不成立,因此不增加 TTL。
源码证据:Prompt admission 同步失败时会立即 revokePrompt;已接收的 promptPromise 无论 resolve/reject 都会在 finally 中安排 60 秒 grace 后撤销;成功消费则由 consume 立即删除。
Crash 行为也有现成回归测试:queued prompt rejects when the channel crashes before it starts 同时验证 active/queued prompt 均 reject,session/pending count 清零。我在该 head 单独重跑:1 passed / 438 skipped。默认上限还是每 workspace 20 sessions、每 session 5 pending prompts。一个仍在运行且未配置 deadline 的长 Prompt 可以保留授权,但那是活跃请求而非遗失条目;任意 TTL 反而会让合法长任务的最终投递失效。
| if (typeof value !== 'object' || value === null) return false; | ||
| const request = value as Record<string, unknown>; | ||
| return ( | ||
| isNonEmptyString(request['deliveryId']) && |
There was a problem hiding this comment.
[P1] No queue size limit on pendingChannelDeliveries — a legitimate but high-volume caller can grow this Map unbounded. Add a MAX_PENDING cap and return channel_delivery_queue_full when exceeded.
— qwen3.8-max-preview via Qwen Code /review
There was a problem hiding this comment.
确认此项成立,将按合入阻塞项修复。
源码证据:supervisor 的 pendingChannelDeliveries 当前没有容量门槛;每次调用会在创建 timer 后直接登记 Map 并发送 IPC。worker 侧虽然有 16 个 active delivery 上限,但这个检查发生在消息抵达 worker 之后,约束不了 supervisor Map 和 Node IPC backlog。
修复边界:在 supervisor 分配 message/timer/Promise 之前应用与 worker 一致的共享 16 in-flight 上限,超限复用现有 channel_delivery_queue_full;补回归测试验证第 17 个请求不进入 IPC,并验证 result/timeout/worker-exit 后容量可释放。不会改变 best-effort、无重试或 HTTP 503 契约。
There was a problem hiding this comment.
已在 8b3f5f8a9 修复并推送。
源码证据:
- 共享上限只定义一次:
MAX_CHANNEL_DELIVERIES_IN_FLIGHT = 16。 - supervisor 在创建 IPC message 和调用
send之前检查pendingChannelDeliveries.size,第 17 个请求直接返回channel_delivery_queue_full:channel-worker-supervisor.ts#L1200-L1221。 - worker 复用同一常量,在 adapter send 前保留第二层防护,并在
finally删除 active entry:daemon-worker.ts#L865-L910。 - 回归测试断言仅发送前 16 个、第 17 个明确失败、首个完成后可立即接纳 replacement:
channel-worker-supervisor.test.ts#L2454-L2500。
验证:相关 CLI 5 个测试文件 400/400 通过;build、typecheck、lint 通过。
yiliang114
left a comment
There was a problem hiding this comment.
Solid architecture with clear separation (route → authorization → manager → group → supervisor → IPC → adapter). Security model is sound with one-time tokens and strict key validation. 3 P1 findings posted inline: authorization key collision risk, prompt map memory leak, and missing queue size limit.
— qwen3.8-max-preview via Qwen Code /review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. 2 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here. Not reviewed: reverse audit — an auditor ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and what the agent was actually asked is not what this skill certifies.
— qwen3.7-max via Qwen Code /review
| "status": "failed", | ||
| "promptId": "prompt-1", | ||
| "code": "channel_worker_unavailable", |
There was a problem hiding this comment.
[Suggestion] promptId appears at both the top-level SSE envelope and inside data in both channel_delivery_result event examples, with no explanation of whether the two values are always identical or can differ. — Concrete cost: a client implementer cannot determine which to use for correlation. If they assume the two can differ and build separate handling, they add unnecessary complexity.
Consider adding a one-line note explaining the duplication (e.g., "The top-level promptId is the SSE correlation key; data.promptId is always identical for prompt-sourced events") or removing the inner promptId from data if redundant.
— qwen3.7-max via Qwen Code /review
| it('delivers existing text on the matching channel without an agent turn', async () => { | ||
| const sdk = createSdk(); | ||
| const deliverProactive = vi.fn().mockResolvedValue(undefined); |
There was a problem hiding this comment.
[Suggestion] No test exercises the channel_worker_unavailable error path through deliverChannelMessage. The success path is tested, but a regression that silently succeeds or throws a different error type when the channel is unavailable would not be caught. — Concrete cost: the error classification in classifyChannelDeliveryError (daemon-worker.ts ~line 1090) maps this to channel_delivery_failed, but without a test, a refactor could change the mapping silently.
it('rejects delivery for channels that are not running', async () => {
const sdk = createSdk();
const handle = await runChannelDaemonWorker({ /* ... */ });
await expect(
handle.deliverChannelMessage({ ...deliveryRequest, channelName: 'slack' }),
).rejects.toThrow(/Channel "slack" is not running/);
});— qwen3.7-max via Qwen Code /review
| 'workspace_extensions', | ||
| 'session_branch', | ||
| 'workspace_reload', | ||
| 'channel_delivery', |
There was a problem hiding this comment.
[Suggestion] This integration test file is outside every npm workspace, so npm test never collects it. It only runs via explicit CI integration test commands (e.g., test:integration:cli:sandbox:none). — Concrete cost: the Integration Tests (CLI, No Sandbox) CI check is currently skipped for this PR, meaning this test ships without any automated gate at this commit. Confirm the CI job that exercises this file is not among those that get skipped.
— qwen3.7-max via Qwen Code /review
| resp.status === 408 || resp.status === 429 || resp.status >= 500 | ||
| ? 'transient' | ||
| : 'permanent', |
There was a problem hiding this comment.
[Suggestion] HTTP 401 is classified as 'permanent' even though line 766 clears this.tokenCache = undefined on 401, preparing for a fresh token fetch. — Failure scenario: today no consumer branches on disposition for retry, so this is latent. When retry logic is added that respects disposition, a stale-token 401 will be treated as unrecoverable even though the cache was already invalidated and a fresh token is available — the code's own recovery action contradicts its error signal.
| resp.status === 408 || resp.status === 429 || resp.status >= 500 | |
| ? 'transient' | |
| : 'permanent', | |
| resp.status === 401 || resp.status === 408 || resp.status === 429 || resp.status >= 500 | |
| ? 'transient' | |
| : 'permanent', |
— qwen3.7-max via Qwen Code /review
合入前修复结论(head
|
|
@qwen-code /review |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline.
— qwen3.7-max via Qwen Code /review
|
|
||
| ```json | ||
| { "stopReason": "end_turn" } | ||
| { "promptId": "session-id########1", "lastEventId": 42 } |
There was a problem hiding this comment.
[Suggestion] The 202 response example omits the eventEpoch field that the implementation returns.
The implementation at packages/cli/src/serve/routes/session.ts:2654 returns three fields:
res.status(202).json({ promptId, lastEventId, eventEpoch });but this example and the prose description (202 {promptId,lastEventId}) show only two. A client implementing the protocol from this doc would not know eventEpoch exists — it is the daemon-side epoch used to detect daemon restarts.
| { "promptId": "session-id########1", "lastEventId": 42 } | |
| { "promptId": "session-id########1", "lastEventId": 42, "eventEpoch": 1 } |
— qwen3.7-max via Qwen Code /review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. 3 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here. Not reviewed: reverse audit — an auditor ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and what the agent was actually asked is not what this skill certifies.
— qwen3.7-max via Qwen Code /review
| try { | ||
| opts.onTasksRead?.(tasks); | ||
| } catch (err) { | ||
| log.debug('keepalive: onTasksRead failed', err); |
There was a problem hiding this comment.
[Suggestion] onTasksRead failures are caught and logged at debug level only. If the callback throws on every heartbeat (e.g. malformed task data in the cron store), the authorization store is never populated for delivery-enabled tasks. All subsequent delivery attempts fail with opaque errors, and operators monitoring warn/error logs see nothing.
Failure scenario: Persistent onTasksRead failure silently prevents all channel deliveries, with the root cause invisible at default log levels.
Suggested fix: Log at warn level, or add a counter that escalates to warn after N consecutive failures.
— qwen3.7-max via Qwen Code /review
Code Review —
|
|
@qwen-code /triage |
| 'Channel worker IPC send failed.', | ||
| ); | ||
| } | ||
| if (pendingChannelDeliveries.size >= MAX_CHANNEL_DELIVERIES_IN_FLIGHT) { |
There was a problem hiding this comment.
[Suggestion] Good — MAX_CHANNEL_DELIVERIES_IN_FLIGHT now properly caps the supervisor's pending queue. The shared constant in channel-delivery-ipc.ts and its use in daemon-worker.ts eliminates the duplicated local limit. Previous P1 on queue size is resolved.
— qwen3.8-max-preview via Qwen Code /review
| const SHUTDOWN_FORCE_CLOSE_MS = 5_000; | ||
| const DAEMON_LOG_FORCED_FLUSH_BUDGET_MS = 250; | ||
|
|
||
| function channelDeliveryPublicError( |
There was a problem hiding this comment.
[Suggestion] Good — channelDeliveryPublicError() ensures typed delivery failures return sanitized public messages. Combined with normalizeWorkerDiagnostic and the workspace path removal in channel-worker-group.ts, the error surface no longer leaks internal paths or secrets.
— qwen3.8-max-preview via Qwen Code /review
yiliang114
left a comment
There was a problem hiding this comment.
Incremental changes since last review address 2 of 3 P1 findings: (1) delivery queue now has a size cap (MAX_CHANNEL_DELIVERIES_IN_FLIGHT=16) enforced in supervisor, (2) error messages sanitized — no more workspace paths or secrets in public errors. Also improved cronScheduler one-shot jitter handling. Remaining items (authorization key collision, prompt map cleanup) can be follow-ups.
— qwen3.8-max-preview via Qwen Code /review
|
Triage re-run completed without a new review. The stage comments above were updated with the latest result. View workflow run. |

What this PR does
Adds one explicit Channel delivery contract for daemon notifications, Agent prompt finals, and scheduled-task finals. Authenticated callers select a named Channel plus a typed user or chat target; the daemon routes only to the matching worker owned by the resolved workspace, and adapters return sanitized permanent or retryable transport failures.
A synchronous notification endpoint reports success only after the adapter accepts the message. Prompt requests keep their asynchronous admission response and emit a correlated delivery-result event after a successful final answer. Scheduled tasks persist the same optional destination, run in their existing dedicated session, and deliver the completed final without starting another Agent turn.
Existing Channel webhooks keep their independent inbound execution path and asynchronous admission contract. Calls that omit delivery retain their current responses, events, session behavior, and scheduling semantics.
Why it's needed
Web alerts and daemon-driven Agent work need a production-visible way to reach a specific IM user or chat. The previous scheduled-only draft in #7153 coupled delivery to one producer and introduced a durable retry outbox before that behavior was required. A smaller shared boundary supports the immediate use cases while keeping execution failure separate from delivery failure and making stopped workers, invalid targets, authentication errors, throttling, and platform failures explicit.
Reviewer Test Plan
How to verify
Start a daemon with a configured Channel worker and mutation authentication. Verify that a direct notification returns success only after provider acceptance; an unavailable worker or invalid target returns a sanitized explicit error. Submit a prompt with delivery and confirm the request remains an asynchronous admission response, the Agent emits its normal completion event, and a correlated delivery-result event follows. Create a one-shot scheduled task with the same destination and confirm it fires automatically, produces its final in the bound session, sends exactly once, and is removed after execution. Repeat prompt and scheduled calls without delivery and confirm their existing behavior is unchanged. Confirm inbound Channel webhooks still return their existing asynchronous admission response.
Evidence (Before & After)
Before: daemon clients could not use one explicit destination contract across immediate notifications, prompt finals, and scheduled finals; the open draft covered scheduled delivery only.
After: all three producers use the same workspace-owned worker and adapter boundary. A real DingTalk run connected through Stream, delivered a synchronous notification, delivered a prompt final after asynchronous admission and turn completion, and delivered an automatically fired one-shot scheduled final. Provider acceptance was observed for each message; temporary credentials and runtime artifacts were removed afterward.
Tested on
Environment (optional)
Node.js 25 local development runtime, isolated daemon, real DingTalk application credentials and Stream connection. Build and workspace typecheck passed. Focused validation passed: CLI 1,656 tests; Core scheduler 166; Channel base 504; DingTalk 76; Feishu 73; ACP Bridge 482; TypeScript SDK 405.
Risk & Scope
Linked Issues
Closes #7387
Supersedes #7153 and the scheduled-only scope in #7152.
中文说明
本 PR 做了什么
新增统一、显式的 Channel 投递契约,支持 daemon 通知、Agent Prompt 最终答案和定时任务最终答案。已鉴权调用方指定 Channel 名称及用户或群聊目标;daemon 只会路由到当前 workspace 所属的匹配 Worker,Adapter 会返回经过脱敏的永久性或可重试传输错误。
同步通知接口只有在 Adapter 接受消息后才返回成功。Prompt 请求保持异步受理响应,在成功产生最终答案后发布可关联的投递结果事件。定时任务持久化同一种可选目标,在现有独立 Session 中执行,并直接投递完成后的最终答案,不会再启动一次 Agent 执行。
现有 Channel Webhook 继续使用独立的入站执行路径和异步受理契约。未传 delivery 的调用保持原有响应、事件、Session 行为和定时语义不变。
为什么需要
Web 告警和 daemon 驱动的 Agent 任务需要一种生产可见的能力,将结果发送到指定 IM 用户或群聊。旧的 #7153 仅覆盖定时任务,并在当前需求尚未要求时引入持久化重试信箱。更小的统一底座可以覆盖即时场景,同时将执行失败与投递失败分离,并明确暴露 Worker 未启动、目标非法、鉴权失败、限流及平台故障。
Reviewer 测试计划
如何验证
使用已配置 Channel Worker 和写操作鉴权的 daemon 启动服务。验证直接通知只有在平台接受后返回成功,Worker 不可用或目标非法时返回明确且脱敏的错误。提交带 delivery 的 Prompt,确认接口仍为异步受理,Agent 正常产生完成事件,随后发布可关联的投递结果事件。创建带相同目标的一次性定时任务,确认由调度器自动触发,在绑定 Session 中产生最终答案,只发送一次并在执行后移除。再验证不带 delivery 的 Prompt 和定时任务行为保持不变,并确认入站 Channel Webhook 仍保持原有异步受理响应。
前后对比证据
之前:daemon 客户端无法通过统一的显式目标契约覆盖即时通知、Prompt 最终答案和定时任务最终答案;现有 Draft 只支持定时投递。
之后:三类生产者共用同一套 workspace 所属 Worker 和 Adapter 底座。真实钉钉测试通过 Stream 建连,成功投递同步通知、在 Prompt 异步受理及执行完成后投递最终答案,并成功投递由调度器自动触发的一次性定时任务最终答案。三条消息均获得平台接受;测试后已清理临时凭证和运行产物。
测试平台
环境
Node.js 25 本地开发运行时、隔离 daemon、真实钉钉应用凭证及 Stream 连接。构建及 workspace typecheck 通过。重点验证通过:CLI 1,656 条、Core 调度 166 条、Channel Base 504 条、DingTalk 76 条、Feishu 73 条、ACP Bridge 482 条、TypeScript SDK 405 条。
风险与范围
关联 Issue
Closes #7387
替代 #7153 以及 #7152 中仅限定时任务的范围。