diff --git a/.agents/notes/proposed/feature/2026-10-29-event-triggered-automation-tasks.i18n.yaml b/.agents/notes/proposed/feature/2026-10-29-event-triggered-automation-tasks.i18n.yaml
new file mode 100644
index 0000000000..8db24c9d07
--- /dev/null
+++ b/.agents/notes/proposed/feature/2026-10-29-event-triggered-automation-tasks.i18n.yaml
@@ -0,0 +1,43 @@
+# Bilingual-pair consistency record for 2026-10-29-event-triggered-automation-tasks.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/proposed/feature/2026-10-29-event-triggered-automation-tasks.md
+/agent-note-event-triggered-automation-tasks:
+ en: fe57a78907eab083
+ zh: 4ac96eeb365ac012
+/agent-note-event-triggered-automation-tasks/problem:
+ en: 491a791a201d0b2c
+ zh: 5a98f693ac4b5e77
+/agent-note-event-triggered-automation-tasks/proposal:
+ en: 7906d9f791e0ba02
+ zh: 3a083d451344f072
+/agent-note-event-triggered-automation-tasks/proposal/layers:
+ en: 8bf2fc8ccb162c59
+ zh: e993b9ae72f8d9a2
+/agent-note-event-triggered-automation-tasks/proposal/sessions:
+ en: 7ec83684c8c19e31
+ zh: 27df2725836e5f7b
+/agent-note-event-triggered-automation-tasks/proposal/configuration:
+ en: 21be7e27081aff54
+ zh: 7973f7f0d064db35
+/agent-note-event-triggered-automation-tasks/proposal/mechanisms:
+ en: 2403ec84b12279ec
+ zh: 7e245af54dcf4a1a
+/agent-note-event-triggered-automation-tasks/proposal/product-scope:
+ en: c003c908f407ccb2
+ zh: 6113569a44d52202
+/agent-note-event-triggered-automation-tasks/proposal/event-triggers-as-infrastructure:
+ en: 4c62e16718896e9b
+ zh: 9c61ea9b6e1f4f22
+/agent-note-event-triggered-automation-tasks/proposal/delivery-order:
+ en: 79cf498bf3566051
+ zh: 4537f1bd3df527b4
+/agent-note-event-triggered-automation-tasks/alternatives-considered:
+ en: 21df079f7d7c3f4d
+ zh: 67f4ab221aacf2cf
+/agent-note-event-triggered-automation-tasks/acceptance-criteria:
+ en: 504feb2cc59f4457
+ zh: c78cd50f2f336562
+/agent-note-event-triggered-automation-tasks/risks:
+ en: 35edb8d5b6a1cfa4
+ zh: cb8dbc339c6055be
diff --git a/.agents/notes/proposed/feature/2026-10-29-event-triggered-automation-tasks.md b/.agents/notes/proposed/feature/2026-10-29-event-triggered-automation-tasks.md
new file mode 100644
index 0000000000..af3f170fca
--- /dev/null
+++ b/.agents/notes/proposed/feature/2026-10-29-event-triggered-automation-tasks.md
@@ -0,0 +1,153 @@
+# Agent Note: Event-triggered automation tasks
+
+Status: proposed
+
+English | [中文](2026-10-29-event-triggered-automation-tasks.zh.md)
+
+## Problem
+
+Automation tasks run only on time rules today. The Host stores each rule, and every due time queues the task's instruction as a follow-up message in the task's original Session ([Host-owned scheduled messages](../../implemented/architecture/2026-09-16-host-schedule-storage.md)); the model creates and edits tasks through preset-scoped tools ([Preset-scoped Schedule tools](../../implemented/architecture/2026-09-24-preset-scoped-schedule-tools.md)). Work that should start when something else changes, such as a pull request's checks failing, a reviewer requesting changes, or a message arriving, cannot be expressed as a task, so users check those sources by hand across Sessions.
+
+Reacting to changes brings two problems that time rules do not have.
+
+- **Cost and noise.** Most observations of a source contain nothing worth a model turn. If every observation reaches the model and silence depends on the model replying with a marker, each poll costs a full turn, and a model that holds a messaging tool reports "nothing new" anyway.
+- **Untrusted input.** Event content is written by other people: comments, reviews, messages. A turn started by that content must not edit, push, or send anything before the user has seen it.
+
+## Proposal
+
+An automation task becomes an instruction, a source with parameters, and a Session mode. Time is one source among several, so a scheduled task is an automation task whose source is time. Each observation passes three layers, and only the last one calls a model: a source event passes the trigger's filter and becomes a candidate, the candidate is delivered to a Session where the model judges it, and only a notify call produces a notice for the user. This partially supersedes the two notes linked above: tasks stop being bound to their original Session, and task records stop meaning inbox delivery only.
+
+```mermaid
+flowchart TD
+ Task["Task: instruction, source parameters, Session mode
written by the model, shown read-only"]
+ Prov["Source providers: plugins, code
time, GitHub; later mail, IM, memory"]
+ Trig["Event trigger: shared code, state per task
compare with stored state, match parameters, merge"]
+ Rec[("Run records")]
+ Src{"Source"}
+ Exec["Run the instruction in the target Session
with its permissions"]
+ RO["Read-only turn in the task's own Session
the model judges the candidate"]
+ Note["Notice: action card, unread mark,
system notification"]
+ Quiet["No notice"]
+ Acc["User accepts: the action runs in the Session
that created the task, with its permissions"]
+ Task --> Trig
+ Prov -- "source events" --> Trig
+ Trig -- "each change or failure" --> Rec
+ Trig -- candidate --> Src
+ Src -- time --> Exec
+ Src -- "other sources" --> RO
+ RO -- notify --> Note
+ RO -- "no notify call" --> Quiet
+ Note --> Acc
+```
+
+| Layer | Decides | Model call | Output |
+|---|---|---|---|
+| Source provider | What changed in the source | No | Events |
+| Event trigger | Whether the change matters to this task | No | A run record; a candidate or nothing |
+| Agent | Whether the user needs to act, and what to propose | Only for delivered candidates | A notice through notify, or nothing |
+
+### Layers
+
+**Source providers** are code, packaged as plugins; they provide event sources and are unrelated to model, SSH, or credential providers. A provider owns one kind of source: it polls or subscribes, reports changes as events with a subject, a one-line summary, detail, and links, and declares the JSON schema of the parameters a task can set. One provider serves every task of its kind. Time is the first provider, and today's schedule rules become its parameters. Any installed plugin can register a provider, including one written in Creator mode; a task can name only a registered provider.
+
+**The event trigger** is shared code, registered per source kind and version; each task holds only its parameters and the source state it last observed. For each observation it compares the new state with the stored one, and an observation without a change only updates the last-checked time. A check that turns from passing to failing is a change, a check that stays failing is not, and a pass followed by a new failure is a change again. For a change, the trigger matches the task's conditions, which are parameter fields such as conclusion is failure, author is the user, or commenter is not a bot; merges changes to the same subject inside the provider's merge window, so one push that fails three checks becomes one event; stores the new state with the task; and either delivers a candidate to the Session or drops the change. Each change and each failure writes a run record, listed in the task's Records tab next to today's delivery records: candidate delivered, filtered with its reason, or failed. Consecutive failures with the same cause share one record with a count. Delivery ends the trigger's work: it does not wait for the turn or read a receipt. One queue per task orders its evaluations.
+
+**The agent** receives the candidate as a message in the target Session, together with the task's instruction in the user's words. What the turn may do depends on the source.
+
+- **Time**: the turn runs the instruction with the Session's permissions, and its reply is the result, as scheduled tasks behave today.
+- **Other sources**: the turn gets the read-only tool set described under Mechanisms. It can read files and the source's state, such as check logs and pull request diffs, but has no tool that edits, pushes, or sends. One new tool, **notify** (`automation_notify`), takes a title, a summary, and a proposed action and produces a notice: an [action card](../../../../packages/experimental/action-cards/README.md) with the proposed action in the Session, the Session's unread mark, and a desktop system notification. The model decides whether to call it. A turn without notify produces no notice; there is no end tool, and nothing checks how the turn ended. When the user accepts the card, the Host sends a follow-up to the Session that handles the task, as described under Sessions; the follow-up names the task and the event and quotes the proposed action as the automation's proposal, not as the user's words, and that turn runs with that Session's permissions.
+
+### Sessions
+
+A task delivers to its target Session in one of three modes: a specified Session, a new Session per trigger, or a new Session on the first trigger that later triggers reuse. Time tasks keep today's target, the Session they were created in. Event tasks separate judging from handling. The read-only turn runs in the task's own Session, created on the first trigger and reused afterwards, because it needs the instruction, the event, and the task's earlier decisions rather than the long history of the conversation the task came from. When the model creates a task in a conversation, it writes the facts the judgment needs into the instruction, such as which check is known to be flaky. When the user accepts a card, the proposed action goes to the Session the task was created in, where the code and its history are. The automation page's New action already creates tasks in a new Session, which then handles them; a task enabled from an official template, which a later step adds, continues in its own Session. The user can change the mode on the task.
+
+No task runs while the Host is stopped. On restart, each task compares the current source state with its stored state once and does not replay intermediate changes. Time keeps today's rule: a recurring task runs its latest missed occurrence once.
+
+### Configuration
+
+Configuration has two layers. Provider code decides what can be observed and how; task parameters decide what one task watches. The model writes the parameters from the user's words through the task tools, and the provider's schema validates them. A task carries no scripts or other code, so observing something new means changing or installing a provider plugin. The automation page shows source parameters read-only, and they change through conversation; the time editor stays.
+
+### Mechanisms
+
+- **Event-turn tool set.** Sandbox read-only mode limits only the filesystem, so it cannot stop a push or an outbound request. An event turn is offered a fixed tool set instead of the Session's: file reads and searches, the source provider's read operations, and `automation_notify`. Shell, file writes, web fetch, and MCP tools are absent. The tool set is recorded in the Session log, and a user message queued during the turn starts a normal turn after it ends.
+- **Notify card.** The automation package registers an action-card kind, and `automation_notify` creates its card through the action-card service; the tool exists only in event turns, so no skill load precedes it. Accepting runs the kind's handler, which queues the follow-up through the Host, so the handling Session need not be open. A newer card replaces an unanswered older one in the same Session, as the service does today; the older card is in that Session's history, so the newer turn sees it and carries over what is still open. A refused card returns an error to the turn.
+- **Unread mark.** The client derives the unread mark from turn completion. A task's own Session is marked as an automation Session, and for such a Session the client shows the mark only for notices, so a turn without notify leaves no mark.
+- **Logging.** The delivered candidate, the turn's tool set, and the notify call are Session events, so replay rebuilds the turn; the TypeScript and Python SDK expected outputs and a recorded-session snapshot cover them.
+- **Storage.** Source, parameters, Session mode, trigger state, and run records extend the Host task domain in a new record version with an adjacent migration. A version-1 task reads as a time task, is not rewritten until it changes, and gets no invented records. Run records have their own count and age bounds, separate from delivery records.
+- **Provider configuration.** Polling intervals and request budgets are validated Config fields of each provider in cordis.yml.
+
+### Product scope
+
+- No new entry point and no page redesign: scheduled and event-triggered tasks share the automation page.
+- The task list adds a source-type filter next to the status filter.
+- Task details add the source with its parameters, read-only, and the Session mode, editable.
+- A task's Records tab also lists filtered and failed evaluations.
+- Notifications reuse the Session unread mark and action cards; the desktop app adds a system notification.
+
+### Event triggers as infrastructure
+
+The trigger layer is shared infrastructure for DSH, not a feature of the automation page. A feature that watches something and hands changes to an agent becomes a task on this layer instead of building its own polling, state, records, and notifications.
+
+- **DSH's own state as sources**: a Session turn ending in an error, a background job settling, and, once memory lands, a memory consolidation pass finishing or memory entries changing.
+- **Features on those sources**: plugin recommendations, where a workflow the user repeats by hand shows up after consolidation and the agent proposes a plugin on an action card, and task recommendations, where a review of the day's Sessions proposes follow-up work. Each is a template task with a DSH source, an instruction, and the same notify step. The plugin recommendations package, which proposes cards from conversation turns today, can move onto such a source once consolidation lands.
+- **What every consumer keeps**: change detection and condition matching in code, run records, Session modes, read-only event turns, and notify as the only way a task reaches the user.
+
+### Delivery order
+
+1. Task–Session decoupling with the three Session modes.
+2. The trigger layer with time as its first provider and the task-domain migration; existing scheduled tasks behave as before.
+3. The GitHub provider for pull request checks, reviews, and comments; the event-turn tool set and `automation_notify`; action cards leave experimental status or ship with the automation bundle.
+4. Mail and IM providers and official templates, then DSH's own sources.
+
+## Alternatives considered
+
+**Judge every observation in a resident Session.** The model reads each poll and replies with a silence marker when nothing changed. It lost because each observation costs a full turn over the Session's context, polls spaced beyond the prefix cache lifetime read that context uncached, and silence depends on the model following an instruction while it holds a messaging tool. Here code drops unchanged and unmatched observations, and only a notify call reaches the user.
+
+**Judge in the Session that created the task.** That conversation already holds the code and its history, so the instruction would need no extra facts. It lost because most candidates produce no notice, and each of them would add a turn over the conversation's whole context, read uncached once the prefix cache has expired, and queue behind or interrupt the user's own work there. Only accepted work goes to that Session.
+
+**Let the agent choose the Session for each event.** The choice could differ from one event to the next, so the user could not predict where a notification appears, and deciding whether the history is needed requires reading it first.
+
+**Condition scripts per task.** A task would carry a script that fetches and decides. It lost because the script runs with the user's credentials without the review a plugin installation gets, and each task would carry its own fetch logic instead of sharing a provider.
+
+**Notify from code when code can decide.** A failed check could notify without a model. It lost because the notification would carry only the raw status. With one path, the agent reads the logs first and the notification states the cause; change-only delivery keeps these turns rare.
+
+**One model call outside any Session.** A single call per event costs less and leaves no Session record. It lost because the call cannot read files or logs before deciding, and acting on its result needs a Session anyway.
+
+**Event turns with the Session's permissions.** Acting directly would save one click. It lost because event content comes from other people, and a read-only turn keeps that text from driving edits, pushes, or messages before the user accepts.
+
+**A read-only sandbox for event turns.** It lost because sandbox read-only mode limits only the filesystem, and a turn in it can still push or send requests.
+
+**An end tool and a check on how the turn ended.** A second tool would record why a turn produced no notice, and the trigger would wait for that outcome. It lost because nothing acts on that reason: delivery ends the trigger's work, and a turn without notify is already silent.
+
+**Parameter forms on the automation page.** Each provider would need its own form, validation, and display. Parameters written by the model and shown read-only cover creation and change through conversation, and the provider schema keeps a generated form possible later.
+
+**A separate notification entry.** An inbox page or tab would separate the notification from the Session that holds its context. The unread mark and the system notification lead to that Session, and the automation page keeps the records.
+
+## Acceptance criteria
+
+- Existing scheduled tasks behave the same after time becomes a provider: the same due times, the same catch-up rule, and the same delivery into the Session.
+- A version-1 task loads as a time task without being rewritten and without invented records.
+- A GitHub task on a pull request's checks delivers one candidate when a check turns from passing to failing, none while it stays failing, and another after a pass followed by a new failure.
+- One push that fails three checks produces one candidate.
+- An observation without a change writes no record and starts no turn; a change that fails the conditions writes a filtered record and starts no turn; consecutive failures with the same cause write one record with a count.
+- A run record ends at delivery: it records the candidate, not what the turn did.
+- An event turn is offered no shell, file-write, web-fetch, or MCP tool, and its tool set appears in the Session log; after the user accepts its card, the next turn runs with the Session's permissions.
+- A notify call produces the card, the unread mark, and the desktop system notification together.
+- An event turn without a notify call leaves no unread mark and sends no system notification.
+- An event task's read-only turns run in the task's own Session, and accepting its card sends the proposed action to the Session the task was created in.
+- Each Session mode delivers as defined: a specified Session receives every trigger, a new Session opens per trigger, or one Session opens on the first trigger and is reused.
+- Replaying the Session log rebuilds an event turn's candidate, tool set, and notify call.
+- After a restart, a task compares the current source state with its stored state once and does not replay intermediate changes.
+- The task list filters by source type, task details show source parameters read-only, and the Session mode can be changed.
+
+## Risks
+
+- The read-only turn sees the instruction, the event, and the task's earlier decisions, not the conversation the task came from. Facts the judgment needs from that conversation must be written into the instruction when the task is created; a user who switches the task to its original Session gets the history back at the cost of reading the long context on each delivery.
+- A notification appears in the task's own Session, not in the conversation that created the task; accepting its card moves the work to that conversation.
+- The agent may notify too often or stay silent when it should not. Accepted and dismissed cards show over-notification, but a missed notice leaves no trace in the run records, which end at delivery.
+- Source parameters cannot be edited by hand; every change goes through the model.
+- A fix on an event waits for the user to accept the card instead of running unattended.
+- A source without a provider plugin cannot be watched.
+- Tasks run only while the Host runs: nothing runs while the app is closed or the machine sleeps.
+- Event tasks depend on action cards, which are experimental and mounted only by the Personalized recommendations bundle today.
+- The read-only tool set limits judgment to what files and the provider's read operations show; for a source whose provider offers no read operations, the agent judges from the event alone.
diff --git a/.agents/notes/proposed/feature/2026-10-29-event-triggered-automation-tasks.zh.md b/.agents/notes/proposed/feature/2026-10-29-event-triggered-automation-tasks.zh.md
new file mode 100644
index 0000000000..c23e4684c8
--- /dev/null
+++ b/.agents/notes/proposed/feature/2026-10-29-event-triggered-automation-tasks.zh.md
@@ -0,0 +1,153 @@
+# Agent Note: 事件触发的自动化任务
+
+Status: proposed
+
+[English](2026-10-29-event-triggered-automation-tasks.md) | 中文
+
+## 问题
+
+目前自动化任务只能按时间规则运行。Host 保存每条规则,每到一个到期时间,就把任务的指令作为一条后续消息排进任务的原会话([Host 拥有的定时消息](../../implemented/architecture/2026-09-16-host-schedule-storage.zh.md));模型通过按 preset 提供的工具创建和修改任务([按 preset 提供的 Schedule 工具](../../implemented/architecture/2026-09-24-preset-scoped-schedule-tools.zh.md))。有些工作应当在别处发生变化时开始,例如 PR(Pull Request)的检查失败、评审人要求修改、收到一条消息,这些都无法建成任务,用户只能在多个会话之间手动查看这些来源。
+
+响应变化会带来两个时间规则没有的问题。
+
+- **成本和噪音。** 对来源的大多数观察里,没有值得花一个模型轮次处理的内容。如果每次观察都交给模型,并依靠模型回复一个标记来保持安静,每次轮询都要花一个完整轮次;手里有发消息工具的模型仍然会报告“没有新情况”。
+- **不可信的输入。** 事件内容由其他人写成:评论、评审、消息。由这类内容启动的轮次,在用户看到之前不能修改、推送或发送任何东西。
+
+## 提案
+
+自动化任务改为由指令、带参数的来源和会话模式组成。时间是多种来源中的一种,因此定时任务就是来源为时间的自动化任务。每次观察依次经过三层,只有最后一层调用模型:来源事件经过触发器过滤成为候选事件(candidate),候选事件投递到会话,由模型在会话中判断,只有调用通知工具才会为用户产生一条通知(notice)。本提案部分取代上面链接的两篇笔记:任务不再绑定原会话,任务记录也不再只表示收件箱投递。
+
+```mermaid
+flowchart TD
+ Task["Task: instruction, source parameters, Session mode
written by the model, shown read-only"]
+ Prov["Source providers: plugins, code
time, GitHub; later mail, IM, memory"]
+ Trig["Event trigger: shared code, state per task
compare with stored state, match parameters, merge"]
+ Rec[("Run records")]
+ Src{"Source"}
+ Exec["Run the instruction in the target Session
with its permissions"]
+ RO["Read-only turn in the task's own Session
the model judges the candidate"]
+ Note["Notice: action card, unread mark,
system notification"]
+ Quiet["No notice"]
+ Acc["User accepts: the action runs in the Session
that created the task, with its permissions"]
+ Task --> Trig
+ Prov -- "source events" --> Trig
+ Trig -- "each change or failure" --> Rec
+ Trig -- candidate --> Src
+ Src -- time --> Exec
+ Src -- "other sources" --> RO
+ RO -- notify --> Note
+ RO -- "no notify call" --> Quiet
+ Note --> Acc
+```
+
+| 层 | 判断什么 | 是否调用模型 | 输出 |
+|---|---|---|---|
+| 来源提供方(source provider) | 来源里发生了什么变化 | 否 | 事件 |
+| 事件触发器(event trigger) | 这个变化与本任务是否相关 | 否 | 一条运行记录;一个候选事件,或者没有 |
+| agent(智能体) | 用户是否需要处理,以及建议怎么处理 | 只对已投递的候选事件 | 通过通知工具产生通知,或者没有 |
+
+### 分层
+
+**来源提供方**是代码,以插件形式提供;它们提供事件来源,与模型、SSH 或凭据的提供方无关。一个来源提供方负责一类来源:它轮询或订阅来源,把变化报告为事件(包含对象、一行摘要、详细内容和链接),并声明任务可设置参数的 JSON schema。同类的所有任务共用同一个来源提供方。时间是第一个来源提供方,现有的定时规则成为它的参数。任何已安装的插件都可以注册来源提供方,包括在创造模式中写出的插件;任务只能指定已注册的来源提供方。
+
+**事件触发器**是共享的代码,按来源类型和版本注册;每个任务只保存自己的参数和上次观察到的来源状态。每次观察时,它把新状态与保存的状态比较;没有变化的观察只更新最近检查时间。检查从通过变为失败算一次变化,持续失败不算,通过之后再次失败又算一次变化。对一次变化,触发器先匹配任务的条件(参数中的字段,例如结论为失败、作者是用户本人、评论者不是 bot),再在来源提供方的合并窗口内合并同一对象的多次变化(一次推送导致三项检查失败,只算一个事件),然后把新状态和任务一起保存,向会话投递一个候选事件,或者丢弃这次变化。每次变化和每次失败都写一条运行记录,列在任务的运行记录页签中,与现有的投递记录并列:已投递候选事件、已过滤(附原因)或失败。同一原因的连续失败共用一条记录并计数。投递之后触发器的工作就结束了,不等待该轮次,也不读取回执。每个任务用一个队列排定自己的判定顺序。
+
+**agent** 在目标会话中以一条消息收到候选事件,消息附带以用户原话写成的任务指令。轮次能做什么取决于来源。
+
+- **时间**:轮次按会话的权限执行指令,回复就是结果,与现在的定时任务一致。
+- **其他来源**:轮次使用“机制”一节所述的只读工具集。它可以读文件和来源的状态,例如检查日志和 PR diff,但没有修改、推送或发送的工具。新增一个工具 **通知**(`automation_notify`),接收标题、摘要和建议的操作,产生一条通知:会话中出现带有建议操作的[操作卡片](../../../../packages/experimental/action-cards/README.zh.md)、会话显示未读标记、桌面端发送系统通知。是否调用由模型决定。没有调用通知的轮次不产生通知;没有结束工具,也不检查轮次如何结束。用户接受卡片后,Host 向处理该任务的会话(见“会话”一节)发送一条后续消息:消息写明任务和事件,并把建议的操作作为自动化提出的建议引用,而不是作为用户的话;该轮次按该会话的权限执行。
+
+### 会话
+
+任务按三种模式之一投递到目标会话:指定的会话、每次触发新建会话、首次触发时新建会话并在之后的触发中复用。定时任务沿用现在的目标,即创建它的会话。事件任务把判断和处理分开。只读轮次在任务自己的会话中运行,首次触发时新建,之后复用,因为它需要的是指令、本次事件和该任务之前的判断,而不是任务来源会话的长历史。模型在会话中创建任务时,把判断需要的事实写进指令,例如哪项检查已知不稳定。用户接受卡片后,建议的操作发往创建任务的会话,代码和相关历史都在那里。自动化页的“新建”本来就在一个新会话中创建任务,之后由该会话处理;从官方模板启用的任务(后续步骤加入)在自己的会话中继续执行。用户可以在任务上修改模式。
+
+Host 停止期间不运行任何任务。重启后,每个任务把当前来源状态与保存的状态比较一次,不重放中间的变化。时间沿用现有规则:周期任务只补跑最近一次错过的时间点。
+
+### 配置
+
+配置分两层。来源提供方的代码决定能观察什么、如何观察;任务参数决定一个任务观察什么。模型根据用户原话,通过任务工具写入参数,由来源提供方的 schema 校验。任务不携带脚本或其他代码,因此要观察新的东西,就修改或安装来源提供方插件。自动化页以只读方式显示来源参数,参数通过对话修改;时间编辑器保留。
+
+### 机制
+
+- **事件轮次的工具集。** 沙箱的只读模式只限制文件系统,挡不住推送或外发请求。因此事件轮次使用固定的工具集,而不是会话的工具集:读文件和搜索、来源提供方的读取操作,以及 `automation_notify`。没有 shell、写文件、网页抓取和 MCP 工具。工具集记录在会话日志中;轮次进行中排队的用户消息,在该轮次结束后作为普通轮次执行。
+- **通知卡片。** 自动化包注册一种操作卡片类型,`automation_notify` 通过操作卡片服务创建这类卡片;该工具只在事件轮次中提供,因此调用前不需要加载 Skill。接受卡片时执行该类型的处理器,由处理器通过 Host 排入后续消息,所以处理任务的会话不必处于打开状态。同一会话中,新卡片会替换尚未处理的旧卡片,与操作卡片服务现在的行为一致;旧卡片在该会话的历史中,新的轮次能看到它,并把仍未处理的内容带到新卡片里。被拒绝的卡片会把错误返回给该轮次。
+- **未读标记。** 客户端根据轮次完成推导未读标记。任务自己的会话标记为自动化会话,对这类会话,客户端只为通知显示未读标记,因此没有调用通知的轮次不留下标记。
+- **日志。** 已投递的候选事件、轮次的工具集和通知调用都是会话事件,回放可以重建该轮次;TypeScript 和 Python SDK 的期望输出以及一个录制会话快照覆盖这些内容。
+- **存储。** 来源、参数、会话模式、触发器状态和运行记录以新的记录版本扩展 Host 任务域,并提供相邻迁移。版本 1 的任务按时间任务读取,在发生修改前不重写,也不补造记录。运行记录有自己的数量和时长上限,与投递记录分开。
+- **来源提供方配置。** 轮询间隔和请求额度是每个来源提供方在 cordis.yml 中经过校验的 Config 字段。
+
+### 产品范围
+
+- 不新增入口,不重做页面:定时任务和事件触发的任务共用自动化页。
+- 任务列表在状态筛选旁增加来源类型筛选。
+- 任务详情增加来源及其参数(只读)和会话模式(可修改)。
+- 任务运行记录同时列出已过滤和失败的判定。
+- 通知复用会话的未读标记和操作卡片;桌面端新增系统通知。
+
+### 事件触发作为基础设施
+
+触发层是 DSH 共用的基础设施,而不是自动化页的一项功能。需要观察某样东西并把变化交给 agent 的功能,都作为这一层上的任务实现,不再各自实现轮询、状态、记录和通知。
+
+- **DSH 自身状态作为来源**:会话轮次以错误结束、后台作业结束;memory 落地后,还包括一次 memory 整理完成,或 memory 条目发生变化。
+- **基于这些来源的功能**:插件推荐,即 memory 整理后发现用户反复手动执行的流程,由 agent 在操作卡片上推荐插件;任务推荐,即回顾当天的会话,提出后续要做的事。每一项都是一个模板任务,由一个 DSH 来源、一条指令和同样的通知步骤组成。插件推荐包现在从对话轮次中提出卡片,memory 整理落地后可以改为基于这类来源。
+- **每个使用方都保留的规则**:变化检测和条件匹配由代码完成、运行记录、会话模式、只读的事件轮次,以及只有通知能让任务触达用户。
+
+### 交付顺序
+
+1. 任务与会话解耦,支持三种会话模式。
+2. 上线触发层和任务域迁移,以时间作为第一个来源提供方;现有定时任务行为不变。
+3. GitHub 来源提供方,覆盖 PR 的检查、评审和评论;事件轮次工具集和 `automation_notify`;操作卡片结束实验状态,或随自动化 bundle 一起提供。
+4. 邮件和 IM 来源提供方以及官方模板,然后是 DSH 自身的来源。
+
+## 考虑过的替代方案
+
+**在常驻会话中判断每次观察。** 模型读取每次轮询的结果,没有变化时回复一个静默标记。没有采用,原因是:每次观察都要在会话的完整上下文上花一个轮次;轮询间隔超过前缀缓存的有效期时,每次都要在没有缓存的情况下读取整个上下文;而且模型手里有发消息工具时,能否保持安静取决于它是否遵守指令。本方案由代码丢弃没有变化和不匹配的观察,只有调用通知才会触达用户。
+
+**在创建任务的会话中判断。** 该会话已经有代码和相关历史,指令里不需要额外写事实。没有采用,原因是大多数候选事件不产生通知,每一次都会在该会话的完整上下文上增加一个轮次,前缀缓存过期后要在没有缓存的情况下读取整个上下文,用户正在那里工作时还要排队或打断。只有被接受的工作才会进入该会话。
+
+**由 agent 为每个事件选择会话。** 每次的选择可能不同,用户无法预料通知出现在哪里;而且要判断是否需要历史,得先读取历史。
+
+**每个任务自带条件脚本。** 任务携带一个脚本,由脚本获取数据并作出判断。没有采用,原因是脚本以用户的凭据运行,却没有经过安装插件时的审查;而且每个任务都要自带获取逻辑,无法共用来源提供方。
+
+**代码能判断时由代码直接通知。** 检查失败可以不经模型直接通知。没有采用,原因是这样的通知只包含原始状态。只保留一条路径时,agent 会先读日志,通知中写明失败原因;只在变化时投递,这类轮次很少。
+
+**在会话之外调用一次模型。** 每个事件调用一次模型,成本更低,也不留下会话记录。没有采用,原因是这次调用在判断前无法读取文件或日志,而且要根据结果采取行动仍然需要会话。
+
+**事件轮次沿用会话的权限。** 直接行动可以省去一次点击。没有采用,原因是事件内容来自其他人,只读轮次能防止这些文字在用户接受之前驱动修改、推送或发消息。
+
+**事件轮次使用只读沙箱。** 没有采用,原因是沙箱的只读模式只限制文件系统,处于该模式的轮次仍然可以推送或发出请求。
+
+**结束工具和对轮次结束方式的检查。** 第二个工具可以记录某一轮为什么没有产生通知,触发器则要等待这个结果。没有采用,原因是没有任何环节会使用这个原因:投递之后触发器的工作就结束了,而没有调用通知的轮次本来就是静默的。
+
+**在自动化页提供参数表单。** 每个来源提供方都需要各自的表单、校验和展示。由模型写入参数并以只读方式展示,已经覆盖通过对话创建和修改;来源提供方的 schema 让以后生成表单仍然可行。
+
+**单独的通知入口。** 收件箱页面或标签页会把通知与持有其上下文的会话分开。未读标记和系统通知都指向该会话,自动化页保留记录。
+
+## 验收标准
+
+- 时间改为来源提供方后,现有定时任务行为不变:到期时间相同、补跑规则相同、投递到会话的方式相同。
+- 版本 1 的任务按时间任务加载,不重写,也不补造记录。
+- 观察 PR 检查的 GitHub 任务,在检查从通过变为失败时投递一个候选事件,持续失败期间不再投递,通过之后再次失败时再投递一个。
+- 一次推送导致三项检查失败,只产生一个候选事件。
+- 没有变化的观察不写记录,也不启动轮次;不满足条件的变化写一条已过滤记录,不启动轮次;同一原因的连续失败只写一条带计数的记录。
+- 运行记录到投递为止:记录候选事件,不记录该轮次做了什么。
+- 事件轮次不提供 shell、写文件、网页抓取或 MCP 工具,其工具集出现在会话日志中;用户接受卡片后,下一轮次按会话的权限执行。
+- 一次通知调用同时产生卡片、未读标记和桌面端系统通知。
+- 没有调用通知的事件轮次不留下未读标记,也不发送系统通知。
+- 事件任务的只读轮次在任务自己的会话中运行;接受卡片后,建议的操作发往创建任务的会话。
+- 每种会话模式按定义投递:指定的会话接收每次触发,或每次触发新建一个会话,或首次触发时新建一个会话并在之后复用。
+- 回放会话日志可以重建事件轮次的候选事件、工具集和通知调用。
+- 重启后,任务把当前来源状态与保存的状态比较一次,不重放中间的变化。
+- 任务列表可以按来源类型筛选,任务详情以只读方式显示来源参数,会话模式可以修改。
+
+## 风险
+
+- 只读轮次看到的是指令、本次事件和该任务之前的判断,看不到任务来源的会话。判断需要的、来自那个会话的事实,必须在创建任务时写进指令;用户把任务改成投回原会话,就能拿回历史,代价是每次投递都要读取很长的上下文。
+- 通知出现在任务自己的会话里,而不是创建任务的对话里;接受卡片后,工作转到那个对话。
+- agent 可能通知过于频繁,或者在应该提醒时保持静默。被接受和被忽略的卡片能看出通知过多,但漏掉的通知不会在运行记录中留下痕迹,因为运行记录到投递为止。
+- 来源参数不能手动编辑,每次修改都要经过模型。
+- 针对事件的修复要等用户接受卡片,不会在无人值守时执行。
+- 没有来源提供方插件的来源无法观察。
+- 任务只在 Host 运行时执行:应用关闭或机器休眠期间不运行任何任务。
+- 事件任务依赖操作卡片,而它目前是实验包,只由个性化推荐 Bundle 装入。
+- 只读工具集把判断限制在文件和来源提供方读取操作能看到的内容;来源提供方不提供读取操作时,agent 只能根据事件本身判断。
diff --git a/apps/web/tests/auto-review-denial.e2e.ts b/apps/web/tests/auto-review-denial.e2e.ts
index 58371631a9..d967b4cb9b 100644
--- a/apps/web/tests/auto-review-denial.e2e.ts
+++ b/apps/web/tests/auto-review-denial.e2e.ts
@@ -119,13 +119,16 @@ describe.skipIf(MODE === 'record')('web e2e: cold Auto-review denial', () => {
await ledger.locator('table[data-scroll-ready="true"]').waitFor({ timeout: 15_000 })
const nativeRecord = ledger.locator('tr[data-kind="tool"]').filter({ hasText: 'mystery' })
const innerRecord = ledger.locator('tr[data-kind="subtool"]').filter({ hasText: 'bash' })
- await expect.poll(() => nativeRecord.textContent()).toContain('AUTO_REVIEW_DENIED')
- await expect.poll(() => innerRecord.textContent()).toContain('AUTO_REVIEW_DENIED')
+ await expect.poll(() => nativeRecord.textContent()).toContain('Error: Auto review rejected tool "mystery"; its body was not executed')
+ await expect.poll(() => innerRecord.textContent()).toContain('Error: Auto review rejected tool "bash"; its body was not executed')
await innerRecord.click()
- await page.getByRole('tab', { name: 'Result', exact: true }).click()
- await page.getByRole('tabpanel', { name: 'Result' })
- .getByText('AutoReviewDeniedError: AUTO_REVIEW_DENIED', { exact: true })
- .waitFor({ state: 'visible' })
+ for (const tab of ['Summary', 'Result']) {
+ await page.getByRole('tab', { name: tab, exact: true }).click()
+ const panel = page.getByRole('tabpanel', { name: tab })
+ await panel.getByText('Error: Auto review rejected tool "bash"; its body was not executed', { exact: true })
+ .waitFor({ state: 'visible' })
+ expect(await panel.getByText('AutoReviewDeniedError: AUTO_REVIEW_DENIED', { exact: true }).count()).toBe(0)
+ }
await captureAutoReviewState(page, 'deny-trajectory')
const trajectory = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
.split(SEED_ID).join('{{seededId}}')
diff --git a/packages/client/ui-trajectory/README.i18n.yaml b/packages/client/ui-trajectory/README.i18n.yaml
index bb680650bd..262d052c0b 100644
--- a/packages/client/ui-trajectory/README.i18n.yaml
+++ b/packages/client/ui-trajectory/README.i18n.yaml
@@ -18,14 +18,14 @@
en: 4bd17d05b8385675
zh: 5174e27f754f2a03
/deepseek-ai-dsh-client-ui-trajectory/use-this-package/inspecting-records:
- en: 10cd682fb4d7a3df
- zh: c57679f1cc77f837
+ en: 4bf16077921c643c
+ zh: 08ded05f8f0afded
/deepseek-ai-dsh-client-ui-trajectory/use-this-package/the-timing-overview:
en: 0f4ee2189324ac06
zh: 12e76fed22c09bc1
/deepseek-ai-dsh-client-ui-trajectory/understand-the-implementation:
- en: 0eaa2a895454e053
- zh: 6fb5c2682698fca9
+ en: 0d22ad8a6bfc0eab
+ zh: 3221c4dd5874c585
/deepseek-ai-dsh-client-ui-trajectory/understand-the-implementation/virtual-rows:
en: c85e342bc7eee4e6
zh: 9feab66ef1755b94
diff --git a/packages/client/ui-trajectory/README.md b/packages/client/ui-trajectory/README.md
index b63301d743..265c6fffd9 100644
--- a/packages/client/ui-trajectory/README.md
+++ b/packages/client/ui-trajectory/README.md
@@ -31,7 +31,7 @@ Open the Trajectory tab in the conversation's view ring to inspect agent activit
The toolbar folds turns, calls, and subcalls independently. Subcalls fold into a summary beneath their parent tool; click the summary or press Enter or Space to expand it. Search includes folded subcalls, and cross-view inspection expands the requested call. Toolbar controls and search wrap onto additional lines when the pane is narrow.
-Calls are identified by their recorded tool name. For `run_code`, the row shows the program description and the inspector opens numbered, highlighted source. The Code tab provides wrapping, a `{}` toggle for the original JSON arguments, and exact source copying, including trailing newlines. Copying the original arguments retains their recorded JSON whitespace. Each newly opened code view takes the last wrapping choice; changing it leaves other open views as they are. Output preserves the recorded text, using a tree for complete JSON objects or arrays. Highlighting uses only an unambiguous TypeScript or Python hint in the recorded tool schema; missing or conflicting hints leave plain source.
+Calls are identified by their recorded tool name. For `run_code`, the row shows the program description and the inspector opens numbered, highlighted source. The Code tab provides wrapping, a `{}` toggle for the original JSON arguments, and exact source copying, including trailing newlines. Copying the original arguments retains their recorded JSON whitespace. Each newly opened code view takes the last wrapping choice; changing it leaves other open views as they are. Output preserves the recorded text, using a tree for complete JSON objects or arrays. Highlighting reads the leading `Execute a program in` language declaration and the legacy `code` parameter description from the recorded tool schema. Only TypeScript and Python are supported. Missing or conflicting hints, or an explicit declaration of another language, leave plain source; later examples in the tool description do not affect language selection.
Selection, timeline navigation, folding, and search cover the React-visible window. Request numbers and cumulative usage cover the complete resident snapshot. Selecting a record opens a local inspector for token usage, duration, Input, Output, Timing, and durable images. Image URLs use the Conversation-owned per-session cache, so Chat and Trajectory share one authorized read per attachment. The Trajectory view declares `conversation.trajectory.images`; ledger children call this Slot through the framework `renderSlot` seat, passing image references, display options, and the authorized loader explicitly. Its official `MessageImages` entry calls the shared `conversation.images.content` Factory. A user record shows both nonzero image and ordinary-file counts beside its text, including attachment-only records. A standalone compaction request appears chronologically in its own `Between turns` section, while a numbered compaction remains inside its owning turn.
@@ -59,7 +59,9 @@ The view is a pure projection: Trajectory-owned Definitions assemble business re
User, steering, and context inputs and request prompt changes share their request's Step group in log order, before the request separator and assistant output.
-Native and nested PTC Tool results retain their raw structured error details and presentation metadata. The Result tab stacks the live canonical value above the rendered content, with one scroll area for the entire panel and no section height limits. Both JSON trees preview long strings up to 12 lines and allow expansion. In-flight PTC calls show “Running…” until a result is available. Historical, failed, evicted, and unloaded raw results show “Raw results are only available for live tool calls” while rendered content remains available. There is no result-mode preference. The inspector subscribes only to the selected result while the Result tab is open, releasing it on selection changes or disposal; Session unload and connection loss clear the displayed value. JSON primitives, including `null`, remain inspectable. Summary previews continue to show rendered content. Failed records show the error code in the ledger and the error name and code in the inspector.
+Native and nested PTC Tool results retain their raw structured error details and presentation metadata. The Result tab stacks the live canonical value above the rendered content, with one scroll area for the entire panel and no section height limits. Both JSON trees preview long strings up to 12 lines and allow expansion. In-flight PTC calls show “Running…” until a result is available. Historical, failed, evicted, and unloaded raw results show “Raw results are only available for live tool calls” while rendered content remains available. There is no result-mode preference. The inspector subscribes only to the selected result while the Result tab is open, releasing it on selection changes or disposal; Session unload and connection loss clear the displayed value. JSON primitives, including `null`, remain inspectable. Summary previews continue to show rendered content.
+
+Failed records use non-empty recorded result text for ledger previews, inspector output, and output copying, preserving whitespace-only text. JSON failure output stays as recorded text in Summary and the rendered section of Result. With that text present, structured error names and codes are neither displayed nor separately indexed for search. Only failures without any non-empty text block fall back to the error code in the ledger and the error name and code in the inspector; image content remains visible beside that fallback.
Tool records begin at durable tool/call events and retain complete argument text with a lazy `args` view, reused by paired results. Chat's transient preparing stage does not create Trajectory tool rows or alter historical tool timing.
diff --git a/packages/client/ui-trajectory/README.zh.md b/packages/client/ui-trajectory/README.zh.md
index 3f5f9ccfc0..bed828aa7f 100644
--- a/packages/client/ui-trajectory/README.zh.md
+++ b/packages/client/ui-trajectory/README.zh.md
@@ -31,7 +31,7 @@ Trajectory 标签页让你以按轮次组织的事件记录表和交互式时间
工具栏可以分别折叠轮次、调用和子调用。子调用收起后,在父工具下保留一条摘要;点击摘要或按 Enter、空格键即可展开。搜索包含已折叠的子调用,从其他视图检查调用时会自动展开目标调用。 面板较窄时,工具栏按钮与搜索框会自动换行。
-调用按已记录的工具名识别。对于 `run_code`,记录行展示程序说明,检查器显示带行号和语法高亮的源码。“代码”页提供换行、查看原始 JSON 参数的 `{}` 切换按钮,以及保留末尾换行的源码原样复制。复制原始参数时保留记录中 JSON 的空白。每个新打开的代码视图采用上次选择的换行状态;切换时,其他已打开视图保持原状。输出保留记录中的文本,完整 JSON 对象或数组使用树形展示。高亮只采用已记录工具 Schema 中明确且无冲突的 TypeScript 或 Python 提示;提示缺失或冲突时显示纯文本源码。
+调用按已记录的工具名识别。对于 `run_code`,记录行展示程序说明,检查器显示带行号和语法高亮的源码。“代码”页提供换行、查看原始 JSON 参数的 `{}` 切换按钮,以及保留末尾换行的源码原样复制。复制原始参数时保留记录中 JSON 的空白。每个新打开的代码视图采用上次选择的换行状态;切换时,其他已打开视图保持原状。输出保留记录中的文本,完整 JSON 对象或数组使用树形展示。高亮从已记录的工具 Schema 中读取开头的 `Execute a program in` 语言声明及旧式 `code` 参数说明,仅支持 TypeScript 和 Python。提示缺失或冲突,或明确声明了其他语言时,显示纯文本源码;工具说明后文的示例不影响语言选择。
选择、时间线导航、折叠与搜索只覆盖 React 可见窗口。请求编号与累计用量覆盖完整的驻留快照。选择记录会打开局部检查器,查看 token 用量、耗时、输入、输出、计时与持久保留的图片。图片 URL 使用 Conversation 拥有的逐会话缓存,因此 Chat 与 Trajectory 对每个附件共享一次已授权读取。Trajectory 视图声明 `conversation.trajectory.images`;账本子组件通过框架 `renderSlot` 席位调用该 Slot,显式传入图片引用、显示参数及授权 loader。官方 `MessageImages` 条目调用共享的 `conversation.images.content` Factory。用户记录会在文本旁同时显示非零的图片与普通文件数量,仅含附件的记录也显示这些数量。独立运行的压缩请求会按时间顺序显示在自己的 `Between turns` 区段中,而带编号的压缩仍位于其所属轮次内。
@@ -59,7 +59,9 @@ Trajectory 标签页让你以按轮次组织的事件记录表和交互式时间
用户消息、中途引导、上下文输入及请求提示词变更按日志顺序放在所属请求的 Step 分组内,位于请求分隔行和助手输出之前。
-原生与嵌套 PTC 工具结果保留原始结构化错误详情和展示元数据。结果标签页从上到下展示实时规范值与渲染内容,整个面板共用一个滚动区域,不限制各 section 的高度。两侧 JSON 树的长字符串均预览最多 12 行,并可展开。执行中的 PTC 调用在结果可用前显示“运行中”。历史、失败、已淘汰及已卸载的原始结果显示“原始结果仅在实时工具调用中可用”,渲染内容仍可查看。不再保留结果模式偏好。检查器仅在结果标签页打开时订阅当前选中的结果,在切换选择或销毁时释放;Session 卸载及连接断开会清除已展示的值。包括 `null` 在内的 JSON 基本值均可查看。概述预览仍显示渲染内容。失败记录在记录表中显示错误代码,在检查器中显示错误名称和代码。
+原生与嵌套 PTC 工具结果保留原始结构化错误详情和展示元数据。结果标签页从上到下展示实时规范值与渲染内容,整个面板共用一个滚动区域,不限制各 section 的高度。两侧 JSON 树的长字符串均预览最多 12 行,并可展开。执行中的 PTC 调用在结果可用前显示“运行中”。历史、失败、已淘汰及已卸载的原始结果显示“原始结果仅在实时工具调用中可用”,渲染内容仍可查看。不再保留结果模式偏好。检查器仅在结果标签页打开时订阅当前选中的结果,在切换选择或销毁时释放;Session 卸载及连接断开会清除已展示的值。包括 `null` 在内的 JSON 基本值均可查看。概述预览仍显示渲染内容。
+
+失败记录在记录表预览、检查器输出和输出复制中使用已记录的非空结果文本,并保留仅含空白字符的文本。JSON 失败输出在“概述”和“结果”的渲染区域保持原文。存在这类文本时,结构化错误名称和代码既不显示,也不单独纳入搜索索引。仅在失败结果没有任何非空文本块时,记录表才回退到错误代码,检查器才回退到错误名称和代码;图片内容仍显示在回退信息旁边。
工具记录从持久 tool/call 事件开始,保留完整参数文本和由配对结果复用的懒计算 `args` 视图。Chat 的瞬态 preparing 阶段不创建 Trajectory 工具行,也不改变历史工具计时。
diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx
index 15d9ca3515..6b7acc0dca 100644
--- a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx
+++ b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx
@@ -1502,7 +1502,7 @@ function ToolOutputBlocks({
}: {
blocks: readonly TrajectorySourceBlock[]
error: boolean
- /** Failure name and code preserved beside image-only error content. */
+ /** Failure name and code shown only when no recorded text block is non-empty. */
errorDetail?: string | undefined
preview: boolean
renderSlot: PropsRenderSlots<'conversation.trajectory.images'>['renderSlot']
@@ -1516,6 +1516,7 @@ function ToolOutputBlocks({
].filter((value): value is string => value !== undefined).join(' ')}
>
{error && errorDetail !== undefined && errorDetail !== ''
+ && !blocks.some(block => block.type === 'text' && block.content !== '')
&&
{errorDetail}}
{blocks.map((block, index) => (
block.attachment !== undefined
@@ -1743,7 +1744,7 @@ function RecordPayload({
const payloadClass = preview ? css.jsonPreview : css.jsonPayload
const payloadClassName = error ? `${payloadClass} ${css.errorPayload}` : payloadClass
- const json = parseJsonContainer(value)
+ const json = error ? undefined : parseJsonContainer(value)
const singleTextResult = direction === 'output'
&& record.cell.outputBlocks?.length === 1
&& record.cell.outputBlocks[0]?.type === 'text'
diff --git a/packages/client/ui-trajectory/src/client/code-program.ts b/packages/client/ui-trajectory/src/client/code-program.ts
index e444029a20..fc618cf843 100644
--- a/packages/client/ui-trajectory/src/client/code-program.ts
+++ b/packages/client/ui-trajectory/src/client/code-program.ts
@@ -30,17 +30,24 @@ function parseRecord(value: string | undefined): Record