merge: integrate current account bonus notices with onboarding

This commit is contained in:
zhc committed 2026-09-23 23:43:53 +08:00
commit 6233f998a2
460 files changed
+43185 -6520

No files matched your search

@@ -3,20 +3,20 @@
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.md
/agent-note-deepseek-account-browser-login:
en: 666b3732ccc1c2fb
zh: 37dffad1bb4e26ae
en: 8328647bcdf8a666
zh: 445a07c7ec4bcca4
/agent-note-deepseek-account-browser-login/problem:
en: 67c00444d9dcf06d
zh: ef294910952ba052
/agent-note-deepseek-account-browser-login/decision:
en: db9297d04d25d0f6
zh: 20938f92c1563146
en: 3b6c781b5374a8af
zh: 2a3fb7c63dfdaf83
/agent-note-deepseek-account-browser-login/alternatives-considered:
en: b7694861ea5282b6
zh: a5ff57bdcc9fcf60
en: 3c2dd2c948ffb51b
zh: d019197df0ee3dcf
/agent-note-deepseek-account-browser-login/consequences:
en: 348fcdb33c193cf2
zh: ad82da41941d3afb
en: dd448790204d8dc7
zh: a78c4fa4cb0b8916
/agent-note-deepseek-account-browser-login/verification:
en: 881edcb454b941a6
zh: 7dc5a9b7a066f623
@@ -4,7 +4,7 @@ Status: implemented
English | [中文](2026-09-14-deepseek-account-login.zh.md)
Sign-in captures the initiating UI language per attempt so browser authorization follows the Desktop or Settings locale without depending on the Platform default.
Sign-in captures the initiating UI's client metadata per attempt so browser authorization follows the Desktop or Settings language without depending on the Platform default.
## Problem
@@ -20,11 +20,11 @@ Authorization registers a temporary /oauth/callback route on the Host webServer.
Platform navigation and authorization share one validated platformOrigin from Cordis configuration. Private profile patches or environment expressions supply deployment-specific origins without committing development addresses.
The Desktop profile supplies the native platform for Host API headers. Account and update-policy clients share one mapping to avoid different client identities for the same installation. This identity does not depend on a login attempt, since stored grants also issue requests after restart. Embedded pages use the same platform identity and retain their other separately configured headers.
Platform client identity belongs to the calling UI rather than to the Host. Every account operation carries the caller's client version, active language, and current UTC offset, and the provider derives the five Platform client headers for that call from them plus the composed native platform. Client identity headers override same-named deployment headers, so one Host shared by several callers reports the UI that made each request instead of the last one it saw, and the update-policy client keeps the same header meanings for the installed shell. PlatformSession deliberately omits caller identity; the embedding client composes its own five headers and limits them to the configured origin.
## Alternatives considered
Putting the protocol in Electron duplicates the implementation for Web consumers and gives the shell credential ownership. Putting tokens in DEEPSEEK_API_KEY loses the distinction between account grants and user API keys. Treating a browser callback as success before persistence admits false success. Waiting for a remote cancel acknowledgment leaves local cancellation dependent on network availability. These alternatives are rejected.
Putting the protocol in Electron duplicates the implementation for Web consumers and gives the shell credential ownership. Putting tokens in DEEPSEEK_API_KEY loses the distinction between account grants and user API keys. Treating a browser callback as success before persistence admits false success. Waiting for a remote cancel acknowledgment leaves local cancellation dependent on network availability. Remembering one client identity in the Host would mislabel every other caller that shares it or reaches it through a forward. These alternatives are rejected.
## Consequences
@@ -56,6 +56,8 @@ The embedded Platform document stays hidden during loading because native child
Separate accountRequestHeaders route account data and embedded Platform traffic independently of authorization and logout. Cookie overrides merge by name, retaining deployment authentication. Host passes the resolved headers over private process IPC; Electron injects them only at the configured origin and omits them from bootstrap.
Bonus notices use Platform-provided text and order identities. Reads occur when an account becomes active — signed-in startup and successful sign-in — and once per Settings entry or return from the top-up view, with no periodic polling. The card is painted as soon as the server offers it, including underneath the open Settings panel, so display is never deferred. The client acknowledges an order after a presented frame or the user closes it; nothing is written to browser storage, so a later sign-in re-displays an order the server still offers. Failed acknowledgments retry with capped exponential backoff for the rest of the signed-in session. Platform acknowledgments suppress delivery on other devices, but concurrent displays before acknowledgment are possible because the API does not reserve delivery. Fetch success alone never records a display.
The Host binds private Platform sessions to the account provider lifetime. Removal or watch termination clears Electron’s session; replacement subscribes to the new provider, and disposed reads cannot publish old credentials.
The client distinguishes plugin disposal from terminal account-stream failure. RemoteStream aborts its signal after either outcome, so the plugin owns a separate disposal flag to preserve failure feedback while suppressing reports after unload.
@@ -4,7 +4,7 @@ Status: implemented
[English](2026-09-14-deepseek-account-login.md) | 中文
登录申请保存发起界面的语言,使浏览器授权跟随桌面或设置页的语言,不依赖平台默认值。
登录申请保存发起界面的客户端元数据,使浏览器授权跟随桌面或设置页的语言,不依赖平台默认值。
## 问题
@@ -20,11 +20,11 @@ Status: implemented
平台导航与授权共享 Cordis 配置中同一个经过校验的 platformOrigin。私有 profile patch 或环境表达式提供部署来源,开发地址不进入仓库。
Desktop profile 为 Host API 请求头提供原生平台信息。账号与更新策略客户端共用一份映射,避免同一安装实例发送不同的客户端标识。此标识不依赖登录尝试,因为重启后保存的授权仍会发起请求。内嵌页面使用同一平台标识,并保留独立配置的其他请求头。
Platform 客户端身份属于发起调用的界面,而不属于 Host。每个账号操作都携带调用方的客户端版本、当前语言和当前 UTC 偏移,provider 据此与组合出的原生平台推导该次调用的五个 Platform 客户端请求头。客户端身份请求头覆盖部署配置中的同名请求头,因此一个被多方共享的 Host 报告的是每次请求的发起界面,而不是它上一次见到的调用方;更新策略客户端对已安装的壳沿用同一组请求头语义。PlatformSession 有意不携带调用方身份;内嵌客户端自行组装同样的五个请求头,并仅在配置来源发送。
## 替代方案
把协议放在 Electron 会为 Web 使用方重复实现,并让外壳拥有凭证。把 token 放入 DEEPSEEK_API_KEY 会丢失账号授权与用户 API Key 的区别。在持久化前将浏览器回调视为成功会产生虚假成功。等待远程取消确认会使本地取消依赖网络。这些方案均被拒绝。
把协议放在 Electron 会为 Web 使用方重复实现,并让外壳拥有凭证。把 token 放入 DEEPSEEK_API_KEY 会丢失账号授权与用户 API Key 的区别。在持久化前将浏览器回调视为成功会产生虚假成功。等待远程取消确认会使本地取消依赖网络。在 Host 中记住一份客户端身份,会让共享该 Host 或经转发接入的其他调用方被标错。这些方案均被拒绝。
## 影响
@@ -56,6 +56,8 @@ exchange 的 user 数据在凭证提交后供首次资料读取使用,展示
独立的 accountRequestHeaders 将账号资料及内嵌 Platform 流量与授权、退登分开路由。Cookie 覆盖按名称合并,保留部署认证。Host 通过私有进程 IPC 传递合并后的请求头;Electron 仅在配置来源注入,并从 bootstrap 排除。
赠金通知使用 Platform 提供的文案和订单标识。账号进入登录态(已登录启动、登录成功)时读取一次,每次进入设置页或从充值视图返回时再读取一次,不进行定时轮询。服务端下发的通知立即绘制,包括在打开的设置面板之下,因此展示从不延后。客户端在通知卡完成一帧绘制后或用户关闭卡片时确认订单。不写入任何浏览器存储,因此之后再次登录会重新展示服务端仍在下发的订单。确认失败会在本次登录会话内按带上限的指数退避重试。Platform 的确认记录阻止其他设备继续获取通知,但接口不预留展示资格,因此确认前仍可能并发展示。仅获取成功不会记录为已展示。
Host 将私有 Platform 会话绑定到账号提供者的生命周期。提供者移除或订阅结束时清空 Electron 会话;替换后订阅新的提供者,已释放的读取不得发布旧凭据。
客户端区分插件卸载和账户状态流的终止错误。RemoteStream 在两种情况下都会中止其信号,因此插件独立记录卸载状态,保留错误反馈并抑制卸载后的报告。
@@ -0,0 +1,19 @@
# Bilingual-pair consistency record for 2026-09-16-host-schedule-storage.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/implemented/architecture/2026-09-16-host-schedule-storage.md
/agent-note-host-owned-scheduled-messages:
en: 11854b220af809ea
zh: 0f0575de37107ed8
/agent-note-host-owned-scheduled-messages/problem:
en: 2c774d05ae75784f
zh: 619ba7f281b13233
/agent-note-host-owned-scheduled-messages/decision:
en: 4a38d9bd933241b0
zh: b246c74f68e92a25
/agent-note-host-owned-scheduled-messages/alternatives-considered:
en: 21d7ec9a9ff3cbf2
zh: 93d03b8b3012aa7e
/agent-note-host-owned-scheduled-messages/consequences:
en: 5b8d0a26024f795e
zh: c4d6f36d54783c47
@@ -0,0 +1,65 @@
# Agent Note: Host-owned scheduled messages
Status: implemented
English | [中文](2026-09-16-host-schedule-storage.zh.md)
## Problem
Scheduled messages must remain discoverable after restart without loading every Session. A timer attached to a live Agent cannot dispatch a task whose Session remains cold. Users also need to inspect pending and delivered tasks without activating their conversation; removing a task after delivery loses that management record.
## Decision
The existing Schedule package owns one Host storage domain containing each task's original Session id, reminder record, `active` or `inactive` status, latest delivery receipt, and saved delivery history. The domain is authoritative for retained tasks. The Host scheduler restores timers only for active tasks; a due task uses the Session controller's existing resolution operation and queues an ordinary follow-up in the original Session. Querying and deleting tasks do not resolve an Agent. Stored records lacking status normalize to `active` without scanning or recreating historical Sessions.
The task domain declares the whole-unit layout. Tasks are authoritative, so the JSON backend rejects an unreadable file, malformed document, unsupported version, or invalid task as a complete load failure instead of interpreting it as deletion. Failed recovery leaves `schedule.json` unchanged, while an initially absent file opens as an empty domain; repairing the file preserves task identity. `Service.init` waits for both storage validation and runtime setup, management methods wait for the same initialization result, and an unloading owner releases late-acquired storage without starting dispatch. Dispatch admission is separate from initialization: an ancestor can close Agent admission after a task mutation commits but before runtime cleanup starts. That refusal is logged without automatic retry and does not turn the persisted mutation into a failed management result. Invalid storage and failed cleanup registration still reject startup.
This supersedes the Session-log authority, live-only activation, and read-only catalog decisions in the [Session-local reminders note](../feature/2026-08-05-durable-web-schedule.md). Its explicit absolute-time validation, fixed-rate arithmetic, and distinction between queue admission and model completion remain applicable. Historical `schedule/change` creation, fold, and invariant types remain restricted to After, At, and Every through `LegacyScheduleRecord`; a separate Host decoder accepts the current `ScheduleRecord`, including Daily, Weekly, and Cron. The Host record decoder requires a stored `title` that is non-empty after trimming, already trimmed, and at most 120 characters, and the strict task schema rejects the whole domain open instead of dropping such a task. The historical change decoder tolerates an absent `title` so an already-written Session log stays readable, and it applies the same three checks when the member is present. The storage domain remains version 1. Historical events do not populate that domain, and existing `at` tasks are not converted into daily or weekly rules. Loading a Session with active legacy reminders warns that they must be recreated.
A plugin-sourced `followup()` synchronously appends the message to the Session inbox. Successful Session flush acknowledges durable delivery. One task-record put stores `lastDelivery`, appends a delivery record with the occurrence target, acknowledgment time, actual message id, and instruction snapshot, and updates the one-shot's `inactive` status or the recurring task's next target and status. Task state and its saved history therefore share one commit. Inactive tasks remain stored and never schedule another delivery; explicit deletion stops future delivery and removes the row together with its saved records. These records acknowledge inbox delivery, not user receipt or model execution completion.
New tasks initialize an empty `deliveryHistory`. A task without that optional field exposes its existing latest receipt, if any, and marks earlier records unavailable. Reading never rewrites the old task or invents the lost history or instruction snapshot. Its next acknowledged delivery preserves that available receipt and appends the new one. Strict decoding rejects duplicate message ids and disagreement between the latest receipt and the last history entry. History follows append order, not wall-clock sorting, so clock rollback does not reverse acknowledged deliveries.
A recurring task contributes its latest due occurrence once, including after Host restart. Every remains creation-aligned until an interval edit establishes a new anchor; Daily retains a local time and explicit IANA zone, and Weekly adds a non-empty ISO weekday set stored as unique ascending numbers. Every, Daily, Weekly, and Cron use `renderRecurringReminderBatchFraming` and share one follow-up message and its real message id when due for the same Session. A recurring task with no representable next target remains inactive with its last receipt. A stopped Host does not send messages.
Daily is a fourth mutually exclusive creation selector, Weekly a fifth, and Cron a sixth, none a conversion of `every_seconds` or `at`. Weekly selects every local date whose ISO weekday belongs to its stored set; the decoder rejects an unnormalized stored set, and the selector rejects an empty, repeated, out-of-range, or non-integer one. Their first targets are strictly future. Missing local times and dates are skipped; overlaps select the earlier instant once per date. The next target is after the delivery decision and on a local date later than the selected occurrence's date. Only UTC targets are restricted to years 0001–9999; local dates may cross into year 0 or 10000 at the edges. After Session restoration, the runtime rechecks due members against the current clock so a rollback does not admit future tasks.
The maintained `@js-temporal/polyfill` 0.5.1 resolves local dates and overlap/gap behavior using the Host's IANA data; Schedule does not own offset-sampling logic or add another package layer. Each daily or weekly record stores normalized `HH:mm:ss.SSS` time, its zone, its weekday set where applicable, and a committed next UTC instant. Decoding and restart do not recompute that instant. Valid zone aliases remain readable across canonical-name changes, while subsequent targets use current Host time-zone data.
Model `schedule_list`, Remote `list`, and the Session header's right-aligned reminder clock show only active tasks. The independent Automation tasks page opens from the sidebar and reads active and inactive Host tasks through the read-only Remote `catalog` method, without activating Sessions or reading their history. That clock is absent while the Session has no active reminder, opens a single task's detail directly when exactly one remains, and otherwise lists them so one can be chosen; both paths open one right-Sidebar task tab that renders the same detail component the Automation tasks page uses, so neither path selects the main panel. The tab names its task and original Session through its navigation parameters and carries that binding into every mutation, so opening or deleting from it never activates a Session. The page has one All, Enabled, and Inactive status filter row, retained inactive details, latest delivery metadata, and an original-Session control that sits in the detail tab strip beside the task actions. Its header's New action starts a new Session, where the reminder is created by asking the model; the page offers no creation form. Navigation waits for Session and archive metadata, explains unavailable targets, and never restores an archived Session implicitly. Task retention remains independent of original-Session availability. Every, Daily, Weekly, and Cron are recurring rules; the page groups tasks only by status. Daily and Weekly rule labels retain the saved local time, exact rule zone, and stored weekday set rather than substituting the browser zone used for next-target display. Explicit confirmed deletion reuses `delete` with the entry's original Session binding, requiring that the supplied binding match the stored task. This argument check is not independent caller authorization: model tools capture the current Agent's Session, while the global user interface selects the task's original binding. Cross-Session model modification policy remains a product decision, not a consequence of the storage layout. A payload-free change notification invalidates queries, and reconnect queries recover missed notifications. Deletion prevents subsequent dispatch but does not retract a queued message.
The second detail level separates Rules from Delivery records. Opening records requests `history` with the task's original Session binding and an explicit page limit of 1–100. An exclusive message-id cursor reads older receipts without changing position when new deliveries append. Catalogs retain only latest-receipt metadata rather than duplicating full histories in every task-list response. Missing tasks and unknown cursors are distinct failures, not empty successful history. The records view loads lazily, rejects stale responses, and keeps original-Session navigation separate from inspection.
Name, instruction, and timing updates share the dispatch and deletion FIFO. Comparing the complete observed record inside that queue prevents a stale draft from overwriting a saved rule or a target advanced by delivery. This avoids adding a persisted revision counter, at the cost of rejecting drafts after normal delivery changes the target. One task put replaces the rule, name, or instruction, preserving identity, original Session binding, status, latest receipt, and saved history. A request may omit the timing change, the name, or the instruction; an omitted field keeps its stored value, and a name or instruction change alone leaves the committed target untouched. Normalized no-ops leave the committed target and stored bytes unchanged. Changed one-shots retain their identity as At records; changed fixed intervals use a new accepted-save anchor; changed wall-clock rules re-resolve their first future target from the same accepted-save sample, and an equivalent normalized rule stays a no-op. A timing change may select a different recurrence kind, and the new rule computes its target exactly as creation would from that accepted-save sample. Deletion removes the row together with its saved records ([consequences of that reversal](2026-09-22-schedule-hard-delete-and-bounded-history.md)), so the runtime never schedules the task again and `history` no longer resolves for its original binding. Inactive tasks are not reopened.
The page uses the existing Host task domain and scheduler, not a mock engine or a Session projection. The name, the instruction, and the run-time rows edit one local draft for the shown task, and one Save commits that draft as a single update carrying the complete expected record; Cancel restores the stored values, a failed save retains the draft, an unsaved draft survives an authoritative refresh, and a committed save and a failed catalog refresh remain separate outcomes. Pause and execution-history operations remain unsupported. Lifecycle status and latest delivery acknowledgment do not describe model results.
The client presentation derives from existing persisted data instead of widening a persisted format. The `schedule_create` transcript card reads the created task out of the call's model-visible result JSON, which already serializes the canonical task value, rather than declaring `presentationMeta`: a persisted metadata channel would add a durable field for a card that Session-log replay reconstructs from the result alone, and previously recorded Sessions render the card without a format change. The Sidebar Session-row clock mark occupies the root-scoped `sidebar.session.row.leading` seat rather than requesting a per-Session provider, because a Session-scoped seat would bind every listed Session and thereby activate and retain it; the same root-scoped owner share feeds `sidebar.session.row.hover`, and both read the shared Host catalog snapshot `schedule/catalog()` without reading a Session log.
Session inbox persistence and the task put occur in separate persistence systems. Inbox persistence precedes the task put to avoid marking delivery before its message exists. A crash or task-write failure after inbox persistence can repeat a message; storing status, target, and receipt together does not make the two stores atomic. Dispatch failures retain the task and produce a diagnostic; there is no independent retry timer. This feature does not create or replace a Goal.
## Alternatives considered
**Load every Session at startup.** Session activation is unnecessary for discovering tasks and would couple timer restoration to Agent initialization and conversation size.
**Introduce separate rule, run, scheduler, and delivery packages.** Durable schedules and their saved inbox receipts support task inspection without independent execution records or model completion tracking.
**Write history separately from the task.** The domain exposes independent per-record writes, not a multi-record transaction. Keeping history in the task row makes status, next target, latest receipt, and appended history commit together. A separate history store requires an explicit transactional or recovery design before adopting it.
**Advance before enqueue or promise exactly-once delivery.** Advance-first can lose a message on enqueue failure. Exactly-once delivery needs a shared transaction or a durable idempotency protocol that the two stores do not provide.
**Import every historical Session automatically.** A startup scan would reintroduce the cold-history dependency. Explicit recreation keeps the persisted authority unambiguous; historical decoding preserves access to existing logs.
**Represent daily reminders as 86,400-second intervals.** Fixed elapsed time cannot preserve a local wall-clock rule when offsets change. A distinct Daily record retains the rule needed to resolve each later date. Weekly extends the same decision with an explicit weekday set rather than a 604,800-second interval, so a later offset change still resolves each selected local date.
**Sample offsets manually or recalculate saved targets during decoding.** Temporal supplies maintained calendar and disambiguation behavior without a Schedule-owned offset search. A committed target remains due even when newer time-zone data would resolve the rule differently; only later targets use that data.
## Consequences
**Keep the mock's hourly choice out of the stored vocabulary.** The approved mock lists 每小时整点 beside the daily, weekly and one-shot choices. Hourly and fixed-rate intervals are the same elapsed-time rule, so the card maps that menu slot onto the existing interval kind instead of adding a persisted kind. A dedicated hourly kind would need a new durable variant, its decoder, its target resolution, and its tool schema, all to express what an interval already expresses.
Task discovery and timers do not depend on loaded Sessions. The after, absolute-time, and fixed-rate selectors retain their semantics alongside Daily, Weekly, and Cron; reading existing saved tasks does not rewrite them. The Host must have its ordinary Session persistence and recovery services available, and the opt-in overlay remains responsible for enabling Schedule. Explicit deletion stops future delivery and removes the task from `list` and `catalog` without deleting conversation messages; the removed row takes its saved receipts with it. History is pruned to the configured window and record cap when an acknowledgment is appended, loaded into Host memory, and rewritten with the task on each acknowledgment. Pagination limits the returned record count; the plugin configuration (`deliveryHistoryDays`, default 30, and `deliveryHistoryRecords`, default 200) limits storage growth. Earlier overwritten receipts and deliveries whose task write failed cannot be reconstructed. Pause and execution-history operations remain unsupported.
Required coverage includes startup from stored tasks, missing-status normalization, cold Session restoration, latest-only catch-up, inactive-task retention without repeat dispatch, real message receipts, deletion across restart, removal of a deleted task's saved deliveries, queue/persistence failures, and disposal. Wall-clock obligations include gaps and skipped dates, earlier-only overlaps, fractional seconds, UTC-range edges, committed-target and alias preservation, and post-restoration clock rollback. Weekly adds weekday normalization and validation, next-target selection across a gap and an overlap, and stored set preservation. Historical decoders must reject Daily, Weekly, and Cron while Host storage accepts them. Client coverage includes every recurring kind, exact rule time, zone, and weekday set, All/Enabled/Inactive status filters, retained inactive details and original-Session navigation, authoritative refresh after confirmed deletion and notification, empty lists, reconnect, and late responses. Presentation coverage includes the page's New action starting a Session, the transcript card's narrowing of persisted result JSON including an incomplete or replayed result, the header clock's direct open for exactly one task and its count label, the idle-only row mark with the hover-card two-task limit and omission line. History coverage includes legacy byte preservation, append and restart, immutable sent instructions, strict latest-receipt consistency, stable cursor pagination, binding checks, failed persistence, and late page replies during refresh or task changes. Cross-store crash duplication remains an explicit limitation rather than a completion claim.
Name, instruction, and timing coverage includes all supported rule kinds, complete-record conflicts, delivery/save ordering, cancellation, normalized no-ops, unchanged-history persistence, pending controls, draft retention, and collision-free task identity. The [built CLI process test](../../../../apps/cli/tests/profiles/web/tests/schedule-timing.expected.e2e.ts) drives public Remote requests and reloads actual task-file bytes in a fresh Host. Its task and receipt seeds establish retained-data inputs, not real creation, delivery, or recorded-model evidence; those require ordinary Session operations and captured model requests.
@@ -0,0 +1,65 @@
# Agent Note: Host 拥有的定时消息
Status: implemented
[English](2026-09-16-host-schedule-storage.md) | 中文
## Problem
定时消息必须能在重启后被发现,无须加载每个 Session。挂在 live Agent 上的 timer 无法投递目标 Session 仍为 cold 的任务。用户还需要在不激活对话的情况下查看待投递和已投递任务;投递后删除任务会丢失这条管理记录。
## Decision
现有 Schedule 包拥有一个 Host storage domain,保存每个任务的原始 Session id、提醒记录、`active` 或 `inactive` 状态,最近一次投递回执,以及已保存的投递历史。该 domain 是保留任务的权威来源。Host scheduler 仅为活动任务恢复 timer;到期任务使用 Session controller 现有的解析操作,在原 Session 中排入普通 follow-up。查询和删除任务不会解析 Agent。已存储但缺少状态的记录规范化为 `active`,不扫描或重建历史 Session。
任务 domain 声明整 unit 布局。任务是权威数据,因此 JSON 后端会把文件不可读、文档损坏、版本不受支持或任务非法作为整批加载失败,而不是解释为删除。恢复失败不会改写 `schedule.json`,初始不存在的文件则作为空 domain 打开;修复该文件后会保留任务身份。`Service.init` 等待存储校验和运行时初始化,管理方法等待同一初始化结果;拥有方卸载时释放迟到的存储资源,而不开始投递。投递调度准入与初始化分开:任务变更提交后、运行时清理开始前,祖先可能关闭 Agent 准入。此类拒绝会记录日志而不自动重试,不会将已持久化的变更变成失败的管理结果。存储非法和清理注册失败仍拒绝启动。
此决策替代[会话内提醒记录](../feature/2026-08-05-durable-web-schedule.zh.md)中的 Session 日志权威来源、仅 live 时激活和只读目录决策。其中显式绝对时间校验、固定速率计算,以及区分排队接纳与模型完成的决策仍然适用。历史 `schedule/change` 的创建、fold 和 invariant 类型通过 `LegacyScheduleRecord` 保持为 After、At 和 Every;独立的宿主解码器接受包含 Daily、Weekly 与 Cron 的当前 `ScheduleRecord`。宿主记录解码器要求已存储的 `title` 去除首尾空白后非空、本身已去除首尾空白且不超过 120 个字符,严格的 task schema 会拒绝整个 domain 的打开,而不是丢弃这样的任务。历史变更解码器容忍缺失的 `title`,以便已写入的 Session 日志仍可读取;该成员存在时,同样按这三项检查校验。storage domain 保持版本 1。历史事件不会填充该 domain,已有 `at` 任务也不会转换为每日或每周规则。加载含有活动旧提醒的 Session 时,会警告需要重新创建提醒。
插件来源的 `followup()` 同步将消息追加到 Session 收件箱。Session flush 成功后确认持久投递。一次任务记录 put 存储 `lastDelivery`,追加包含发生目标、确认时间、真实消息 id 和指令快照的发送记录,并更新一次性任务的 `inactive` 状态或重复任务的下一目标和状态。因此,任务状态与其保存的历史共用一次提交。未运行任务仍保留在存储中,且不会再次安排投递;显式删除会阻止后续投递,并把该行连同其已保存记录一并移除。这些记录确认收件箱投递,不表示用户收到消息或模型执行完成。
新任务初始化空的 `deliveryHistory`。缺少该可选字段的任务会展示已有的最近一次回执(如果存在),并标明更早记录不可用。读取不会重写旧任务,也不会虚构已丢失的历史或指令快照。下一次确认投递时,保留已有回执并追加新记录。严格解码拒绝重复消息 id,以及最近一次回执与历史末项不一致的情况。历史按追加顺序排列,不按墙钟排序,因此时钟回拨不会颠倒已确认投递的顺序。
重复任务只投递一次最新到期发生时点,Host 重启后也一样。Every 保持与创建时间对齐,直到修改间隔后建立新的起算时点;Daily 保留本地时间与显式 IANA 时区,Weekly 另存非空 ISO 星期集合(唯一且升序)。Every、Daily、Weekly 与 Cron 均使用 `renderRecurringReminderBatchFraming`,同一 Session 的到期任务共用一条 follow-up 消息及其真实消息 id。没有可表示的下一目标时,重复任务保留未运行状态及最近一次回执。停止的 Host 不会发送消息。
Daily 是互斥的第四个创建选择器,Weekly 是第五个,Cron 是第六个,三者都不是对 `every_seconds` 或 `at` 的转换。Weekly 选择本地日期 ISO 星期属于其所存集合的每个日期;解码器拒绝未规范化的已存集合,选择器拒绝空、重复、超出范围或非整数的集合。它们的首个目标严格晚于当前时间。缺失的本地时间或日期被跳过;重叠时间选择较早时点,每个日期一次。下一目标晚于投递判断时刻,且其本地日期晚于所选发生时点的日期。仅 UTC 目标限制在 0001–9999 年;边缘处的本地日期可以进入第 0 年或第 10000 年。Session 恢复后,运行时依据当前时钟重新核对到期成员,避免时钟回拨时接纳未来任务。
受维护的 `@js-temporal/polyfill` 0.5.1 使用宿主 IANA 数据解析本地日期及重叠或缺口;Schedule 不拥有偏移量采样逻辑,也不增加包层级。每日或每周记录保存规范化后的 `HH:mm:ss.SSS` 时间、时区、适用的星期集合及已提交的下一 UTC 时点。解码和重启不会重新计算该时点。规范名称变化后,有效时区别名仍可读取;后续目标使用宿主当前的时区数据。
模型 `schedule_list`、Remote `list` 和 Session 顶部右侧的提醒时钟仅显示活动任务。独立的自动化任务页面从侧边栏打开,通过只读 Remote `catalog` 方法读取活动和未运行 Host 任务,不激活 Session 或读取其历史。该时钟在该 Session 没有活动提醒时不显示;恰好有一个时直接打开该任务详情,否则列出提醒供选择。两条路径都打开同一个右侧边栏任务页签,该页签渲染自动化任务页面所用的同一详情组件,因此都不会切换主面板。页签通过导航参数指明任务及其原 Session,并把该绑定带入每个修改操作,因此从页签打开或删除都不会激活 Session。页面提供一行“全部/已开启/未运行”状态筛选,保留未运行任务详情、最近一次投递元数据和固定在位于详情页签条内、与任务操作并排的原 Session 入口。页头的“新建”操作会打开一个新会话,提醒由模型在该会话中创建;页面不提供创建表单。导航等待 Session 与归档元数据,解释不可用目标,且不会隐式恢复已归档 Session。任务保留与原 Session 可用性相互独立。Every、Daily、Weekly 和 Cron 均为重复规则;页面只按状态分组。Daily 与 Weekly 规则标签保留保存的本地时间、精确规则时区和所存星期集合,不替换为下一目标显示所用的浏览器时区。显式确认删除复用 `delete`,传入条目的原始 Session 绑定,要求传入绑定与已存任务一致。这项参数校验不是独立的调用者鉴权:模型工具捕获当前 Agent 的 Session,全局用户界面则选择任务的原始绑定。跨会话模型修改政策仍是产品决策,不由存储布局直接决定。无载荷的变更通知使查询失效,重新连接时重新查询以恢复漏掉的通知。删除阻止后续投递,但不撤回已排队消息。
第二层详情将“规则”与“发送记录”分开。打开发送记录时,`history` 请求携带任务的原始 Session 绑定,以及 1–100 的显式分页数量。排他的消息 id 游标用于读取更早回执,新投递追加后不会改变其定位。目录仅保留最近一次回执元数据,不在每次任务列表响应中复制完整历史。任务不存在和游标未知属于独立失败,不是查询成功的空历史。记录视图按需加载,拒绝晚到的过期响应,并将原 Session 导航与查看记录分开。
名称、指令和时间更新与投递、删除共用 FIFO。在该队列内比较完整的已观察记录,可防止旧草稿覆盖已保存规则或投递已推进的目标。这样无须增加持久版本计数器,代价是正常投递改变目标后也会拒绝旧草稿。一次任务 put 替换规则、名称或指令,保留身份、原始 Session 绑定、状态、最近一次回执和已保存历史。请求可以省略时间变更、名称或指令;省略的字段保留已存储值,仅修改名称或指令不会改变已提交目标。归一化后等价的修改保留已提交目标与存储字节。修改后的一次性任务以 At 记录保留身份;固定间隔改变后以接纳保存时刻重新起算;钟表规则改变后以同一接纳保存时刻重新解析首个未来目标,而归一化后等价的规则不产生变更。时间变更可以选择不同的重复类型,新规则按创建时相同的方式、以同一接纳保存时刻计算目标。删除会移除该行及其已保存记录([该反转的后果](2026-09-22-schedule-hard-delete-and-bounded-history.zh.md)),因此运行时不会再调度该任务,`history` 也不再按其原始绑定解析。未运行任务不会被重新启用。
页面使用现有 Host 任务 domain 和调度器,不使用 mock 引擎或 Session projection。名称、指令和运行时间行编辑当前任务的同一份本地草稿,一次保存把该草稿作为携带完整预期记录的单个更新提交;取消恢复已存储值,保存失败保留草稿,未保存的草稿会保留到权威刷新之后,保存已提交与目录刷新失败仍是不同结果。暂停和执行历史操作仍不受支持。生命周期状态与最近一次投递确认不描述模型结果。
客户端的呈现从已有持久化数据派生,而不扩展持久化格式。`schedule_create` transcript(文本记录)卡片从该调用对模型可见的结果 JSON 中读出创建的任务——该 JSON 已经序列化规范任务值——而不是声明 `presentationMeta`:持久化 metadata 通道会为一张仅凭结果即可在 Session 日志回放中重建的卡片增加持久字段,而早于该卡片录制的 Session 无需格式变更即可渲染它。侧栏 Session 行的时钟标记占用 root 作用域的 `sidebar.session.row.leading` slot,而不是请求逐 Session provider,因为 Session 作用域的 slot 会绑定列表中每个 Session,从而激活并保留它;同一个 root 作用域属主约定也供给 `sidebar.session.row.hover`,两者都读取共享的 Host 目录快照 `schedule/catalog()`,不读取 Session 日志。
Session 收件箱持久化和任务 put 发生在两个持久化系统中。收件箱持久化先于任务 put,以免消息尚不存在时就标记已投递。收件箱持久化后崩溃或任务写入失败可能重复发送消息;状态、目标和回执一起存储,并不使两个存储具有原子性。投递失败保留任务并输出诊断;没有独立的重试 timer。此功能不创建或替换 Goal。
## Alternatives considered
**启动时加载每个 Session。** 发现任务不需要激活 Session,这会让 timer 恢复依赖 Agent 初始化和对话大小。
**引入独立的 rule、run、scheduler 和 delivery 包。** 持久 schedule 与已保存的收件箱回执足以支持任务查看,无须独立执行记录或模型完成跟踪。
**将历史与任务分开写入。** domain 提供独立的逐记录写入,不提供多记录事务。将历史保存在任务行内,可使状态、下一目标、最近一次回执和追加历史一起提交。采用独立历史存储之前,需要明确的事务或恢复设计。
**入队前推进,或承诺恰好投递一次。** 先推进可能在入队失败时丢失消息。恰好投递一次需要共享事务或持久幂等协议,这两个存储并不提供。
**自动导入全部历史 Session。** 启动扫描会重新引入对 cold 历史的依赖。显式重建使持久化权威来源保持明确;历史解码保留了现有日志的访问能力。
**以 86,400 秒间隔表示每日提醒。** 固定经过时长无法在偏移量变化时保留本地钟表规则。独立的 Daily 记录保留了解析每个后续日期所需的规则。Weekly 以显式星期集合而非 604,800 秒间隔延续同一决策,因此偏移量变化后仍能解析每个所选本地日期。
**手动采样偏移量或在解码时重新计算已保存目标。** Temporal 提供受维护的日历与消歧行为,无须由 Schedule 实现偏移量搜索。即使较新的时区数据对规则给出不同解析,已提交目标仍然到期;仅后续目标使用该数据。
## Consequences
**不把 mock 的「每小时整点」引入持久词表。** 设计稿把它与每天、每周、仅一次并列。每小时整点与固定间隔在语义上是同一种按经过时间计量的规则,因此详情里的该项直接映射到既有的间隔类型,而不是新增一个持久种类。若真要新增,需要新的持久变体、解码器、目标解析与工具 schema,而它表达的能力现有间隔已经覆盖。
任务发现和 timer 不依赖已加载的 Session。延迟、绝对时间和固定速率选择器在与 Daily、Weekly、Cron 并存时保留自身语义;读取已有任务不会重写它们。Host 必须提供普通 Session 持久化和恢复服务,Schedule 仍由显式启用的 overlay 加载。显式删除会阻止后续投递,并将任务从 `list` 和 `catalog` 中移除,不删除对话消息;被移除的行会带走其已保存回执。历史在追加确认时按配置的窗口与条数上限裁剪,随后加载到 Host 内存,并在每次确认时随任务一起重写。分页限制返回记录数量,不限制存储增长或指令字节数。此前被覆盖的回执,以及任务写入失败的投递记录,无法重建。暂停和执行历史操作仍不受支持。
必要覆盖范围包括从已存储任务启动、缺失状态规范化、冷 Session 恢复、仅补发最新发生时点、保留未运行任务且不重复投递、真实消息回执、重启后的删除结果、删除会移除已保存发送记录、排队或持久化失败,以及销毁。钟表规则的验证要求包括缺口和跳过的日期、仅选择较早时点的重叠、小数秒、UTC 范围边缘、已提交目标和别名保留,以及恢复后的时钟回拨。Weekly 另需覆盖星期集合的规范化与校验、跨缺口与重叠的下一目标选择,以及已存集合的保留。历史解码器必须拒绝 Daily、Weekly 与 Cron,而宿主存储接受它们。客户端覆盖范围包括每种重复规则、精确规则时间、时区和星期集合、全部/已开启/未运行状态筛选、保留未运行任务详情和原 Session 导航、确认删除和通知后的权威刷新、空列表、重新连接和晚到响应。呈现覆盖范围包括页面“新建”操作打开新会话、transcript 卡片从不完整或回放的结果 JSON 中收窄任务、页头时钟在恰好一个任务时直接打开并给出数量标签、仅空闲行标记及其悬浮卡片两个任务上限与遗漏行。历史覆盖范围包括旧文件字节保留、追加与重启、不可变的发送指令、最近回执一致性严格校验、稳定游标分页、绑定校验、持久化失败,以及刷新或切换任务期间晚到的分页响应。跨存储崩溃导致的重复投递仍是明确限制,不能被表述为完成保证。
名称、指令和时间编辑覆盖范围包括所有支持的规则类型、完整记录冲突、投递与保存的顺序、取消、规范化后的无变更、历史持久化不变、等待期间的控件、草稿保留,以及无碰撞的任务标识。[已构建 CLI 进程测试](../../../../apps/cli/tests/profiles/web/tests/schedule-timing.expected.e2e.ts) 驱动公开 Remote 请求,并在新 Host 中重新加载实际任务文件字节。任务和回执种子只是已保留数据的输入,不是真实创建、投递或模型录制证据;这些证据需要普通 Session 操作与捕获的模型请求。
@@ -9,11 +9,11 @@
en: 4b02ed98a500205c
zh: e1a075f6a8251059
/agent-note-developer-messages-for-incremental-session-changes/decision:
en: 5fecb076fa475b6d
zh: b59c76222ec00123
en: 986e4806b1d7da88
zh: dedecc5248fdd5f3
/agent-note-developer-messages-for-incremental-session-changes/alternatives-considered:
en: a9ec42fb650d6cd8
zh: c748181c603e675f
/agent-note-developer-messages-for-incremental-session-changes/consequences:
en: 28e92dab15059d0c
zh: 31b4f1955d82c02c
en: e98265bb86e38a2d
zh: a9749feb7d90c22a
@@ -20,7 +20,7 @@ Dynamic tool changes need durable, ordered records before providers can preserve
The V4 codec confines developer roles to the `developer/message` slot, including rejection from inbox and title-request arrays. Native validation checks required developer fields, historical header bindings, and developer-only tool-change blocks while preserving additional JSON properties. Optional metadata additions therefore remain readable under the persistence checker's optional-property rule; additional fields cannot weaken required-field, role, or open-step checks. An ignorable marker does not establish reader support: physical decoding preserves developer payloads until vocabulary-aware admission, while writers and native readers reject malformed known data. Developer admission leaves malformed ordinary message slots to decoder recovery. Installed Session adoption validates surface relationships without converting developer records into released-V3 user records. The [released migration rules](2026-08-31-released-session-format-migrations.md) continue to protect committed predecessor generations.
The producer requirement has one scoped exception: finalized Session V4 reserves the persisted representation needed by issue #4146. Waiting for provider and UI implementations would couple the format release to those independent integrations. The reservation permits storage and validation only; no shipped profile emits developer records, and unsupported consumers throw. Production emission requires provider, UI, and compaction support together.
Session V4 reserves the persisted representation needed by issue #4146. Production emission, provider projection, UI presentation, and compaction integration are described in the [dynamic tool update decision](2026-09-20-dynamic-tool-updates.md).
## Alternatives considered
@@ -32,4 +32,4 @@ The producer requirement has one scoped exception: finalized Session V4 reserves
## Consequences
Session V4 persists the tool-change representation for issue #4146. Automatic emission, provider serialization, deferred loading, and UI presentation are intentionally deferred so this type-and-persistence change does not block the V4 merge. Both DeepSeek protocols and pi-ai throw for developer history and deferred-loading requests; Chat and Trajectory throw for developer events. Shipped model-output and SDK snapshot scenarios contain no developer records.
Session V4 persists the tool-change representation for issue #4146. Emission, route projection, DeepSeek serialization, and Chat and Trajectory presentation are owned by the [dynamic tool update decision](2026-09-20-dynamic-tool-updates.md).
@@ -20,7 +20,7 @@ Status: implemented
V4 编解码器将 developer 角色限定在 `developer/message` 字段内,并拒绝 inbox 与标题请求数组中的该角色。原生验证检查 developer 必需字段、历史请求头绑定及工具变更块仅限 developer 的约束,同时保留附加 JSON 属性。因此,可选元数据添加符合持久化检查器的可选属性规则,仍可读取;附加字段不能削弱必需字段、角色或打开 step 的检查。可忽略标记不代表读取方支持该事件:物理解码保留 developer 载荷,直到按词表执行接纳;写入方和原生读取方仍拒绝格式错误的已知数据。developer 接纳逻辑将格式错误的普通消息字段留给解码器恢复。已安装的 Session 接纳逻辑验证 surface 关系,不把 developer 记录转换为已发布 V3 的 user 记录。[已发布格式迁移规则](2026-08-31-released-session-format-migrations.zh.md)继续保护已提交的前代文件。
产生方要求有一个限定例外:已定稿 Session V4 为 issue #4146 保留所需的持久化表示。等待提供方与 UI 实现会将格式发布与这些独立集成耦合。该预留仅允许存储和验证;已提供的 profile 不发出 developer 记录,不支持的消费者会抛出错误。生产环境发出这些记录要求提供方、UI 与压缩支持一起落地。
Session V4 预留 issue #4146 所需的持久表示。生产发出、提供方投影、UI 展示和压缩集成见[动态工具更新决策](2026-09-20-dynamic-tool-updates.zh.md)。
## Alternatives considered
@@ -32,4 +32,4 @@ V4 编解码器将 developer 角色限定在 `developer/message` 字段内,并
## Consequences
Session V4 为 issue #4146 持久化工具变更表示。自动发出事件、提供方序列化、延迟加载与 UI 展示有意留待后续实现,使此类型与持久化变更不阻塞 V4 合并。DeepSeek 两种协议与 pi-ai 对 developer 历史及延迟加载请求抛出错误;Chat 与 Trajectory 对 developer 事件抛出错误。已提供的模型输出与 SDK 快照场景不包含 developer 记录。
Session V4 持久化 issue #4146 的工具变更表示。发出、路由投影、DeepSeek 序列化及 Chat 和 Trajectory 展示由[动态工具更新决策](2026-09-20-dynamic-tool-updates.zh.md)负责。
@@ -0,0 +1,19 @@
# Bilingual-pair consistency record for 2026-09-20-dynamic-tool-updates.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/implemented/architecture/2026-09-20-dynamic-tool-updates.md
/agent-note-active-tool-headers-and-stateful-request-projection:
en: c40dd7568bba391f
zh: 295ce33341e0aab7
/agent-note-active-tool-headers-and-stateful-request-projection/problem:
en: 97fe58a13135b4a1
zh: 7490373a8d941da5
/agent-note-active-tool-headers-and-stateful-request-projection/decision:
en: 0c40a9283650c643
zh: 91a18d752d09a9ce
/agent-note-active-tool-headers-and-stateful-request-projection/alternatives-considered:
en: b78785f2a603eba8
zh: b8c268228da7e2e7
/agent-note-active-tool-headers-and-stateful-request-projection/consequences:
en: 93c32b795b71a1cc
zh: d42b5c563341c898
@@ -0,0 +1,37 @@
# Agent Note: Active tool headers and stateful request projection
Status: implemented
English | [中文](2026-09-20-dynamic-tool-updates.zh.md)
## Problem
Plugin and MCP tools change during conversations. Providers differ in whether they activate deferred definitions or remove tools through history. Storing retained provider declarations as active tools makes comparisons repeat removals and miss restorations.
## Decision
`request/header.tools` records assembled active tools independently of model capability. The loop compares names against the preceding header and emits one `developer/message` with additions and removals under the `tool-registry` source. Additions reference the new header through `headerSeq`. Definition changes remain header changes; unchanged names emit no update. Existing header equality and request-series rules remain in force.
`Session.toolHistory()` caches a capability-independent fold of committed headers and developer messages. First access restores inherited events; later reads consume only unseen events. Immutable snapshots carry initial declarations and ordered update identities with additions resolved through their historical headers. Explicit series starts and changed retained definitions rebuild declarations. Restoring a retained name with an unchanged definition continues the series: the projection keeps the declaration unchanged and lets the recorded addition re-offer the tool after its earlier removal, so a removal never disables a declared tool and a loading mode never rewrites an earlier declaration.
Loop and compaction requests carry this snapshot as `GenerateOptions.toolHistory`. At adapter dispatch, `projectToolUpdates` uses the prepared route's `toolUpdate`. Unsupported routes receive active definitions without `deferLoading` or developer messages. Supported routes defer historical additions; `in-history` retains removed definitions and removal blocks, while `addition-only` omits both. Only current-series update identities survive. Missing history or a request prefix omitting recorded updates uses current declarations without developer updates. Explicit deferred loading remains available on capable routes.
DeepSeek translates projected updates into system-role `tool_addition` and `tool_removal` blocks and `defer_loading` declarations, sending the tool-changes beta header when update blocks occur. The default `deepseek-flash` entry declares `addition-only`; pi-ai declares no mode. Chat and Trajectory display developer messages as context nodes, using localized tool-change notices: single changes show the tool name; multiple changes show counts and expandable name lists.
## Alternatives considered
**Store provider declarations in headers.** Retained removed definitions are not active tools. Mixing those meanings makes durable updates capability-dependent and breaks change detection.
**Reconstruct definitions in adapters from messages alone.** Addition messages contain names, not schemas or event-level header references. Session owns that historical relationship; adapters translate projected requests.
**Scan the complete log on every request.** The cached fold consumes new events and supplies stable snapshots to ordinary and auxiliary calls without adding arbitrary synchronous history readers.
**Reject same-name schema changes during tool registration.** This restriction is not enforced. Immediate failure for a plugin registering an incompatible tool requires comparing the new registration with historical definitions of tools called on each affected current surface, while excluding calls removed by compaction. That registration-time historical and scope-aware check is deferred because of its implementation complexity. Same-name schema changes remain allowed; they rebuild declarations and invalidate prefix reuse even on `in-history` routes.
## Consequences
Tool lists and logical update events are capability-independent; only outgoing declarations and messages vary. Calls to removed retained tools fail through the existing `UNKNOWN_TOOL` path. Changed definitions and incomplete auxiliary prefixes sacrifice prefix reuse for coherent declarations; unchanged restoration keeps the cached prefix on `in-history` routes only while the request series continues. Routes supporting tool updates and in-history system prompts preserve earlier messages when tools are added alongside a changed prompt or after an earlier prompt update. Routes without tool-update support still consolidate on tool changes; explicit series starts and surface replacements still consolidate on every route. No persisted fields or Session format revision are added.
The authored [headless prompt/tool scenario](../../../../snapshots/session/dynamic-tool-prompt-updates/snapshot.yml) pins an appended prompt beside a deferred tool addition and verifies that the first request's messages stay unchanged. It shares the stable-prompt scenario's tool lifecycle and schema expectations.
The [real DeepSeek SDK test](../../../../apps/cli/tests/profiles/sdk/dynamic-tool-cache.e2e.ts) measures the first request after a native tool addition with both unchanged and simultaneously updated instructions. Unique user history prevents a shared system-prompt cache hit from satisfying the preceding-input assertion. Removal checks tool availability without requiring cache retention; each case disables retries.
@@ -0,0 +1,37 @@
# Agent Note: 有效工具请求头与有状态请求投影
Status: implemented
[English](2026-09-20-dynamic-tool-updates.md) | 中文
## Problem
插件和 MCP 工具会在对话期间变化。不同提供方对通过历史激活延迟定义或移除工具的支持不同。把为提供方保留的声明当作有效工具存储,会使比较重复记录移除并遗漏恢复。
## Decision
`request/header.tools` 记录组装得到的有效工具,不依赖模型能力。循环将名称与前一请求头比较,以 `tool-registry` 来源发出一条包含添加和移除的 `developer/message`。添加通过 `headerSeq` 引用新请求头。定义变化仍体现为请求头变化;名称未变时不发出更新。现有请求头相等判断和请求序列规则保持有效。
`Session.toolHistory()` 缓存对已提交请求头和 developer 消息的归约,不依赖模型能力。首次访问恢复继承事件;后续读取仅处理新增事件。不可变快照携带初始声明和有序更新标识,添加定义通过历史请求头解析。显式新序列和保留定义变化会重建声明。以未变化的定义恢复保留名称时序列继续:投影保持声明不变,由记录的添加在先前移除之后重新提供该工具,因此移除不会禁用已声明的工具,加载模式也不会改写先前声明。
循环和压缩请求通过 `GenerateOptions.toolHistory` 携带此快照。适配器分发时,`projectToolUpdates` 使用已准备路由的 `toolUpdate`。不支持的路由接收有效定义,不携带 `deferLoading` 或 developer 消息。支持的路由延迟历史添加;`in-history` 保留已移除定义和移除块,`addition-only` 则省略两者。仅保留当前序列的更新标识。缺少历史或请求前缀遗漏已记录更新时,使用当前声明且不发送 developer 更新。显式延迟加载在支持的路由上仍然可用。
DeepSeek 把投影后的更新转换为 system 角色的 `tool_addition` 和 `tool_removal` 块以及 `defer_loading` 声明,在存在更新块时发送工具变更 beta 请求头。默认 `deepseek-flash` 条目声明 `addition-only`;pi-ai 不声明模式。Chat 和 Trajectory 将 developer 消息显示为上下文节点,工具变更使用本地化通知:单项变更显示工具名,多项变更显示数量及可展开的名称列表。
## Alternatives considered
**在请求头存储提供方声明。** 为提供方保留的已移除定义不是有效工具。混合两种含义会使持久更新依赖能力,并破坏变更检测。
**仅凭消息在适配器重建定义。** 添加消息包含名称,不包含模式或事件级请求头引用。Session 拥有该历史关系;适配器转换投影后的请求。
**每次请求扫描完整日志。** 缓存归约处理新事件,为普通和辅助调用提供稳定快照,不增加任意同步历史读取接口。
**在工具注册时拒绝同名 schema 变化。** 当前不执行此限制。要让注册不兼容工具的插件立即收到异常,需要将新注册与各受影响当前 surface 中已调用工具的历史定义比较,同时排除已被压缩移除的调用。由于实现复杂,注册时结合历史和作用域的检查留待后续实现。同名 schema 变化仍被允许;它们会重建声明,即使在 `in-history` 路由上也会使前缀复用失效。
## Consequences
工具列表和逻辑更新事件不依赖能力;只有发出的声明和消息不同。调用已移除但保留声明的工具时,通过现有 `UNKNOWN_TOOL` 路径失败。定义变化和不完整辅助前缀为保证声明一致而放弃前缀复用;仅当请求序列继续时,未变化的恢复才在 `in-history` 路由上保留缓存前缀。同时支持工具更新和历史内系统提示词的路由,在工具添加与提示词变化同时发生或晚于此前提示词更新时,保留此前消息。不支持工具更新的路由仍在工具变化时归并提示词;所有路由仍在显式新序列和 surface 替换时归并。不增加持久字段或 Session 格式版本。
手写的[无界面提示词/工具场景](../../../../snapshots/session/dynamic-tool-prompt-updates/snapshot.yml)钉住追加提示词与延迟工具添加同时发生的行为,并验证首次请求的消息保持不变。它复用稳定提示词场景的工具生命周期和 schema 期望值。
[真实 DeepSeek SDK 测试](../../../../apps/cli/tests/profiles/sdk/dynamic-tool-cache.e2e.ts) 分别在指令不变和指令同时更新时测量原生工具新增后的首次请求。独有的用户历史确保仅命中共享系统提示词缓存无法满足前序输入断言。移除场景检查工具可用性,不要求保留缓存;每个用例均禁用重试。
@@ -0,0 +1,19 @@
# Bilingual-pair consistency record for 2026-09-22-schedule-hard-delete-and-bounded-history.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/implemented/architecture/2026-09-22-schedule-hard-delete-and-bounded-history.md
/agent-note-schedule-deletes-task-rows-and-bounds-delivery-history:
en: 17a73230ad22ec0b
zh: 5bcf27d7c0e86764
/agent-note-schedule-deletes-task-rows-and-bounds-delivery-history/problem:
en: 98e82a0add324882
zh: c10e85a3f2a522f8
/agent-note-schedule-deletes-task-rows-and-bounds-delivery-history/decision:
en: 56328745115714b5
zh: f30d39f59aa06a13
/agent-note-schedule-deletes-task-rows-and-bounds-delivery-history/alternatives-considered:
en: 0c4eb01054be1141
zh: 95980f8097fa36fd
/agent-note-schedule-deletes-task-rows-and-bounds-delivery-history/consequences:
en: 05121f4b77b8ee12
zh: 1751c9b1a752ce6b
@@ -0,0 +1,33 @@
# Agent Note: Schedule deletes task rows and bounds delivery history
Status: implemented
English | [中文](2026-09-22-schedule-hard-delete-and-bounded-history.zh.md)
## Problem
The Schedule domain is one whole-unit JSON document that the backend rewrites on every mutation, so every retained byte in a task row grows every write. Saved delivery records were unbounded: each acknowledgment appended to the array, and a deleted task kept its row and records so the open detail could still read them. The document's size therefore followed a task's delivery count, and deletion added a state every reader had to skip.
## Decision
Deletion is hard deletion. `ScheduleService.delete` removes the storage row through `tasks.delete`, so the task stops scheduling, leaves `list` and `catalog`, and `history` answers `schedule_not_found` for the same `(sessionId, id)`; the row's saved delivery records are removed with it. The task schema has no `deleted` field, and no client surface offers a recycle bin or a read of an already deleted task's records.
Delivery history is bounded by plugin `Config`: `deliveryHistoryDays` (default 30, 1–3650) and `deliveryHistoryRecords` (default 200, 1–10000). `appendDelivery` applies both bounds on the only write path. It keeps the records whose `deliveredAt` is at or after the appended receipt's `deliveredAt` minus the configured day window, keeps the newest `deliveryHistoryRecords` of those, and always retains the appended latest receipt; a pruned window sets `earlierRecordsUnavailable` and `earlierRecordsPruned`, which later appends preserve. The latter flag is optional in stored rows and confirms actual removal; legacy unavailability alone does not imply pruning. The history response includes the current retention limits, and the Client shows the cleanup notice only for confirmed pruning after loading the final nonempty page.
The [Host-owned scheduled messages](2026-09-16-host-schedule-storage.md) note keeps the rest of its decision: one authoritative version-1 task domain, timer restoration for active tasks only, cold-Session delivery through the Session controller, the catalog and detail surfaces, and the historical-versus-Host decoder split. Its status vocabulary is `active` or `inactive`; `inactive` is the sole non-active name, and the client filter row reads All, Enabled, and Inactive. That note stays active: this supersession is partial, so it is neither archived nor frozen, and it remains the owner of everything above while the deletion and retention decisions it recorded are replaced here.
## Alternatives considered
**Keep the retained `deleted` row.** The flag existed so `history` could answer after deletion. Nothing read those records after deletion, the row kept every prompt snapshot in the storage document, and the state had to be excluded by hand from `list`, `catalog`, and the timer. Removing the row makes deletion mean one thing.
**Bound only by record count.** A count cap alone keeps arbitrarily old receipts when deliveries are sparse, and it drops a burst of recent ones when they are not. The day window bounds staleness while the cap bounds the document.
**Measure the window from the task instead of from each receipt.** Anchoring the window to the task's creation or committed target would discard a newly written receipt of an old task. Measuring back from the appended receipt's `deliveredAt` keeps the window anchored to the data being kept.
**Keep history unbounded and let operators clean up.** The domain publishes one whole-unit document per write, so unbounded history makes the storage document's size a function of delivery counts. Retention is a bound the storage layout requires, not a retention preference.
## Consequences
Deletion now offers no recovery: a deleted task's saved deliveries cannot be read back, and a task that still needs its full history must be read before deletion. Configured retention also discards receipts the task once had; the retained flag records that a window was pruned rather than pretending the history is complete.
In exchange, the persisted task set has one deletion semantics, the storage document's size is bounded by configuration instead of by delivery counts, and `inactive` is the only non-active lifecycle name. The plugin `Config` is the place to raise the bounds when a deployment needs longer delivery history.
@@ -0,0 +1,33 @@
# Agent Note: Schedule 删除任务行并限制投递历史
Status: implemented
[English](2026-09-22-schedule-hard-delete-and-bounded-history.md) | 中文
## Problem
Schedule domain 是后端每次变更都会重写的单个整 unit JSON 文档,因此任务行中保留的每个字节都会随每次写入增长。已保存的发送记录没有上限:每次确认都会向数组追加,而被删除的任务会保留其行与记录,以便已打开的详情仍能读取。于是文档大小随任务的投递次数变化,删除还引入了一个所有读取方都必须跳过的状态。
## Decision
删除是硬删除。`ScheduleService.delete` 通过 `tasks.delete` 移除存储行,因此任务停止调度、离开 `list` 与 `catalog`,`history` 对相同的 `(sessionId, id)` 返回 `schedule_not_found`;该行已保存的投递记录随行一并移除。任务 schema 没有 `deleted` 字段,也没有任何界面提供回收站或读取已删除任务记录的能力。
投递历史受插件 `Config` 限制:`deliveryHistoryDays`(默认 30,1–3650)与 `deliveryHistoryRecords`(默认 200,1–10000)。`appendDelivery` 在唯一的写入路径上同时应用这两个上限。它保留 `deliveredAt` 不早于「本次追加回执的 `deliveredAt` 减去配置的天数窗口」的记录,在其中保留最新的 `deliveryHistoryRecords` 条,并始终保留本次追加的最近一次回执;发生裁剪时设置 `earlierRecordsUnavailable` 和 `earlierRecordsPruned`,后续追加会保留这两个标志。后者在存储行中可选,用于确认实际删除;仅有旧格式的不可用标志不表示发生过裁剪。历史响应携带当前保留上限,客户端仅在加载完最后一页且历史非空、确认裁剪时显示清理提示。
[Host-owned scheduled messages](2026-09-16-host-schedule-storage.zh.md) 笔记保留其余决策:唯一的权威版本 1 任务 domain、只为活动任务恢复定时器、通过 Session controller 投递到冷会话、目录与详情界面,以及历史解码器与宿主解码器的划分。其状态词汇为 `active` 或 `inactive`;`inactive` 是唯一的非活动名称,客户端筛选行为全部、活动和未运行。该笔记保持活动状态:这是一次部分取代,因此它既未归档也未冻结,仍是上述内容的归属笔记,而它记录的删除与保留决策由本笔记取代。
## Alternatives considered
**保留带 `deleted` 标志的任务行。** 该标志的存在是为了让 `history` 在删除后仍能回答。删除后没有任何代码读取这些记录,而该行把每个提示文本快照都留在存储文档中,还必须手工把它排除在 `list`、`catalog` 和定时器之外。移除该行让删除只有一种含义。
**只按条数设上限。** 仅有条数上限时,投递稀疏的任务会保留任意久远的回执,投递密集时又会丢弃近期的一批回执。天数窗口限制陈旧程度,条数上限限制文档大小。
**以任务而非每条回执为窗口起点。** 把窗口锚定在任务的创建时间或已提交目标上,会丢弃旧任务刚写入的回执。从本次追加回执的 `deliveredAt` 向前计算,使窗口锚定在所要保留的数据上。
**保留无上限的历史,由运维清理。** 该 domain 每次写入都发布整个 unit 文档,因此无上限的历史会让存储文档的大小随投递次数增长。保留上限是存储布局所要求的约束,而不是保留策略偏好。
## Consequences
删除不再提供恢复能力:已删除任务的已保存投递无法再读回,仍需要完整历史的必须在删除前读取。配置的保留策略也会丢弃任务曾经拥有的回执;保留的标志记录窗口发生过裁剪,而不是假装历史完整。
换取的是:持久任务集合只有一种删除语义,存储文档大小由配置而非投递次数决定,且 `inactive` 是唯一的非活动生命周期名称。当某个部署需要更长的投递历史时,应提高插件 `Config` 中的上限。
@@ -0,0 +1,22 @@
# Bilingual-pair consistency record for 2026-09-23-schedule-local-menu.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/implemented/architecture/2026-09-23-schedule-local-menu.md
/agent-note-own-the-schedule-detail-menus-inside-the-schedule-client:
en: a9ed8f9cf0bdfd66
zh: ff11822efe6b48b7
/agent-note-own-the-schedule-detail-menus-inside-the-schedule-client/problem:
en: 076e6ca799bf2045
zh: d3f87030015cdf3c
/agent-note-own-the-schedule-detail-menus-inside-the-schedule-client/decision:
en: c35ab9fa2549866d
zh: 01855f87b884dc93
/agent-note-own-the-schedule-detail-menus-inside-the-schedule-client/alternatives-considered:
en: 3648e55bb76a4381
zh: 83c98e0789a6df9c
/agent-note-own-the-schedule-detail-menus-inside-the-schedule-client/consequences:
en: f6380c38e35bce37
zh: 1379ff1763d0eb10
/agent-note-own-the-schedule-detail-menus-inside-the-schedule-client/testing:
en: f1de601175678a9a
zh: 58fddec29a462b98
@@ -0,0 +1,43 @@
# Agent Note: Own the Schedule detail menus inside the Schedule client
Status: implemented
English | [中文](2026-09-23-schedule-local-menu.zh.md)
## Problem
The Automation tasks detail needs a menu whose list carries a pinned control: the Time zone row's search field must stay above the scrolling zone rows. The shared `Menu` in `@deepseek-ai/dsh-client-ui-primitives` renders rows, separators, labels, a selection mark, and a keyboard walk, but it has no header slot. The Delivery records tab shows a saved record's message id behind an info glyph, and the reader must be able to read and copy that id.
Serving either need by extending `ui-primitives` puts one feature's behavior into the package every client plugin consumes. The same branch had already removed unrelated `ui-layout` and `ui-sidebar-right` edits after review asked why a Schedule change touched them, and a shared primitive extended for a single caller still costs every future reader of that primitive.
## Decision
`packages/client/ui-schedule` owns its menu. `src/client/TaskMenu.tsx` with `TaskMenu.module.css` holds a schedule-local copy that renders rows, separators, labels, disabled and destructive rows, a trailing `selectedId` mark, the pointer and keyboard walk, portaled placement, the pinned `header` slot, and the `data-menu-field` focus handover. It imports the shared `useAnchoredPosition` and `useDismissOnOutsidePointer` hooks, which are the placement and dismissal behavior any anchored panel needs, and it imports no runtime value from `ui-primitives`. The four detail menus use it.
The Delivery records tab names and copies a record's message id through an info button. The button's accessible name states the action and the record (`Copy message ID: <id>`), the shared `Tooltip` states the id on hover and on keyboard focus, and activation copies it through the shared `writeClipboard` helper and reports `Copied` in a status region beside the glyph. `Tooltip` therefore keeps its master shape: the mode that held one bubble open under the pointer is gone, and the Delivery records view does not use the `HoverCard` that opens on the pointer alone.
`packages/client/ui-primitives` returns to master except the clock glyph's stroke. The client bundle purity gate in `packages/client/tsdown.client.ts` is the neighboring rule for the other direction of this boundary: a client plugin may import another plugin's package only as types or as a module-table request, so shared runtime vocabulary travels through an inline-safe layer or a cordis service rather than a package edge.
## Alternatives considered
**Add a `header` prop to the shared `Menu`.** This is what the branch first did: an optional slot plus the `data-menu-field` focus handover, with tests in the shared package. It gives the smallest diff and keeps one menu implementation, and it was rejected because the only caller is the Schedule detail: `ui-primitives` would carry API that exists for one feature, and every client plugin's bundle would read a header contract none of them use.
**Add an `interactive` mode to the shared `Tooltip`.** Shipped first and then removed: a grace period between anchor and bubble, pointer hold, click toggling, and selectable text, with six tests. It lost because the shared tooltip already opens on focus and the shared clipboard helper already performs the copy, so the mode added a second way to hold one bubble open for a single caller.
**Name and copy the message id through `HoverCard`.** Shipped second and then replaced: the card holds while the pointer rests on it, selects its text, and copies on activation. It lost because `HoverCard` opens on `onPointerEnter` alone, so the already-focusable glyph showed a keyboard reader nothing and offered no copy path.
**Render the zone search field outside the menu list.** A search input above the trigger, or a separate row in the rule card, needs no primitive change. It was rejected because the field would scroll away from the list it filters or sit in a different surface than the rows it filters.
**Build the zone picker on the schedule-local `PickerPopover`.** The Date and Time pickers already use that panel, so the zone list could reuse it. It was rejected because `PickerPopover` positions and dismisses a panel but renders no list semantics: the keyboard walk, the row roles, and the selected-row mark would have to be rebuilt inside the Schedule client anyway, which is the copy this note describes with more surface than the menu it replaces.
## Consequences
The Schedule client carries a second menu implementation: 346 lines of component, 189 lines of CSS, and 403 lines of tests, in exchange for `ui-primitives` returning to master except the clock glyph's stroke. The copy is trimmed to what the detail uses, and the absent capabilities stay absent: submenus, component-rendered rows, a pinned footer, multiple selection ids, the fill selection mode, dense and compact spacing, mount-time autofocus, a caller-supplied anchor rect, pointer-leave closing, and a side parameter. A later Schedule menu that needs one of them extends the copy or revisits this decision.
Copying is an activation, not a text selection: the tooltip shows the id but does not accept the pointer, so the desktop gesture that copies the id is a click on the glyph. That is the capability the removed interactive mode had and this shape gives up. Shared behavior stays shared: placement, dismissal, focus handover, the duplication gate's exclusion markers naming both owners, and per-file coverage all apply to the copy. The copied region carries `jscpd:ignore-start` and `jscpd:ignore-end` with the two owners named, following the [cross-package value dependencies note](../../archived/process/2026-08-23-client-cross-package-value-dependencies.md); the schedule-specific header, handover, and Tab semantics sit outside that region.
The copy places its list with `useAnchoredPosition`, which clamps every edge by the same 12px margin, while the shared menu's top edge used the frame-top clearance helper. A list that must clamp upward against the top of a macOS window can therefore sit about 12px lower than the shared menu did in the same situation; the stylesheet still reserves the clearance for the list's own maximum height, so the common case is unchanged.
## Testing
`packages/client/ui-schedule/tests/task-menu.client.spec.tsx` pins the pointer and keyboard walk, the `header` field's Home/End ownership, the `data-menu-field` handover after placement, the selected-row mark, disabled rows, portal placement, outside dismissal, and focus return; the three menu-header tests that lived in `ui-primitives` moved there with the capability. `delivery-history.client.spec.tsx` pins the tooltip that keyboard focus raises, the button's accessible name, the clipboard write, and the copy confirmation that clears. The coverage gate reports 100% per file for `packages/client/ui-schedule/src`, and the duplication gate reports no clones across `packages/client/ui-schedule` and `packages/client/ui-primitives`.
@@ -0,0 +1,43 @@
# Agent Note: Schedule 详情菜单由 Schedule 客户端自己拥有
Status: implemented
[English](2026-09-23-schedule-local-menu.md) | 中文
## Problem
Automation tasks 详情需要一个列表顶部钉住控件的菜单:Time zone 行的搜索框必须停在滚动时区行之上。`@deepseek-ai/dsh-client-ui-primitives` 里的共享 `Menu` 会渲染行、分隔线、标题行、选中标记和键盘走位,但没有 header 槽。Delivery records 页把一条已保存记录的 message id 放在 info 图标背后,读者要能读到并复制这个 id。
用扩展 `ui-primitives` 来满足其中任一需求,都会把单个功能的行为放进每个 client 插件都消费的包里。同一条分支此前已经因为 review 质询“Schedule 的改动为什么碰它们”而撤掉了无关的 `ui-layout` 与 `ui-sidebar-right` 编辑;而一个只为单一调用方扩展的共享原语,对之后每一个读它的人都是成本。
## Decision
`packages/client/ui-schedule` 自己拥有菜单。`src/client/TaskMenu.tsx` 与 `TaskMenu.module.css` 保存一份 schedule 本地副本,渲染行、分隔线、标题行、disabled 与 dangerous 行、`selectedId` 尾随标记、指针与键盘走位、portal 定位、钉住的 `header` 槽,以及 `data-menu-field` 焦点交接。它导入共享的 `useAnchoredPosition` 与 `useDismissOnOutsidePointer` hook——任何锚定面板都需要的定位与关闭行为——不从 `ui-primitives` 导入任何运行时值。详情的四个菜单都改用它。
Delivery records 页用一个 info 按钮给出并复制记录的消息 id。该按钮的可访问名称同时说明动作与记录(`Copy message ID: <id>`),共享 `Tooltip` 在悬停与键盘聚焦时给出该 id,激活时通过共享的 `writeClipboard` 复制,并在图标旁用 status 区域报告 `Copied`。因此 `Tooltip` 保持 master 形态:那个让气泡在指针下保持展开的模式已不存在,Delivery records 页也不使用只在指针下打开的 `HoverCard`。
`packages/client/ui-primitives` 除时钟图标描边外回到 master。`packages/client/tsdown.client.ts` 的 client bundle purity gate 是这条边界另一方向上的相邻规则:client 插件只能以类型或 module-table request 的形式导入另一个插件的包,因此共享运行时词汇要么走 inline-safe 层,要么走 cordis service,而不是包与包之间的边。
## Alternatives considered
**给共享 `Menu` 加 `header` prop。** 这是分支最初的做法:一个可选槽加 `data-menu-field` 焦点交接,并在共享包里带测试。它的 diff 最小、只保留一份菜单实现;被否决是因为唯一的调用方是 Schedule 详情:`ui-primitives` 会背上只为单个功能存在的 API,而每个 client 插件的 bundle 都要读一份没人使用的 header 契约。
**给共享 `Tooltip` 加 `interactive` 模式。** 先落地后撤除:锚点与气泡之间的宽限期、指针停留、点击切换、可选中的文字,外加六个测试。被否决是因为共享 tooltip 本来就支持聚焦展开、共享剪贴板助手本来就负责复制,这个模式只是为单一调用方多出了一种保持气泡展开的方式。
**用 `HoverCard` 给出并复制消息 id。** 第二个落地又被替换的版本:指针停在卡片上时保持展开、文字可选中、激活即复制。被否决是因为 `HoverCard` 只在 `onPointerEnter` 打开,于是那个本来就可聚焦的图标对键盘读者什么都不显示,也没有复制入口。
**把时区搜索框渲染在菜单列表之外。** 放在触发器上方的输入框,或规则卡里的独立一行,都不需要改原语。被否决是因为该输入框会与它所过滤的列表分离滚动,或落在与它过滤的行不同的界面层上。
**用 schedule 本地的 `PickerPopover` 实现时区选择器。** Date 与 Time picker 已经用它。被否决是因为 `PickerPopover` 只负责定位与关闭面板,不渲染列表语义:键盘走位、行 role、选中行标记都要在 Schedule 客户端里重建,那正是本 note 描述的副本,而且比它所替代的菜单要维护更多东西。
## Consequences
Schedule 客户端多出一份菜单实现:346 行组件、189 行 CSS、403 行测试,换来 `ui-primitives` 除时钟图标描边外回到 master。副本按详情实际使用裁剪,缺失的能力保持缺失:submenu、组件渲染行、钉住的 footer、多个 selectedId、fill 选中模式、dense 与 compact 间距、挂载时 autofocus、调用方提供锚点矩形、指针离开即关闭,以及 side 参数。日后某个 Schedule 菜单需要其中一项时,要么扩展副本,要么重新审视这个决定。
复制是一次激活,而不是选中文本:tooltip 给出 id 但不接受指针,所以桌面端复制该 id 的手势是点击图标;这正是被撤掉的交互模式所具备、而当前形态放弃的能力。共享行为仍是共享的:定位、关闭、焦点交接、点名两个 owner 的 duplication gate 排除标记,以及 per-file 覆盖率都适用于这份副本。被拷贝的区域带 `jscpd:ignore-start` 与 `jscpd:ignore-end` 并点名两个 owner,遵循 [cross-package value dependencies note](../../archived/process/2026-08-23-client-cross-package-value-dependencies.md);schedule 专有的 header、交接与 Tab 语义不在该区域内。
副本用 `useAnchoredPosition` 定位列表,它对四条边都用同一个 12px margin,而共享菜单的顶边用的是 frame-top clearance helper。因此在 macOS 窗口顶部需要向上钳制的列表,可能比共享菜单原先的位置低约 12px;样式表仍为该列表自身的最大高度保留 clearance,常见情形不变。
## Testing
`packages/client/ui-schedule/tests/task-menu.client.spec.tsx` 钉住指针与键盘走位、`header` 输入框对 Home/End 的独占、落位后的 `data-menu-field` 交接、选中行标记、disabled 行、portal 定位、外部关闭与焦点回返;原先位于 `ui-primitives` 的三个菜单 header 测试随该能力一起迁入。`delivery-history.client.spec.tsx` 钉住键盘聚焦展开的工具提示、按钮的可访问名称、剪贴板写入,以及会自行清除的复制确认。覆盖率门禁对 `packages/client/ui-schedule/src` 报告 per-file 100%,duplication 门禁在 `packages/client/ui-schedule` 与 `packages/client/ui-primitives` 上报告 0 克隆。
@@ -0,0 +1,22 @@
# Bilingual-pair consistency record for 2026-09-18-schedule-task-tab-restore.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/implemented/bug-fix/2026-09-18-schedule-task-tab-restore.md
/agent-note-task-tab-recovery-after-a-reload:
en: d7edcc16307d350a
zh: 63301b138b2a203b
/agent-note-task-tab-recovery-after-a-reload/problem:
en: 244f309423fff24e
zh: ddd2ab66a3f4fd5b
/agent-note-task-tab-recovery-after-a-reload/decision:
en: 16e1bcb4a0860e91
zh: 587715df43d5c96f
/agent-note-task-tab-recovery-after-a-reload/alternatives-considered:
en: 2fb8e2e0688d22ac
zh: a584b183723aace7
/agent-note-task-tab-recovery-after-a-reload/consequences:
en: 20b7e4c551df0bc0
zh: c17426fb981f0b06
/agent-note-task-tab-recovery-after-a-reload/testing:
en: 3581ad83c52ee3b0
zh: fbffd886a46b76c6
@@ -0,0 +1,45 @@
# Agent Note: Task tab recovery after a reload
Status: implemented
English | [中文](2026-09-18-schedule-task-tab-restore.zh.md)
## Problem
The right Sidebar persists a Session's layout as tab records and not the navigation parameters an opener passed, per the [tab type and navigation decision](../architecture/2026-09-05-sidebar-tab-types-and-navigation.md). A `scheduleTask` tab restored by a reload therefore had no task identity: its body could show only its loading feedback and its chip only the constant label, until the user opened the task again from the Session entry. Recovering content belongs to the tab type's provider, as the [layout persistence decision](../architecture/2026-09-14-sidebar-layout-provider-recovery.md) established for terminals, and the Sidebar exposes no parameter store for a page type to reuse.
## Decision
The task tab page keeps a durable binding to the task it last showed and resolves a restored record from it.
The binding key is the Sidebar Session plus the layout record's own id. `dsh.sidebar-right.v1.<sessionId>` persists the tab records and the counter that mints their ids, so a record keeps its id across a reload and no record minted later in that layout reuses it. The record's `contentId` is not a usable key: every tab of one page kind is recorded under the same `sidebar://<kind>` address, and page deduplication is per pane, so two panes may each hold a task tab. Each entry also stores the `kind` and `contentId` of the record it was written for, and a read drops and ignores an entry whose pair differs from the record reading it.
One localStorage document per Session, keyed `dsh.schedule.task-tab.v1.<sessionId>`, holds one entry per tab id, mapping that page to the Session and id of its last task. No task content is stored. A write keeps only the entries whose tab the Session's committed layout still holds, read through `ctx.sidebarRight.tabsIn(sessionId)`; a Session whose layout is not adopted leaves its entries alone. An entry that a read succeeding after the tab appeared cannot resolve is removed, which leaves the tab with the unbound-tab notice, the same state as one that never had a binding.
The body writes the binding in an effect whenever a navigation carries this type's parameters, and reads it only while the record has no navigation parameters; parameters of another kind leave the record in its missing-task state. A recovered identity then follows the existing rules: the detail whenever the retained records hold the task, the catalog feedback until a read requested after the tab's baseline settles without the task, and the query-failure state with its retry when the read fails. A binding such a read cannot resolve shows the unbound-tab notice instead of the loading feedback, and never the missing-task notice, because that notice states that an opener named a task.
The body measures the answer from the catalog read request ordinal it observed for its current navigation, and asks for a read whenever the retained records leave it without a task and no read requested after that ordinal has succeeded, reporting the catalog feedback while that read is pending. The Sidebar navigates a tab again without remounting its body, so the navigation's revision moves the baseline; a read requested before the baseline never states that the tab's task is gone.
## Alternatives considered
**Key the binding by `contentId`.** Rejected: every tab of one page kind is recorded under the same address, and page deduplication is per pane, so two task tabs in two panes would share one binding and restore the same task.
**Persist navigation parameters in the Sidebar layout.** Rejected: it moves content parameters into the layout snapshot that the [navigation decision](../architecture/2026-09-05-sidebar-tab-types-and-navigation.md) keeps transient, and every future page type would inherit the format.
**Declare the kind `multiple: true` so every open mints a unique contentId.** Rejected: it changes what opening a task does — a second task opens a second tab instead of showing in the existing one — to obtain a key, and it cannot recover records restored before the change.
**Resolve the task from the catalog.** Rejected: the catalog holds no per-tab association, and the Session entry the tab was opened from is gone after a reload.
**Reuse the client store's localStorage persistence.** Rejected: `createSnapshotStore`'s `persist` option saves one whole store value under one name and rehydrates it when that store is created, so it needs a store per Session; the binding also has to reject reused ids and prune closed tabs on write, which a keyed document does in one place with no subscription for a body to re-render through.
**Store the task title beside the binding to confirm the recovery.** Rejected: a title is task content, it changes with an edit, and the settled catalog read already decides whether the bound id resolves.
## Consequences
A reloaded Session restores every task tab's detail and chip without reopening the task. The binding is window-local browser state: another browser, a cleared origin, or a tab the layout kit duplicated has none, and such a record states that the tab has no task instead of claiming the task is gone. A discarded layout snapshot can mint a tab id that a binding still names; the kind and contentId guard rejects an entry of another kind, and an entry of the same kind is overwritten by the next navigation of that tab or removed once its task stops resolving.
Entries of Sessions whose layouts are never adopted again remain until their next write; write-time pruning bounds every adopted Session, and no entry holds content.
## Testing
Unit tests cover writing on navigation, reading only without parameters, recovery into the detail and the chip, degradation of an unresolvable binding, committed-tab pruning, reused-id rejection, malformed stored documents, and storage that is absent or rejects writes.
@@ -0,0 +1,45 @@
# Agent Note: 刷新后任务标签页的恢复
Status: implemented
[English](2026-09-18-schedule-task-tab-restore.md) | 中文
## Problem
右 Sidebar 按 [标签类型与导航决策](../architecture/2026-09-05-sidebar-tab-types-and-navigation.zh.md) 只把 Session 布局持久化为标签记录,不持久化打开方传入的导航参数。因此刷新恢复出的 `scheduleTask` 标签没有任务身份:正文只能显示加载反馈,chip 只能显示固定文案,直到用户从 Session 入口重新打开该任务。按 [布局持久化决策](../architecture/2026-09-14-sidebar-layout-provider-recovery.zh.md) 为终端确立的划分,恢复内容属于标签类型自身 provider 的职责,而 Sidebar 没有可供页面类型复用的参数存储。
## Decision
任务标签页保存到它最后显示任务的持久关联,并据此恢复刷新出的记录。
关联键是 Sidebar Session 加布局记录自身的 id。`dsh.sidebar-right.v1.<sessionId>` 持久化标签记录以及铸造其 id 的计数器,因此记录刷新后保持 id,该布局之后铸造的记录也不会复用它。记录的 `contentId` 不能作为键:同一种页面类型的所有标签都记录在同一个 `sidebar://<kind>` 地址下,而页面去重按 pane 进行,两个 pane 可以各有一个任务标签。每条记录还保存写入时该记录的 `kind` 和 `contentId`;读取时若这一对与读取它的记录不一致,就删除该条并视为无关联。
每个 Session 一个 localStorage 文档,键为 `dsh.schedule.task-tab.v1.<sessionId>`,按 tab id 保存一条记录,把该页面映射到其最后任务的 Session 和 id。不保存任何任务内容。写入时只保留 Session 已提交布局仍然持有的标签对应的记录,该布局通过 `ctx.sidebarRight.tabsIn(sessionId)` 读取;布局未被采用的 Session 保持其记录不变。标签页出现之后成功的一次读取无法解析的记录会被删除,使该标签显示未关联页签提示,与从未有关联的标签相同。
只要一次导航携带本类型的参数,正文就在 effect 中写入该关联;只有记录完全没有导航参数时才读取它,其他类型的参数让记录停留在任务缺失状态。恢复出的身份随后遵循既有的渲染规则:保留的记录包含该任务时即显示详情,判据之后请求的读取仍未给出该任务时显示目录反馈,读取失败时显示带重试的查询失败状态。这样的读取仍无法解析的关联显示未关联页签提示,而不是加载反馈,也绝不产生任务缺失提示,因为该提示的含义是打开方指名了一个任务。
正文以它为当前导航观察到的目录读取请求序号作为判据,并在保留的记录使标签页没有任务、且该序号之后没有读取成功时补发一次读取,该读取未落地期间显示目录反馈。Sidebar 会在不重新挂载正文的情况下对同一标签再次导航,因此以导航的 revision 移动判据;早于判据发出的读取永远不能断言该标签的任务已消失。
## Alternatives considered
**用 `contentId` 作为关联键。** 否决:同一种页面类型的所有标签都记录在同一地址下,且页面去重按 pane 进行,两个 pane 的任务标签会共用一个关联并恢复出同一个任务。
**把导航参数持久化进 Sidebar 布局。** 否决:这会把内容参数放进 [导航决策](../architecture/2026-09-05-sidebar-tab-types-and-navigation.zh.md) 保持为临时的布局快照,且之后每种页面类型都会继承该格式。
**把 kind 声明为 `multiple: true`,让每次打开都铸造唯一 contentId。** 否决:为了取得一个键而改变打开任务的行为——打开第二个任务会新开一个标签,而不是在已有标签中显示;而且它无法恢复改动之前已存在的记录。
**从目录解析任务。** 否决:目录不保存按标签的关联,而打开该标签的 Session 入口在刷新后已不存在。
**复用 client store 的 localStorage 持久化。** 否决:`createSnapshotStore` 的 `persist` 选项把一个 store 的整体值保存在一个键下,并在该 store 创建时回填,因此需要按 Session 各建一个 store;而该关联还必须拒绝复用的 id 并在写入时清理已关闭标签,这些由一份按键文档在一处完成,也不需要正文为其订阅重渲染。
**在关联旁保存任务标题以确认恢复。** 否决:标题属于任务内容,会随编辑变化,而已定论的目录读取本身就能判断绑定的 id 是否可解析。
## Consequences
刷新后的 Session 无需重新打开任务即可恢复每个任务标签的详情和 chip。该关联是窗口内的浏览器状态:其他浏览器、被清除的站点数据,或被布局工具包复制出的标签都没有它,这类记录会说明该页签未关联任务,而不会声称任务已不存在。被丢弃的布局快照可能铸造出关联仍然指名的 tab id;kind 与 contentId 守卫会拒绝其他类型的记录,而同类型记录会因该标签的下一次导航被覆盖,或在任务不再可解析时被删除。
布局不再被采用的 Session 的记录会保留到其下次写入;写入时的清理使每个已采用的 Session 有界,且任何记录都不含内容。
## Testing
单元测试覆盖:导航时写入、仅在无参数时读取、详情与 chip 的恢复、无法解析关联的降级、已提交标签的清理、复用 id 的拒绝、损坏的存储文档,以及存储缺失或拒绝写入的情况。
@@ -2,36 +2,30 @@
# 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/implemented/feature/2026-08-05-durable-web-schedule.md
/agent-note-durable-session-local-reminders:
en: c40fc2bc6dd1b469
zh: c3932a1c8c60579b
/agent-note-durable-session-local-reminders/problem:
en: 75d2d50c48475d22
zh: 293c507e9e516f29
/agent-note-durable-session-local-reminders/decision:
en: 7fd21223d6306a39
zh: b792a415d9dccba0
/agent-note-durable-session-local-reminders/decision/session-log-authority-and-tools:
en: 23a928cba17217d6
zh: 7995b144db6e4c70
/agent-note-durable-session-local-reminders/decision/explicit-absolute-time-boundary:
en: 2e1fe906d42245aa
zh: bd21d4a2dd7d8afc
/agent-note-durable-session-local-reminders/decision/bounded-fixed-rate-semantics:
en: 2ebada22d530b18a
zh: 90e7dd128c8ba54a
/agent-note-durable-session-local-reminders/decision/live-delivery-lifecycle:
en: e69281749cf18500
zh: 9ce38e39e486f2d6
/agent-note-durable-session-local-reminders/decision/read-only-web-catalog:
en: 6ea0fcb2512d1434
zh: ef9e430200677c9f
/agent-note-durable-session-local-reminders/alternatives-considered:
en: d86b5e8b0380c22a
zh: e6b6fce158608b08
/agent-note-durable-session-local-reminders/verification:
en: 716fce79e742ecfa
zh: 6abb1480f1b9e2d9
/agent-note-durable-session-local-reminders/consequences:
en: 7abf5e7d73b32153
zh: c178fb3f1f28dee4
/agent-note-durable-scheduled-reminders:
en: 7d312bc635f68bef
zh: f675a214bfe26dac
/agent-note-durable-scheduled-reminders/problem:
en: 85e09d88c1383aeb
zh: 2d90fcbf2fe5ad3c
/agent-note-durable-scheduled-reminders/decision:
en: a15c7c13f167e5ac
zh: 63bcc2ef60c2f344
/agent-note-durable-scheduled-reminders/decision/explicit-absolute-time-boundary:
en: 16cc539a319f99b4
zh: ae06872529f2227d
/agent-note-durable-scheduled-reminders/decision/bounded-fixed-rate-semantics:
en: 4a12a91087629614
zh: 4f23eee242eeadd3
/agent-note-durable-scheduled-reminders/decision/queue-admission-is-not-completion:
en: 1d88821105254303
zh: 08982a1534d8fd18
/agent-note-durable-scheduled-reminders/alternatives-considered:
en: f6d44a87797410ca
zh: 383ac9e712492c93
/agent-note-durable-scheduled-reminders/verification:
en: 113a6d381ff1b145
zh: 59f9693741adbbab
/agent-note-durable-scheduled-reminders/consequences:
en: 2121b1640094e601
zh: 9420468b80ff1155
@@ -1,93 +1,49 @@
# Agent Note: Durable Session-local reminders
# Agent Note: Durable scheduled reminders
Status: implemented
English | [中文](2026-08-05-durable-web-schedule.zh.md)
The [Host-owned scheduled messages decision](../architecture/2026-09-16-host-schedule-storage.md) owns task storage, timer activation, dispatch, delivery records, and the task catalog. This note owns the two timing decisions that survive it — the explicit absolute-time boundary and bounded fixed-rate arithmetic — and the queue-admission boundary they share.
## Problem
A reminder created inside a conversation must remain attributable to that exact Session and survive a process restart. A process-local timer or inbox item cannot provide that durability, while a global scheduler or private database introduces a second identity, persistence, and lifecycle system.
Busy Agents, long waits, wall-clock changes, cold Sessions, forks, persistence failures, absolute calendar input, and teardown make a simple timeout insufficient. The design must distinguish a durable record from its disposable live wait, keep a fork from inheriting its parent's active reminders, and avoid spreading Schedule-specific presentation or time-zone state across unrelated components.
A reminder created inside a conversation must remain attributable to that exact Session and survive a process restart. Busy Agents, long waits, wall-clock changes, cold Sessions, and persistence failures make a simple timeout insufficient. Absolute calendar input and repeated wake-ups raise two timing questions that storage and activation choices do not answer: what an absolute time means when no recurring rule stores a zone, and how a fixed interval catches up after downtime without replaying every missed occurrence.
## Decision
The [Schedule guide](../../../../docs/user/guide/schedule.md) uses an overlay that explicitly loads `@deepseek-ai/dsh-time-context` and `@deepseek-ai/dsh-schedule`, and enables the Web bundle's otherwise-disabled `ui-schedule` row. The default Web startup graph remains inactive for Schedule. Schedule observes only root Agents published after the plugin loads and installs its three tools plus one disposable owner in that Agent scope. Cold history reads, already-published roots, child Agents, and other hosts do not activate the runtime.
The user-visible boundary is `session-local`: the original Session runs an on-time reminder only while live, does no external notification while cold, and processes an overdue reminder after it becomes live again. Due work waits until the Agent is fully idle, then enters the ordinary next-turn queue through `followup()`; it never steers the current turn and has no independent Web receipt ([conversational delivery](../../archived/simplification/2026-08-09-conversational-schedule-delivery.md)).
| Scenario | Durable fact | Live behavior | User-visible result |
| --- | --- | --- | --- |
| Create and manage | `schedule/change` create/delete in the original Session | Agent-scoped tools checkpoint before reads and after mutations | Stable id, UTC target, state, and `session-local` disclosure |
| Due while busy | Active create remains in the fold | Owner waits for idle maintenance, queues one follow-up, then appends dispatch | A later ordinary conversation turn |
| Several Every records are overdue | Each active record retains its earliest unaccepted anchor-aligned target | One decision selects each record's latest occurrence and advances it past now | One ordinary follow-up containing one occurrence per record |
| Process stopped or Session cold | Active create remains persisted | No timer or background scan; resume rebuilds the owner | Future target waits; overdue target is attempted |
| Fork | Parent events remain in the inherited prefix | Child fold starts at the exact `inheritedEventCount` | Parent work does not become active in the child |
### Session-log authority and tools
The version-1 `schedule/change` stream is the only durable Schedule authority. A create record owns a Session-local, non-reused branded id, the trimmed prompt, its rule discriminator, and UTC target. Delete and one-shot dispatch are terminal transitions. Every dispatch stores its id and decision time so the fold advances that record directly past missed occurrences. The strict decoder and pure fold reject unknown versions, extra fields, reused ids, mismatched dispatch shapes, and transitions against inactive records. A normal Session folds its complete stream; a fork folds only events at or after the `inheritedEventCount` passed into projection initialization.
When `ctx.sessionProjections` exists, Schedule registers a strict unit that uses the same transition and publishes the complete active `ScheduleRecord[]`; the shared [projection state decision](../../archived/architecture/2026-08-19-session-projection-state-and-client-views.md) owns its initialization and restore contract. Corrupt durable input fails the existing read path rather than yielding a partial array. The browser-safe record vocabulary is exposed through the type-only `@deepseek-ai/dsh-schedule/client` subpath.
The current rule union accepts a non-empty prompt and exactly one selector. `after_seconds` is a positive safe-integer delay whose record is `{ id, kind: 'after', prompt, afterSeconds, scheduledAt }`. `at` is either strict RFC 3339 with `Z` or a numeric offset, or structured `{ date, time, time_zone }` with an explicit zone; its record is `{ id, kind: 'at', prompt, scheduledAt }`. `every_seconds` is a safe integer of at least 300 whose `{ id, kind: 'every', prompt, everySeconds, scheduledAt }` record stays aligned to its creation-plus-interval sequence. One-shot dispatch stores only the id; Every dispatch stores `id + acceptedAt`. Tool values derive `scheduled` or `overdue` and include `deliveryMode: 'session-local'`.
An Agent-scoped FIFO serializes management transactions and the live owner's due transaction from preflight through post-append barriers. Every tool read first awaits `ctx.sessions.flush(session)`. Create rejects input-shape failures before the FIFO when possible, preflights, allocates an id, appends, and checkpoints again. Delete validates its id before the FIFO, preflights before deciding whether it is active, and checkpoints again only after append. List and not-found delete never answer from an unconfirmed live suffix. Failed barriers return `persistence_uncertain` rather than guessing whether an eager write committed.
Every successful management preflight asks the live owner to recompute. A later list can therefore confirm a retained create after a previous post-append rejection and arm it without a private persistence-retry timer.
### Explicit absolute-time boundary
Natural-language interpretation and Schedule parsing are deliberately separate ([time-zone simplification](../simplification/2026-08-09-explicit-schedule-time-zone.md)). Each browser prompt carries its Host-validated IANA zone only on that durable user message. Time-context tells the model to assume that zone for otherwise-unqualified dates and times. Schedule neither imports that plugin nor stores a Session zone: the model must turn its interpretation into an offset-bearing RFC 3339 value or a local object with explicit `time_zone`.
Schedule validates exact calendar shapes, offsets, zone names, and a strictly future four-digit-year instant. A local time inside a daylight-saving gap is rejected; an overlap chooses its first, earlier instant. A successful create stores only canonical UTC `scheduledAt`, not the original offset, local fields, or zone.
Schedule validates exact calendar shapes, offsets, zone names, and a strictly future four-digit-year instant. A structured local time inside a daylight-saving gap is rejected; an overlap chooses its first, earlier instant. A one-time `at` or `after` record stores only the canonical UTC `scheduledAt`, not the input offset, local fields, or zone. A `daily`, `weekly`, or `cron` record instead stores its normalized local rule with an explicit `timeZone` ([timing selectors](../../../../packages/schedule/schedule/README.md)).
### Bounded fixed-rate semantics
Every is a fixed-duration interval, not a calendar rule. The first target is creation time plus the interval. At a due decision, integer division selects the latest sequence point at or before the sampled wall clock and the first sequence point after it. The selected occurrence is presented once and the record advances directly to the future target, so a cold Session never accumulates a replay backlog and delayed model work never shifts the sequence.
Every is a fixed-duration interval, not a calendar rule. Its first target is creation time plus the interval. At a due decision, integer division selects the latest sequence point at or before the sampled wall clock and the first sequence point after it. The selected occurrence is presented once and the record advances directly to the future target, so a cold Session never accumulates a replay backlog and delayed model work never shifts the sequence. Every, Daily, Weekly, and Cron records overdue for one Session share one message, and each record contributes only its latest occurrence ([storage and dispatch](../architecture/2026-09-16-host-schedule-storage.md)).
All distinct overdue Every records participate in one batch, each with one latest occurrence and one shared `acceptedAt`. There is no cross-record cooldown, gate, quota, or retained batch timestamp. A five-minute minimum bounds wake and model-request frequency. If the next sequence point would exceed the four-digit-year storage range, dispatch terminates that record.
A five-minute minimum bounds wake and model-request frequency. There is no cross-record cooldown, gate, quota, or retained batch timestamp. If the next sequence point would exceed the four-digit-year storage range, dispatch terminates that record.
Calendar and Cron expressions are deliberately absent ([bounded recurrence simplification](../../archived/simplification/2026-08-09-bounded-fixed-rate-schedule.md)); supporting them would add a time-zone-sensitive calendar language, evaluator dependency, validation surface, and tzdata replay policy unrelated to fixed-rate reminders.
### Queue admission is not completion
### Live delivery lifecycle
The Agent-scoped owner derives its earliest target from the durable fold. Long targets use bounded timer segments, and every wake reads the wall clock again, so a rollback cannot fire early and a forward jump becomes overdue. Due one-shots have priority and are admitted one at a time; otherwise every overdue Every record enters one batch in target and creation order. If a turn or maintenance task owns the Agent, `runMaintenance()` rejects the claim; the records stay active and one `whenIdle()` wait triggers another attempt. A rejected preflight or contained framing/enqueue failure also leaves them active without starting a private retry timer.
The accepted path clears pending persistence and claims the true idle phase. It refolds the exact Session suffix, samples the decision clock, constructs fixed reminder framing with JSON-escaped values, synchronously queues one `followup()`, and appends dispatch before releasing maintenance. A one-shot appends an id-only terminal dispatch. A fixed-rate batch appends one `id + acceptedAt` transition per participating record. Waking input remains parked until release, so the message cannot be claimed before dispatch enters the log; afterward the owner checkpoints dispatch.
Dispatch records queue admission, not model completion or user receipt. Framing or synchronous enqueue failure appends no dispatch. An append failure faults that owner because the message may already be queued. Agent or plugin disposal cancels timers, stops new work, unwinds tool registrations, and awaits in-flight work without deleting durable records. A crash after follow-up admission but before durable dispatch can repeat the reminder after recovery; the design makes no exactly-once promise.
### Read-only Web catalog
The Schedule overlay enables the otherwise-disabled [`dsh-client-ui-schedule`](../../../../packages/client/ui-schedule/README.md) client together with the Host service. The complete active projection also feeds [`dsh-client-ui-workspace`](../../../../packages/client/ui-workspace/README.md). This note owns that opt-in read-only presentation boundary: the projection is current active state, not a dispatch or delivery receipt, so ordinary Assistant turns remain the delivery presentation. The catalog is a fixed `document.body` portal whose left edge follows the trigger when space permits and shifts left to retain a 16px viewport margin near the right edge. `useAnchoredPosition` owns measurement and resize, captured-scroll, panel-resize, and cleanup behavior; Schedule supplies the trigger and portal refs, bottom placement, a 5px gap, and the existing inside/outside dismissal boundary without adding a general popover abstraction.
Dispatch records queue admission, not model completion or user receipt. Framing or synchronous enqueue failure appends no dispatch. A crash after follow-up admission but before the durable receipt can repeat the reminder after recovery; the design makes no exactly-once promise.
## Alternatives considered
**Use `ctx.jobs`.** Jobs own process-local work, outcomes, and notifications rather than Session-log state and conversation follow-ups.
**Store reminders in a private database or global scheduler.** This could run cold Sessions but requires a second identity map, startup scan, ownership lease, crash protocol, and notification policy.
**Use `ctx.jobs`.** Jobs own process-local work, outcomes, and notifications rather than durable task state and conversation follow-ups.
**Persist a Session time zone and infer local `at`.** This spreads one interpretive default through Session core, Host create/fork, persistence formats, clients, and mismatch recovery. Request-local model guidance plus an explicit tool boundary deletes that coupling.
**Keep an independent durable Web receipt.** Dispatch is an internal queue fact, not the user's reminder. Rendering the ordinary assistant answer avoids a second delivery meaning and removes Schedule code from Host and client layers.
**Claim dispatch before `followup()` or add exactly-once fencing.** Claim-first can silently lose a reminder when enqueue fails. Cross-process exactly-once needs a lease, outbox, acknowledgement, and downstream idempotency boundary outside this design.
**Add a general recurring-rule engine.** Fixed-duration intervals need only anchor arithmetic. A shared recurrence abstraction, global admission gate, and calendar evaluator would enlarge replay and runtime state without serving the retained product behavior.
**Claim dispatch before `followup()` or add exactly-once fencing.** Claim-first can silently lose a reminder when enqueue fails. Cross-process exactly-once needs a lease, outbox, acknowledgement, and downstream idempotency boundary outside this Session-local scope.
**Adopt existing roots or register global tools.** Late adoption makes plugin load order activate unseen timers and exposes tools outside the supported root composition.
**Add a general recurring-rule engine for fixed intervals.** Fixed-duration intervals need only anchor arithmetic. A shared recurrence abstraction, global admission gate, and calendar evaluator would enlarge replay and runtime state without serving fixed-rate behavior. The cron selector is a separate wall-clock rule with its own Host-owned dialect and evaluation ([cron input](../../../../docs/subsystems/schedule.md#cron-wall-clock-input)).
## Verification
Package tests pin strict replay, one-shot and Every transitions, creation-anchor arithmetic, latest-only catch-up, multi-record batching, fork suffixes, id reuse, offset and local-calendar profiles, IANA validation, daylight-saving gaps and overlaps, time bounds, timer segmentation, wall-clock movement, overdue admission, fixed framing, enqueue and append failures, barrier recovery, projection registration and restoration, registration rollback, and quiescent disposal at per-file 100% coverage. A property test compares Every calculation and replay across varied intervals and skipped spans. A production JSONL restart test proves one overdue reminder dispatches through the real Agent lifecycle and does not redispatch after another restart. Focused client suites own catalog and sidebar behavior, including the body portal, spacious left alignment, portal-inside pointer handling, outside dismissal, Escape, live empty, and timer cleanup. The shared primitive suite owns the positioning hook's resize, captured-scroll, panel-resize, and cleanup lifecycle. Keyless assembled Web scenarios retain ordinary After/At/Every delivery evidence plus one 900×900 Schedule-catalog smoke for overlay reachability, fixed portal placement, right-edge clamping, width and overflow, ordinary/search alarms, narrow dark layout, a light-theme browser screenshot, and one live empty update.
Package tests pin fixed-rate arithmetic, creation anchors, latest-only catch-up, batch selection, IANA validation, daylight-saving gaps and overlaps, and time bounds at per-file 100% coverage. A property test compares Every calculation and replay across varied intervals and skipped spans.
## Consequences
- Reminder state survives restart through ordinary Session persistence without a new database or public service.
- Cold Sessions do no work and send no external notification; reopening one may deliver overdue work.
- Absolute input is deterministic without persistent Session-zone state or a dependency from Schedule to time-context.
- Users see normal conversation output; dispatch never overstates model success or acknowledgement.
- Opt-in Web users can inspect the complete active set and recognize cache-known active Sessions in ordinary or search rows without creating a second durable state, runtime signal, or delivery meaning.
- Each live root adds only fold-derived timers, an optional idle wait, and one in-flight operation.
- Fixed-rate recurrence is bounded by a five-minute minimum, latest-only catch-up, and one batched occurrence per overdue record; calendar recurrence remains outside this product boundary.
- A fixed interval stays aligned to its creation anchor, presents one occurrence per overdue record, and cannot accumulate a replay backlog.
- Delivery never overstates model success or acknowledgment, and a crash between queue admission and the durable receipt can repeat a message.
@@ -1,93 +1,49 @@
# Agent Note: 持久、仅限 Session 内的提醒
# Agent Note: 持久定时提醒
Status: implemented
[English](2026-08-05-durable-web-schedule.md) | 中文
[Host 拥有的定时消息决策](../architecture/2026-09-16-host-schedule-storage.zh.md)拥有任务存储、timer 激活、投递、发送记录和任务目录。本记录拥有在该决策之下仍然成立的两个时间决策——显式绝对时间边界与有界固定速率计算——以及二者共有的队列准入边界。
## 问题
在对话中创建的提醒必须始终归属于确切的那个 Session,并且跨进程重启存活。进程本地 timer 或 inbox 项无法提供这种持久性,而全局 scheduler 或私有数据库又会引入第二套身份、持久化和生命周期系统。
繁忙的 Agent(智能体)、长等待、墙钟变化、cold Session、fork、持久化失败、绝对日历输入和资源释放,使简单 timeout 无法满足要求。设计必须区分持久记录与可丢弃的 live wait,阻止 fork 继承父 Session 的活动提醒,并避免把 Schedule 专属的呈现或时区状态扩散到无关组件。
在对话中创建的提醒必须始终归属于确切的那个 Session,并且跨进程重启存活。繁忙的 Agent(智能体)、长等待、墙钟变化、cold Session 和持久化失败使简单 timeout 无法满足要求。绝对日历输入与重复唤醒带来两个计时问题,而存储与激活方式的选择并不能回答它们:当重复规则不保存时区时,绝对时间意味着什么;固定间隔在停机后如何追赶,而不重放每一个错过的发生时点。
## 决策
[Schedule 指南](../../../../docs/user/guide/schedule.zh.md)使用显式加载 `@deepseek-ai/dsh-time-context` 与 `@deepseek-ai/dsh-schedule`,并启用 Web bundle 中默认 disabled 的 `ui-schedule` row 的 overlay。默认 Web 启动图不会激活 Schedule。Schedule 只观察插件加载后发布的根 Agent,并在该 Agent scope 中安装三个工具和一个可丢弃 owner。cold history 读取、已发布的根、child Agent 与其他 host 都不会激活 runtime。
用户可见边界是 `session-local`:原 Session 只有在 live 时才会准时运行提醒,cold 期间不发送任何外部通知;该 Session 再次 live 后才会处理 overdue 提醒。到期工作会等待 Agent 完全 idle,再通过 `followup()` 进入普通的下一轮队列;它绝不会中途引导当前轮次,也没有独立 Web 回执([对话式交付](../../archived/simplification/2026-08-09-conversational-schedule-delivery.md))。
| 场景 | 持久事实 | live 行为 | 用户可见结果 |
| --- | --- | --- | --- |
| 创建与管理 | 原 Session 中的 `schedule/change` create/delete | Agent-scoped 工具在读取前、变更后执行 checkpoint | 稳定 id、UTC 目标、状态与 `session-local` 说明 |
| 到期时繁忙 | 活动 create 仍在 fold 中 | owner 等待 idle maintenance,排入一个 follow-up,再追加 dispatch | 后续一个普通对话轮次 |
| 多条 Every 记录逾期 | 每条活动记录都保留最早一个尚未接受且与锚点对齐的目标 | 一次决策选择每条记录的最新发生时点,并将其推进到当前时刻之后 | 一个普通 follow-up,其中每条记录各有一个发生时点 |
| 进程停止或 Session cold | 活动 create 仍在 persistence 中 | 不存在 timer 或后台扫描;resume 重建 owner | 未来目标继续等待;overdue 目标会被尝试 |
| fork | 父 event 留在继承前缀 | child fold 从精确 `inheritedEventCount` 开始 | 父工作不会在 child 中变为活动状态 |
### Session 日志权威与工具
版本 1 `schedule/change` stream 是唯一持久的 Schedule 权威。create 记录拥有一个 Session 内不复用的品牌 id、trim 后的提示词、规则判别字段和 UTC 目标。delete 与一次性 dispatch 是终结转换。Every dispatch 会存储 id 与决策时点,使 fold 将该记录直接推进到错过的发生时点之后。严格 decoder 与纯 fold 会拒绝未知版本、额外字段、重复使用的 id、形状不匹配的 dispatch,以及针对非活动记录的转换。普通 Session 折叠完整 stream;fork 只折叠传入 projection 初始化的 `inheritedEventCount` 位置及其后的 event。
`ctx.sessionProjections` 存在时,Schedule 会注册一个复用同一 transition 的严格单元,并发布完整的活动 `ScheduleRecord[]`;共享的 [projection state 决策](../../archived/architecture/2026-08-19-session-projection-state-and-client-views.md)拥有其初始化与 restore 约定。损坏的持久输入会使既有读取路径失败,而不会产生部分数组。浏览器安全的记录词汇通过纯类型子路径 `@deepseek-ai/dsh-schedule/client` 暴露。
当前规则 union 接受非空提示词和恰好一个 selector。`after_seconds` 是正的安全整数 delay,其记录为 `{ id, kind: 'after', prompt, afterSeconds, scheduledAt }`。`at` 可以是带 `Z` 或数值偏移量且严格符合 RFC 3339 的值,也可以是带显式时区的结构化 `{ date, time, time_zone }`;其记录为 `{ id, kind: 'at', prompt, scheduledAt }`。`every_seconds` 是不小于 300 的安全整数,其 `{ id, kind: 'every', prompt, everySeconds, scheduledAt }` 记录始终与从创建时刻加一个间隔开始的序列对齐。一次性 dispatch 只存储 id;Every dispatch 存储 `id + acceptedAt`。工具值派生 `scheduled` 或 `overdue`,并包含 `deliveryMode: 'session-local'`。
一个 Agent-scoped FIFO 会将管理事务与 live owner 的到期事务从 preflight 到 post-append barrier 全程串行化。每项工具读取都会先等待 `ctx.sessions.flush(session)`。create 会尽可能在进入 FIFO 前拒绝输入形状错误,随后执行 preflight、分配 id、追加记录并再次 checkpoint。delete 会在进入 FIFO 前验证 id,在判断其是否活动前执行 preflight,并且只在追加后再次 checkpoint。list 与 not-found delete 绝不会根据未经确认的 live 后缀作答。barrier 失败会返回 `persistence_uncertain`,而不是猜测 eager write 是否已经提交。
每次成功的管理 preflight 也会要求 live owner 重新计算。因此,如果先前的 post-append 被拒绝,后续 list 可以确认保留的 create 并将其 arm,而无需私有的 persistence 重试 timer。
### 显式绝对时间边界
自然语言解释与 Schedule 解析被有意分开([时区简化](../simplification/2026-08-09-explicit-schedule-time-zone.zh.md))。每条浏览器提示词只在其对应的持久 user message 上携带由 Host 校验过的 IANA 时区。Time-context 会告诉模型,把未明确限定时区的日期和时间解释为该时区。Schedule 既不导入该插件,也不存储 Session 时区:模型必须把其解释结果转换为带偏移量的 RFC 3339 值,或带显式 `time_zone` 的本地对象。
Schedule 会校验精确的日历形状、偏移量、时区名称,以及一个严格位于未来、年份为四位数的时点。落在夏令时缺口内的本地时间会被拒绝;遇到重叠时会选择第一次出现的较早时点。创建成功后只存储规范化后的 UTC `scheduledAt`,不会存储原始偏移量、本地字段或时区。
Schedule 会校验精确的日历形状、偏移量、时区名称,以及严格位于未来、年份为四位数的时点。结构化本地时间落在夏令时缺口内会被拒绝;遇到重叠时选择第一次出现的较早时点。一次性 `at` 或 `after` 记录只存储规范化的 UTC `scheduledAt`,不存储输入的偏移量、本地字段或时区。`daily`、`weekly` 或 `cron` 记录则存储规范化后的本地规则及其显式 `timeZone`([时间选择器](../../../../packages/schedule/schedule/README.zh.md))。
### 有界固定速率语义
Every 是固定时长间隔,而不是日历规则。第一个目标是创建时刻加上一个间隔。作出到期决策时,整数除法会选出不晚于所采样墙钟的最新序列点,以及其后的第一个序列点。选中的发生时点只呈现一次,记录会直接推进到未来目标,因此 cold Session 绝不会积累回放任务,延迟执行的模型工作也绝不会使该序列漂移。
Every 是固定时长间隔,而不是日历规则。其首个目标是创建时刻加上该间隔。作出到期决策时,整数除法选出不晚于所采样墙钟的最新序列点,以及其后的第一个序列点。选中的发生时点只呈现一次,记录直接推进到未来目标,因此 cold Session 绝不会积累回放任务,延迟执行的模型工作也绝不会使该序列漂移。同一 Session 内到期的 Every、Daily、Weekly 和 Cron 记录共用一条消息,每条记录只贡献其最新一次发生时点([存储与投递](../architecture/2026-09-16-host-schedule-storage.zh.md))。
所有不同的逾期 Every 记录都会参与同一个批次,每条记录各自提供一个最新发生时点,并共享同一个 `acceptedAt`。系统不存在跨记录的冷却、门控、配额或保留的批次时间戳。至少 5 分钟的限制约束了唤醒与模型请求频率。如果下一个序列点会超出四位年份存储范围,dispatch 会终结该记录。
至少五分钟的下限约束唤醒与模型请求频率。系统不存在跨记录的冷却、门控、配额或保留的批次时间戳。如果下一个序列点会超出四位年份存储范围,dispatch 会终结该记录。
日历表达式与 Cron 表达式被有意排除([有界周期性简化](../../archived/simplification/2026-08-09-bounded-fixed-rate-schedule.md));支持这些表达式需要增加时区敏感的日历语言、求值器依赖、校验范围和 tzdata 回放策略,而这些都与固定速率提醒无关。
### 队列准入不是完成
### Live 交付生命周期
Agent-scoped owner 从持久 fold 派生最早目标。超长目标使用有界 timer 分段,每次 wake 都会重新读取墙钟,因此回拨不会提前触发,前跳则会形成 overdue。已到期的一次性提醒优先,每次准入一条;否则,所有逾期 Every 记录会按目标时间和创建顺序进入同一个批次。如果 Agent 已被某个轮次或另一项 maintenance task 占用,`runMaintenance()` 会拒绝此次认领;这些记录保持活动,并由一次 `whenIdle()` wait 触发另一次尝试。被拒绝的 preflight 或被收容的 framing/入队失败同样会使其保持活动,但不会启动私有重试 timer。
获得准入的路径会刷新所有 pending persistence 并认领真正的 idle phase。它会重新折叠确切的 Session 后缀、采样 decision clock、用经过 JSON 转义的值构造固定提醒 framing、同步排入一个 `followup()`,并在释放 maintenance 前追加 dispatch。一次性提醒会追加只含 id 的终结 dispatch。固定速率批次会为每条参与记录追加一个 `id + acceptedAt` 转换。触发唤醒的 input 会保持 parked,直到 maintenance 释放,因此在 dispatch 进入日志前,消息不会被认领;随后 owner 会为 dispatch 执行 checkpoint。
dispatch 记录的是队列准入,而不是模型完成或用户收到提醒。framing 构造或同步入队失败不会追加 dispatch。append 失败会使该 owner fault,因为消息可能已经入队。Agent 或插件 dispose 会取消 timer、停止新工作、撤销工具注册,并等待进行中的工作,且不会删除持久记录。follow-up 获得准入后、持久 dispatch 前发生崩溃,可能使提醒在恢复后重复;本设计不作 exactly-once 承诺。
### 只读 Web 目录
Schedule overlay 会把默认禁用的 [`dsh-client-ui-schedule`](../../../../packages/client/ui-schedule/README.zh.md) client 与 Host 服务一同启用。完整活动 projection 也会交给 [`dsh-client-ui-workspace`](../../../../packages/client/ui-workspace/README.zh.md)。本 Note 拥有这条 opt-in 只读呈现边界:该 projection 表示当前活动状态,而非 dispatch 或交付回执,因此普通 Assistant 轮次仍是交付呈现。目录是挂到 `document.body` 的 fixed portal;空间足够时左边缘跟随触发按钮,靠近视口右侧时向左避让并保留 16px 边距。`useAnchoredPosition` 拥有测量以及 resize、捕获阶段 scroll、面板 resize 与清理行为;Schedule 提供触发器与 portal ref、bottom 放置、5px 间距和既有内外 dismissal 边界,不增加通用 popover 抽象。
dispatch 记录的是队列准入,而不是模型完成或用户收到提醒。framing 构造或同步入队失败不会追加 dispatch。follow-up 获得准入后、持久回执前发生崩溃,可能使提醒在恢复后重复;本设计不作 exactly-once 承诺。
## 已考虑的替代方案
**使用 `ctx.jobs`。** Task 拥有进程本地工作、结果和通知,而不是 Session 日志状态和对话 follow-up。
**把提醒存入私有数据库或全局 scheduler。** 这样可以运行 cold Session,却需要第二套身份映射、启动扫描、ownership lease、崩溃协议和通知策略。
**使用 `ctx.jobs`。** Task 拥有进程本地工作、结果和通知,而不是持久任务状态和对话 follow-up。
**持久化 Session 时区并推断本地 `at`。** 这会让一个解释默认值扩散到 Session core、Host create/fork、持久化格式、client 和不匹配恢复中。请求本地的模型指导与显式工具边界消除了这种耦合。
**保留独立的持久 Web 回执。** dispatch 是内部队列事实,而不是用户的提醒。渲染普通 assistant 回答既避免了第二种交付含义,也从 Host 与 client 层移除了 Schedule 代码。
**在 `followup()` 前认领 dispatch,或增加 exactly-once fencing。** claim-first 会在入队失败时静默丢失提醒。跨进程 exactly-once 需要 lease、outbox、acknowledgement 与下游幂等边界,超出了本设计范围。
**增加通用周期规则引擎。** 固定时长间隔只需要锚点运算。共享的周期抽象、全局准入门控和日历求值器会扩大回放与运行时状态,却不能服务于保留的产品行为。
**在 `followup()` 前认领 dispatch,或增加 exactly-once fencing。** claim-first 会在入队失败时静默丢失提醒。跨进程 exactly-once 需要 lease、outbox、acknowledgement 与下游幂等边界,超出了此 Session-local 范围。
**接管既有根或注册全局工具。** 晚接管会让插件加载顺序激活不可见的 timer,并把工具暴露到受支持的根组合之外。
**为固定间隔增加通用周期规则引擎。** 固定时长间隔只需要锚点运算。共享的周期抽象、全局准入门控和日历求值器会扩大回放与运行时状态,却不能服务于固定速率行为。cron 选择器是独立的钟表规则,拥有自己的 Host 所有方言与求值([cron 输入](../../../../docs/subsystems/schedule.zh.md#cron-wall-clock-input))。
## 验证
包测试以逐文件 100% coverage 固定严格回放、一次性与 Every 状态转换、创建锚点运算、只追赶最新一次、多记录批处理、fork 后缀、id 复用、偏移量与本地日历 profile、IANA 校验、夏令时缺口与重叠、时间边界、timer 分段、墙钟变化、overdue 准入、固定 framing、入队与 append 失败、barrier 恢复、projection 注册与恢复、注册 rollback 和完全停稳的 dispose。属性测试会在不同间隔与跳过跨度下比较 Every 计算与回放。production JSONL restart 测试证明一条 overdue 提醒会经过真实 Agent 生命周期 dispatch,并且再次 restart 后不会重复 dispatch。聚焦 client suite 拥有目录与侧边栏行为,包括 body portal、空间充足时的左对齐、portal 内指针处理、外部 dismissal、Escape、live empty 与 timer 清理。共享 primitive suite 拥有定位 hook 的 resize、捕获阶段 scroll、面板 resize 与清理生命周期。无密钥组装 Web 场景保留普通 After/At/Every 交付证据,再由一个 900×900 Schedule 目录 smoke 覆盖 overlay 可达性、fixed portal 定位、右侧钳制、宽度与 overflow、普通/搜索闹钟、窄屏暗色布局、浅色主题浏览器截图与一次 live empty 更新。
包测试以逐文件 100% coverage 固定固定速率计算、创建锚点、只追赶最新一次、批次选择、IANA 校验、夏令时缺口与重叠以及时间边界。属性测试会在不同间隔与跳过跨度下比较 Every 计算与回放。
## 后果
- 提醒状态通过普通 Session persistence 跨重启存活,无需新数据库或公开 service。
- cold Session 不工作、不发送外部通知;重新打开后可能交付 overdue 工作。
- 无需持久 Session 时区状态或从 Schedule 到 time-context 的依赖,绝对时间输入仍然具有确定性。
- 用户看到普通对话输出;dispatch 绝不会夸大模型成功或 acknowledgement。
- 显式启用 Schedule 的 Web 用户可以查看完整活动集合,并在普通行或搜索结果中辨认 cache 已知的活动 Session,而不会引入第二份持久状态、runtime 信号或第二种交付含义。
- 每个 live 根只增加从 fold 派生的 timer、可选 idle wait 与一个 in-flight operation。
- 固定速率周期性受到至少 5 分钟、只追赶最新一次,以及每条逾期记录只在一个批次中贡献一个发生时点的约束;日历周期性仍在此产品边界之外。
- 固定间隔始终与其创建锚点对齐,每条逾期记录只呈现一个发生时点,且不会积累回放任务。
- 投递绝不会夸大模型成功或 acknowledgement;队列准入与持久回执之间发生崩溃可能使消息重复。
@@ -6,8 +6,8 @@
en: 7b69ac848815886a
zh: db7a9f4000fe5359
/agent-note-in-history-system-prompt-replacement-for-cache-stable-prompt-changes/problem:
en: 03d0dad3061794af
zh: 4dbef64cd45c4064
en: 87965b88be0e831f
zh: d96767a2cef52269
/agent-note-in-history-system-prompt-replacement-for-cache-stable-prompt-changes/decision:
en: da6d3f0677f0343b
zh: 8b5dcf7e1108edc9
@@ -15,8 +15,8 @@
en: 2f23fa55e1ce652c
zh: 335695fc654ab1db
/agent-note-in-history-system-prompt-replacement-for-cache-stable-prompt-changes/decision/the-decision-rule:
en: 3734096cc35b0c86
zh: 7b6f4ceaae602c87
en: 554d01427ebebee1
zh: 664c9bf0a9337a04
/agent-note-in-history-system-prompt-replacement-for-cache-stable-prompt-changes/decision/presentation-and-accounting:
en: 00a91b4fb592e1e4
zh: dac2e4f0a3ee9ecf
@@ -24,11 +24,11 @@
en: c21a3e7a773860e5
zh: 566669e245ccdbaf
/agent-note-in-history-system-prompt-replacement-for-cache-stable-prompt-changes/alternatives-considered:
en: bb8f951694de2aea
zh: d53960bf0ec052c7
en: f3dec242b2550529
zh: 0397fa07cc1806bb
/agent-note-in-history-system-prompt-replacement-for-cache-stable-prompt-changes/consequences:
en: beda064bcd194a2a
zh: 8a3bb81d5edcd60a
/agent-note-in-history-system-prompt-replacement-for-cache-stable-prompt-changes/testing:
en: 4b09d253309f2d4d
zh: f897ffad3a1b2fbe
en: 3f9750d67404ac92
zh: 80cb9aced02e2950
@@ -8,7 +8,7 @@ English | [中文](2026-09-02-in-history-system-prompt-replacement.zh.md)
Every system prompt change costs the whole provider prefix cache. The loop renders the prompt on every step; when the bytes differ — a plan-mode section entering or leaving, a skill or tool guidance section registering, an agent-scoped persona shadow, a changed `{{model}}` variable — the request's message 0 changes and the DeepSeek context cache misses from the first token. Long agentic sessions pay this repeatedly, and the [runtime-context snapshot design](../../archived/feature/2026-07-30-current-sandbox-policy-context.md) exists precisely because moving a changing fact out of the prompt was the only way to keep the prefix stable.
A DeepSeek model, recorded here as a model fact supplied for this work, removes that constraint: it accepts a `system` message at any position of the conversation and treats the latest one as the complete effective system prompt, replacing the leading one. Tool schemas remain part of the cached prefix, so a tool-set change still invalidates the cache. With that model the harness can append the new prompt after the cached history instead of rewriting message 0, and the prefix stays warm.
A DeepSeek model, recorded here as a model fact supplied for this work, removes that constraint: it accepts a `system` message at any position of the conversation and treats the latest one as the complete effective system prompt, replacing the leading one. Tool schemas remain part of the cached prefix; [dynamic tool updates](../architecture/2026-09-20-dynamic-tool-updates.md) can defer additions, while changed retained definitions still invalidate reuse. With that model the harness can append the new prompt after the cached history instead of rewriting message 0, and the prefix stays warm.
The harness has the representation for this because the [system prompt is surface node 0](../architecture/2026-09-02-system-prompt-as-surface-node.md): a prompt change is an operation on `system/message` surface nodes, and the choice between "replace the latest system node" and "append a new node" is a per-route decision.
@@ -33,7 +33,7 @@ The loop records the mode in the session: `RequestContext.systemPromptUpdate` jo
| `in-history` | non-empty rendering, a new series starts | log empty replacements for non-empty later system nodes, then rewrite the first system node if needed, even when the latest effective text is unchanged |
| any | the rendered prompt is empty | log empty replacements for non-empty later system nodes, then empty the head if needed; no prompt version remains in derived messages |
`startsSeries` is true when the `agent/pre-step` decision declares `startsRequestSeries`, when the surface replace generation moved since the last request (a compaction or any other replacement), or when the visible tool-schema set changed. A provider or model swap alone is not a series start for this rule: on a capable destination route the changed prompt is appended, which costs nothing because the route change already misses the cache. A series start already costs the cache, so consolidation keeps only the current prompt in model history. Logged per-node empty replacements remove later prompts from derived messages without a surface delete operation or any replacement of intervening conversation nodes. This also keeps compaction recovery from appending a system update after users already admitted by the failed attempt.
`startsSeries` is true when the `agent/pre-step` decision declares `startsRequestSeries`, when the surface replace generation moved since the last request (a compaction or any other replacement), or when the visible tool-schema set changed on a route without `toolUpdate` support. Supported tool additions can accompany a prompt append without starting a series. A provider or model swap alone is not a series start for this rule: on a capable destination route the changed prompt is appended, which costs nothing because the route change already misses the cache. A series start already costs the cache, so consolidation keeps only the current prompt in model history. Logged per-node empty replacements remove later prompts from derived messages without a surface delete operation or any replacement of intervening conversation nodes. This also keeps compaction recovery from appending a system update after users already admitted by the failed attempt.
The first attempt admits the prompt after assembly, an accepted `agent/pre-step` decision, `step/start`, the `agent/request` waterfall, and `prepareCall()`. A rejected or empty first input opens no step. Neither async request phase commits the pending system prompt or accepted users, and cancellation during either commits neither. Every attempt synchronously reconciles the same rendered assembly after its own `agent/request` and `prepareCall()`, appends the accepted user batch only on the first attempt, logs header/context as needed, and derives and freezes the request before streaming through the same prepared call. Retries do not repeat assembly, `agent/pre-step`, or user admission. Reconciliation sees both pre-step compaction (`compaction-basic` with `auto: true`) and recovery compaction, and consolidates non-empty prompt text at the head when either starts a new series. Resume is series-continuing — the `resume` header is not a series start — so a prompt that changed across a restart is appended; the provider cache may still be warm across a process boundary.
@@ -69,7 +69,7 @@ Chat and Trajectory interpret the effective prompt through the pure `uiConversat
**Use the preceding request context for admission.** It describes the previous call, not the adapter bound after request middleware. It can select the wrong prompt representation on the first call, after resume, or after a route or capability change. Resolving before prompt and user commits also keeps cancellation from admitting unsent content. Rejected.
**Treat a provider or model swap as a series start.** It would fold the prompt into node 0 on every route change, matching the tools case. The header already records the change and the cache misses either way, so the extra rule bought nothing but a special case in the loop. Rejected.
**Treat a provider or model swap as a series start.** It would fold the prompt into node 0 on every route change, matching a tool change on an incapable route. The header already records the change and the cache misses either way, so the extra rule bought nothing but a special case in the loop. Rejected.
**Clear only the latest system node.** Empty nodes project to no message, so an older prompt would become effective again. Clearing all active versions preserves the meaning of an empty rendering without deleting conversation history. Rejected.
@@ -89,7 +89,7 @@ Lifecycle verification requires no event for an unchanged prompt and an appended
- `packages/core/agent-loop/tests/system-prompt-admission.spec.ts` covers capable-to-incapable routing with changed or unchanged text, incapable-to-capable routing, resumed-route admission, cancellation in request middleware or preparation, and a concurrent selection change while the prepared route stays bound. Retry-compaction cases shadow the latest prompt with or without an earlier surviving update and verify reuse of the admitted assembly, one user admission, and no extra series header on an unchanged retry. Clear cases on capable and incapable routes remove three active prompt versions, keep repeated requests and seeded resume empty without extra prompt events, and restore only the new text; log reconstruction and the pi converter retain no old instructions. Focused coverage of `src/agent.ts` and `src/runtime-context.ts` reaches 100% for statements, branches, functions, and lines.
- `packages/core/agent-loop/tests/system-prompt-projection.spec.ts` pins the append on a continuing series, the re-baseline at a series start with or without surviving later nodes and with changed or unchanged effective text, the empty-prompt clearing of all active versions, and the replace-only behaviour without the capability.
- `packages/core/agent-loop/tests/request-reconstruction.spec.ts` pins the appended node under an inherited header with `request/context` carrying `systemPromptUpdate`, the series-start fold into node 0, the compaction-driven re-baseline, and the tool-schema change re-baseline under a `change` header that starts a series.
- `packages/core/agent-loop/tests/request-reconstruction.spec.ts` pins the appended node under an inherited header with `request/context` carrying `systemPromptUpdate`, the series-start fold into node 0, the compaction-driven re-baseline, and the tool-schema change re-baseline on a route without `toolUpdate`, under a `change` header that starts a series.
- `packages/llm/llm/tests/service.spec.ts`, `packages/llm/llm-deepseek/tests/adapter.spec.ts`, and `packages/test-support/llm-replay/tests/llm-replay.spec.ts` pin the declared mode on resolved model info and the load-time rejection of any other value.
- `packages/llm/token-meter/tests/context-breakdown-projection.spec.ts` pins newest/middle prompt removal, exact heuristic totals, surface ordering after head rewrites, extra source-event citations, dormant empties and fallback clears, immutable transitions, compact retained checkpoints, late registration, replay, and version invalidation.
- `packages/client/ui-conversation`, `ui-chat`, and `ui-trajectory` client specs pin the update card, the same-step header dedupe, the absent system change after an update, and the synthetic trajectory header.
@@ -8,7 +8,7 @@ Status: implemented
每一次系统提示词变更都要付出整个提供方前缀缓存的代价。循环在每个步骤渲染提示词;一旦字节不同——plan 模式片段进入或退出、某个 skill 或工具指引片段完成注册、agent 作用域的 persona 遮蔽、`{{model}}` 变量改变——请求的消息 0 随之改变,DeepSeek 上下文缓存从第一个 token 起失效。长时间的 agent 会话反复为此付费,而[运行时上下文快照设计](../../archived/feature/2026-07-30-current-sandbox-policy-context.md)之所以存在,正是因为把会变化的事实移出提示词是保持前缀稳定的唯一办法。
一个 DeepSeek 模型——在此按为本项工作提供的模型事实记录——移除了这一限制:它接受对话任意位置的 `system` 消息,并把最新一条视为完整的有效系统提示词,替换最前面那条。工具 schema 仍属于被缓存的前缀,因此工具集变更仍会使缓存失效。有了这样的模型,harness 可以把新提示词追加到已缓存的历史之后而不是重写消息 0,前缀就能保持热态。
一个 DeepSeek 模型——在此按为本项工作提供的模型事实记录——移除了这一限制:它接受对话任意位置的 `system` 消息,并把最新一条视为完整的有效系统提示词,替换最前面那条。工具 schema 仍属于被缓存的前缀;[动态工具更新](../architecture/2026-09-20-dynamic-tool-updates.zh.md)可以延迟新增声明,而保留定义的变化仍使复用失效。有了这样的模型,harness 可以把新提示词追加到已缓存的历史之后而不是重写消息 0,前缀就能保持热态。
因为[系统提示词是 surface 第 0 号节点](../architecture/2026-09-02-system-prompt-as-surface-node.zh.md),harness 拥有实现这一点的表示:提示词变更是对 `system/message` surface 节点的操作,而「替换最新的系统节点」与「追加新节点」之间的选择是逐路由的决定。
@@ -33,7 +33,7 @@ Status: implemented
| `in-history` | 非空渲染文本,新序列开始 | 为非空的后续系统节点记录空内容替换,再按需重写首个系统节点,即使最新有效文本未变也执行 |
| 任意 | 渲染后的提示词为空 | 为非空的后续系统节点记录空内容替换,再按需清空头节点;派生消息中不保留任何提示词版本 |
`startsSeries` 在以下情况为真:`agent/pre-step` 决定声明了 `startsRequestSeries`、surface 的替换代数自上次请求以来发生了移动(压缩或任何其他替换)、可见工具 schema 集合发生了变化。仅 provider 或 model 切换对本规则不算序列开始:目标路由具备能力时,变更后的提示词被追加,这不花任何代价,因为路由变更本身已经使缓存未命中。序列开始已经付出了缓存代价,因此归并让模型历史只保留当前提示词。有日志记录的逐节点空内容替换会从派生消息中移除后续提示词,无需 surface 删除操作,也不替换其间的对话节点。这也使压缩恢复不会在失败尝试已接纳的用户消息之后追加系统更新。
`startsSeries` 在以下情况为真:`agent/pre-step` 决定声明了 `startsRequestSeries`、surface 的替换代数自上次请求以来发生了移动(压缩或任何其他替换)、不支持 `toolUpdate` 的路由上可见工具 schema 集合发生了变化。受支持的工具添加可以与提示词追加一起发生,无需开启新序列。仅 provider 或 model 切换对本规则不算序列开始:目标路由具备能力时,变更后的提示词被追加,这不花任何代价,因为路由变更本身已经使缓存未命中。序列开始已经付出了缓存代价,因此归并让模型历史只保留当前提示词。有日志记录的逐节点空内容替换会从派生消息中移除后续提示词,无需 surface 删除操作,也不替换其间的对话节点。这也使压缩恢复不会在失败尝试已接纳的用户消息之后追加系统更新。
首次尝试在组装、被接纳的 `agent/pre-step` 决策、`step/start`、`agent/request` waterfall 与 `prepareCall()` 之后才接纳提示词。被拒绝或为空的首次输入不打开步骤。两个异步请求阶段都不提交待处理的系统提示词与已接纳用户消息,在任一阶段取消都不会提交这两者。每次尝试都在各自的 `agent/request` 与 `prepareCall()` 之后同步协调同一份已渲染组装结果、仅在首次尝试追加已接纳用户批次、按需记录 header/context、派生并冻结请求,再通过同一个已准备调用发起流式请求。重试不重复组装、`agent/pre-step` 或用户消息准入。协调过程可见 pre-step 压缩(`auto: true` 的 `compaction-basic`)与恢复压缩,并在任一种压缩开启新序列时将非空提示词文本归并到头部。恢复属于序列延续——`resume` header 不是序列开始——因此跨重启发生变化的提示词被追加;提供方缓存在进程边界之后可能仍是热的。
@@ -69,7 +69,7 @@ Chat 与 Trajectory 通过纯操作 `uiConversation.inspectSystemPrompt` 解释
**用先前的请求上下文决定准入。** 它描述上一次调用,而非请求中间件之后绑定的适配器。在首次调用、恢复之后、路由或能力变更之后,它可能选错提示词表示。在提交提示词与用户消息之前解析,还能防止取消时接纳未发送的内容。被否决。
**把 provider 或 model 切换视为序列开始。** 它会在每次路由变更时把提示词折回第 0 号节点,与 tools 的情形一致。header 已经记录了该变更,缓存无论如何都会未命中,因此这条额外规则除了在循环中多一个特例之外没有任何收益。被否决。
**把 provider 或 model 切换视为序列开始。** 它会在每次路由变更时把提示词折回第 0 号节点,与不具备能力的路由上的工具变更情形一致。header 已经记录了该变更,缓存无论如何都会未命中,因此这条额外规则除了在循环中多一个特例之外没有任何收益。被否决。
**仅清除最新系统节点。** 空节点不投影为消息,因此更早的提示词会重新生效。清除所有生效版本才能保留空渲染文本的含义,同时不删除对话历史。被否决。
@@ -89,7 +89,7 @@ Chat 与 Trajectory 通过纯操作 `uiConversation.inspectSystemPrompt` 解释
- `packages/core/agent-loop/tests/system-prompt-admission.spec.ts` 覆盖文本变化或未变时从具备能力切换到不具备能力的路由、反向路由切换、恢复时的路由准入、请求中间件或准备阶段取消,以及已准备路由保持绑定时并发选择发生变化。重试压缩用例覆盖遮蔽最新提示词后有或没有更早更新存活的情况,并验证复用已接纳的组装结果、用户消息仅接纳一次,以及未变的后续重试不会多记序列 header。具备和不具备能力路由的清除用例会移除三个生效提示词版本,验证重复请求与带 seed 的恢复保持为空且不多记提示词事件,并仅恢复新文本;日志重建与 pi 转换器都不保留旧指令。`src/agent.ts` 与 `src/runtime-context.ts` 的聚焦覆盖率在语句、分支、函数和行四项均达到 100%。
- `packages/core/agent-loop/tests/system-prompt-projection.spec.ts` 钉住序列延续时的追加、序列开始时无论是否存在后续存活节点、有效文本是否变化都执行的重新基线化、空提示词对所有生效版本的清除,以及不具备能力时只做替换的行为。
- `packages/core/agent-loop/tests/request-reconstruction.spec.ts` 钉住继承 header 下追加的节点及携带 `systemPromptUpdate` 的 `request/context`、序列开始时折回第 0 号节点、由压缩驱动的重新基线化,以及在开启序列的 `change` header 下由工具 schema 变更驱动的重新基线化。
- `packages/core/agent-loop/tests/request-reconstruction.spec.ts` 钉住继承 header 下追加的节点及携带 `systemPromptUpdate` 的 `request/context`、序列开始时折回第 0 号节点、由压缩驱动的重新基线化,以及在不支持 `toolUpdate` 的路由上、开启序列的 `change` header 下由工具 schema 变更驱动的重新基线化。
- `packages/llm/llm/tests/service.spec.ts`、`packages/llm/llm-deepseek/tests/adapter.spec.ts` 与 `packages/test-support/llm-replay/tests/llm-replay.spec.ts` 钉住已解析模型信息上声明的模式,以及加载时对任何其他值的拒绝。
- `packages/llm/token-meter/tests/context-breakdown-projection.spec.ts` 钉住最新与中间提示词移除、精确启发式总量、头部改写后的 surface 顺序、额外来源引用、休眠空节点与回退清空、不可变转换、紧凑保留检查点、延迟注册、重放和版本失效。
- `packages/client/ui-conversation`、`ui-chat` 与 `ui-trajectory` 的客户端测试钉住更新卡片、同一步骤 header 的去重、更新之后不存在系统变更,以及合成的轨迹 header。
@@ -9,8 +9,8 @@
en: f1aeaa7df97a744f
zh: 039b2cccd9a45198
/agent-note-desktop-mandatory-update-client-and-blocking-window/decision:
en: 4e7f0acf3c9b8b57
zh: 08624b26bb3a2183
en: 7d8ece211be36774
zh: d25c3ebac6420187
/agent-note-desktop-mandatory-update-client-and-blocking-window/alternatives-considered:
en: 136afb04a37db4a1
zh: 2b049e0d9fd8aa27
@@ -14,7 +14,7 @@ Native update overlays own their parent-visibility subscription until closure. O
The shell polls the guest protocol in the [API proposal](../../proposed/feature/2026-09-08-desktop-mandatory-update-api.md), independently of business traffic. Configuration and application identity are embedded at packaging; packaged applications ignore environment overrides. The [Desktop README](../../../../apps/desktop/README.md) owns configuration fields and defaults. Packaging requires the policy origin selected by the updater deployment before artifact preparation or signing. Test uses isolated Feishu authentication; production remains anonymous. The test login window opens detached DevTools on F12 while retaining its sandbox, navigation allowlist, and memory-only Session. Only unpackaged development can omit policy configuration. This prevents a missing policy setting from silently producing a release without mandatory checks or a test build querying production policy.
A coordinator owns one immutable installed-client identity, one in-flight request, a deadline, interval jitter, and bounded failure backoff. Manual checks bypass scheduling but join in-flight work. Disposal aborts the request, awaits settlement, and prevents late publication. A flattened `40005` establishes blocking independently of optional title, detail, and download-page fields; localized fallback text and a retry action remain available, while an absent or unapproved page is not offered. Transport, JSON, business-code, or HTTP failures cannot clear a known block. Only a valid fresh no-force response clears it. Same-version offline persistence is not implemented because its product behavior remains undecided.
A coordinator owns one in-flight request, a deadline, interval jitter, and bounded failure backoff. The request identifies the installed client with the same five Platform client headers as account requests — platform, client version, locale, UTC offset in seconds, and an intentionally empty bundle id — and adds architecture, update channel, and bundled runtime version. Platform, architecture, update channel, and bundled runtime version are fixed at packaging, while the locale and UTC offset are sampled from the shell for each request and the version is the shared client build version rather than the Electron bundle version. Manual checks bypass scheduling but join in-flight work. Disposal aborts the request, awaits settlement, and prevents late publication. A flattened `40005` establishes blocking independently of optional title, detail, and download-page fields; localized fallback text and a retry action remain available, while an absent or unapproved page is not offered. Transport, JSON, business-code, or HTTP failures cannot clear a known block. Only a valid fresh no-force response clears it. Same-version offline persistence is not implemented because its product behavior remains undecided.
User-initiated ordinary checks trigger an independent policy refresh without waiting for or presenting its failures. A confirmed block alone cancels ordinary dialogs and transfers presentation to the mandatory dialog. Test authentication waits until an active ordinary dialog finishes; cancellation and failure leave the updater result intact. Concurrent callers reuse the active ordinary or authentication operation instead of creating competing windows.
@@ -14,7 +14,7 @@ Status: implemented
壳独立于业务流量轮询[接口提案](../../proposed/feature/2026-09-08-desktop-mandatory-update-api.zh.md)中的游客协议。配置和应用身份在打包时嵌入;打包应用忽略环境覆盖。[Desktop README](../../../../apps/desktop/README.zh.md)负责配置字段与默认值。打包在准备产物或签名前要求提供由 updater 部署环境选定的策略源站。测试环境使用隔离的飞书鉴权;正式环境保持匿名。测试登录窗口按 F12 会打开独立的 DevTools,同时保留沙箱、导航允许列表和仅驻内存的 Session。只有未打包开发模式可以省略策略配置。这避免因遗漏策略配置而静默产出没有强更检查的发布包,或让测试包查询正式环境策略。
协调器拥有一份不可变的已安装客户端身份、单一在途请求、截止时间、间隔抖动和有上限的失败退避。手动检查绕过调度,但复用在途请求。dispose(资源释放)中止请求、等待结算,并阻止迟到发布。扁平化 `40005` 建立阻塞不依赖可选的标题、详情和下载页面字段;本地化兜底文案与重试操作仍可使用,缺失或未获批准的页面地址不会展示。传输、JSON、业务码或 HTTP 错误都不能清除已知阻塞。只有新的有效无需强更响应才能清除它。同版本离线持久化未实现,因为其产品行为尚未确定。
协调器持有单一在途请求、截止时间、间隔抖动和有上限的失败退避。请求用与账号请求相同的五个 Platform 客户端请求头标识已安装客户端——平台、客户端版本、语言、以秒为单位的 UTC 偏移,以及有意保持为空的 bundle id——并额外携带架构、更新通道和内置运行时版本。平台、架构、更新通道和内置运行时版本在打包时固定;语言与 UTC 偏移在每次请求时从壳采样,版本取自共享的客户端构建版本而非 Electron 打包版本。手动检查绕过调度,但复用在途请求。dispose(资源释放)中止请求、等待结算,并阻止迟到发布。扁平化 `40005` 建立阻塞不依赖可选的标题、详情和下载页面字段;本地化兜底文案与重试操作仍可使用,缺失或未获批准的页面地址不会展示。传输、JSON、业务码或 HTTP 错误都不能清除已知阻塞。只有新的有效无需强更响应才能清除它。同版本离线持久化未实现,因为其产品行为尚未确定。
用户发起常规检查时会触发独立的策略刷新,但不会等待或展示策略失败。只有已确认的强更决定会取消常规弹窗,并把展示移交给强更弹窗。测试环境鉴权会等待当前常规弹窗结束;取消和失败会保留 updater 结果。并发调用方复用当前常规操作或鉴权操作,不创建相互竞争的窗口。
@@ -9,11 +9,11 @@
en: 7926983089aebab2
zh: 366e32db00674a83
/agent-note-archiving-a-session-with-running-work/decision:
en: ba02c1b57e6ca76b
zh: 2908fcda98c4734e
en: 23adfbe72d671d6c
zh: 6fec976acfa54720
/agent-note-archiving-a-session-with-running-work/alternatives-considered:
en: c9d0a5c8d9924ad6
zh: f848ba8fa3c0d030
en: 708268e1fe4b4b75
zh: 64a8f6d6f333e090
/agent-note-archiving-a-session-with-running-work/consequences:
en: 977e6f36e62c0618
zh: 706de6bc6bb06282
en: cb3fdefd60483d62
zh: 87d3cead6dc8c37c
@@ -14,7 +14,7 @@ Archive never hides running work. The registry owns the rule and declares one ca
- **Refuse by default.** `archiveSession(sessionId)` rejects a non-empty activity answer with `WorkspaceActiveSessionError`; the API maps it to `workspace/session-active`, whose details carry the families with item ids and labels. A caller that did not ask to stop work never hides it, whichever client or SDK it is.
- **Stop on request, after the write.** `archiveSession(sessionId, { stopActivity: true })` skips the activity check, writes the archive, and then dispatches the stop event: the durable archive set is what the pre-step gate reads, so every wake a stop induces — a cancelled child's settlement, a queued follow-up — is already blocked. The stops are issued and never awaited to settlement, so the Remote returns once every provider's request is out and the user's row hides at once; a stop that fails is a logged warning, not a reason to undo the archive.
- **The stop is the user's stop.** The Agent registry cancels the turn with `agent.cancel({ kind: 'user' })` — the stop button's path, so the aborted tool call settles as a `tool/result` and the turn ends as `aborted`, but without the button's `keepInbox`, so queued input is discarded with a logged inbox splice. The job registry seam kills the owned jobs, installed by its constructor for every implementation through the abstract `list` and `kill`. The Subagent runtime cancels running subagent descendants (durable lineage, subagent origin, any depth, never a fork) as their parent would. Each request is contained on its own, so one child or job that throws does not keep the others running. The Schedule plugin deletes every active reminder as a management delete in its transaction queue — the same durable change the `schedule_delete` tool records, between the same two persistence barriers — answering from its owned runtime's fold of the live log rather than from the projection registry, which is a Client view. Each provider owns exactly the vocabulary it already has: the Agent registry knows turns, the job seam knows jobs, the Subagent runtime knows children, the Schedule plugin knows reminders; the API Session Controller keeps only the archived-lineage step gate. No new event type and no write inside another owner's turn, so a restored Session continues from a regularly ended log and unarchive brings nothing back that was stopped.
- **The stop is the user's stop.** The Agent registry cancels the turn with `agent.cancel({ kind: 'user' })` — the stop button's path, so the aborted tool call settles as a `tool/result` and the turn ends as `aborted`, but without the button's `keepInbox`, so queued input is discarded with a logged inbox splice. The job registry seam kills the owned jobs, installed by its constructor for every implementation through the abstract `list` and `kill`. The Subagent runtime cancels running subagent descendants (durable lineage, subagent origin, any depth, never a fork) as their parent would. Each request is contained on its own, so one child or job that throws does not keep the others running. The Schedule plugin deletes every active Host task stored for the Session ([archive admission reads the stored tasks](2026-09-22-archive-admission-reads-stored-tasks.md)) inside one slot of the same serialized queue the tools use. It reads the stored rows rather than a runtime fold of the Session log, because a Host task outlives its Session's Agent and is the only place a due reminder can be seen. Each provider owns exactly the vocabulary it already has: the Agent registry knows turns, the job seam knows jobs, the Subagent runtime knows children, the Schedule plugin knows reminders; the API Session Controller keeps only the archived-lineage step gate. No new event type and no write inside another owner's turn, so a restored Session continues from a regularly ended log and unarchive brings nothing back that was stopped.
- **Archived means no model step.** The API Session Controller's `agent/pre-step` listener rejects a step proposed for an archived Session or for a subagent descendant of one, which the loop ends as `blocked` without a request. This closes the window in which a late waking delivery — a subagent settlement, a queued follow-up, a turn started between the activity answer and the write — would run a hidden turn, and keeps a child whose cancel was ignored from working for a parent nobody can see; unarchiving lifts the gate for the lineage.
- **The refusal is the confirmation.** The sidebar sends the plain archive first; a quiet Session archives without a dialog as before. The `workspace/session-active` refusal opens the stop-and-archive dialog, whose list comes from the Host's answer rather than a client-side mirror of running state, so the dialog names exactly what the Host will stop. Confirming resends with `stopActivity`; the notice offers undo, which restores the Session without resuming the stopped work.
@@ -32,10 +32,10 @@ Archive never hides running work. The registry owns the rule and declares one ca
**Report the families from the API Session Controller.** The controller sees every live Agent, the job registry, and the lineage fields on Session headers, so it could enumerate turns, children, and jobs itself — but then it would know each owner's vocabulary (descriptors and labels, the parent-cause cancel, job ownership). The Agent registry, the job seam, and the Subagent runtime already own that vocabulary, so each family lives with its owner; the controller keeps only the lineage step gate, which reads the archive set and header fields alone. The gate cannot move into `dsh-workspace` in turn: the owners now depend on its types, so the workspace package cannot depend on the Agent event vocabulary without a cycle.
**Refuse cold Sessions with active reminders.** A cold reminder fires only after a resume; an archived Session cannot be opened from the sidebar, and a resume through another operation meets the pre-step gate, so nothing runs hidden; only live Sessions report the `schedule` family.
**Report the `schedule` family from live Sessions only.** The stored tasks are authoritative and outlive their Session's Agent, so consulting liveness would let a cold Session with armed rows archive silently, and its due row then dispatches into a turn the gate ends as `blocked`, consuming the occurrence without a model step ([archive admission reads the stored tasks](2026-09-22-archive-admission-reads-stored-tasks.md)).
## Consequences
An archived Session is guaranteed idle: it either was quiet, or its work was stopped the way the user stops it, and no step runs for it until it is restored. The pin-and-archive decision's "archive needs no confirmation" holds for quiet Sessions only; the running case asks. Stop settlement is best effort: a job that ignores kill or a child that ignores cancel is logged, and the pre-step gate keeps the Session from spending model requests meanwhile. The activity check and the archive write are two steps, so a turn starting between them is hidden, and every step whose pre-step precedes the write still runs with its tool calls, until the first step after the write is blocked. Two residues follow from the write-first order and the gate. A crash between the archive write and the Schedule plugin's delete barrier leaves an archived Session whose reminders are still recorded; unarchiving shows them again, as if the stop had never been requested. A cold archived Session reports no reminders, and an operation that resumes its Agent — rename, model, or queue edits address a Session without unarchiving it — re-arms them: a due one-shot then dispatches its follow-up into a turn the gate ends as `blocked`, so that occurrence is consumed without a model step and is not replayed after unarchive.
An archived Session is guaranteed idle: it either was quiet, or its work was stopped the way the user stops it, and no step runs for it until it is restored. The pin-and-archive decision's "archive needs no confirmation" holds for quiet Sessions only; the running case asks. Stop settlement is best effort: a job that ignores kill or a child that ignores cancel is logged, and the pre-step gate keeps the Session from spending model requests meanwhile. The activity check and the archive write are two steps, so a turn starting between them is hidden, and every step whose pre-step precedes the write still runs with its tool calls, until the first step after the write is blocked. Two residues follow from the write-first order and the gate. A crash between the archive write and the Schedule plugin's stop leaves an archived Session whose stored tasks are still armed; unarchiving shows them again, as if the stop had never been requested, and when such a row's target passes the Schedule runtime selects it on its own timer and resolves the Session's Agent itself, so a one-shot dispatches its follow-up into a turn the gate ends as `blocked` and that occurrence is consumed without a model step and is not replayed after unarchive. Admission itself reports the stored tasks of a Session without a live Agent, so such a Session is refused before that residue can start.
Peer products surveyed for this choice: Codex and Kimi Code stop then archive; Claude Code stops by starving the remote session of writes; Reasonix and MiniMax refuse a busy Session, MiniMax after a bounded cancel; Pi has no archive but aborts before every session switch; OpenCode neither refuses nor stops. Every product that stops does so through its ordinary cancel path so the log ends regularly.
@@ -14,7 +14,7 @@
- **默认拒绝。** `archiveSession(sessionId)` 对非空的活动答案以 `WorkspaceActiveSessionError` 拒绝;API 把它映射为 `workspace/session-active`,details 携带各族及各项的 id 与名称。没有要求停止工作的调用方绝不会把工作藏起来,无论它是哪个客户端或 SDK。
- **按请求停止,先写后停。** `archiveSession(sessionId, { stopActivity: true })` 跳过活动检查,先写入归档,再派发停止事件:pre-step 门禁读取的正是持久化的归档集合,因此停止所引发的每一次唤醒——被取消子代理的结算、排队的 follow-up——都已被拦下。停止请求只发出、从不等待收敛,因此 Remote 在每个提供方的请求发出后即返回,用户的行随即隐藏;停止失败只是一条 warn 日志,不是撤销归档的理由。
- **停止即用户的停止。** Agent 注册表用 `agent.cancel({ kind: 'user' })` 取消回合——与停止按钮同一条路径,因此被打断的工具调用以 `tool/result` 收口、回合以 `aborted` 结束,但不带按钮的 `keepInbox`,因此排队输入被丢弃并记录一次收件箱拼接。任务注册表接缝 kill 所属任务,由其构造函数只通过抽象的 `list` 与 `kill` 为每个实现安装。Subagent runtime 以父身份取消运行中的子代理子孙(持久化血缘、subagent 来源、任意深度、从不包括 fork)。每个请求各自兜住,一个抛错的子代理或任务不会让其余的继续跑。Schedule 插件把每条活动提醒作为其事务队列中的管理删除来删除——与 `schedule_delete` 工具相同的持久化变更,处于同样的两道持久化屏障之间——答案来自其归属 runtime 对活日志的 fold,而不是投影注册表这个 Client 视图。每个提供方只拥有它本来就有的词汇:Agent 注册表认识回合,任务接缝认识任务,Subagent runtime 认识子代理,Schedule 插件认识提醒;API Session Controller 只保留已归档血缘的步骤门禁。没有新事件类型,也不在别的所有者的回合内写入,因此恢复后的 Session 从一份正常收尾的日志继续,取消归档不会带回任何被停止的东西。
- **停止即用户的停止。** Agent 注册表用 `agent.cancel({ kind: 'user' })` 取消回合——与停止按钮同一条路径,因此被打断的工具调用以 `tool/result` 收口、回合以 `aborted` 结束,但不带按钮的 `keepInbox`,因此排队输入被丢弃并记录一次收件箱拼接。任务注册表接缝 kill 所属任务,由其构造函数只通过抽象的 `list` 与 `kill` 为每个实现安装。Subagent runtime 以父身份取消运行中的子代理子孙(持久化血缘、subagent 来源、任意深度、从不包括 fork)。每个请求各自兜住,一个抛错的子代理或任务不会让其余的继续跑。Schedule 插件在与工具相同的串行队列的一个槽位里删除该 Session 已存储的全部活动宿主任务([归档准入读取已存储的任务](2026-09-22-archive-admission-reads-stored-tasks.zh.md))。它读已存储的行,而不是对会话日志做 runtime fold,因为宿主任务比其 Session 的 Agent 活得更久,是到期提醒唯一能被看见的地方。每个提供方只拥有它本来就有的词汇:Agent 注册表认识回合,任务接缝认识任务,Subagent runtime 认识子代理,Schedule 插件认识提醒;API Session Controller 只保留已归档血缘的步骤门禁。没有新事件类型,也不在别的所有者的回合内写入,因此恢复后的 Session 从一份正常收尾的日志继续,取消归档不会带回任何被停止的东西。
- **已归档即不跑模型步。** API Session Controller 的 `agent/pre-step` 监听器拒绝为已归档 Session 或其子代理子孙提出的步骤,loop 以 `blocked` 收口且不发请求。这封住了迟到的唤醒投递——子代理结算、排队的 follow-up、在活动作答与写入之间开始的回合——跑出隐藏回合的窗口,也让无视 cancel 的子代理无法替一个谁也看不见的父级干活;取消归档即为整条血缘解除门禁。
- **拒绝即确认。** 侧栏先发普通归档;静止的 Session 一如既往无对话框直接归档。`workspace/session-active` 拒绝会打开"停止并归档"对话框,其清单来自宿主的答案而非客户端对运行状态的镜像,因此对话框列出的正是宿主将要停止的内容。确认后带 `stopActivity` 重发;提示提供撤销,撤销只恢复 Session,不会让被停止的工作继续。
@@ -32,10 +32,10 @@
**由 API Session Controller 报告各族。** 控制器看得到每个活 Agent、任务注册表和 Session header 上的血缘字段,本可以自己枚举回合、子代理和任务——但那样它就得认识每个 owner 的词汇(描述符与名称、父级原因的取消、任务归属)。Agent 注册表、任务接缝与 Subagent runtime 本来就拥有这些词汇,所以每一族都跟着自己的 owner;控制器只保留只读归档集合与 header 字段的血缘步骤门禁。门禁也不能反过来搬进 `dsh-workspace`:各 owner 现在依赖它的类型,workspace 包再依赖 Agent 的事件词汇就会成环。
**拒绝带活动提醒的冷 Session。** 冷提醒只在 resume 后才触发;已归档的 Session 无法从侧栏打开,经其他操作 resume 也会撞上 pre-step 门禁,所以没有东西在隐藏运行;只有活 Session 报告 `schedule` 族。
**只由活着的 Session 报告 `schedule` 族。** 已存储的任务才是权威,且比其 Session 的 Agent 活得更久,因此以存活为条件会让存储行被武装的冷 Session 静默归档,其到期行随后投递进一个被门禁以 `blocked` 收口的回合,在没有模型步的情况下消耗掉这次触发([归档准入读取已存储的任务](2026-09-22-archive-admission-reads-stored-tasks.zh.md))。
## 后果
已归档的 Session 保证空闲:它要么本来就静止,要么其工作已按用户停止的方式停止,且在恢复之前不会为它跑任何一步。置顶与归档决策里的"归档无需确认"仅对静止 Session 成立;运行中的情形会先询问。停止的收敛是尽力而为:无视 kill 的任务或无视 cancel 的子代理会被记录,pre-step 门禁在此期间阻止该 Session 消耗模型请求。活动检查与归档写入是两步,其间开始的回合会被隐藏,pre-step 先于该写入的每一步连同其工具调用照常执行,直到写入之后的第一步被阻断。先写后停与门禁留下两处残余。在归档写入与 Schedule 插件的删除屏障之间崩溃,会留下一个提醒仍被记录的已归档 Session;取消归档时它们再次出现,与从未请求过停止时一样。冷的已归档 Session 不报告提醒,而恢复其 Agent 的操作——重命名、模型、队列编辑都按 Session 寻址而不取消归档——会重新武装它们:到期的一次性提醒把 follow-up 投递进一个被门禁以 `blocked` 收口的回合,该次触发因此在没有模型步的情况下被消耗,取消归档后也不会重放。
已归档的 Session 保证空闲:它要么本来就静止,要么其工作已按用户停止的方式停止,且在恢复之前不会为它跑任何一步。置顶与归档决策里的"归档无需确认"仅对静止 Session 成立;运行中的情形会先询问。停止的收敛是尽力而为:无视 kill 的任务或无视 cancel 的子代理会被记录,pre-step 门禁在此期间阻止该 Session 消耗模型请求。活动检查与归档写入是两步,其间开始的回合会被隐藏,pre-step 先于该写入的每一步连同其工具调用照常执行,直到写入之后的第一步被阻断。先写后停与门禁留下两处残余。在归档写入与 Schedule 插件的停止之间崩溃,会留下一个已存储任务仍被武装的已归档 Session;取消归档时它们再次出现,与从未请求过停止时一样;当这类行的目标时刻过去,Schedule runtime 会按自己的定时器选中它并自行 resolve 该 Session 的 Agent,一次性提醒于是把 follow-up 投递进一个被门禁以 `blocked` 收口的回合,该次触发因此在没有模型步的情况下被消耗,取消归档后也不会重放。准入本身会报告没有活 Agent 的 Session 的已存储任务,因此这类 Session 在残余发生之前就被拒绝。
为此决策调研的同行产品:Codex 与 Kimi Code 先停止再归档;Claude Code 通过让远端会话无法写入来停止;Reasonix 与 MiniMax 拒绝忙碌的 Session,MiniMax 在有界取消之后才拒绝;Pi 没有归档但在每次切换会话前 abort;OpenCode 既不拒绝也不停止。每一个会停止的产品都走自己的常规取消路径,以便日志正常收尾。
@@ -0,0 +1,19 @@
# Bilingual-pair consistency record for 2026-09-22-archive-admission-reads-stored-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/implemented/feature/2026-09-22-archive-admission-reads-stored-tasks.md
/agent-note-archive-admission-reads-the-stored-tasks:
en: 8be4b1d210d5d8bf
zh: e9124b48467818b3
/agent-note-archive-admission-reads-the-stored-tasks/problem:
en: 093de39b40f7cc97
zh: 50c712bde174bc96
/agent-note-archive-admission-reads-the-stored-tasks/decision:
en: afbf818442c0794a
zh: 4749e63d9757bbbb
/agent-note-archive-admission-reads-the-stored-tasks/alternatives-considered:
en: 099360f2c97a50ea
zh: eac4746c4602f152
/agent-note-archive-admission-reads-the-stored-tasks/consequences:
en: cb2883b34c2c73fa
zh: 9bb1489c9711cf99
@@ -0,0 +1,27 @@
# Agent Note: Archive admission reads the stored tasks
Status: implemented
English | [中文](2026-09-22-archive-admission-reads-stored-tasks.zh.md)
## Problem
Archive admission asks every provider what still runs for a Session before the archive is written, and a stop-archive then asks those providers to end it ([archiving a Session with running work](2026-09-21-archive-stops-running-session-work.md)). The Schedule provider answered from its owned runtime's fold of the live Session log, so only a Session with a live Agent reported the `schedule` family, and the stop deleted through the management `delete` path.
Host tasks are stored in the Schedule domain document and outlive their Session's Agent ([the storage decision](../architecture/2026-09-16-host-schedule-storage.md)), so that fold no longer exists: `ScheduleRuntime.activeRecords()` is gone, and the stored task table is the only place a due reminder can be seen. A cold Session whose stored rows were still armed therefore reported nothing, archived without the confirmation dialog the running case asks for, and nobody was told which reminder the Host would keep.
## Decision
The Schedule provider answers archive admission from the stored Host task rows, and a stop deletes them. `workspace/session-activity` reads `list({ sessionId })` — the stored rows whose binding is that Session and whose status is active — reports them as the `schedule` family, one item per task with its stored id and title, and prepends that family to `next()` so every other provider keeps its entries. `workspace/session-stop` removes those rows directly inside one slot of the same serialized queue the tools use, so a stop is ordered behind a create whose write is still in flight and cannot miss the row that create is about to commit; it attempts every row, because a row left armed would still fire, and it notifies observers for the rows that landed before the first failure reaches the caller. Session liveness is never consulted: an idle or cold Session whose stored rows are armed refuses the archive, and the confirmation dialog names exactly those reminders. Both listeners are registered in one effect named `schedule.archiveAdmission()`, so unloading the plugin withdraws them together.
## Alternatives considered
**Report the `schedule` family from live Sessions only.** The stored tasks are authoritative and outlive their Session's Agent, so consulting liveness would let a cold Session with armed rows archive silently, and its due row then dispatches into a turn the gate ends as `blocked`, consuming the occurrence without a model step. The admission answer costs one table read either way.
**Delete each row through the public management `delete` path.** Re-entering the serialized queue from inside the slot that already holds it deadlocks, so the stop removes the rows itself: the same durable change, with one notification and one timer re-drive per stop instead of one per row.
**Keep a runtime-side mirror of the Session's active tasks.** A mirror duplicates the authoritative document, drifts after a restart, and disappears with the Agent — the exact case admission must still answer for.
## Consequences
Admission costs one stored-table read, and a Session with no live Agent can refuse the archive: a Session that looks quiet may now ask for confirmation because its stored reminders are armed. A stop deletes rows one at a time — the domain table offers no batch delete — so a failure partway leaves the earlier deletes committed; observers hear about those, every row was attempted, and the first failure still reaches the caller. One residue follows. A crash between the archive write and the stop leaves stored rows armed in an archived Session: unarchiving shows them again, and when such a row's target passes the Schedule runtime selects it on its own timer and resolves the Session's Agent itself — that resolution is ungated, and only `agent/pre-step` refuses the model step — so a one-shot dispatches a follow-up into a turn the gate ends as `blocked` and that occurrence is consumed without a model step. The seam itself is unchanged: the workspace package still knows no Schedule vocabulary, and this provider owns only reminders.
@@ -0,0 +1,27 @@
# Agent Note:归档准入读取已存储的任务
状态:implemented
[English](2026-09-22-archive-admission-reads-stored-tasks.md) | 中文
## 问题
归档准入在写入归档前询问每个提供方:该 Session 还有什么在跑;随后「停止并归档」再要求它们结束这些工作([归档仍有工作在跑的 Session](2026-09-21-archive-stops-running-session-work.zh.md))。Schedule 提供方原先回答的是其归属 runtime 对活会话日志的 fold,因此只有带活 Agent 的 Session 才报告 `schedule` 族,停止也走管理 `delete` 路径。
宿主任务存储在 Schedule 领域文档中,且比其 Session 的 Agent 活得更久([存储决策](../architecture/2026-09-16-host-schedule-storage.zh.md)),因此那个 fold 已不存在:`ScheduleRuntime.activeRecords()` 已经移除,已存储的任务表是到期提醒唯一能被看见的地方。存储行仍被武装的冷 Session 因此什么也不报告,归档时不会弹出运行态才会弹出的确认对话框,也没有人被告知宿主会继续保留哪条提醒。
## 决策
Schedule 提供方从已存储的宿主任务行回答归档准入,并由停止删除这些行。`workspace/session-activity` 读取 `list({ sessionId })`——绑定到该 Session 且状态为活动的已存储行——把它们作为 `schedule` 族报告,每条任务一项、携带其已存储 id 与 title,并把该族前插到 `next()` 的结果之前,使其他提供方保留各自条目。`workspace/session-stop` 在与工具相同的串行队列的一个槽位里直接删除这些行,因此停止会排在写入尚未完成的创建之后,不会漏掉那个创建即将提交的行;它会尝试每一行,因为留下的行仍会触发;对已经落地的行先通知观察者,再把首个失败交给调用方。是否存活从不参与判断:存储行仍被武装的空闲或冷 Session 会拒绝归档,确认对话框会准确列出这些提醒。两个监听器注册在同一个名为 `schedule.archiveAdmission()` 的 effect 中,卸载插件即一并撤回。
## 已考虑的备选
**只由活着的 Session 报告 `schedule` 族。** 已存储的任务才是权威,且比其 Session 的 Agent 活得更久,因此以存活为条件会让存储行被武装的冷 Session 静默归档,其到期行随后投递进一个被门禁以 `blocked` 收口的回合,在没有模型步的情况下消耗掉这次触发。两种做法在准入上都只需一次读表。
**每行都走公开的管理 `delete` 路径删除。** 在已经持有该槽位的串行队列内部再次进入队列会自锁,因此停止自己删行:同样的持久化变更,每次停止一次通知、一次计时器重算,而不是每行各一次。
**在 runtime 侧保留该 Session 活动任务的镜像。** 镜像会重复权威文档、在重启后漂移、并随 Agent 一起消失——而这正是准入必须仍能回答的情形。
## 后果
准入的代价是一次读已存储的表,而没有活 Agent 的 Session 也能拒绝归档:看起来安静的 Session 现在可能因为其已存储提醒仍被武装而要求确认。停止逐行删除——领域表不提供批量删除——因此删到一半失败时,先前删除已提交;观察者会被告知这些,每一行都已被尝试,首个失败仍交给调用方。由此剩下一处残余:在归档写入与停止之间崩溃,会在已归档的 Session 里留下仍被武装的已存储行;取消归档时它们再次出现;当这类行的目标时刻过去,Schedule runtime 会按自己的定时器选中它并自行 resolve 该 Session 的 Agent——这一步没有门禁,只有 `agent/pre-step` 拒绝模型步——一次性提醒于是把 follow-up 投递进一个被门禁以 `blocked` 收口的回合,该次触发在没有模型步的情况下被消耗。接缝本身未变:workspace 包仍然不认识 Schedule 词汇,而这个提供方只拥有提醒。
@@ -15,8 +15,8 @@
en: 6c82820724930bb5
zh: 5f6a0bbfd6fb6b16
/agent-note-desktop-mandatory-update-api/proposal/request:
en: d015126512123574
zh: a77e66d4f1808402
en: fb66b137a600f3a6
zh: 83f5c74a1716d57c
/agent-note-desktop-mandatory-update-api/proposal/no-mandatory-update:
en: 4814b1bd7002d6ed
zh: 3f0d77beb046e2fc
@@ -24,8 +24,8 @@
en: d3ae240665143695
zh: 486115cd2fdf35ed
/agent-note-desktop-mandatory-update-api/proposal/errors-and-policy-matching:
en: 5da5e6a38a739201
zh: 5af5743f9f162dd4
en: ce7ab9adf3cbc840
zh: d6f9734ab54b8f33
/agent-note-desktop-mandatory-update-api/proposal/policy-publication-and-current-decisions:
en: d4d70e1541b2c2d1
zh: a1b6e62a05b239be
@@ -28,21 +28,23 @@ The query itself must return the complete mandatory response; do not rely on int
GET /api/v0/check_client_update?scenario=launch
x-client-platform: desktop-win
x-client-version: 0.1.3-rc.2
x-client-bundle-id: com.deepseek.dsh
x-client-locale: zh-CN
x-client-bundle-id:
x-client-locale: zh_CN
x-client-timezone-offset: 28800
x-client-arch: x64
x-client-update-channel: nightly
x-client-bundled-dsh-version: 0.1.3-rc.2
```
All listed headers are required for Desktop. Values describing installed software come from the application and its release metadata, not editable UI fields.
All listed headers are required on every Desktop request; a required header may still carry an empty value, as `x-client-bundle-id` does. The values describing installed software come from the application and its release metadata, not editable UI fields.
| Header | Meaning and allowed values |
|---|---|
| `x-client-platform` | `desktop-win` or `desktop-mac` |
| `x-client-version` | Full Desktop SemVer, retaining prerelease identifiers; initially equal to bundled dsh |
| `x-client-bundle-id` | Application identity, for example `com.deepseek.dsh`; distinguishes Harness from Chat |
| `x-client-locale` | UI locale, for example `zh-CN`; selects localized content, not region |
| `x-client-version` | Shared client build version, inlined as `DSH_CLIENT_VERSION` at packaging; full SemVer retaining prerelease identifiers, initially equal to the bundled dsh version |
| `x-client-bundle-id` | Platform uses this field to identify the Chat application; the field does not apply to Harness, so Desktop sends it empty |
| `x-client-locale` | `zh_CN` or `en_US`, taken from the active UI language's primary subtag; selects localized content, not region |
| `x-client-timezone-offset` | Whole-second offset from UTC, positive east of Greenwich, sampled from the shell for every request |
| `x-client-arch` | Windows `x64`; macOS `x64` or `arm64` |
| `x-client-update-channel` | Initially always `nightly`, independent of version suffix |
| `x-client-bundled-dsh-version` | Full bundled dsh version from release metadata |
@@ -102,7 +104,7 @@ Do not add `mode`, `force_update`, `show_key`, `target_version`, button copy, or
Missing required headers, invalid SemVer, and unsupported platform/architecture/channel combinations return explicit parameter errors, not no-force success or a mandatory policy. Prefer the existing business meanings of `biz_code = 1` for a missing version and `biz_code = 2` for an invalid version; final error allocation belongs to the backend. Service failures must not masquerade as success because success may remove an existing block.
Match application identity, platform, architecture, Desktop version, bundled dsh version, and channel using server-owned ranges and precedence. Use complete SemVer, not lexical sorting or truncated prerelease values. Initially the two versions are equal and channel is fixed Nightly; independent revisions and channel switching are deferred. Do not require a downgrade or stop returning `40005` merely because a client has queried or displayed it before.
Match platform, architecture, Desktop version, bundled dsh version, and channel using server-owned ranges and precedence. Use complete SemVer, not lexical sorting or truncated prerelease values. Initially the two versions are equal and channel is fixed Nightly; independent revisions and channel switching are deferred. Do not require a downgrade or stop returning `40005` merely because a client has queried or displayed it before.
### Policy publication and current decisions
@@ -28,21 +28,23 @@ GET /api/v0/check_client_update
GET /api/v0/check_client_update?scenario=launch
x-client-platform: desktop-win
x-client-version: 0.1.3-rc.2
x-client-bundle-id: com.deepseek.dsh
x-client-locale: zh-CN
x-client-bundle-id:
x-client-locale: zh_CN
x-client-timezone-offset: 28800
x-client-arch: x64
x-client-update-channel: nightly
x-client-bundled-dsh-version: 0.1.3-rc.2
```
以上 header 对 Desktop 均为必填。描述已安装软件的值来自应用与发布元数据,不来自可编辑 UI 字段。
以上 header 在每次 Desktop 请求中均为必填;必填 header 的取值可以为空,`x-client-bundle-id` 即如此。描述已安装软件的值来自应用与发布元数据,不来自可编辑 UI 字段。
| Header | 含义与取值 |
|---|---|
| `x-client-platform` | `desktop-win` 或 `desktop-mac` |
| `x-client-version` | 完整 Desktop SemVer,保留预发布标识;一期等于内置 dsh |
| `x-client-bundle-id` | 应用身份,例如 `com.deepseek.dsh`,区分 Harness 与 Chat |
| `x-client-locale` | UI 语言,例如 `zh-CN`;选择本地化内容,不代表区域 |
| `x-client-version` | 共享的客户端构建版本,在打包时内联为 `DSH_CLIENT_VERSION`;完整 SemVer,保留预发布标识,一期等于内置 dsh 版本 |
| `x-client-bundle-id` | Platform 用该字段标识 Chat 应用;该字段对 Harness 不适用,因此 Desktop 发送空值 |
| `x-client-locale` | `zh_CN` 或 `en_US`,取自当前 UI 语言的主语言子标签;选择本地化内容,不代表区域 |
| `x-client-timezone-offset` | 与 UTC 的整秒偏移,东为正,每次请求时从壳采样 |
| `x-client-arch` | Windows 为 `x64`;macOS 为 `x64` 或 `arm64` |
| `x-client-update-channel` | 一期固定 `nightly`,独立于版本后缀 |
| `x-client-bundled-dsh-version` | 发布元数据中的完整内置 dsh 版本 |
@@ -102,7 +104,7 @@ x-client-bundled-dsh-version: 0.1.3-rc.2
缺少必填 header、非法 SemVer 或不支持的平台/架构/通道组合返回明确参数错误,不能伪装为无强制成功或强制策略。优先沿用 `biz_code = 1` 表示缺少版本、`biz_code = 2` 表示版本非法的既有业务含义;最终错误码分配由后端负责。服务异常不得伪装成成功,因为成功可能解除已有阻断。
按应用身份、平台、架构、Desktop 版本、内置 dsh 版本与通道匹配,由服务端维护版本范围和优先级。使用完整 SemVer,不按字符串排序或截断预发布部分。一期两个版本相等、通道固定 Nightly,独立修订与通道切换延后。不得要求降级,也不得仅因客户端曾查询或展示过就停止返回 `40005`。
按平台、架构、Desktop 版本、内置 dsh 版本与通道匹配,由服务端维护版本范围和优先级。使用完整 SemVer,不按字符串排序或截断预发布部分。一期两个版本相等、通道固定 Nightly,独立修订与通道切换延后。不得要求降级,也不得仅因客户端曾查询或展示过就停止返回 `40005`。
### 策略发布与当前判定
+1
View File
@@ -43,6 +43,7 @@ External packages installed for runtime use or distributed inside the prebuilt b
| [`@fortune-sheet/react`](https://github.com/ruilisi/fortune-sheet) | MIT |
| [`@joplin/turndown-plugin-gfm`](https://github.com/laurent22/joplin-turndown-plugin-gfm) | MIT |
| [`@jridgewell/gen-mapping`](https://github.com/jridgewell/sourcemaps) | MIT |
| [`@js-temporal/polyfill`](https://github.com/js-temporal/temporal-polyfill) | ISC |
| [`@lexical/history`](https://github.com/facebook/lexical) | MIT |
| [`@lexical/plain-text`](https://github.com/facebook/lexical) | MIT |
| [`@lexical/text`](https://github.com/facebook/lexical) | MIT |
+1 -2
View File
@@ -1,5 +1,4 @@
# Opt-in Schedule patch over the shipped Web composition. The owner observes
# only roots published after this overlay loads.
# Opt-in host Schedule service and its Session task catalog.
- insert:
- id: time-context
+1
View File
@@ -129,6 +129,7 @@
"@deepseek-ai/dsh-plugin-package-inventory-deepseek": "workspace:*",
"@deepseek-ai/dsh-sandbox-local": "workspace:*",
"@deepseek-ai/dsh-sandbox-policy": "workspace:*",
"@deepseek-ai/dsh-sdk-client": "workspace:*",
"@deepseek-ai/dsh-session": "workspace:*",
"@deepseek-ai/dsh-session-checkpoint-policy": "workspace:*",
"@deepseek-ai/dsh-session-log-deepseek": "workspace:*",
@@ -1,11 +1,11 @@
{"type":"session","version":3,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0}
{"type":"session","version":4,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0}
{"type":"turn/start","data":{"turn":1}}
{"type":"step/start","data":{"turn":1,"step":1}}
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Perform one side-effecting remote mutation."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"assistant/message","data":{"turn":1,"step":1,"stream":[],"message":{"role":"assistant","content":[{"type":"tool-call","id":"unknown-outcome-call","name":"write_remote","arguments":"{\"value\":1}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"}},"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":1,"callId":"unknown-outcome-call","name":"write_remote","arguments":"{\"value\":1}"}}
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"id":"interrupted-tool-result-unknown-outcome-call-6","role":"user","source":{"kind":"tool","callId":"unknown-outcome-call"},"content":[{"type":"tool-result","toolCallId":"unknown-outcome-call","isError":true,"content":[{"type":"text","text":"The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly."}]}]},"error":{"name":"ToolOutcomeUnknownError","code":"TOOL_OUTCOME_UNKNOWN"}},"surfaceOp":"append","sourceEventSeqs":[5]}
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"id":"interrupted-tool-result-unknown-outcome-call-6","role":"tool","toolCallId":"unknown-outcome-call","isError":true,"source":{"kind":"tool","callId":"unknown-outcome-call"},"content":[{"type":"text","text":"The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly."}]},"error":{"name":"ToolOutcomeUnknownError","code":"TOOL_OUTCOME_UNKNOWN"}},"surfaceOp":"append","sourceEventSeqs":[5]}
{"type":"step/end","data":{"turn":1,"step":1}}
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"interrupted"}}}
{"type":"session/end-seed","data":{}}
@@ -16,10 +16,10 @@
{"type":"turn/start","data":{"turn":2}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","data":{"turn":2,"step":1}}
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
{"type":"user/message","data":{"content":[{"type":"text","text":"Continue safely from the interrupted operation."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}}
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
{"type":"session/title","data":{"title":"Perform one side-effecting remote mutati","messageSeqs":[3],"source":{"kind":"fallback"}}}
{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"I will verify the external state before deciding whether to retry the side-effecting operation."}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["I will verify the external state before deciding whether to retry the side-effecting operation."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"I will verify the external state before deciding whether to retry the side-effecting operation."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
@@ -1,7 +1,7 @@
{"type":"session","version":3,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0}
{"type":"session","version":4,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0}
{"type":"turn/start","data":{"turn":1}}
{"type":"step/start","data":{"turn":1,"step":1}}
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Start a background job."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":1}}
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
@@ -14,15 +14,15 @@
{"type":"turn/start","data":{"turn":2}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","data":{"turn":2,"step":1}}
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
{"type":"user/message","data":{"content":[{"type":"text","text":"Call list_agents once and report what it shows."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}}
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
{"type":"session/title","data":{"title":"Start a background job.","messageSeqs":[3],"source":{"kind":"fallback"}}}
{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"list-once","name":"list_agents","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"list-once","name":"list_agents","args":["{}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"list-once","name":"list_agents","arguments":"{}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":2,"step":1,"callId":"list-once","name":"list_agents","arguments":"{}"}}
{"type":"tool/result","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool","callId":"list-once"},"content":[{"type":"tool-result","toolCallId":"list-once","content":[{"type":"text","text":"{{sessionId}} [inactive] — descriptorless child"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"}
{"type":"tool/result","data":{"turn":2,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"list-once"},"toolCallId":"list-once","content":[{"type":"text","text":"{{sessionId}} [inactive] — descriptorless child"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":2,"step":1}}
{"type":"step/start","data":{"turn":2,"step":2}}
{"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"The catalogued subagent is ready. PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["The catalogued subagent is ready. PARENT_DONE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"The catalogued subagent is ready. PARENT_DONE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
@@ -1,7 +1,7 @@
{"type":"session","version":3,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0}
{"type":"session","version":4,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0}
{"type":"turn/start","data":{"turn":1}}
{"type":"step/start","data":{"turn":1,"step":1}}
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Tighten this session to read-only."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"sandbox/mode","data":{"mode":"read-only"}}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"}},"reason":"initial"}}
@@ -14,16 +14,17 @@
{"type":"turn/start","data":{"turn":2}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","data":{"turn":2,"step":1}}
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
{"type":"user/message","data":{"content":[{"type":"text","text":"Delegate the write probe to a subagent."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"},"tools":"{{tools}}"},"reason":"resume"}}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","write"]},"reason":"resume","startsSeries":true}}
{"type":"developer/message","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"bash"},{"type":"tool-addition","toolName":"create_goal"},{"type":"tool-addition","toolName":"edit"},{"type":"tool-addition","toolName":"exit_plan_mode"},{"type":"tool-addition","toolName":"get_goal"},{"type":"tool-addition","toolName":"glob"},{"type":"tool-addition","toolName":"grep"},{"type":"tool-addition","toolName":"interrupt_agent"},{"type":"tool-addition","toolName":"job_kill"},{"type":"tool-addition","toolName":"job_list"},{"type":"tool-addition","toolName":"job_output"},{"type":"tool-addition","toolName":"list_agents"},{"type":"tool-addition","toolName":"read"},{"type":"tool-addition","toolName":"read_image"},{"type":"tool-addition","toolName":"send_message"},{"type":"tool-addition","toolName":"skill"},{"type":"tool-addition","toolName":"subagent"},{"type":"tool-addition","toolName":"subagent_fork"},{"type":"tool-addition","toolName":"todo_write"},{"type":"tool-addition","toolName":"update_goal"},{"type":"tool-addition","toolName":"web_fetch"},{"type":"tool-addition","toolName":"web_search"},{"type":"tool-addition","toolName":"workflow"},{"type":"tool-addition","toolName":"write"}],"role":"developer","id":"{{sessionId}}"},"headerSeq":18},"surfaceOp":"append"}
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
{"type":"session/title","data":{"title":"Tighten this session to read-only.","messageSeqs":[3],"source":{"kind":"fallback"}}}
{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"delegate-write","name":"subagent","args":["{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":2,"step":1,"callId":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}}
{"type":"subagent/catalog","data":{"version":0,"childId":"{{sessionId}}","childCreatedAt":0,"mode":"one-shot","label":"Delegated write probe"}}
{"type":"tool/result","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool","callId":"delegate-write"},"content":[{"type":"tool-result","toolCallId":"delegate-write","content":[{"type":"text","text":"CHILD_DENIED [sandbox: file access denied under read-only mode]"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"}
{"type":"tool/result","data":{"turn":2,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"delegate-write"},"toolCallId":"delegate-write","content":[{"type":"text","text":"CHILD_DENIED [sandbox: file access denied under read-only mode]"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[23],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":2,"step":1}}
{"type":"step/start","data":{"turn":2,"step":2}}
{"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"The delegated child was denied by the sandbox. PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["The delegated child was denied by the sandbox. PARENT_DONE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"The delegated child was denied by the sandbox. PARENT_DONE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
@@ -1,7 +1,7 @@
{"type":"session","version":3,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0}
{"type":"session","version":4,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0}
{"type":"turn/start","data":{"turn":1}}
{"type":"step/start","data":{"turn":1,"step":1}}
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Remember the workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nOld workspace instruction.\n</system-reminder>"}],"source":{"kind":"agent-instructions","form":"instructions","baseline":true,"baselineIdentity":"{\"projectRoot\":\"\",\"projectRootMarkers\":[\".git\"],\"maxBytes\":65536,\"maxSourceBytes\":1048576,\"instructionFileCandidates\":[\"AGENTS.md\",\"CLAUDE.md\"],\"localInstructionFileCandidates\":[\"AGENTS.local.md\",\"CLAUDE.local.md\"]}","changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"ba65bdb41810f4d0129129dcbd6cadcd643c069d"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":1}}
@@ -14,11 +14,11 @@
{"type":"turn/start","data":{"turn":2}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","data":{"turn":2,"step":1}}
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
{"type":"user/message","data":{"content":[{"type":"text","text":"Acknowledge the current workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"<system-reminder>\nUpdated instructions from: AGENTS.md\n\nThis file changed after it was loaded. Use the following content instead of the previously loaded instructions from this file.\n\nNew workspace instruction after offline edit.\n\n</system-reminder>"}],"source":{"kind":"agent-instructions","form":"instructions","changes":[{"action":"replace","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"d8375b516f158718bd3463bc8eb7ed42c011b29f"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}}
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
{"type":"session/title","data":{"title":"Remember the workspace instruction.","messageSeqs":[3],"source":{"kind":"fallback"}}}
{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"RESUME_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["RESUME_DONE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"RESUME_DONE"}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
@@ -1,7 +1,7 @@
{"type":"session","version":3,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0}
{"type":"session","version":4,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0}
{"type":"turn/start","data":{"turn":1}}
{"type":"step/start","data":{"turn":1,"step":1}}
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Remember the workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: CLAUDE.md\n\nOld CLAUDE rule.\n\nInstructions from: AGENTS.md\n\nOld AGENTS rule.\n</system-reminder>"}],"source":{"kind":"agent-instructions","form":"instructions","baseline":true,"baselineIdentity":"{\"projectRoot\":\"\",\"projectRootMarkers\":[\".git\"],\"maxBytes\":65536,\"maxSourceBytes\":1048576,\"instructionFileCandidates\":[\"CLAUDE.md\",\"AGENTS.md\"],\"localInstructionFileCandidates\":[\"AGENTS.local.md\",\"CLAUDE.local.md\"]}","changes":[{"action":"set","scope":".\u0000CLAUDE.md","path":"CLAUDE.md","digest":"b525eb8a6d3660b732dad4b0aff1b7c63ab32890"},{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"3113bd093ae91976207dcef7390bdc0b2bfcfa10"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":1}}
@@ -14,11 +14,11 @@
{"type":"turn/start","data":{"turn":2}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","data":{"turn":2,"step":1}}
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
{"type":"user/message","data":{"content":[{"type":"text","text":"Acknowledge the current workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"<system-reminder>\nThis complete workspace instruction baseline replaces all earlier workspace instruction baselines. The following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nCurrent AGENTS rule.\n\n\nInstructions from: CLAUDE.md\n\nCurrent CLAUDE rule.\n\n</system-reminder>"}],"source":{"kind":"agent-instructions","form":"instructions","baseline":true,"baselineIdentity":"{\"projectRoot\":\"\",\"projectRootMarkers\":[\".git\"],\"maxBytes\":65536,\"maxSourceBytes\":1048576,\"instructionFileCandidates\":[\"AGENTS.md\",\"CLAUDE.md\"],\"localInstructionFileCandidates\":[\"AGENTS.local.md\",\"CLAUDE.local.md\"]}","changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"7f53d2327837129750aef117f9754a001c46cf68"},{"action":"set","scope":".\u0000CLAUDE.md","path":"CLAUDE.md","digest":"5b1e9e3fd759eee6b43ceff899e47fb10c64701a"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}}
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
{"type":"session/title","data":{"title":"Remember the workspace instruction.","messageSeqs":[3],"source":{"kind":"fallback"}}}
{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"RESUME_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["RESUME_DONE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"RESUME_DONE"}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
@@ -0,0 +1,121 @@
/** Real provider cache usage across registry additions through the built SDK profile. */
import { randomUUID } from 'node:crypto'
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { TokenUsage } from '@deepseek-ai/dsh-llm'
import { DeepSeekHarness } from '@deepseek-ai/dsh-sdk-client'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import { describe, expect, it, onTestFinished } from 'vitest'
const fixturePath = fileURLToPath(new URL('./fixtures/dynamic-tool-cache.mjs', import.meta.url))
const dshBin = fileURLToPath(new URL('../../../lib/bin.js', import.meta.url))
const sampleTool = 'cache_sample'
const sampleLabelGuidance = 'Place each generated sample label on its own line.'
// Provider cache blocks may leave a short uncached suffix of an unchanged request.
const cacheSuffixTokens = 256
interface ObservedRequest {
stage: 'initial' | 'added' | 'removed'
turn: number
step: number
body: {
system?: string
messages: { role: string; content: { type: string; text?: string; tool?: { name: string } }[] }[]
tools?: { name: string; defer_loading?: true }[]
}
}
function usageFor(request: ObservedRequest, events: SessionEvent[]): TokenUsage {
const settled = events.find(event => event.type === 'assistant/message'
&& event.data.turn === request.turn && event.data.step === request.step)
if (settled?.type !== 'assistant/message' || settled.data.usage === undefined) {
throw new Error(`Missing provider usage for turn ${request.turn}, step ${request.step}`)
}
return settled.data.usage
}
function totalInput(usage: TokenUsage): number {
return usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0)
}
describe.skipIf(!process.env.DEEPSEEK_API_KEY)('SDK native tool updates with real DeepSeek', () => {
it.each([false, true])('keeps the preceding conversation cached after a tool addition (prompt update: %s)', { retry: 0 }, async (updatePrompt) => {
const root = await mkdtemp(join(tmpdir(), 'dsh-sdk-tool-cache-'))
onTestFinished(async () => { await rm(root, { recursive: true, force: true }) })
const sampleLabel = `CACHE_SAMPLE_${randomUUID()}`
const evidencePath = join(root, 'requests.jsonl')
const callsPath = join(root, 'calls.txt')
const patch = join(root, 'dynamic-tool-cache.patch.yml')
await writeFile(patch, JSON.stringify([
{ id: 'llm-deepseek', config: {
apiKeyEnv: 'DEEPSEEK_API_KEY', thinking: 'disabled', maxTokens: 1024,
retryPolicy: { maxRetries: 0 },
models: [{ id: 'deepseek-flash', systemPromptUpdate: 'in-history', toolUpdate: 'addition-only' }],
} },
{ id: 'tools', config: { mode: 'native' } },
{ id: 'session-log-deepseek', disabled: true },
{ id: 'plugin-package-inventory-deepseek', disabled: true },
{ insert: [{ id: 'sdk-dynamic-tool-cache-fixture', name: fixturePath, config: { evidencePath, callsPath, sampleLabel, updatePrompt } }] },
]))
const harness = new DeepSeekHarness({
dshBin, profile: 'sdk', patches: [patch], dshHome: join(root, 'home'),
cwd: root, processCwd: root, provider: 'deepseek-official', model: 'deepseek-flash',
env: { ...process.env, DSH_TELEMETRY_DISABLED: '1', DSH_PERMISSION_MODE: 'danger-full-access' },
initializeTimeoutMs: 30_000,
})
onTestFinished(async () => { await harness.close() })
await harness.start()
const session = harness.session()
const events: SessionEvent[] = []
const run = async (prompt: string) => {
const result = await session.run(prompt)
events.push(...result.events)
return result.finalResponse
}
await run('Do not call tools. Reply READY.')
// Unique user history makes hits on shared profile instructions insufficient.
const records = Array.from({ length: 96 }, (_, index) => `record-${index}: ${randomUUID()}`).join('\n')
await run(`Keep these reference records. Do not call tools. Reply READY.\n${records}`)
await run('Keep the reference records. Do not call tools. Reply READY.')
expect(await run(`Use cache_tool_control with action add. After it returns, call ${sampleTool} exactly once and report the generated sample label. This is synthetic example data for this run. Call these separately in order.`)).toContain(sampleLabel)
expect(await readFile(callsPath, 'utf8')).toBe('add\nsample\n')
expect(await run('Use cache_tool_control with action remove. After it returns, reply REMOVED and do not call the sample tool.')).toContain('REMOVED')
expect(await readFile(callsPath, 'utf8')).toBe('add\nsample\nremove\n')
const requests = (await readFile(evidencePath, 'utf8')).trim().split('\n').map(line => JSON.parse(line) as ObservedRequest)
expect(events.filter(event => event.type === 'assistant/attempt')).toHaveLength(0)
expect(requests).toHaveLength(events.filter(event => event.type === 'assistant/message').length)
const [initial, history, warm] = requests
const addedIndex = requests.findIndex(request => request.stage === 'added')
const before = requests[addedIndex - 1]
const added = requests[addedIndex]
const removed = requests.find(request => request.stage === 'removed')
if (!initial || !history || !warm || !before || !added || !removed) throw new Error('Missing cache transition requests')
const historyInput = totalInput(usageFor(history, events))
const precedingInput = totalInput(usageFor(before, events))
const cached = usageFor(added, events).cacheReadTokens ?? 0
process.stdout.write(`SDK tool addition cache: ${JSON.stringify({ updatePrompt, historyInput, precedingInput, cached, deficit: precedingInput - cached, removedCached: usageFor(removed, events).cacheReadTokens })}\n`)
expect(historyInput - totalInput(usageFor(initial, events))).toBeGreaterThan(1024)
expect(usageFor(warm, events).cacheReadTokens ?? 0).toBeGreaterThanOrEqual(historyInput - cacheSuffixTokens)
expect(added.turn).toBe(before.turn)
expect(added.step).toBe(before.step + 1)
expect(before.body.system).toBe(initial.body.system)
expect(added.body.system).toBe(initial.body.system)
expect(added.body.messages.slice(0, before.body.messages.length)).toEqual(before.body.messages)
expect(added.body.messages.some(message => message.role === 'system'
&& message.content.some(block => block.type === 'text' && block.text?.includes(sampleLabelGuidance)))).toBe(updatePrompt)
expect(cached, 'the first post-addition request must reuse the preceding conversation input').toBeGreaterThanOrEqual(precedingInput - cacheSuffixTokens)
expect(initial.body.tools?.map(tool => tool.name)).toEqual(['cache_tool_control'])
expect(added.body.tools?.slice(0, initial.body.tools?.length)).toEqual(initial.body.tools)
expect(added.body.tools?.find(tool => tool.name === sampleTool)).toMatchObject({ name: sampleTool, defer_loading: true })
expect(added.body.messages.flatMap(message => message.content)).toContainEqual({ type: 'tool_addition', tool: { type: 'tool_reference', name: sampleTool } })
expect(removed.body.tools?.map(tool => tool.name)).toEqual(['cache_tool_control'])
expect(removed.body.messages.flatMap(message => message.content).some(block => block.type === 'tool_addition')).toBe(false)
expect(events.filter(event => event.type === 'developer/message').map(event => event.data.message.content)).toEqual([
[{ type: 'tool-addition', toolName: sampleTool }],
[{ type: 'tool-removal', toolName: sampleTool }],
])
})
})
@@ -0,0 +1,68 @@
/** SDK fixture for native registry changes and unmodified DeepSeek request observation. */
import { appendFile } from 'node:fs/promises'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'sdk-dynamic-tool-cache-fixture'
export const inject = ['tools', 'systemPrompt', 'deepseekLlmApiExtensions']
/** Observe each request and let the model add or remove a scoped tool. */
export function apply(ctx, config) {
const states = new Map()
ctx.on('session/event', (session, event) => {
const state = states.get(session.id)
if (state !== undefined && event.type === 'step/start') {
state.turn = event.data.turn
state.step = event.data.step
}
})
ctx.effect(() => ctx.deepseekLlmApiExtensions.register('dynamic_tool_cache_observer', {
async prepare(request) {
if (request.purpose !== undefined) return undefined
const state = states.get(request.sessionId)
if (state === undefined) throw new Error('The SDK request has no observed agent.')
await appendFile(config.evidencePath, `${JSON.stringify({ ...state, body: request.body })}\n`)
return undefined
},
}))
ctx.on('agent/created', ({ agent }) => {
const scope = agent.ctx
const state = { stage: 'initial', turn: 0, step: 0 }
states.set(agent.id, state)
scope.effect(() => () => { states.delete(agent.id) })
scope.effect(() => scope.tools.restrict({ allow: [] }))
let guidance = ''
scope.systemPrompt.section({ name: 'sdk:dynamic-tool-guidance', order: 400, text: () => guidance })
let removeTool
scope.effect(() => scope.tools.register(defineTool({
name: 'cache_tool_control',
description: 'Add or remove the temporary cache_sample tool. It becomes available on the model step after add returns.',
parameters: { action: { type: 'string', enum: ['add', 'remove'], required: true } },
output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }] },
async execute({ action }) {
if (action === 'add') {
if (removeTool !== undefined) throw new Error('The sample tool is already registered.')
if (config.updatePrompt) guidance = 'Place each generated sample label on its own line.'
removeTool = scope.effect(() => scope.tools.register(defineTool({
name: 'cache_sample',
description: 'Read a randomly generated sample label created for this example run.',
parameters: {},
output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }] },
async execute() {
await appendFile(config.callsPath, 'sample\n')
return config.sampleLabel
},
})))
state.stage = 'added'
await appendFile(config.callsPath, 'add\n')
return 'Tool added. Call cache_sample on the next model step to obtain the generated sample label.'
}
if (removeTool === undefined) throw new Error('The sample tool is not registered.')
await removeTool()
removeTool = undefined
state.stage = 'removed'
await appendFile(config.callsPath, 'remove\n')
return 'Tool removed. The sample tool is unavailable.'
},
})))
})
}
@@ -5,7 +5,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { get } from 'node:http'
import { request as httpRequest } from 'node:http'
import type { IncomingHttpHeaders } from 'node:http'
import { resolveExampleLaunch } from '@deepseek-ai/dsh-loader-smoke'
import ts from 'typescript'
@@ -18,6 +18,7 @@ const repoRoot = fileURLToPath(new URL('../../../../../../', import.meta.url))
/** Live process whose IPC observer reports independently collected runtime state. */
interface DefaultWeb {
root: string
url: string
request: (command: 'roster' | 'mount-experimental' | 'mount-experimental-entry') => Promise<RuntimeRoster>
}
@@ -26,13 +27,19 @@ interface DefaultWeb {
* Boot the built Web profile under plain Node and dispose it to quiescence after an assertion callback.
* @param test - owning Vitest case, including its timeout, cancellation, and cleanup hooks.
* @param inspect - assertions against the running process and its ephemeral loopback URL.
* @param options - additional profile patches and test data prepared before startup.
*/
export async function withDefaultWeb(test: TestContext, inspect: (app: DefaultWeb) => Promise<void>): Promise<void> {
export async function withDefaultWeb(
test: TestContext,
inspect: (app: DefaultWeb) => Promise<void>,
options: { patches?: readonly string[]; prepare?: (root: string) => Promise<void> } = {},
): Promise<void> {
const root = await mkdtemp(join(tmpdir(), 'dsh-web-default-isolation-'))
let removal: Promise<void> | undefined
const removeRoot = (): Promise<void> => removal ??= rm(root, { recursive: true, force: true })
test.onTestFinished(removeRoot)
try {
await options.prepare?.(root)
await writeFile(join(root, 'package.json'), JSON.stringify({ type: 'module' }) + '\n')
for (const relative of ['runtime-roster.ts', 'fixtures/runtime-roster-observer.ts']) {
const source = await readFile(new URL(relative, import.meta.url), 'utf8')
@@ -53,7 +60,10 @@ export async function withDefaultWeb(test: TestContext, inspect: (app: DefaultWe
const launch = resolveExampleLaunch({
srcBin: join(repoRoot, 'apps/cli/src/bin.ts'),
mode: 'lib',
configArgs: ['--profile', 'web', '--patch', patch, '--host', '127.0.0.1', '--port', '0', '--no-open'],
configArgs: [
'--profile', 'web', ...options.patches?.flatMap(path => ['--patch', path]) ?? [],
'--patch', patch, '--host', '127.0.0.1', '--port', '0', '--no-open',
],
env: {
NODE_OPTIONS: undefined,
NODE_PATH: undefined,
@@ -145,7 +155,7 @@ export async function withDefaultWeb(test: TestContext, inspect: (app: DefaultWe
return /dsh web: (http:\/\/[^\s]+)/u.exec(stdout)?.[1]
}, { timeout: test.task.timeout }).toBeDefined()
const url = /dsh web: (http:\/\/[^\s]+)/u.exec(stdout)![1]!
await inspect({ url, request })
await inspect({ root, url, request })
} finally {
const result = await close()
test.signal.removeEventListener('abort', abort)
@@ -167,13 +177,26 @@ export async function withDefaultWeb(test: TestContext, inspect: (app: DefaultWe
* @param headers - optional authentication cookie.
* @returns complete response after its stream ends.
*/
export function webGet(url: string | URL, signal: AbortSignal, headers: Record<string, string> = {}): Promise<{
status: number | undefined
headers: IncomingHttpHeaders
text: string
}> {
export function webGet(
url: string | URL, signal: AbortSignal, headers: Record<string, string> = {},
): ReturnType<typeof webRequest> {
return webRequest(url, signal, { method: 'GET', headers })
}
/**
* Send one public-protocol request directly to the test-owned loopback process.
* @param url - Exact URL of the owned process.
* @param signal - Owning test cancellation signal.
* @param options - HTTP method, headers, and optional serialized request body.
* @returns Complete response after its stream ends.
*/
export function webRequest(
url: string | URL,
signal: AbortSignal,
options: { method: 'GET' | 'POST'; headers?: Record<string, string>; body?: string },
): Promise<{ status: number | undefined; headers: IncomingHttpHeaders; text: string }> {
return new Promise((resolve, reject) => {
const request = get(url, { headers, agent: false, signal }, (response) => {
const request = httpRequest(url, { method: options.method, headers: options.headers, agent: false, signal }, (response) => {
response.setEncoding('utf8')
let text = ''
response.on('data', (chunk: string) => { text += chunk })
@@ -181,5 +204,6 @@ export function webGet(url: string | URL, signal: AbortSignal, headers: Record<s
response.once('end', () => { resolve({ status: response.statusCode, headers: response.headers, text }) })
})
request.once('error', reject)
request.end(options.body)
})
}
@@ -0,0 +1,120 @@
/** Built CLI, public Remote requests, and persisted task files; seeded receipts are not delivery evidence. */
import { randomUUID } from 'node:crypto'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { DailyScheduleRecord, ScheduleCatalogEntry } from '@deepseek-ai/dsh-schedule'
import { expect, it } from 'vitest'
import { withDefaultWeb, webGet, webRequest } from './default-web-process.ts'
const overlay = fileURLToPath(new URL('../../../../config/examples/schedule/cordis.yml', import.meta.url))
const id = 'retained-remote-task' as DailyScheduleRecord['id']
const sessionId = 'unloaded-owner' as ScheduleCatalogEntry['sessionId']
const record: DailyScheduleRecord = {
id, kind: 'daily', title: 'Retained instruction', prompt: 'Retained instruction\nwith a second line',
time: '09:00:00.125', timeZone: 'Asia/Shanghai', scheduledAt: '2099-01-01T01:00:00.125Z',
}
const lastDelivery = {
scheduledAt: '2026-01-01T01:00:00.125Z', deliveredAt: '2026-01-01T01:00:01.250Z', messageId: 'saved-receipt',
}
const task = {
sessionId, record, status: 'active', lastDelivery,
deliveryHistory: { earlierRecordsUnavailable: false, records: [{ ...lastDelivery, prompt: record.prompt }] },
}
const taskPath = (root: string) => join(root, 'home', 'storages', 'schedule.json')
async function seed(root: string, bytes: string): Promise<void> {
await mkdir(join(root, 'home', 'storages'), { recursive: true })
await writeFile(taskPath(root), bytes)
}
function encoded(value: unknown): string {
return `${JSON.stringify({ unit: { name: 'schedule', version: 1 }, global: null, tables: { tasks: { [id]: value } } }, null, 2)}\n`
}
async function connect(url: string, signal: AbortSignal) {
const auth = await webGet(url, signal)
const cookie = auth.headers['set-cookie']?.[0]?.split(';', 1)[0]
if (cookie === undefined) throw new Error('Owned Web process did not issue its authentication cookie')
return async (method: string, args: Record<string, unknown>) => {
const rpcId = randomUUID()
const response = await webRequest(new URL(`/api/${method}`, url), signal, {
method: 'POST', headers: { cookie, 'content-type': 'application/json' },
body: JSON.stringify({ type: 'client-request', rpcId, method, payload: { args } }),
})
expect(response.status, response.text).toBe(200)
const envelope = JSON.parse(response.text) as { type?: unknown; rpcId?: unknown; result?: unknown }
expect(envelope).toMatchObject({ type: 'server-response', rpcId })
return envelope.result
}
}
it('edits a retained task through built Remote bindings and preserves its identity and seeded history after cold startup', async (test) => {
let persisted = encoded(task)
let updated = record
const historyRequest = { request: { sessionId, id, limit: 20 } }
const expectedHistory = {
ok: true,
value: {
id, records: task.deliveryHistory.records, earlierRecordsUnavailable: false,
earlierRecordsPruned: false, retention: { days: 30, records: 200 },
},
}
await withDefaultWeb(test, async ({ root, url }) => {
const rpc = await connect(url, test.signal)
expect(await rpc('schedule/catalog', {})).toEqual({ ok: true, value: [{ ...record, sessionId, status: 'active', lastDelivery }] })
expect(await rpc('schedule/history', historyRequest)).toEqual(expectedHistory)
const target = new Date(Date.now() + 12 * 60 * 60 * 1_000)
target.setUTCMilliseconds(125)
updated = { ...record, time: target.toISOString().slice(11, 23), timeZone: 'UTC', scheduledAt: target.toISOString() }
const change = { kind: 'daily', daily: { time: updated.time, time_zone: 'UTC' } }
expect(await rpc('schedule/update', { request: { sessionId, id, expected: record, change } }))
.toEqual({ ok: true, value: { id, updated: true, record: updated } })
persisted = await readFile(taskPath(root), 'utf8')
expect(JSON.parse(persisted)).toEqual({
unit: { name: 'schedule', version: 1 },
global: null,
tables: { tasks: { [id]: { ...task, record: updated } } },
})
expect(await rpc('schedule/history', historyRequest)).toEqual(expectedHistory)
expect(await rpc('schedule/update', { request: { sessionId, id, expected: record, change } }))
.toEqual({ ok: true, value: { id, updated: false, code: 'schedule_conflict' } })
expect(await rpc('schedule/update', { request: { sessionId: 'other-owner', id, expected: updated, change } }))
.toEqual({ ok: true, value: { id, updated: false, code: 'schedule_not_found' } })
expect(await readFile(taskPath(root), 'utf8')).toBe(persisted)
}, { patches: [overlay], prepare: root => seed(root, persisted) })
await withDefaultWeb(test, async ({ root, url }) => {
const rpc = await connect(url, test.signal)
expect(await rpc('schedule/catalog', {})).toEqual({ ok: true, value: [{ ...updated, sessionId, status: 'active', lastDelivery }] })
expect(await rpc('schedule/history', historyRequest)).toEqual(expectedHistory)
expect(await rpc('schedule/update', { request: {
sessionId, id, expected: updated, change: { kind: 'daily', daily: { time: updated.time, time_zone: 'Etc/UTC' } },
} })).toEqual({ ok: true, value: { id, updated: false, record: updated } })
expect(await readFile(taskPath(root), 'utf8')).toBe(persisted)
}, { patches: [overlay], prepare: root => seed(root, persisted) })
})
it('creates and idempotently adopts the explicit Session id bound to seeded task data without touching its task file', async (test) => {
const persisted = encoded(task)
const expectedList = { ok: true, value: [record] }
const expectedHistory = {
ok: true,
value: {
id, records: task.deliveryHistory.records, earlierRecordsUnavailable: false,
earlierRecordsPruned: false, retention: { days: 30, records: 200 },
},
}
await withDefaultWeb(test, async ({ root, url }) => {
const rpc = await connect(url, test.signal)
const create = { request: { sessionId, cwd: root } }
const expectedCreate = { ok: true, value: { sessionId, agentPreset: 'standard' } }
expect(await rpc('session/create', create)).toEqual(expectedCreate)
expect(await rpc('session/create', create)).toEqual(expectedCreate)
expect(await rpc('schedule/list', { request: { sessionId } })).toEqual(expectedList)
expect(await rpc('schedule/history', { request: { sessionId, id, limit: 20 } })).toEqual(expectedHistory)
expect(await readFile(taskPath(root), 'utf8')).toBe(persisted)
}, { patches: [overlay], prepare: root => seed(root, persisted) })
})
@@ -17,6 +17,8 @@ class Account extends DeepSeekAccount {
override async getState() { return state }
override async getProfile() { return null }
override async getBalance() { return null }
override async getUnnotifiedBonuses() { return null }
override async ackBonusNotified() { return false }
override async startSignIn() { return state }
override async cancelSignIn() { return state }
override async signOut() { return state }
+4 -4
View File
@@ -3,8 +3,8 @@
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write apps/desktop/README.md
/deepseek-harness-desktop:
en: 907e8bc5ebef2678
zh: f23af881b27cad94
en: b8487c3bea6d86bb
zh: d936a91a17efd1da
/deepseek-harness-desktop/key-technical-decisions:
en: 069fdfd72d2ea26a
zh: 81c683ada1fe3e8d
@@ -51,8 +51,8 @@
en: acbd12b8fc5ec5fd
zh: 031e15f931479f27
/deepseek-harness-desktop/updates/mandatory-update-policy:
en: b99d35abf25d5347
zh: 2f39041146268a20
en: 2f789a4949fafb12
zh: d8b28c01cfee11b7
/deepseek-harness-desktop/updates/local-updater-qualification:
en: bf34de891411c618
zh: aa6923fd5060174b
+2 -2
View File
@@ -12,7 +12,7 @@ Creator and the Web Plugin Manager use Desktop’s bundled pnpm under Electron N
Embedded Platform documents use a dedicated non-persistent WebContentsView session. Host sends account credentials through private Node IPC; account RPC and the Harness renderer receive no token. Before page scripts execute, the Platform preload performs one synchronous IPC to read the prepared main-process credential. It exposes displayMode, synchronous getAuthToken() and getLocale() getters, and onLocaleChange(listener), which returns an unsubscribe function. Both getters read preload memory without further IPC. The bootstrap includes the resolved Desktop language (`zh_CN` or `en_US`); Settings language changes update the preload cache and notify the open Platform document without reloading. Platform applies this language before its first render and does not persist it as a browser preference. The main-process handler performs only sender validation and an in-memory read; it never waits for Host, disk, or network. Trusted-page initialization failures retain embedded mode and make the getter throw, preventing browser-credential fallback. Only the owned Platform main frame on the configured issuer origin can initialize. Sign-out, credential replacement, Host shutdown, and view closure destroy the document. Cross-origin document navigation is blocked. HTTPS links requesting a new window open in the system browser without the embedded session or token; other schemes and URL credentials are rejected. Native views occupy the viewport below the Account feature’s return bar.
Desktop Host Platform API requests use the same `x-client-platform` mapping as update-policy requests. The account provider owns [API-only header configuration](../../packages/credentials/deepseek-account-platform/README.md#use-this-package).
Desktop Host Platform API requests and update-policy requests identify the installed client with the same Platform client headers: platform, client version, locale, timezone offset in seconds, and a bundle id that is intentionally empty. Account operations take the calling UI's identity per call; the account provider owns [API-only header configuration](../../packages/credentials/deepseek-account-platform/README.md#use-this-package). Update policy additionally reports architecture, update channel, and bundled runtime version.
A server-expired account credential returns to Welcome when no official API key is configured; an available API key keeps the workspace open. Explicit sign-out follows the same rule. Both Welcome and the workspace display the localized expiry notice.
@@ -381,7 +381,7 @@ The [mandatory client decision](../../.agents/notes/implemented/feature/2026-09-
| `maxBackoffMs` | Maximum failed-request interval including jitter; default `3600000`, at least `intervalMs` |
| `jitter` | Random additional interval fraction; default `0.2`, allowed range `0` through `1` |
Durations are integers from 1000 through 2147483647 milliseconds. Startup and scheduled polling are independent of business requests; foreground/resume checks respect the next due time, while manual checks bypass it and join any request in flight. The client sends the installed platform, architecture, exact shell and bundled dsh versions, application ID, locale, and fixed Nightly. It uses no business login credentials or installation ID.
Durations are integers from 1000 through 2147483647 milliseconds. Startup and scheduled polling are independent of business requests; foreground/resume checks respect the next due time, while manual checks bypass it and join any request in flight. The client sends the installed platform, architecture, DSH_CLIENT_VERSION, bundled dsh version, current locale and UTC offset, an empty bundle ID, and fixed Nightly. It uses no business login credentials or installation ID.
With `feishu-test`, an HTTP 401 JSON response containing `error.code: "UNAUTHENTICATED"` offers login during user-initiated checks and the packaged application's initial startup check, without waiting for the local backend. A localized explanation identifies the test build, the need for Feishu authentication, and that login neither downloads nor installs updates. Confirmation closes the explanation before opening a sandboxed window at the configured origin’s root, not a response-provided login URL. Concurrent checks reuse the entire confirmation/login operation and focus its existing window. Press F12 in the test login window to open detached DevTools for diagnosis. Cancellation does not trigger repeated prompts from periodic or foreground checks; users can retry manually.
+2 -2
View File
@@ -12,7 +12,7 @@ Creator 和 Web Plugin Manager 在 Electron Node 模式下使用 Desktop 内置
内嵌 Platform 文档使用独立且不持久化的 WebContentsView 会话。Host 通过私有 Node IPC 发送账号凭证;账号 RPC 和 Harness 渲染进程不接收 token。Platform preload 在页面脚本执行前通过一次同步 IPC 读取主进程中已准备的凭证。它暴露 displayMode、同步的 getAuthToken() 和 getLocale() getter,以及返回取消订阅函数的 onLocaleChange(listener)。两个 getter 都只读取 preload 内存,不再调用 IPC。bootstrap 包含 Desktop 已解析的语言(`zh_CN` 或 `en_US`);Settings 语言变更会更新 preload 缓存并通知已打开的 Platform 文档,无需重载。Platform 在首屏渲染前应用该语言,且不将其持久化为浏览器偏好。主进程处理器仅校验调用来源并读取内存,不等待 Host、磁盘或网络。可信页面初始化失败时保留内嵌模式,由 getter 抛错,避免回退到浏览器凭证。只有受控 Platform 页面中、位于所配置签发来源的主 frame 能完成初始化。退登、凭证替换、Host 关闭及视图关闭都会销毁文档。跨来源文档导航被阻止。请求新窗口的 HTTPS 链接在系统浏览器中打开,不携带内嵌会话或 token;其他协议及带 URL 凭证的链接被拒绝。原生视图占据 Account 功能返回栏下方的视口。
Desktop Host 的 Platform API 请求与更新策略请求使用相同的 `x-client-platform` 映射。账号 provider 管理[仅 API 使用的请求头配置](../../packages/credentials/deepseek-account-platform/README.zh.md#use-this-package)。
Desktop Host 的 Platform API 请求与更新策略请求用相同的 Platform 客户端请求头标识已安装客户端:平台、客户端版本、语言、以秒为单位的时区偏移,以及有意保持为空的 bundle id。账号操作按调用逐次传入调用界面的身份;账号 provider 管理[仅 API 使用的请求头配置](../../packages/credentials/deepseek-account-platform/README.zh.md#use-this-package)。更新策略额外上报架构、更新通道和内置运行时版本。
账号凭据被服务端判定失效后,未配置官方 API key 时返回 Welcome;有可用 API key 时保持工作区打开。主动退出登录遵循相同规则。Welcome 和工作区均显示本地化的登录失效提示。
@@ -381,7 +381,7 @@ macOS 打包在组装 App 时、代码签名前写入 `Contents/Resources/app-up
| `maxBackoffMs` | 含抖动的失败请求最大间隔;默认 `3600000`,不小于 `intervalMs` |
| `jitter` | 随机增加的间隔比例;默认 `0.2`,范围为 `0` 至 `1` |
时长必须是 1000 至 2147483647 毫秒的整数。启动与定时轮询独立于业务请求;前台/恢复检查遵守下次到期时间,手动检查绕过该时间并复用在途请求。客户端发送已安装平台、架构、完整壳与内置 dsh 版本、应用 ID、语言和固定 Nightly。不使用业务登录凭据或安装 ID。
时长必须是 1000 至 2147483647 毫秒的整数。启动与定时轮询独立于业务请求;前台/恢复检查遵守下次到期时间,手动检查绕过该时间并复用在途请求。客户端发送已安装平台、架构、DSH_CLIENT_VERSION、内置 dsh 版本、当前语言与 UTC 偏移、空 bundle ID 和固定 Nightly。不使用业务登录凭据或安装 ID。
启用 `feishu-test` 时,包含 `error.code: "UNAUTHENTICATED"` 的 HTTP 401 JSON 响应会在用户主动检查和打包应用首次启动检查时提供登录入口,不等待本地后端就绪。本地化说明指出这是测试版、需要飞书鉴权,且登录不会下载或安装更新。确认后先关闭说明,再打开配置源站根路径的沙箱窗口,不使用响应中的登录 URL。并发检查复用整个确认/登录流程,并聚焦已有窗口。在测试环境登录窗口按 F12 可打开独立的 DevTools 进行排查。取消后,定时或前台检查不会反复弹窗;用户可手动重试。
+8 -7
View File
@@ -2,7 +2,7 @@
import { randomUUID } from 'node:crypto'
import WebSocket from 'ws'
import { parseRemoteStreamServerMessage, REMOTE_STREAM_MUX_PATH } from '@deepseek-ai/dsh-api-gateway/stream-protocol'
import type { AccountView, SignInAttemptId } from '@deepseek-ai/dsh-deepseek-account/types'
import type { AccountClientMetadata, AccountView, SignInAttemptId } from '@deepseek-ai/dsh-deepseek-account/types'
/** Authenticated unary caller shared with native onboarding. */
export type AccountInvoke = (request: { namespace: string; method: string; args: Record<string, unknown> }) => Promise<unknown>
@@ -57,12 +57,12 @@ function validateBrowserDestination(value: string): void {
export interface DesktopAccountBackend {
/** @returns current account snapshot. */
state(): Promise<AccountView>
/** @param locale - active desktop UI language. @returns new or already-running login attempt. */
start(locale: string): Promise<AccountView>
/** @param client - this window's identity and current language. @returns new or already-running login attempt. */
start(client: AccountClientMetadata): Promise<AccountView>
/** @param id - attempt to cancel. @returns cancellation or completed commit state. */
cancel(id: SignInAttemptId): Promise<AccountView>
/** @returns state after local sign-out. */
signOut(): Promise<AccountView>
/** @param client - this window's identity. @returns state after local sign-out. */
signOut(client: AccountClientMetadata): Promise<AccountView>
/**
* @param listener - state recipient.
* @param failed - stream failure recipient.
@@ -83,8 +83,9 @@ export function desktopAccountBackend(origin: string, invoke: AccountInvoke, coo
const call = async (method: string, args: Record<string, unknown> = {}): Promise<AccountView> =>
accountView(await invoke({ namespace: 'account', method, args }))
return {
state: () => call('getState'), start: locale => call('startSignIn', { locale, callbackOrigin: new URL(origin).origin, loginSource: 'desktop' }),
cancel: attemptId => call('cancelSignIn', { attemptId }), signOut: () => call('signOut'),
state: () => call('getState'),
start: client => call('startSignIn', { client, callbackOrigin: new URL(origin).origin, loginSource: 'desktop' }),
cancel: attemptId => call('cancelSignIn', { attemptId }), signOut: client => call('signOut', { client }),
watch(listener, failed, expired) {
let closed = false
let socket: WebSocket | undefined
+29
View File
@@ -0,0 +1,29 @@
/** Desktop client identity for one Platform account call. */
import type { AccountClientMetadata } from '@deepseek-ai/dsh-deepseek-account/types'
/**
* Read the client build version inlined by the Desktop build.
* @returns the version embedded in this application build.
* @throws Error when the build carries no client version, instead of reporting a guessed one.
*/
export function desktopClientVersion(): string {
const version = process.env.DSH_CLIENT_VERSION
if (version === undefined || version === '') {
throw new Error('desktop account: this application build carries no DSH_CLIENT_VERSION')
}
return version
}
/**
* Sample the Desktop client identity for one Platform request.
* @param locale - current resolved Desktop language.
* @returns this call's build version, the raw active language, and the UTC offset in whole seconds east.
*/
export function desktopClientMetadata(locale: string): AccountClientMetadata {
return {
version: desktopClientVersion(),
locale,
// Date.getTimezoneOffset reports minutes west of UTC; Platform wants seconds east.
timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60,
}
}
+5 -9
View File
@@ -43,6 +43,7 @@ import { DesktopUpdateSchedule, resolveDesktopUpdateScheduleConfig } from './upd
import { desktopUpdateErrorSummary, presentDesktopUpdate } from './update-presentation.ts'
import { desktopErrorState } from './startup-error.ts'
import { DesktopMandatoryUpdatePolicy, resolveDesktopPolicyConfig, type DesktopPolicyState } from './mandatory-update-policy.ts'
import { desktopClientMetadata } from './client-metadata.ts'
import { DesktopMandatoryUpdateWindow } from './mandatory-update-window.ts'
import { DesktopPolicyTestAuth } from './policy-test-auth.ts'
import { DesktopUpdateDialog, type UpdateDialogOptions } from './update-dialog.ts'
@@ -380,7 +381,7 @@ async function main(): Promise<void> {
return next.promise
}
const platformView = new DesktopPlatformView(join(app.getAppPath(), 'lib', 'preload-platform-account.cjs'),
() => locale.id === 'zh-CN' ? 'zh_CN' : 'en_US')
() => locale.id === 'zh-CN' ? 'zh_CN' : 'en_US', process.platform === 'win32' ? 'win32' : 'darwin')
const backend = new DesktopBackendController((onFailure) => {
const hostInspectPort = developmentHostInspectPort(development)
const host = new DesktopHostProcess(resources.node, resources.dsh, activeProject,
@@ -997,7 +998,7 @@ async function main(): Promise<void> {
},
startSignIn: async () => {
if (welcomeBackend === undefined) throw new Error('desktop welcome: backend unavailable')
return welcomeBackend.account.start(locale.id)
return welcomeBackend.account.start(desktopClientMetadata(locale.id))
},
cancelSignIn: async (id) => {
if (welcomeBackend === undefined) throw new Error('desktop welcome: backend unavailable')
@@ -1111,16 +1112,11 @@ async function main(): Promise<void> {
() => mandatoryUI?.confirmationWindow ?? currentDialogWindow(),
(event) => { console.info(`desktop policy authentication: ${event}`); updateJournal?.action(`policy-login-${event}`) })
}
const bundleId = app.isPackaged
? ('dshDesktopAppId' in manifest ? manifest.dshDesktopAppId : undefined)
: process.env.DSH_DESKTOP_APP_ID
if (typeof bundleId !== 'string' || bundleId.trim() === '') throw new Error('desktop policy: missing application bundle ID')
if (!['win32', 'darwin'].includes(process.platform) || !['x64', 'arm64'].includes(process.arch)) throw new Error('desktop policy: unsupported platform')
let wasBlocking = false
mandatoryPolicy = new DesktopMandatoryUpdatePolicy(policyConfig, {
platform: process.platform as 'win32' | 'darwin', arch: process.arch as 'x64' | 'arm64',
version: app.getVersion(), bundledDshVersion: app.isPackaged ? readDesktopRuntime(resources.dsh).release.version : app.getVersion(),
bundleId, locale: locale.id,
bundledDshVersion: app.isPackaged ? readDesktopRuntime(resources.dsh).release.version : app.getVersion(),
}, (state) => {
if (state.error !== 'authentication-required') policyAuthenticationQueued = false
if (state.blocking) {
@@ -1130,7 +1126,7 @@ async function main(): Promise<void> {
mandatoryUI?.sync()
if (state.blocking && !wasBlocking) void updateSchedule.check(false, true).catch((error: unknown) => { console.error(error) })
wasBlocking = state.blocking
}, policyAuth?.request)
}, policyAuth?.request, () => desktopClientMetadata(locale.id))
const policy = mandatoryPolicy
mandatoryUI = new DesktopMandatoryUpdateWindow({
overlays: updateOverlays,
+17 -16
View File
@@ -1,15 +1,12 @@
/** Mandatory-update policy, independent of local business traffic and updater artifacts. */
import { valid } from 'semver'
import { desktopClientHeaders } from '@deepseek-ai/dsh-deepseek-account'
import { platformClientHeaders, type AccountClientMetadata } from '@deepseek-ai/dsh-deepseek-account'
/** Installed release identity; no field is supplied by a renderer. */
export interface DesktopPolicyIdentity {
readonly platform: 'win32' | 'darwin'
readonly version: string
readonly bundledDshVersion: string
readonly bundleId: string
readonly locale: string
readonly arch: 'x64' | 'arm64'
}
@@ -127,7 +124,7 @@ function parsePolicy(body: unknown, ok: boolean, config: DesktopPolicyConfig): D
throw new Error('desktop policy: response does not contain a valid mandatory or no-force decision')
}
/** Owns one immutable installed-client context, its in-flight request, and polling schedule. */
/** Owns one installed-client context, its in-flight request, and polling schedule. */
export class DesktopMandatoryUpdatePolicy {
private current: DesktopPolicyState = { blocking: false, checking: false }
private pending: Promise<DesktopPolicyState> | undefined
@@ -136,30 +133,34 @@ export class DesktopMandatoryUpdatePolicy {
private disposed = false
private failures = 0
private nextCheck = -Infinity
private readonly headers: Readonly<Record<string, string>>
/**
* @param config - Resolved deployment settings.
* @param identity - Installed software identity, copied once for this lifetime.
* @param identity - Installed software identity, fixed for this application build.
* @param publish - Receives policy changes without controlling downloads or existing tasks.
* @param request - Anonymous Fetch or the dedicated test-authentication Session transport.
* @param client - UI build version, language, and UTC offset, sampled for every check.
* @param random - Jitter source, replaceable for clock-driven tests.
*/
constructor(
private readonly config: DesktopPolicyConfig,
identity: DesktopPolicyIdentity,
private readonly identity: DesktopPolicyIdentity,
private readonly publish: (state: DesktopPolicyState) => void,
private readonly request: typeof fetch = fetch,
private readonly client: () => AccountClientMetadata,
private readonly random: () => number = Math.random,
) {
if (valid(identity.version) === null || valid(identity.bundledDshVersion) === null || identity.bundleId.trim() === ''
if (valid(this.client().version) === null || valid(identity.bundledDshVersion) === null
|| (identity.platform === 'win32' && identity.arch !== 'x64')) throw new Error('desktop policy: invalid installed client identity')
this.headers = Object.freeze({
...desktopClientHeaders(identity.platform), 'x-client-version': identity.version,
'x-client-bundle-id': identity.bundleId, 'x-client-locale': identity.locale,
'x-client-arch': identity.arch, 'x-client-update-channel': 'nightly',
'x-client-bundled-dsh-version': identity.bundledDshVersion,
})
}
/** Platform headers for one check; the calling UI's language and UTC offset are read now. */
private requestHeaders(): Record<string, string> {
return {
...platformClientHeaders(this.identity.platform, this.client()),
'x-client-arch': this.identity.arch, 'x-client-update-channel': 'nightly',
'x-client-bundled-dsh-version': this.identity.bundledDshVersion,
}
}
/** Latest policy; failures never erase a known mandatory decision. */
@@ -185,7 +186,7 @@ export class DesktopMandatoryUpdatePolicy {
try {
const url = new URL('/api/v0/check_client_update', this.config.origin)
url.searchParams.set('scenario', scenario)
const response = await this.request(url, { headers: this.headers, signal: controller.signal,
const response = await this.request(url, { headers: this.requestHeaders(), signal: controller.signal,
credentials: this.config.authentication === 'feishu-test' ? 'include' : 'omit', cache: 'no-store', redirect: 'error' })
const body: unknown = await response.json()
if (this.config.authentication === 'feishu-test' && response.status === 401
+17 -6
View File
@@ -2,7 +2,8 @@
import type { EventEmitter } from 'node:events'
import { randomUUID } from 'node:crypto'
import { WebContentsView, session, shell, type View, type WebFrameMain } from 'electron'
import { mergePlatformCookies, type PlatformSession } from '@deepseek-ai/dsh-deepseek-account'
import { mergePlatformCookies, platformClientHeaders, type PlatformSession } from '@deepseek-ai/dsh-deepseek-account'
import { desktopClientMetadata } from './client-metadata.ts'
import { PLATFORM_IPC, type PlatformLocale } from './platform-ipc.ts'
@@ -47,8 +48,10 @@ export class DesktopPlatformView {
/**
* @param preload - bundled sandboxed Platform preload path.
* @param getLocale - current resolved Desktop language.
* @param platform - operating system this shell runs on, reported to Platform.
*/
constructor(private readonly preload: string, private readonly getLocale: () => PlatformLocale) {}
constructor(private readonly preload: string, private readonly getLocale: () => PlatformLocale,
private readonly platform: 'darwin' | 'win32') {}
/** @param next - private Host credential snapshot; replacement invalidates the current document. */
setSession(next: PlatformSession | null): void {
@@ -75,6 +78,9 @@ export class DesktopPlatformView {
browserSession.setPermissionRequestHandler((_contents, _permission, callback) => { callback(false) })
browserSession.setPermissionCheckHandler(() => false)
const deploymentHeaders = account.requestHeaders ?? {}
const injectedNames = new Set([...Object.keys(deploymentHeaders),
...Object.keys(platformClientHeaders(this.platform, desktopClientMetadata(this.getLocale())))]
.map(name => name.toLowerCase()))
const injectedRequests = new Set<number>()
browserSession.webRequest.onCompleted((details) => { injectedRequests.delete(details.id) })
browserSession.webRequest.onErrorOccurred((details) => { injectedRequests.delete(details.id) })
@@ -82,12 +88,17 @@ export class DesktopPlatformView {
let headers = Object.fromEntries(Object.entries(details.requestHeaders).map(([name, value]) => [name.toLowerCase(), value]))
if (new URL(details.url).origin === account.origin) {
injectedRequests.add(details.id)
// The document identifies the UI that is asking, so language and UTC offset are sampled now.
const injected = {
...deploymentHeaders,
...platformClientHeaders(this.platform, desktopClientMetadata(this.getLocale())),
}
const cookie = headers.cookie ?? ''
Object.assign(headers, deploymentHeaders)
if (deploymentHeaders.cookie !== undefined) headers.cookie = mergePlatformCookies(cookie, deploymentHeaders.cookie)
Object.assign(headers, injected)
if (injected.cookie !== undefined) headers.cookie = mergePlatformCookies(cookie, injected.cookie)
} else if (injectedRequests.has(details.id)) {
// Redirected subresources must not carry deployment headers to another origin.
headers = Object.fromEntries(Object.entries(headers).filter(([name]) => !Object.hasOwn(deploymentHeaders, name)))
// Redirected subresources must not carry injected headers to another origin.
headers = Object.fromEntries(Object.entries(headers).filter(([name]) => !injectedNames.has(name.toLowerCase())))
}
callback({ requestHeaders: headers })
})
+7 -2
View File
@@ -23,8 +23,13 @@ it('uses account Remote commands without returning additional wire fields', asyn
requests.push(request)
return Promise.resolve({ links: { usageUrl: 'http://localhost/usage', topUpUrl: 'http://localhost/top_up' }, status: 'signed-out', attempt: null, token: 'private' })
}, () => Promise.resolve(''))
expect(await backend.start('en')).toEqual({ links: { usageUrl: 'http://localhost/usage', topUpUrl: 'http://localhost/top_up' }, status: 'signed-out', attempt: null })
expect(requests).toEqual([{ namespace: 'account', method: 'startSignIn', args: { locale: 'en', callbackOrigin: 'http://127.0.0.1:1234', loginSource: 'desktop' } }])
const client = { version: '1.2.3', locale: 'en', timezoneOffsetSeconds: 28_800 }
expect(await backend.start(client)).toEqual({ links: { usageUrl: 'http://localhost/usage', topUpUrl: 'http://localhost/top_up' }, status: 'signed-out', attempt: null })
expect(await backend.signOut(client)).toMatchObject({ status: 'signed-out' })
expect(requests).toEqual([
{ namespace: 'account', method: 'startSignIn', args: { client, callbackOrigin: 'http://127.0.0.1:1234', loginSource: 'desktop' } },
{ namespace: 'account', method: 'signOut', args: { client } },
])
})
it('does not replay an expiry reason from an account snapshot', () => {
@@ -0,0 +1,18 @@
import { afterEach, expect, it, vi } from 'vitest'
import { desktopClientMetadata, desktopClientVersion } from '../src/client-metadata.ts'
afterEach(() => { vi.unstubAllEnvs() })
it('reports the inlined client build version and the requested language', () => {
vi.stubEnv('DSH_CLIENT_VERSION', '1.2.3')
expect(desktopClientVersion()).toBe('1.2.3')
expect(desktopClientMetadata('zh-CN')).toMatchObject({ version: '1.2.3', locale: 'zh-CN',
timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60 })
})
it('refuses a build that carries no client version instead of guessing one', () => {
vi.stubEnv('DSH_CLIENT_VERSION', undefined)
expect(() => desktopClientVersion()).toThrow(/DSH_CLIENT_VERSION/)
vi.stubEnv('DSH_CLIENT_VERSION', '')
expect(() => desktopClientMetadata('en')).toThrow(/DSH_CLIENT_VERSION/)
})
+3 -2
View File
@@ -240,8 +240,9 @@ async function main() {
const config = resolveDesktopPolicyConfig({ origin: server.url, allowedPageOrigins: ['https://downloads.example.com'],
intervalMs: 600_000, timeoutMs: 1000 }, true)
let modal
const policy = new DesktopMandatoryUpdatePolicy(config, { platform: 'win32', arch: 'x64',
version: '1.0.0', bundledDshVersion: '1.0.0', bundleId: 'com.deepseek.dsh', locale: 'zh-CN' }, () => { modal?.sync() })
const policy = new DesktopMandatoryUpdatePolicy(config, { platform: 'win32', arch: 'x64', bundledDshVersion: '1.0.0' },
() => { modal?.sync() }, undefined,
() => ({ version: '1.0.0', locale: 'zh-CN', timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60 }))
modal = new DesktopMandatoryUpdateWindow({
preload: fileURLToPath(new URL('../../lib/preload-mandatory.cjs', import.meta.url)),
locale: resolveDesktopLocale('zh-CN'), allowedPageOrigins: config.allowedPageOrigins,
+6 -1
View File
@@ -346,6 +346,7 @@ beforeEach(() => {
vi.stubEnv('DSH_DESKTOP_DEV_PROJECT_DIR', undefined)
vi.stubEnv('DSH_DESKTOP_MANDATORY_UPDATE_CONFIG', undefined)
vi.stubEnv('DSH_DESKTOP_UPDATE_JOURNAL_DIR', undefined)
vi.stubEnv('DSH_CLIENT_VERSION', '1.2.3')
})
afterEach(async () => {
@@ -1074,7 +1075,11 @@ describe('desktop main startup', () => {
expect(modal.isDestroyed()).toBe(false)
expect(modal.webContents.send.mock.calls.at(-1)).toMatchObject([MANDATORY_IPC.state, { policy: { blocking: false } }])
expect(host.stop).not.toHaveBeenCalled()
expect(request.mock.calls[0]![1]!.headers).toMatchObject({ 'x-client-bundle-id': 'com.deepseek.dsh', 'x-client-version': '1.0.0' })
expect(request.mock.calls[0]![1]!.headers).toMatchObject({
'x-client-bundle-id': '', 'x-client-platform': 'desktop-win', 'x-client-version': '1.2.3',
'x-client-arch': 'x64', 'x-client-update-channel': 'nightly', 'x-client-bundled-dsh-version': '1.0.0',
'x-client-locale': 'en_US', 'x-client-timezone-offset': String(-new Date().getTimezoneOffset() * 60),
})
})
it('keeps one checking dialog open until the manual check settles, then reports the current version', async () => {
@@ -1,8 +1,9 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { AccountClientMetadata } from '@deepseek-ai/dsh-deepseek-account/types'
import { DesktopMandatoryUpdatePolicy, desktopPolicyPage, resolveDesktopPolicyConfig, type DesktopPolicyState } from '../src/mandatory-update-policy.ts'
const identity = { platform: 'win32', arch: 'x64', version: '0.1.5-rc.1', bundledDshVersion: '0.1.5-rc.1',
bundleId: 'com.deepseek.dsh', locale: 'zh-CN' } as const
const identity = { platform: 'win32', arch: 'x64', bundledDshVersion: '0.1.5-rc.1' } as const
const client: AccountClientMetadata = { version: '1.2.3', locale: 'zh-CN', timezoneOffsetSeconds: 28_800 }
const force = { code: 40005, data: { show_content: { title: '<b>Update</b>', detail: 'Required upgrade' },
desktop_app_link: 'https://downloads.example.com/desktop?os=win' } }
const clear = { code: 0, msg: '', data: { biz_code: 0, biz_msg: '', biz_data: null } }
@@ -10,11 +11,12 @@ const deployment = { origin: 'https://policy.example.com', allowedPageOrigins: [
intervalMs: 10_000, timeoutMs: 1_000, maxBackoffMs: 80_000, jitter: 0 }
const instances: DesktopMandatoryUpdatePolicy[] = []
function fixture(authentication: 'anonymous' | 'feishu-test' = 'anonymous') {
function fixture(authentication: 'anonymous' | 'feishu-test' = 'anonymous',
clientSource: () => AccountClientMetadata = () => client) {
const request = vi.fn<typeof fetch>().mockResolvedValue(Response.json(clear))
const publish = vi.fn<(state: DesktopPolicyState) => void>()
const policy = new DesktopMandatoryUpdatePolicy(resolveDesktopPolicyConfig({ ...deployment, authentication, ...(authentication === 'feishu-test' ? { allowedAuthOrigins: ['https://login.example.com'] } : {}) })!,
identity, publish, request)
identity, publish, request, clientSource)
instances.push(policy)
return { policy, request, publish }
}
@@ -58,12 +60,23 @@ describe('mandatory update policy', () => {
expect(url).toBeInstanceOf(URL)
expect((url as URL).href).toBe('https://policy.example.com/api/v0/check_client_update?scenario=launch')
expect(options).toMatchObject({ credentials: 'omit', cache: 'no-store', redirect: 'error', headers: {
'x-client-platform': 'desktop-win', 'x-client-version': '0.1.5-rc.1', 'x-client-bundle-id': 'com.deepseek.dsh',
'x-client-locale': 'zh-CN', 'x-client-arch': 'x64', 'x-client-update-channel': 'nightly',
'x-client-platform': 'desktop-win', 'x-client-version': '1.2.3', 'x-client-bundle-id': '',
'x-client-locale': 'zh_CN', 'x-client-timezone-offset': '28800', 'x-client-arch': 'x64', 'x-client-update-channel': 'nightly',
'x-client-bundled-dsh-version': '0.1.5-rc.1',
} })
})
it('sends the language and UTC offset sampled for each check', async () => {
let current = { ...client, locale: 'en', timezoneOffsetSeconds: -18_000 }
const { policy, request } = fixture('anonymous', () => current)
await policy.check('launch')
current = { ...client, locale: 'zh-CN', timezoneOffsetSeconds: 28_800 }
await policy.check('manual', true)
const headers = request.mock.calls.map(call => (call[1]?.headers ?? {}) as Record<string, string>)
expect(headers[0]).toMatchObject({ 'x-client-locale': 'en_US', 'x-client-timezone-offset': '-18000' })
expect(headers[1]).toMatchObject({ 'x-client-locale': 'zh_CN', 'x-client-timezone-offset': '28800' })
})
it('coalesces manual checks and honors the interval on foreground/resume', async () => {
const { policy, request } = fixture()
const pending = Promise.withResolvers<Response>()
+59 -4
View File
@@ -1,5 +1,5 @@
import { EventEmitter } from 'node:events'
import { afterEach, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { DesktopPlatformView, platformBounds } from '../src/platform-view.ts'
const state = vi.hoisted(() => ({
@@ -30,16 +30,32 @@ vi.mock('electron', () => ({
},
}))
afterEach(() => { state.views.length = 0; state.sessions.length = 0; state.loadFailure = undefined; vi.clearAllMocks() })
beforeEach(() => { vi.stubEnv('DSH_CLIENT_VERSION', '1.2.3') })
afterEach(() => {
state.views.length = 0; state.sessions.length = 0; state.loadFailure = undefined
vi.clearAllMocks(); vi.unstubAllEnvs()
})
function setup() {
const removeChildView = vi.fn()
const owner = Object.assign(new EventEmitter(), {
webContents: new EventEmitter(), contentView: { addChildView: vi.fn(), removeChildView }, isDestroyed: () => false,
})
const manager = new DesktopPlatformView('/bundled/preload.cjs', () => 'en_US')
const manager = new DesktopPlatformView('/bundled/preload.cjs', () => 'en_US', 'darwin')
manager.setSession({ origin: 'https://platform.deepseek.com', token: 'fixture-secret' })
return { manager, owner, removeChildView }
}
/** Invoke the first registered header interceptor for one request. */
function interceptHeaders(id: number, url: string, requestHeaders: Record<string, string>): Record<string, string> {
const browserSession = state.sessions.at(-1) as { webRequest: { onBeforeSendHeaders: ReturnType<typeof vi.fn> } }
const handler = browserSession.webRequest.onBeforeSendHeaders.mock.calls[0]![0] as (
details: { id: number; url: string; requestHeaders: Record<string, string> },
callback: (value: { requestHeaders: Record<string, string> }) => void,
) => void
const callback = vi.fn()
handler({ id, url, requestHeaders }, callback)
return (callback.mock.calls[0]![0] as { requestHeaders: Record<string, string> }).requestHeaders
}
function view() {
return state.views.at(-1) as {
setVisible: ReturnType<typeof vi.fn>
@@ -107,6 +123,43 @@ it('blocks cross-origin navigation and redirects', async () => {
manager.close()
})
it('injects the deployment and client identity headers only at the Platform origin', async () => {
const { manager, owner } = setup()
manager.setSession({ origin: 'https://platform.deepseek.com', token: 'fixture-secret',
requestHeaders: { cookie: 'gate=synthetic', 'x-deployment': 'harness' } })
await manager.open(owner, 'usage', bounds)
const request = { Cookie: 'route=user', 'x-client-platform': 'stale', 'x-deployment': 'stale' }
expect(interceptHeaders(1, 'https://platform.deepseek.com/api/v0/users/current', request)).toEqual({
'x-deployment': 'harness', cookie: 'route=user; gate=synthetic', 'x-client-bundle-id': '',
'x-client-platform': 'desktop-mac', 'x-client-version': '1.2.3', 'x-client-locale': 'en_US',
'x-client-timezone-offset': String(-new Date().getTimezoneOffset() * 60),
})
// A redirect to another origin keeps the injected headers out of the follow-up request.
expect(interceptHeaders(1, 'https://login.example.com/authorize', request)).toEqual({})
manager.close()
})
it('samples the language and UTC offset on every Platform request', async () => {
let locale: 'en_US' | 'zh_CN' = 'en_US'
const owner = Object.assign(new EventEmitter(), {
webContents: new EventEmitter(), contentView: { addChildView: vi.fn(), removeChildView: vi.fn() }, isDestroyed: () => false,
})
const manager = new DesktopPlatformView('/bundled/preload.cjs', () => locale, 'darwin')
manager.setSession({ origin: 'https://platform.deepseek.com', token: 'fixture-secret' })
await manager.open(owner, 'usage', bounds)
const offset = vi.spyOn(Date.prototype, 'getTimezoneOffset').mockReturnValue(480)
expect(interceptHeaders(1, 'https://platform.deepseek.com/usage', {})).toMatchObject({
'x-client-locale': 'en_US', 'x-client-timezone-offset': '-28800',
})
locale = 'zh_CN'
offset.mockReturnValue(-300)
expect(interceptHeaders(1, 'https://platform.deepseek.com/usage', {})).toMatchObject({
'x-client-locale': 'zh_CN', 'x-client-timezone-offset': '18000',
})
offset.mockRestore()
manager.close()
})
it.each([null, {}, { ...bounds, width: NaN }, { ...bounds, x: -1 }, { ...bounds, y: Infinity }])('rejects malformed IPC rectangles', (value) => {
expect(() => platformBounds(value)).toThrow()
})
@@ -162,6 +215,8 @@ it('injects deployment headers only at the Platform origin and excludes them fro
intercept({ id: 1, url: `https://platform.deepseek.com${path}`, requestHeaders: { Cookie: 'route=old; browser=keep', Accept: 'application/json' } }, callback)
expect(callback).toHaveBeenLastCalledWith({ requestHeaders: {
cookie: 'route=new; browser=keep; gate=private', accept: 'application/json', 'x-private-gate': 'private', 'x-client-platform': 'desktop-mac',
'x-client-bundle-id': '', 'x-client-version': '1.2.3', 'x-client-locale': 'en_US',
'x-client-timezone-offset': String(-new Date().getTimezoneOffset() * 60),
} })
}
intercept({ id: 1, url: 'https://other.example/api', requestHeaders: {
@@ -242,7 +297,7 @@ it('does not reveal a pending view after the owner reloads or remove a replaceme
it('bootstraps the current language and updates an open view without reloading', async () => {
const { owner } = setup()
let locale: 'en_US' | 'zh_CN' = 'zh_CN'
const manager = new DesktopPlatformView('/bundled/preload.cjs', () => locale)
const manager = new DesktopPlatformView('/bundled/preload.cjs', () => locale, 'win32')
manager.setSession({ origin: 'https://platform.deepseek.com', token: 'fixture-secret' })
manager.notifyLocaleChanged()
await manager.open(owner, 'usage', bounds)
+15 -5
View File
@@ -10,6 +10,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import { DesktopHostProcess } from '../src/host-process.ts'
import { DesktopProjectManager } from '../src/project-manager.ts'
import { resolveDesktopPaths } from '../src/paths.ts'
import { desktopClientMetadata } from '../src/client-metadata.ts'
import { connectDesktopWelcome, type DesktopWelcomeBackend } from '../src/welcome-backend.ts'
import { DESKTOP_HOST_PROTOCOL_VERSION } from '../src/host-protocol.ts'
import { prepareDevelopmentProject } from '../scripts/development-project.ts'
@@ -27,7 +28,15 @@ async function mockPlatform() {
let init: Record<string, string> = {}
let failExchange = false
const server = createServer((req, res) => {
if (req.headers['x-client-platform'] !== (process.platform === 'win32' ? 'desktop-win' : 'desktop-mac')) { res.writeHead(400).end(); return }
// The real Host composition identifies every Platform request with the five client headers.
const expected = {
'x-client-bundle-id': '', 'x-client-platform': process.platform === 'win32' ? 'desktop-win' : 'desktop-mac',
'x-client-version': '1.2.3', 'x-client-locale': 'en_US',
'x-client-timezone-offset': String(-new Date().getTimezoneOffset() * 60),
}
for (const [name, value] of Object.entries(expected)) {
if (req.headers[name] !== value) { res.writeHead(400).end(); return }
}
if (req.headers.cookie !== 'test_gate=synthetic') { res.writeHead(403).end(); return }
if (req.url === '/auth-api/v0/users/logout' && req.method === 'POST') {
if (req.headers['x-dsh-auth-token'] !== 'dsh_mock_composition_test') { res.writeHead(401).end(); return }
@@ -75,6 +84,7 @@ async function mockPlatform() {
describe.skipIf(!existsSync(builtHost))('built Desktop welcome flow', () => {
it('persists explicit API keys and browser account login independently across Host restarts', async () => {
vi.stubEnv('DSH_CLIENT_VERSION', '1.2.3')
const root = mkdtempSync(join(tmpdir(), 'dsh-desktop-welcome-'))
let host: DesktopHostProcess | undefined
const platform = await mockPlatform()
@@ -147,7 +157,7 @@ describe.skipIf(!existsSync(builtHost))('built Desktop welcome flow', () => {
expect(await status()).toMatchObject({ hasApiKey: true, localePreference: 'zh' })
await restart()
expect(await status()).toMatchObject({ hasApiKey: true })
await backend!.account.start('en')
await backend!.account.start(desktopClientMetadata('en'))
await expect.poll(async () => (await backend!.account.state()).attempt?.phase).toBe('waiting-browser')
expect(new URL(platform.callback()).origin).toBe(hostOrigin)
platform.failExchange(true)
@@ -156,7 +166,7 @@ describe.skipIf(!existsSync(builtHost))('built Desktop welcome flow', () => {
expect(await backend!.account.state()).toMatchObject({ status: 'signed-out', attempt: { phase: 'failed' } })
expect(await status()).toMatchObject({ hasApiKey: true, loggedIn: false })
platform.failExchange(false)
await backend!.account.start('en')
await backend!.account.start(desktopClientMetadata('en'))
await expect.poll(async () => (await backend!.account.state()).attempt?.phase).toBe('waiting-browser')
const response = await fetch(platform.callback(), { redirect: 'manual' })
expect(response.status).toBe(302)
@@ -165,9 +175,9 @@ describe.skipIf(!existsSync(builtHost))('built Desktop welcome flow', () => {
await restart()
expect(await status()).toMatchObject({ hasApiKey: true, loggedIn: true })
// A failed remote logout must not block local sign-out through the real Host composition.
await backend!.account.signOut()
await backend!.account.signOut(desktopClientMetadata('en'))
expect(await status()).toMatchObject({ hasApiKey: true, loggedIn: false })
await backend!.account.start('en')
await backend!.account.start(desktopClientMetadata('en'))
await expect.poll(async () => (await backend!.account.state()).attempt?.phase).toBe('waiting-browser')
const waiting = await backend!.account.state()
const late = platform.callback()
+14
View File
@@ -2,10 +2,24 @@ import { defineConfig } from 'tsdown'
import { build } from 'vite'
import { fileURLToPath } from 'node:url'
import { readFile } from 'node:fs/promises'
import { repositoryClientBuildEnvironment, resolveClientBuildEnvironment } from '../../scripts/client-build-environment.ts'
const REPOSITORY_ROOT = fileURLToPath(new URL('../..', import.meta.url))
/** Use the client environment this build already received, otherwise the repository's own version and commit. */
const clientEnvironment = resolveClientBuildEnvironment(process.env.DSH_CLIENT_VERSION === undefined
? repositoryClientBuildEnvironment(REPOSITORY_ROOT, process.env)
: process.env)
const clientVersion = clientEnvironment.DSH_CLIENT_VERSION
if (clientVersion === undefined) throw new Error('desktop build: the client environment carries no DSH_CLIENT_VERSION')
/** Inline the one public build value the Node entry reads; every other variable stays a runtime lookup. */
const clientVersionDefine = { 'process.env.DSH_CLIENT_VERSION': JSON.stringify(clientVersion) }
export default defineConfig([
{
entry: ['lib/types/main.js'],
define: clientVersionDefine,
onSuccess: async () => {
await build({
configFile: false,
+572
View File
@@ -0,0 +1,572 @@
// Real-composition acceptance for the user-side bonus notice. The scaffold boots the
// shipped Web Loader tree, so the real account provider issues the bonus HTTP requests
// and the real client renders the card in a real Chromium. The loopback Platform double
// is the observation point: the scenario asserts what the product sent and what the user
// could read on screen.
import { createServer, type Server } from 'node:http'
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { chromium, type Browser, type Locator, type Page } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import {
assertFixtureInventory, compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode,
type WebScaffold,
} from './scaffold.ts'
import { openSettings, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/bonus-notice', import.meta.url))
const OVERLAY_TEMPLATE = fileURLToPath(new URL('./fixtures/bonus-notice/cordis.patch.yml', import.meta.url))
const MODE = webSnapshotMode()
const TOKEN = 'dsh_bonus_notice_test'
const ACCOUNT_ID = 'bonus-user'
const ORDER_FIRST = '22222222-2222-4222-8222-222222222222'
const ORDER_LATER = '33333333-3333-4333-8333-333333333333'
const ORDER_EN = '44444444-4444-4444-8444-444444444444'
const ORDER_RETRY = '55555555-5555-4555-8555-555555555555'
const ORDER_REPEAT = '66666666-6666-4666-8666-666666666666'
const ORDER_TOPUP = '77777777-7777-4777-8777-777777777777'
const CLAIM_DAYS = 30
/** Eligibility window the double reports; neither the card nor the golden renders a date. */
function expiresAt(): string {
return new Date(Date.now() + CLAIM_DAYS * 86_400_000).toISOString()
}
interface Granted {
readonly orderId: string
readonly amount: string
readonly zh_CN: string
readonly en_US: string
}
interface GetRecord {
readonly locale: string | undefined
readonly query: string
}
interface AckRecord {
readonly method: string
readonly path: string
readonly locale: string | undefined
readonly orderId: string | null
readonly body: string
readonly query: string
readonly contentType: string | undefined
/** Monotonic arrival time, so the backoff gap cannot be distorted by a wall-clock jump. */
readonly at: number
}
/** Loopback Platform double: auth gate, profile, wallets, unnotified bonuses, and the acknowledgement. */
async function mockPlatform() {
const unnotified: Granted[] = []
const gets: GetRecord[] = []
/** Reads already answered; a request the client is still awaiting is not evidence of a delivered response. */
const served: GetRecord[] = []
const acks: AckRecord[] = []
/** Wallet reads, one per balance refresh the settings panel performs. */
const summaries: GetRecord[] = []
let normalBalance = '12.34'
let bonusBalance = '5.00'
/** Order whose acknowledgement attempts the double fails, and how many failures it still owes. */
let ackFailure: { orderId: string; remaining: number } | undefined
/** Whether an acknowledged order stays unnotified, as a backend that has not applied the acknowledgement would serve it. */
let keepUnnotified = false
/** Refuse the wallet read, which is how a Platform outage or an unparsable payload reaches the client. */
let failSummary = false
/** Reply held back while the test controls when a read can answer. */
let heldReply: (() => void) | undefined
const grant = (orderId: string, amount: string): void => {
unnotified.unshift({
orderId, amount,
zh_CN: `已赠送您 ${amount} 元 DSH 体验赠金。`,
en_US: `You received a CNY ${amount} DSH trial credit.`,
})
}
/** @param value - granted bonus. @param locale - request language. @returns its wire fields. */
const wire = (value: Granted, locale: string | undefined): unknown => ({
order_id: value.orderId, campaign: 'dsh_login_bonus', amount: value.amount, currency: 'CNY',
granted_at: new Date().toISOString(), expires_at: expiresAt(),
msg: locale === 'en_US' ? value.en_US : value.zh_CN,
})
const server: Server = createServer((req, res) => {
const url = new URL(req.url ?? '/', 'http://127.0.0.1')
const header = req.headers['x-client-locale']
const locale = typeof header === 'string' ? header : undefined
const reply = (payload: unknown): void => {
res.writeHead(200, { 'content-type': 'application/json' })
res.end(JSON.stringify(payload))
}
if (req.headers['x-dsh-auth-token'] !== TOKEN) { res.writeHead(401).end(); return }
if (url.pathname === '/auth-api/v0/users/current') {
// `email` is required by the provider's profile schema even when the account has none.
reply({ code: 0, data: { biz_code: 0, biz_data: {
id: ACCOUNT_ID, email: '', id_profile: { name: 'Bonus User', picture: null },
} } })
return
}
if (url.pathname === '/api/v0/users/get_user_summary') {
summaries.push({ locale, query: url.search })
if (failSummary) { res.writeHead(500, { 'content-type': 'text/plain' }).end('Internal Server Error'); return }
reply({ code: 0, data: { biz_code: 0, biz_data: {
normal_wallets: [{ currency: 'CNY', balance: normalBalance }],
bonus_wallets: [{ currency: 'CNY', balance: bonusBalance }],
} } })
return
}
if (url.pathname === '/api/v0/users/get_unnotified_bonuses') {
const record = { locale, query: url.search }
gets.push(record)
const payload = { code: 0, data: { biz_code: 0, biz_data: unnotified.map(value => wire(value, locale)) } }
const answer = (): void => { served.push(record); reply(payload) }
if (heldReply === undefined) { answer(); return }
heldReply = answer
return
}
if (url.pathname === '/api/v0/users/ack_bonus_notified' && req.method === 'POST') {
let body = ''
req.setEncoding('utf8')
req.on('data', (chunk: string) => { body += chunk })
req.on('end', () => {
const payload: unknown = JSON.parse(body)
const orderId = typeof payload === 'object' && payload !== null && 'order_id' in payload
&& typeof payload.order_id === 'string' ? payload.order_id : null
acks.push({ method: req.method ?? '', path: url.pathname, locale, orderId, body,
query: url.search, contentType: req.headers['content-type'], at: performance.now() })
if (ackFailure !== undefined && ackFailure.orderId === orderId && ackFailure.remaining > 0) {
ackFailure.remaining--
// A transient gateway failure: the client retries it after its backoff.
res.writeHead(502, { 'content-type': 'text/plain' }).end('Internal Server Error')
return
}
if (orderId === null) {
reply({ code: 0, msg: '', data: { biz_code: 1, biz_msg: 'BONUS_ORDER_NOT_FOUND', biz_data: null } }); return
}
const index = unnotified.findIndex(value => value.orderId === orderId)
if (index >= 0 && !keepUnnotified) unnotified.splice(index, 1)
reply({ code: 0, msg: '', data: { biz_code: 0, biz_msg: '', biz_data: null } })
})
return
}
if (url.pathname === '/auth-api/v0/users/logout') { reply({ code: 0, data: { biz_code: 0, biz_data: null } }); return }
res.writeHead(404).end()
})
await new Promise<void>((resolve) => { server.listen(0, '127.0.0.1', resolve) })
const address = server.address()
if (address === null || typeof address === 'string') throw new Error('bonus notice: missing mock listener')
return {
origin: `http://127.0.0.1:${String(address.port)}`,
grant, gets, served, acks, summaries,
/** @param value - recharge balance the settings page reads next. */
setNormalBalance: (value: string): void => { normalBalance = value },
/** @param value - bonus balance the settings page reads next. */
setBonusBalance: (value: string): void => { bonusBalance = value },
/** @param value - whether the wallet read fails. */
setFailSummary: (value: boolean): void => { failSummary = value },
/** Drop every unnotified grant, so a later scenario starts from a server with nothing to show. */
clearUnnotified: (): void => { unnotified.length = 0 },
/** @param value - whether acknowledged orders keep arriving in the unnotified read. */
setKeepUnnotified: (value: boolean): void => { keepUnnotified = value },
/** @param orderId - order to refuse. @param count - its acknowledgement attempts that fail before one succeeds. */
failNextAcks: (orderId: string, count: number): void => { ackFailure = { orderId, remaining: count } },
/** Hold the next unnotified-bonus read so the test can observe the in-flight state. */
holdNextGet: (): void => { heldReply = () => undefined },
releaseGet: (): void => { const held = heldReply; heldReply = undefined; held?.() },
close: () => new Promise<void>((resolve) => { server.close(() => { resolve() }); server.closeAllConnections() }),
}
}
/** @param page - page under test. @returns every rendered bonus notice card. */
function noticeCards(page: Page): Locator {
return page.locator('aside').filter({ hasText: '赠金已到账' })
}
/** @param card - a rendered notice card. @returns its non-empty visible lines joined for the golden. */
async function cardText(card: Locator): Promise<string> {
return (await card.innerText()).split('\n').map(line => line.trim()).filter(line => line !== '').join(' / ')
}
/** Wait for the card carrying this copy, then return the text the user could read from it. */
async function shownNotice(page: Page, copy: string): Promise<string> {
const card = noticeCards(page).filter({ hasText: copy })
await card.waitFor({ state: 'visible', timeout: 30_000 })
return cardText(card)
}
/** Wait for the double to answer this many acknowledgements, then return the order ids in arrival order. */
async function acked(platform: { acks: AckRecord[] }, count: number): Promise<string> {
await expect.poll(() => platform.acks.length, { timeout: 30_000 }).toBe(count)
return platform.acks.map(item => String(item.orderId)).join(',')
}
/**
* Native Platform bridge calls recorded in one page. `setBounds` is resize-driven and is
* deliberately not recorded, so the calls here are the opens and closes a user caused.
* @param page - page under test.
* @returns the recorded call log in order.
*/
async function platformCalls(page: Page): Promise<readonly string[]> {
return page.evaluate(() => (globalThis as typeof globalThis & { __bonusPlatformCalls?: readonly string[] })
.__bonusPlatformCalls ?? [])
}
describe.skipIf(MODE === 'record')('web e2e: bonus notice', () => {
let root: string | undefined
let scaffold: WebScaffold
let browser: Browser
let platform: Awaited<ReturnType<typeof mockPlatform>>
const tripwires: ReturnType<typeof watchConsole>[] = []
/** @param locale - browser language driving the requested notice locale. @returns a desktop-renderer page. */
const openDesktopPage = async (locale: string): Promise<Page> => {
const context = await browser.newContext({ viewport: { width: 1440, height: 960 }, locale })
const opened = await context.newPage()
tripwires.push(watchConsole(opened))
await opened.addInitScript(() => {
Object.defineProperty(globalThis, 'dshDesktop', { value: { protocolVersion: 1 } })
// The embedded Platform view is a native child window owned by the Desktop shell.
// The double answers immediately and records the calls, so the scenario observes
// which page was opened and that returning destroyed the view.
const calls: string[] = []
Object.defineProperty(globalThis, '__bonusPlatformCalls', { value: calls })
Object.defineProperty(globalThis, 'dshPlatform', {
value: {
open: (page: string) => { calls.push(`open:${page}`); return Promise.resolve() },
setBounds: () => Promise.resolve(),
close: () => { calls.push('close'); return Promise.resolve() },
},
})
})
return opened
}
beforeAll(async () => {
platform = await mockPlatform()
platform.grant(ORDER_FIRST, '5.00')
root = await mkdtemp(join(tmpdir(), 'dsh-bonus-notice-'))
const home = join(root, 'home')
await mkdir(home, { recursive: true })
// A stored grant bound to the double's origin starts the account credential-stored
// without driving the browser sign-in flow.
await writeFile(join(home, '.credentials.yaml'),
`version: 1\nrefs: {}\nrecords:\n deepseek-account-platform/default:\n kind: grant\n payload:\n version: 1\n token: ${TOKEN}\n issuer: ${platform.origin}\n`,
{ mode: 0o600 })
const overlay = join(root, 'bonus-notice.overlay.yml')
await writeFile(overlay, (await readFile(OVERLAY_TEMPLATE, 'utf8')).replaceAll('{{origin}}', platform.origin))
scaffold = await launchWebScaffold({ harnessHome: home, extraOverlayPath: overlay })
browser = await chromium.launch()
})
afterAll(async () => {
await browser?.close()
await scaffold?.close()
await platform?.close()
if (root !== undefined) await rm(root, { recursive: true, force: true })
})
it('acknowledges a notice shown under settings, refreshes once per entry, and localizes the request', async () => {
const zhFirst = '已赠送您 5.00 元 DSH 体验赠金。'
const zhLater = '已赠送您 8.00 元 DSH 体验赠金。'
const enBonus = 'You received a CNY 9.00 DSH trial credit.'
const observations: string[] = []
const page = await openDesktopPage('zh-CN')
onTestFailed(() => saveFailureShot(page, 'web-e2e-bonus-notice'))
/** @returns every browser storage key of the started page, so a baseline can be taken before a notice shows. */
const storageKeys = (): Promise<string[]> => page.evaluate(() => Object.keys(localStorage).sort())
// A read the renderer never received cannot become a displayed notice: while the
// response is held there is no card and no acknowledgement.
platform.holdNextGet()
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
await expect.poll(() => platform.gets.length, { timeout: 30_000 }).toBe(1)
observations.push(`startup.held cards=${String(await noticeCards(page).count())} acks=${String(platform.acks.length)}`)
expect(await noticeCards(page).count()).toBe(0)
expect(platform.acks).toEqual([])
platform.releaseGet()
// Startup signs in and reads the notice; the card shows it and only then is it acknowledged.
observations.push(`startup.notice=${await shownNotice(page, zhFirst)}`)
observations.push(`startup.ack orders=${await acked(platform, 1)}`)
const get = platform.gets[0]!
const ack = platform.acks[0]!
observations.push(`startup.get locale=${String(get.locale)} query=${get.query}`)
observations.push(`startup.ack method=${ack.method} path=${ack.path} locale=${String(ack.locale)} order_id=${String(ack.orderId)} body=${JSON.stringify(ack.body)}`)
expect(get.locale).toBe('zh_CN')
expect(get.query).toBe('')
expect(ack).toMatchObject({
method: 'POST', path: '/api/v0/users/ack_bonus_notified', locale: 'zh_CN', orderId: ORDER_FIRST,
body: JSON.stringify({ order_id: ORDER_FIRST }), query: '', contentType: 'application/json',
})
// Closing the card only hides it; the acknowledgement already sent is not repeated.
await noticeCards(page).getByRole('button', { name: '关闭', exact: true }).click()
expect(await noticeCards(page).count()).toBe(0)
expect(platform.acks).toHaveLength(1)
// A new grant and balance arrive before settings opens; one panel entry refreshes both once.
platform.grant(ORDER_LATER, '8.00')
platform.setBonusBalance('13.00')
const getsBefore = platform.gets.length
const summariesBefore = platform.summaries.length
// Hold the panel's notice read so the test observes its in-flight state instead of
// racing a fast local response.
platform.holdNextGet()
await openSettings(page, 'zh')
const settings = page.getByRole('dialog', { name: '设置', exact: true })
await settings.waitFor()
await expect.poll(() => platform.gets.length, { timeout: 30_000 }).toBe(getsBefore + 1)
// One open, one read: no control in the panel drives a second one.
expect(await settings.getByRole('button', { name: '刷新余额', exact: true }).count()).toBe(0)
observations.push(`open.held cards=${String(await noticeCards(page).count())} acks=${String(platform.acks.length)} gets=${String(platform.gets.length - getsBefore)}`)
expect(await noticeCards(page).count()).toBe(0)
expect(platform.acks).toHaveLength(1)
expect(platform.summaries.length).toBeGreaterThan(summariesBefore)
platform.releaseGet()
// The answered notice is displayed and acknowledged while the panel stays open.
const openNotice = await shownNotice(page, zhLater)
observations.push(`open.shown notice=${openNotice}`)
observations.push(`open.shown ack orders=${await acked(platform, 2)}`)
// Hit-testing the card's own point proves the settings layer paints above it.
const paintedUnderOverlay = await noticeCards(page).filter({ hasText: zhLater }).evaluate((element) => {
const rect = element.getBoundingClientRect()
const hit = document.elementFromPoint(rect.left + rect.width / 2, rect.top + 20)
return hit !== null && !element.contains(hit) && hit.closest('[role="presentation"]') !== null
})
observations.push(`open.shown painted-under-settings=${String(paintedUnderOverlay)} cards=${String(await noticeCards(page).count())} acks=${String(platform.acks.length)}`)
expect(paintedUnderOverlay).toBe(true)
await expect.poll(async () => (await settings.textContent()) ?? '', { timeout: 30_000 }).toContain('¥13.00')
// Still one read after the refresh settled.
expect(platform.gets.length).toBe(getsBefore + 1)
const openGet = platform.served.at(-1)!
const panel = (await settings.textContent()) ?? ''
const usage = await settings.getByRole('link', { name: '查询用量', exact: true }).getAttribute('href')
observations.push(`open.get locale=${String(openGet.locale)} query=${openGet.query} served=true gets=${String(platform.gets.length - getsBefore)}`)
observations.push(`open.balance bonus-row=${String(panel.includes('赠金余额'))} amount=${String(panel.includes('¥13.00'))} dated=${String(/\d{4}-\d{2}-\d{2}/.test(panel))}`)
observations.push(`open.cards-while-open=${String(await noticeCards(page).count())} acks=${String(platform.acks.length)}`)
observations.push(`open.usage-link=${String(usage).replace(platform.origin, '{{origin}}')}`)
expect(openGet.locale).toBe('zh_CN')
expect(openGet.query).toBe('')
expect(panel).toContain('赠金余额')
expect(panel).toContain('¥13.00')
expect(panel).not.toMatch(/\d{4}-\d{2}-\d{2}/)
expect(usage).toBe(`${platform.origin}/usage`)
expect(await noticeCards(page).count()).toBe(1)
expect(platform.acks).toHaveLength(2)
// Closing settings repeats no acknowledgement and leaves the card on the sidebar.
const launcher = page.getByRole('button', { name: '账号菜单', exact: true })
await page.keyboard.press('Escape')
await settings.waitFor({ state: 'detached', timeout: 30_000 })
// The shell restores focus to the launcher once the close commits.
await expect.poll(() => launcher.evaluate(element => element === document.activeElement), { timeout: 30_000 }).toBe(true)
observations.push(`open.closed settings cards=${String(await noticeCards(page).count())} acks=${String(platform.acks.length)}`)
expect(await noticeCards(page).count()).toBe(1)
expect(platform.acks).toHaveLength(2)
// Closing the card hides it; the double already stopped serving this acknowledged order.
await noticeCards(page).getByRole('button', { name: '关闭', exact: true }).click()
expect(await noticeCards(page).count()).toBe(0)
expect(platform.acks).toHaveLength(2)
observations.push(`open.dismissed cards=0 acks=${String(platform.acks.length)}`)
// The next entry refreshes once more; switching sections inside one open is not an entry.
const reopenGetsBefore = platform.gets.length
const reopenSummariesBefore = platform.summaries.length
await openSettings(page, 'zh')
const reopened = page.getByRole('dialog', { name: '设置', exact: true })
await reopened.waitFor()
await expect.poll(() => platform.gets.length, { timeout: 30_000 }).toBe(reopenGetsBefore + 1)
await expect.poll(() => platform.summaries.length, { timeout: 30_000 }).toBe(reopenSummariesBefore + 1)
observations.push(`reopen.open gets=${String(platform.gets.length - reopenGetsBefore)} summaries=${String(platform.summaries.length - reopenSummariesBefore)} cards=${String(await noticeCards(page).count())} acks=${String(platform.acks.length)}`)
expect(await noticeCards(page).count()).toBe(0)
expect(platform.acks).toHaveLength(2)
// Arm the hold so a read triggered by navigation would be recorded and observed rather
// than racing this assertion.
const sectionGetsBefore = platform.gets.length
const sectionSummariesBefore = platform.summaries.length
platform.holdNextGet()
const nav = reopened.locator('nav')
await nav.getByRole('button', { name: '模型', exact: true }).click()
await expect.poll(async () => nav.getByRole('button', { name: '模型', exact: true }).getAttribute('aria-current'), { timeout: 30_000 }).toBe('true')
await nav.getByRole('button', { name: '账号与余额', exact: true }).click()
await expect.poll(async () => nav.getByRole('button', { name: '账号与余额', exact: true }).getAttribute('aria-current'), { timeout: 30_000 }).toBe('true')
platform.releaseGet()
const sectionPanel = (await reopened.textContent()) ?? ''
observations.push(`reopen.section-switch gets=${String(platform.gets.length - sectionGetsBefore)} summaries=${String(platform.summaries.length - sectionSummariesBefore)} amount=${String(sectionPanel.includes('¥13.00'))} cards=${String(await noticeCards(page).count())} acks=${String(platform.acks.length)}`)
expect(platform.gets.length).toBe(sectionGetsBefore)
expect(platform.summaries.length).toBe(sectionSummariesBefore)
expect(sectionPanel).toContain('¥13.00')
await page.keyboard.press('Escape')
await reopened.waitFor({ state: 'detached', timeout: 30_000 })
await expect.poll(() => launcher.evaluate(element => element === document.activeElement), { timeout: 30_000 }).toBe(true)
observations.push(`reopen.closed cards=${String(await noticeCards(page).count())} acks=${String(platform.acks.length)}`)
expect(await noticeCards(page).count()).toBe(0)
expect(platform.acks).toHaveLength(2)
// Returning from the native top-up view refreshes balance and notice once each, without
// holding the panel open.
const zhTopUp = '已赠送您 11.00 元 DSH 体验赠金。'
const topUpGetsBefore = platform.gets.length
const topUpSummariesBefore = platform.summaries.length
await openSettings(page, 'zh')
const topUpSettings = page.getByRole('dialog', { name: '设置', exact: true })
await topUpSettings.waitFor()
await expect.poll(() => platform.gets.length, { timeout: 30_000 }).toBe(topUpGetsBefore + 1)
await expect.poll(() => platform.summaries.length, { timeout: 30_000 }).toBe(topUpSummariesBefore + 1)
observations.push(`topup.open gets=${String(platform.gets.length - topUpGetsBefore)} summaries=${String(platform.summaries.length - topUpSummariesBefore)}`)
const callsBefore = (await platformCalls(page)).length
await topUpSettings.getByRole('link', { name: '充值', exact: true }).click()
const topUpOverlay = page.getByRole('dialog', { name: '返回 DeepSeek Harness', exact: true })
await topUpOverlay.waitFor()
// Opening the native view reads nothing by itself.
expect(platform.gets.length).toBe(topUpGetsBefore + 1)
expect(platform.summaries.length).toBe(topUpSummariesBefore + 1)
// The user pays while the view is open; Platform reports both balances in scientific
// notation, so the refresh must render plain currency.
platform.setNormalBalance('0E-16')
platform.setBonusBalance('2.4E+1')
platform.grant(ORDER_TOPUP, '11.00')
// Holding the notice read proves the return does not wait for the refresh it starts.
platform.holdNextGet()
await topUpOverlay.getByRole('button', { name: '返回 DeepSeek Harness', exact: true }).click()
await topUpOverlay.waitFor({ state: 'detached', timeout: 30_000 })
const topUpCalls = (await platformCalls(page)).slice(callsBefore).join(',')
observations.push(`topup.returned bridge=${topUpCalls} cards=${String(await noticeCards(page).count())}`)
expect(topUpCalls).toBe('open:top-up,close')
expect(await noticeCards(page).count()).toBe(0)
platform.releaseGet()
observations.push(`topup.notice=${await shownNotice(page, zhTopUp)}`)
const topUpAttempts = (): AckRecord[] => platform.acks.filter(item => item.orderId === ORDER_TOPUP)
await expect.poll(() => topUpAttempts().length, { timeout: 30_000 }).toBe(1)
await expect.poll(() => platform.gets.length, { timeout: 30_000 }).toBe(topUpGetsBefore + 2)
await expect.poll(() => platform.summaries.length, { timeout: 30_000 }).toBe(topUpSummariesBefore + 2)
const topUpPanel = (await topUpSettings.textContent()) ?? ''
const topUpAmounts = { recharge: topUpPanel.includes('¥0.00'), bonus: topUpPanel.includes('¥24.00') }
observations.push(`topup.refresh gets=${String(platform.gets.length - topUpGetsBefore)} summaries=${String(platform.summaries.length - topUpSummariesBefore)} acks=${String(topUpAttempts().length)} recharge=${String(topUpAmounts.recharge)} bonus=${String(topUpAmounts.bonus)}`)
expect(topUpAmounts).toEqual({ recharge: true, bonus: true })
expect(await topUpSettings.getByRole('button', { name: '刷新余额', exact: true }).count()).toBe(0)
// The card survives closing the panel, and the acknowledgement is not repeated.
await page.keyboard.press('Escape')
await topUpSettings.waitFor({ state: 'detached', timeout: 30_000 })
observations.push(`topup.closed cards=${String(await noticeCards(page).count())} acks=${String(topUpAttempts().length)}`)
expect(await noticeCards(page).count()).toBe(1)
expect(topUpAttempts()).toHaveLength(1)
await noticeCards(page).getByRole('button', { name: '关闭', exact: true }).click()
expect(await noticeCards(page).count()).toBe(0)
// A transient acknowledgement failure keeps the card and retries the same order after
// the backoff; the pending retry is page state, not browser storage.
const zhRetry = '已赠送您 6.00 元 DSH 体验赠金。'
platform.grant(ORDER_RETRY, '6.00')
platform.failNextAcks(ORDER_RETRY, 1)
await openSettings(page, 'zh')
const retryDialog = page.getByRole('dialog', { name: '设置', exact: true })
await retryDialog.waitFor()
observations.push(`retry.notice=${await shownNotice(page, zhRetry)}`)
const retryAttempts = (): AckRecord[] => platform.acks.filter(item => item.orderId === ORDER_RETRY)
await expect.poll(() => retryAttempts().length, { timeout: 30_000 }).toBe(1)
// The failed attempt does not withdraw the card the user is reading.
expect(await noticeCards(page).count()).toBe(1)
await expect.poll(() => retryAttempts().length, { timeout: 30_000 }).toBe(2)
const attempts = retryAttempts()
// The first retry follows the configured first delay rather than arriving immediately.
// Only the lower bound is asserted: load can widen the observed gap, never shorten it
// below the timer's own 1s first step.
const retryGapMs = attempts[1]!.at - attempts[0]!.at
const retryBodyKept = attempts.every(item => item.body === JSON.stringify({ order_id: ORDER_RETRY })
&& item.query === '' && item.contentType === 'application/json')
observations.push(`retry.attempts=${String(attempts.length)} backoff=${String(retryGapMs >= 900)} body-kept=${String(retryBodyKept)} cards=${String(await noticeCards(page).count())}`)
expect(retryGapMs).toBeGreaterThanOrEqual(900)
expect(retryBodyKept).toBe(true)
await page.keyboard.press('Escape')
await retryDialog.waitFor({ state: 'detached', timeout: 30_000 })
await noticeCards(page).getByRole('button', { name: '关闭', exact: true }).click()
// Display follows each response: the double keeps serving an acknowledged order, which
// is what makes a second display of the same order observable.
const zhRepeat = '已赠送您 7.00 元 DSH 体验赠金。'
const storedBeforeShown = await storageKeys()
platform.setKeepUnnotified(true)
platform.grant(ORDER_REPEAT, '7.00')
const repeatAttempts = (): AckRecord[] => platform.acks.filter(item => item.orderId === ORDER_REPEAT)
await openSettings(page, 'zh')
const repeatDialog = page.getByRole('dialog', { name: '设置', exact: true })
await repeatDialog.waitFor()
observations.push(`repeat.first=${await shownNotice(page, zhRepeat)}`)
await expect.poll(() => repeatAttempts().length, { timeout: 30_000 }).toBe(1)
// The mask owns pointer input, so the panel closes before the sidebar card is dismissed.
await page.keyboard.press('Escape')
await repeatDialog.waitFor({ state: 'detached', timeout: 30_000 })
expect(await noticeCards(page).count()).toBe(1)
await noticeCards(page).getByRole('button', { name: '关闭', exact: true }).click()
// Displaying and acknowledging a notice writes nothing, so the key set is unchanged
// against the baseline taken before it was shown.
const shownAdded = (await storageKeys()).filter(key => !storedBeforeShown.includes(key))
expect(shownAdded).toEqual([])
// Restarting replays whatever the server still offers.
await page.reload({ waitUntil: 'load' })
observations.push(`repeat.reload=${await shownNotice(page, zhRepeat)}`)
await expect.poll(() => repeatAttempts().length, { timeout: 30_000 }).toBe(2)
const reloadAdded = (await storageKeys()).filter(key => !storedBeforeShown.includes(key))
observations.push(`repeat.shown added-storage-keys=${String(shownAdded.length)} reload-added=${String(reloadAdded.length)} acks=${String(repeatAttempts().length)} cards=${String(await noticeCards(page).count())}`)
expect(reloadAdded).toEqual([])
await noticeCards(page).getByRole('button', { name: '关闭', exact: true }).click()
// A failed wallet read still leaves both balance rows reaching the Platform entry.
platform.setKeepUnnotified(false)
platform.clearUnnotified()
platform.setFailSummary(true)
const failedGetsBefore = platform.gets.length
const failedSummariesBefore = platform.summaries.length
const failedUsagesBefore = (await platformCalls(page)).filter(call => call === 'open:usage').length
await openSettings(page, 'zh')
const failedSettings = page.getByRole('dialog', { name: '设置', exact: true })
await failedSettings.waitFor()
const failedLinks = failedSettings.getByRole('link', { name: '前往开放平台查看', exact: true })
await expect.poll(() => failedLinks.count(), { timeout: 30_000 }).toBe(2)
await expect.poll(() => platform.summaries.length, { timeout: 30_000 }).toBe(failedSummariesBefore + 1)
const failedHrefs = (await failedLinks.evaluateAll(nodes => nodes.map(node => node.getAttribute('href'))))
.map(href => String(href).replace(platform.origin, '{{origin}}'))
observations.push(`failed.links count=${String(await failedLinks.count())} hrefs=${failedHrefs.join(',')}`)
expect(failedHrefs).toEqual(['{{origin}}/usage', '{{origin}}/usage'])
// Returning from usage refreshes nothing, because only a payment changes the account.
for (const index of [0, 1]) {
await failedLinks.nth(index).click()
const usageOverlay = page.getByRole('dialog', { name: '返回 DeepSeek Harness', exact: true })
await usageOverlay.waitFor()
await usageOverlay.getByRole('button', { name: '返回 DeepSeek Harness', exact: true }).click()
await usageOverlay.waitFor({ state: 'detached', timeout: 30_000 })
}
const failedUsages = (await platformCalls(page)).filter(call => call === 'open:usage').length - failedUsagesBefore
observations.push(`failed.reached usages=${String(failedUsages)} gets=${String(platform.gets.length - failedGetsBefore)} summaries=${String(platform.summaries.length - failedSummariesBefore)}`)
expect(failedUsages).toBe(2)
expect(platform.gets.length).toBe(failedGetsBefore + 1)
expect(platform.summaries.length).toBe(failedSummariesBefore + 1)
platform.setFailSummary(false)
await page.keyboard.press('Escape')
await failedSettings.waitFor({ state: 'detached', timeout: 30_000 })
await page.close()
// Another device in another language gets the copy the server localized for it.
platform.grant(ORDER_EN, '9.00')
const enPage = await openDesktopPage('en-US')
await enPage.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
const enCard = enPage.locator('aside').filter({ hasText: 'Bonus credited' }).filter({ hasText: enBonus })
await enCard.waitFor({ state: 'visible', timeout: 30_000 })
observations.push(`en.notice=${await cardText(enCard)}`)
const enGet = platform.gets.find(entry => entry.locale === 'en_US')!
observations.push(`en.get locale=${String(enGet.locale)} query=${enGet.query}`)
expect(enGet.locale).toBe('en_US')
expect(enGet.query).toBe('')
await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'notice-flow.expected.md'), observations.join('\n'), MODE)
for (const tripwire of tripwires) {
expect(tripwire.warnings).toEqual([])
expect(tripwire.pageErrors).toEqual([])
}
await assertFixtureInventory(SNAPSHOT_DIR, ['notice-flow.expected.md'])
}, 180_000)
})
+1 -1
View File
@@ -717,7 +717,7 @@ describe('web e2e: long Chat scroll contract', () => {
await world.page.getByRole('button', { name: 'Send message', exact: true }).click()
const group = world.page.locator('[data-step-process]:has([data-variant="think"][data-state="running"])')
await group.locator('[data-process-activity]').click()
await group.locator('[data-disclosure-row]').click()
await group.locator('[data-variant="think"][data-state="running"] [data-disclosure-row]').click()
const body = group.locator('[data-step-process-body]')
await nextPaint(world.page)
expect(await body.evaluate(element => element.scrollHeight <= element.clientHeight)).toBe(true)
@@ -0,0 +1,32 @@
startup.held cards=0 acks=0
startup.notice=赠金已到账 / 已赠送您 5.00 元 DSH 体验赠金。
startup.ack orders=22222222-2222-4222-8222-222222222222
startup.get locale=zh_CN query=
startup.ack method=POST path=/api/v0/users/ack_bonus_notified locale=zh_CN order_id=22222222-2222-4222-8222-222222222222 body="{\"order_id\":\"22222222-2222-4222-8222-222222222222\"}"
open.held cards=0 acks=1 gets=1
open.shown notice=赠金已到账 / 已赠送您 8.00 元 DSH 体验赠金。
open.shown ack orders=22222222-2222-4222-8222-222222222222,33333333-3333-4333-8333-333333333333
open.shown painted-under-settings=true cards=1 acks=2
open.get locale=zh_CN query= served=true gets=1
open.balance bonus-row=true amount=true dated=false
open.cards-while-open=1 acks=2
open.usage-link={{origin}}/usage
open.closed settings cards=1 acks=2
open.dismissed cards=0 acks=2
reopen.open gets=1 summaries=1 cards=0 acks=2
reopen.section-switch gets=0 summaries=0 amount=true cards=0 acks=2
reopen.closed cards=0 acks=2
topup.open gets=1 summaries=1
topup.returned bridge=open:top-up,close cards=0
topup.notice=赠金已到账 / 已赠送您 11.00 元 DSH 体验赠金。
topup.refresh gets=2 summaries=2 acks=1 recharge=true bonus=true
topup.closed cards=1 acks=1
retry.notice=赠金已到账 / 已赠送您 6.00 元 DSH 体验赠金。
retry.attempts=2 backoff=true body-kept=true cards=1
repeat.first=赠金已到账 / 已赠送您 7.00 元 DSH 体验赠金。
repeat.reload=赠金已到账 / 已赠送您 7.00 元 DSH 体验赠金。
repeat.shown added-storage-keys=0 reload-added=0 acks=2 cards=1
failed.links count=2 hrefs={{origin}}/usage,{{origin}}/usage
failed.reached usages=2 gets=1 summaries=1
en.notice=Bonus credited / You received a CNY 9.00 DSH trial credit.
en.get locale=en_US query=
@@ -5,7 +5,7 @@
- text: FORK_BRANCH_USER Summarize what this branch knows about the reads. {{clock}}
- button "Copy"
- status: Worked
- button "Took {{duration}}" [disabled]
- button "Took {{duration}}"
- paragraph: FORK_BRANCH_FIRST this branch has no read results, so I would check what the parent completed before retrying. FORK_BRANCH_DONE.
- button "Copy"
- button "Good response"
+8
View File
@@ -0,0 +1,8 @@
# The bonus notice scenario points the shipped account provider at a loopback
# Platform double. The scaffold substitutes {{origin}} after the listener binds,
# so no port is ever committed here. No notice timing is overridden: every read
# comes from the product's own startup and settings-refresh paths.
- id: deepseek-account
config:
platformOrigin: "{{origin}}"
allowLoopbackHttp: true
+10
View File
@@ -437,8 +437,18 @@ async function bootPreview(origin: string, browser: Browser): Promise<void> {
const sessions = page.getByRole('tree', { name: 'Sessions' })
const showcase = sessions.getByRole('treeitem').filter({ hasText: SHOWCASE_TITLE })
await expect.poll(() => showcase.count(), { timeout: 15_000 }).toBe(1)
// The fixture's DSH_HOME carries two active Schedule tasks for this
// Session, so the catalog the Schedule client half reads over the tunnel
// must reach the row mark and the header entry. A composition without the
// schedule overlay, or a Host that dropped the seeded storage, shows
// neither.
await showcase.locator('[data-session-schedule-mark]').waitFor({ timeout: 15_000 })
await showcase.click()
await page.getByText(SHOWCASE_TAIL, { exact: true }).waitFor({ timeout: 30_000 })
await expect.poll(
async () => page.locator('[data-schedule-reminder-entry]').getAttribute('aria-label'),
{ timeout: 15_000 },
).toBe('2 reminders')
expect(await page.getByText(SHOWCASE_OLDEST, { exact: true }).count()).toBe(0)
// Complete Turns can fold while earlier history is still unloaded.
+505 -157
View File
@@ -9,14 +9,15 @@ import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import type { Agent, AgentHandle } from '@deepseek-ai/dsh-agent'
import { bundlePatchPaths, composeEntries, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
import { ToolCallId, createUserMessage, LlmAdapter } from '@deepseek-ai/dsh-llm'
import { MessageId, ToolCallId, createUserMessage, LlmAdapter } from '@deepseek-ai/dsh-llm'
import { appendDelivery } from '../../../packages/schedule/schedule/src/delivery-history.ts'
import type { ScheduleTask } from '@deepseek-ai/dsh-schedule'
import type { ContextFormed, GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import {
ScheduleId,
createEveryScheduleRecord,
foldScheduleEvents,
resolveEveryOccurrence,
type EveryScheduleRecord,
} from '@deepseek-ai/dsh-schedule'
import {
@@ -73,13 +74,17 @@ const WEB_PATCHES = bundlePatchPaths(WEB_BUNDLE, (JSON.parse(readFileSync(join(W
const CATALOG_NOW = Date.parse('2099-08-25T12:00:00.000Z')
const CATALOG_SESSION_ID = SessionId('schedule-catalog-web-e2e')
const CATALOG_TITLE = 'Active schedule catalog'
const REMINDER_TRIGGER_NAME = /^\d+ reminders?$/
const ACTIVE_SCHEDULE_LABEL = 'Has active scheduled task'
const CATALOG_IDS = {
after: ScheduleId('catalog-after'),
at: ScheduleId('catalog-at'),
every: ScheduleId('catalog-every'),
} as const
/**
* Task name seeded for each recorded catalog task.
*
* The recorded `schedule/change` payloads predate titles, so the Host task rows
* this lane asserts are seeded from these names rather than from the fixture.
*/
const CATALOG_TITLES: Readonly<Record<string, string>> = {
'catalog-after': 'Review overdue deployment',
'catalog-at': 'Join release review',
'catalog-every': 'Check exact cadence',
}
/** Emit one complete assistant text response. */
function textResponse(text: string): StreamChunk[] {
@@ -90,6 +95,15 @@ function textResponse(text: string): StreamChunk[] {
]
}
/**
* Wait until the Session header carries no reminder entry. The entry stays
* mounted while a read is loading or failed, so its absence proves a successful
* read that found no active reminder for this Session.
*/
async function expectNoReminderEntry(page: Page): Promise<void> {
await expect.poll(() => page.locator('[data-schedule-reminder-entry]').count(), { timeout: 15_000 }).toBe(0)
}
/** Deterministic model seam that turns one due reminder into ordinary assistant prose. */
class ReminderAdapter extends LlmAdapter {
override async listModels(provider: string) { return [{ provider, id: MODEL, name: `${provider}/${MODEL}` }] }
@@ -154,7 +168,7 @@ class BrowserZoneAtAdapter extends LlmAdapter {
const target = Math.ceil((Date.now() + 5_000) / 1_000) * 1_000
this.selectedAt = localAt(target, AT_BROWSER_ZONE)
this.scheduledAt = new Date(target).toISOString()
const argumentsJson = JSON.stringify({ prompt: AT_PROMPT, at: this.selectedAt })
const argumentsJson = JSON.stringify({ prompt: AT_PROMPT, title: AT_PROMPT, at: this.selectedAt })
const callId = ToolCallId('schedule-at-browser-zone')
yield { type: 'block-start', index: 0, blockType: 'tool-call' }
yield {
@@ -241,25 +255,6 @@ async function liveAgent(scaffold: WebScaffold, sessionId: SessionId): Promise<A
}
}
/** Expand the first Workspace row and open the named Session. */
async function openSession(page: Page, title: string): Promise<void> {
const workspace = page.locator('[role="treeitem"]').first()
await workspace.waitFor({ timeout: 15_000 })
const deadline = Date.now() + 5_000
while (await workspace.getAttribute('aria-expanded') !== 'true') {
if (Date.now() >= deadline) throw new Error('workspace item did not expand')
await workspace.click()
await new Promise<void>(resolve => setTimeout(resolve, 50))
}
const row = page.getByRole('treeitem', { name: new RegExp(title) })
await row.waitFor({ timeout: 15_000 })
await row.click()
// The current crumb renders as plain text, not a button.
await page.getByRole('navigation', { name: 'Session hierarchy' })
.getByText(title, { exact: true })
.waitFor({ timeout: 15_000 })
}
describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
let scaffold: WebScaffold
let afterHandle: AgentHandle
@@ -324,19 +319,23 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
signal: AbortSignal.timeout(10_000),
callId: ToolCallId('schedule-after-create'),
name: 'schedule_create',
arguments: { prompt: AFTER_PROMPT, after_seconds: 1 },
arguments: { prompt: AFTER_PROMPT, title: AFTER_PROMPT, after_seconds: 1 },
agent: afterHandle.agent,
})
if (afterCreated.isError) {
throw new Error(`Schedule After create failed: ${JSON.stringify(afterCreated.value)}`)
}
expect(afterCreated.value).toMatchObject({
id: 'schedule-1',
const createdView: unknown = afterCreated.value
if (typeof createdView !== 'object' || createdView === null || !('id' in createdView) || typeof createdView.id !== 'string') {
throw new Error(`Schedule After create returned no task id: ${JSON.stringify(createdView)}`)
}
expect(createdView.id).toMatch(/^schedule-/)
expect(createdView).toMatchObject({
kind: 'after',
prompt: AFTER_PROMPT,
afterSeconds: 1,
state: 'scheduled',
deliveryMode: 'session-local',
deliveryMode: 'host',
})
afterAssistantReply = await waitForReply(afterHandle, AFTER_REPLY, 15_000)
await afterHandle.agent.whenIdle()
@@ -359,22 +358,25 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
EVERY_PROMPTS[0],
EVERY_INTERVAL_SECONDS,
seededAt - EVERY_FIXTURE_AGE_MS,
'Every primary',
),
createEveryScheduleRecord(
ScheduleId('schedule-every-secondary'),
EVERY_PROMPTS[1],
EVERY_INTERVAL_SECONDS,
seededAt - EVERY_FIXTURE_AGE_MS,
'Every secondary',
),
]
const domain = scaffold.ctx.storageDomain.get('schedule')
if (domain === undefined) throw new Error('Schedule domain was not opened')
for (const record of everyRecords) {
everyHandle.agent.session.append('schedule/change', {
version: 1,
operation: 'create',
schedule: record,
})
await domain.table('tasks').put(record.id, { sessionId: everyHandle.agent.id, record, status: 'active' })
}
await expect(scaffold.ctx.sessions.flush(everyHandle.agent.session)).resolves.toBe(true)
const wake = await scaffold.ctx.schedule.create(everyHandle.agent.id, {
prompt: 'Unused future scheduling wake', title: 'Future wake', after_seconds: 86_400,
})
await scaffold.ctx.schedule.delete({ sessionId: everyHandle.agent.id, id: wake.id })
await workspace.attachSession(everyHandle.agent.id)
const everyListed = await scaffold.ctx.tools.execute({
signal: AbortSignal.timeout(10_000),
@@ -461,52 +463,87 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
await captureStableAria(page, selector, scaffold.workspaceCwd),
MODE,
)
expect(await page.getByRole('button', { name: REMINDER_TRIGGER_NAME }).count()).toBe(0)
await expectNoReminderEntry(page)
await expect.poll(async () => (await scaffold.ctx.schedule.catalog()).some(task => (
task.sessionId === afterHandle.agent.id && task.status === 'inactive' && task.lastDelivery !== undefined
))).toBe(true)
const delivered = (await scaffold.ctx.schedule.catalog()).find(task => task.sessionId === afterHandle.agent.id)
if (delivered?.lastDelivery === undefined) throw new Error('delivered one-shot receipt was not retained')
expect(await scaffold.ctx.schedule.list({ sessionId: afterHandle.agent.id })).toEqual([])
// Archiving the Session shown in the main pane returns the app to its
// workspace-selection view and unmounts the Tasks page with it, so the
// linked-Session assertions run with another Session in the main pane.
await page.getByRole('treeitem', { name: /Explicit local-time reminder/ }).click()
await page.getByRole('button', { name: 'Automation tasks', exact: true }).click()
const manager = page.getByTestId('task-manager-page')
await manager.getByRole('button', { name: 'Inactive', exact: true }).click()
await manager.getByRole('list', { name: 'Task catalog' })
.getByRole('button', { name: AFTER_PROMPT, exact: true }).click()
const detail = manager.getByRole('complementary', { name: 'Task details' })
expect(await detail.getByRole('tab', { name: 'Rules', exact: true }).getAttribute('aria-selected')).toBe('true')
expect(await detail.getByRole('region', { name: 'Saved delivery record' }).count()).toBe(0)
await detail.getByRole('tab', { name: 'Delivery records', exact: true }).click()
await detail.getByRole('tabpanel', { name: 'Delivery records', exact: true }).waitFor()
const receipt = detail.getByRole('region', { name: 'Saved delivery record' })
await receipt.waitFor()
expect(await detail.getByText('Earlier delivery records have been cleared', { exact: true }).count()).toBe(0)
// The row identifies its occurrence by the scheduled instant it renders.
expect(await receipt.locator(`time[datetime="${delivered.lastDelivery.scheduledAt}"]`).count()).toBe(1)
expect(await receipt.textContent()).toContain(AFTER_PROMPT)
expect(await detail.getByText('Next scheduled time', { exact: true }).count()).toBe(0)
// The linked-session row renders on the Rules view only.
await detail.getByRole('tab', { name: 'Rules', exact: true }).click()
const linkedSession = detail.getByRole('button', { name: /^Linked session/ })
await scaffold.ctx.workspaceRegistry.archiveSession(afterHandle.agent.id)
try {
await expect.poll(() => linkedSession.isDisabled(), { timeout: 15_000 }).toBe(true)
expect(await detail.getByText('The original session is archived.', { exact: true }).count()).toBe(1)
expect(await detail.getByText('Scheduled After follow-up', { exact: true }).count()).toBe(1)
const moreActions = detail.getByRole('button', { name: 'Automation task actions', exact: true })
await moreActions.click()
const deleteAction = page.getByRole('menuitem', { name: 'Delete task', exact: true })
await deleteAction.waitFor({ timeout: 15_000 })
expect(await deleteAction.isDisabled()).toBe(false)
await page.keyboard.press('Escape')
// Dismissing the menu leaves the detail it was opened from: the Escape
// must not reach the page's own handler and close the whole panel.
await expect.poll(() => page.getByRole('menu').count(), { timeout: 5_000 }).toBe(0)
await detail.waitFor({ timeout: 5_000 })
expect((await scaffold.ctx.schedule.catalog()).find(task => task.id === delivered.id)).toEqual(delivered)
} finally {
await scaffold.ctx.workspaceRegistry.unarchiveSession(afterHandle.agent.id)
}
await linkedSession.waitFor({ timeout: 15_000 })
await expect.poll(() => linkedSession.isDisabled(), { timeout: 15_000 }).toBe(false)
await linkedSession.click()
await expectNoReminderEntry(page)
}, 60_000)
it('batches one latest occurrence per overdue Every record into an ordinary follow-up', async () => {
it('batches one latest occurrence per overdue Every record and advances both schedules', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-schedule-every'))
const ids = new Set(everyRecords.map(record => record.id))
const dispatches = everyHandle.agent.session.snapshotEvents().filter(event => (
event.type === 'schedule/change'
&& event.data.operation === 'dispatch'
&& ids.has(event.data.id)
))
expect(dispatches).toHaveLength(2)
const acceptedAt = dispatches.map((event) => {
if (event.type !== 'schedule/change' || event.data.operation !== 'dispatch'
|| !('acceptedAt' in event.data)) throw new Error('expected Every dispatch')
return event.data.acceptedAt
})
expect(new Set(acceptedAt).size).toBe(1)
const decision = acceptedAt[0]
if (decision === undefined) throw new Error('missing Every decision time')
const batch = everyHandle.agent.session.snapshotEvents().find(event => (
const messages = everyHandle.agent.session.snapshotEvents().filter(event => (
event.type === 'user/message'
&& event.data.source.kind === 'schedule'
&& event.data.content.some(block => block.type === 'text'
&& block.text.startsWith('[SCHEDULE REMINDER BATCH]'))
))
if (batch?.type !== 'user/message') throw new Error('missing Every batch message')
const batchBlock = batch.data.content.find(block => block.type === 'text')
if (batchBlock?.type !== 'text') throw new Error('missing Every batch text')
expect(messages).toHaveLength(1)
const texts = messages.map((event) => {
if (event.type !== 'user/message') throw new Error('expected reminder message')
return event.data.content.filter(block => block.type === 'text').map(block => block.text).join('')
})
for (const record of everyRecords) {
const occurrenceAt = resolveEveryOccurrence(record, Date.parse(decision)).occurrenceAt
expect(batchBlock.text).toContain(JSON.stringify({
schedule_id: record.id,
occurrence_at: occurrenceAt,
reminder_prompt: record.prompt,
}).slice(1, -1))
expect(texts.filter(text => text.includes(record.id))).toHaveLength(1)
expect(texts.some(text => text.includes(record.prompt))).toBe(true)
}
expect(everyAdapter.requests).toHaveLength(1)
const reminderRequest = everyAdapter.requests[0]
if (reminderRequest === undefined) throw new Error('model did not receive the Every batch')
expect(requestText(reminderRequest)).toContain(batchBlock.text)
expectReminderFraming(reminderRequest)
const active = foldScheduleEvents(everyHandle.agent.session.snapshotEvents()).active
for (const request of everyAdapter.requests) expectReminderFraming(request)
const active = await scaffold.ctx.schedule.list({ sessionId: everyHandle.agent.id })
expect(active).toHaveLength(2)
expect(active.every(record => Date.parse(record.scheduledAt) > Date.parse(decision))).toBe(true)
for (const record of active) {
const original = everyRecords.find(candidate => candidate.id === record.id)
if (original === undefined) throw new Error('unexpected recurring record')
expect(Date.parse(record.scheduledAt) - Date.parse(original.scheduledAt))
.toBe(EVERY_INTERVAL_SECONDS * 1_000)
}
const session = page.getByRole('treeitem', { name: /Fixed-rate reminder batch/ })
await session.click()
@@ -523,6 +560,66 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
)
await page.getByRole('button', { name: '2 reminders', exact: true }).waitFor({ timeout: 15_000 })
expect(await page.getByRole('list', { name: 'Active reminders' }).count()).toBe(0)
const original = everyRecords[0]
const first = await scaffold.ctx.schedule.history({ sessionId: everyHandle.agent.id, id: original.id, limit: 20 })
if ('code' in first || first.records.length !== 1) throw new Error('expected one actual recurring receipt')
await page.getByRole('button', { name: 'Automation tasks', exact: true }).click()
const manager = page.getByTestId('task-manager-page')
await manager.getByRole('list', { name: 'Task catalog' }).getByRole('button', { name: original.title, exact: true }).click()
const detail = manager.getByRole('complementary', { name: 'Task details' })
await detail.getByRole('tab', { name: 'Delivery records', exact: true }).click()
const receipts = detail.getByRole('region', { name: 'Saved delivery record' })
const recordsPanel = detail.getByRole('tabpanel', { name: 'Delivery records', exact: true })
await expect.poll(() => receipts.count()).toBe(1)
expect(await detail.getByText('Earlier delivery records have been cleared', { exact: true }).count()).toBe(0)
const domain = scaffold.ctx.storageDomain.get('schedule')
if (domain === undefined) throw new Error('Schedule domain was not opened')
const latest = first.records[0]!
// Make the next occurrence due through the fixture's existing stored-time input.
await domain.table('tasks').put(original.id, {
sessionId: everyHandle.agent.id,
record: { ...original, scheduledAt: new Date(Date.now() - 1_000).toISOString() },
status: 'active',
lastDelivery: { scheduledAt: latest.scheduledAt, deliveredAt: latest.deliveredAt, messageId: latest.messageId },
deliveryHistory: { records: [...first.records].reverse(), earlierRecordsUnavailable: first.earlierRecordsUnavailable },
})
const wake = await scaffold.ctx.schedule.create(everyHandle.agent.id, { prompt: 'Future history refresh wake', title: 'History refresh wake', after_seconds: 86_400 })
await scaffold.ctx.schedule.delete({ sessionId: everyHandle.agent.id, id: wake.id })
await expect.poll(() => receipts.count(), { timeout: 15_000 }).toBe(2)
const saved = await scaffold.ctx.schedule.history({ sessionId: everyHandle.agent.id, id: original.id, limit: 20 })
if ('code' in saved) throw new Error('recurring delivery history was unavailable')
expect(saved.records).toHaveLength(2)
expect(saved.records[1]).toEqual(latest)
expect(saved.records[0]?.messageId).not.toBe(latest.messageId)
for (const record of saved.records) {
// Each saved occurrence renders its own scheduled instant.
expect(await recordsPanel.locator(`time[datetime="${record.scheduledAt}"]`).count()).toBe(1)
}
expect(await detail.getByRole('tab', { name: 'Delivery records', exact: true }).getAttribute('aria-selected')).toBe('true')
await detail.getByRole('tab', { name: 'Rules', exact: true }).click()
// The interval row states its quantity in the friendly unit the stored
// seconds select: 3600 seconds is one whole hour, so the row shows 1 and
// doubling the interval is two of those hours.
const interval = detail.getByLabel('Repeat every', { exact: true })
expect(await interval.inputValue()).toBe('1')
await interval.fill('2')
// Run-time rows edit a local draft; the save bar is the only commit path.
await detail.getByText('Unsaved changes', { exact: true }).waitFor({ timeout: 15_000 })
await detail.getByRole('button', { name: 'Save changes', exact: true }).click()
await expect.poll(async () => (await scaffold.ctx.schedule.catalog()).find(task => task.id === original.id), { timeout: 15_000 })
.toMatchObject({
id: original.id, sessionId: everyHandle.agent.id, prompt: original.prompt,
kind: 'every', everySeconds: EVERY_INTERVAL_SECONDS * 2, status: 'active',
})
expect(await scaffold.ctx.schedule.history({ sessionId: everyHandle.agent.id, id: original.id, limit: 20 })).toEqual(saved)
await detail.getByRole('tab', { name: 'Delivery records', exact: true }).click()
await expect.poll(() => receipts.count()).toBe(2)
// The linked-session row renders on the Rules view only.
await detail.getByRole('tab', { name: 'Rules', exact: true }).click()
await detail.getByRole('button', { name: 'Linked session Fixed-rate reminder batch: open original session', exact: true }).click()
await page.getByRole('button', { name: '2 reminders', exact: true }).waitFor({ timeout: 15_000 })
}, 60_000)
it('uses request-local browser context to create an explicit local At reminder', async () => {
@@ -556,26 +653,21 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
event.type === 'tool/call' && event.data.name === 'schedule_create'
))
if (toolCall?.type !== 'tool/call') throw new Error('missing schedule_create tool call')
expect(JSON.parse(toolCall.data.arguments)).toEqual({ prompt: AT_PROMPT, at: selectedAt })
const created = atHandle.agent.session.snapshotEvents().find(event => (
event.type === 'schedule/change'
&& event.data.operation === 'create'
&& event.data.schedule.kind === 'at'
expect(JSON.parse(toolCall.data.arguments)).toEqual({ prompt: AT_PROMPT, title: AT_PROMPT, at: selectedAt })
const result = atHandle.agent.session.snapshotEvents().find(event => (
event.type === 'tool/result' && event.data.message.toolCallId === toolCall.data.callId
))
if (created?.type !== 'schedule/change' || created.data.operation !== 'create') {
throw new Error('explicit local At call did not create a durable record')
}
const schedule = created.data.schedule
expect(schedule).toMatchObject({
kind: 'at',
prompt: AT_PROMPT,
scheduledAt,
})
expect(atHandle.agent.session.snapshotEvents().filter(event => (
event.type === 'schedule/change'
&& event.data.operation === 'dispatch'
&& event.data.id === schedule.id
))).toHaveLength(1)
if (result?.type !== 'tool/result') throw new Error('missing schedule_create result')
expect(result.data.message.isError).not.toBe(true)
const resultText = result.data.message.content.find(block => block.type === 'text')
if (resultText?.type !== 'text') throw new Error('missing schedule_create response fields')
expect(JSON.parse(resultText.text)).toMatchObject({ kind: 'at', prompt: AT_PROMPT, scheduledAt })
const reminders = atHandle.agent.session.snapshotEvents().filter(event => (
event.type === 'user/message'
&& event.data.source.kind === 'schedule'
))
expect(reminders).toHaveLength(1)
expect(await scaffold.ctx.schedule.list({ sessionId: atHandle.agent.id })).toEqual([])
expect(atAdapter.requests).toHaveLength(4)
const reminderRequest = atAdapter.requests[3]
if (reminderRequest === undefined) throw new Error('model did not receive the At reminder')
@@ -594,7 +686,7 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
await captureStableAria(page, selector, scaffold.workspaceCwd),
MODE,
)
expect(await page.getByRole('button', { name: REMINDER_TRIGGER_NAME }).count()).toBe(0)
await expectNoReminderEntry(page)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
}, 60_000)
@@ -620,18 +712,21 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => {
extraOverlayPath: OVERLAY,
})
await seedSession(scaffold, fixture, CATALOG_SESSION_ID, 'standard')
const records = foldScheduleEvents(fixture.trim().split('\n').slice(1).map(line => JSON.parse(line) as SessionEvent)).active
const domain = scaffold.ctx.storageDomain.get('schedule')
if (domain === undefined) throw new Error('Schedule domain was not opened')
// The fixture holds the bytes its writer produced before titles existed, so
// each recorded task is seeded under the name this catalog lane asserts.
const seeded = records.map(record => ({ ...record, title: CATALOG_TITLES[record.id] }))
for (const record of seeded) {
await domain.table('tasks').put(record.id, { sessionId: CATALOG_SESSION_ID, record, status: 'active' })
}
expect(await scaffold.ctx.schedule.list({ sessionId: CATALOG_SESSION_ID })).toEqual(seeded)
expect(scaffold.ctx.agents.get(CATALOG_SESSION_ID)).toBeUndefined()
const workspace = await scaffold.ctx.workspaceRegistry.create(scaffold.workspaceCwd)
await workspace.attachSession(CATALOG_SESSION_ID)
// Seed the zero-I/O list view before the Session is opened.
const catalogReader = await scaffold.ctx.sessionPersistence.open(CATALOG_SESSION_ID, 'read')
try {
const catalogEvents = [...(await catalogReader.read()).events]
scaffold.ctx.sessionProjectionCache.coldSnapshot(catalogReader.header, catalogReader.inheritedEventCount, catalogEvents)
} finally {
await catalogReader.close()
}
browser = await chromium.launch()
page = await browser.newPage({
viewport: { width: 900, height: 900 },
@@ -649,15 +744,6 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => {
await openSidebar.click()
await page.getByRole('button', { name: 'Collapse sidebar' }).waitFor({ timeout: 10_000 })
}
const workspaceRow = page.locator('[role="treeitem"]').first()
await workspaceRow.waitFor({ timeout: 15_000 })
const expansionDeadline = Date.now() + 5_000
while (await workspaceRow.getAttribute('aria-expanded') !== 'true') {
if (Date.now() >= expansionDeadline) throw new Error('workspace item did not expand')
await workspaceRow.click()
await new Promise<void>(resolve => setTimeout(resolve, 50))
}
await page.getByRole('treeitem', { name: new RegExp(CATALOG_TITLE) }).waitFor({ timeout: 15_000 })
}, 120_000)
afterAll(async () => {
@@ -668,7 +754,86 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => {
if (failures.length > 1) throw new AggregateError(failures, 'Schedule catalog teardown failed')
})
it('replays the overlay-only catalog and sidebar marker, then removes both live', async () => {
it('manages cross-Session tasks from the sidebar without activating their Sessions', async () => {
const otherSessionId = SessionId('schedule-manager-other-session')
await seedSession(scaffold, await readFile(CATALOG_FIXTURE, 'utf8'), otherSessionId, 'standard')
const other = await scaffold.ctx.schedule.create(otherSessionId, {
prompt: 'Inspect the second workspace deployment', title: 'Second workspace deployment', at: '2099-08-25T12:08:00.000Z',
})
await page.getByRole('button', { name: 'Automation tasks', exact: true }).click()
const manager = page.getByTestId('task-manager-page')
await manager.getByRole('heading', { name: 'Automation tasks', exact: true }).waitFor()
const tasks = manager.getByRole('list', { name: 'Task catalog' })
await expect.poll(() => tasks.getByRole('listitem').count()).toBe(4)
expect(scaffold.ctx.agents.get(CATALOG_SESSION_ID)).toBeUndefined()
expect(scaffold.ctx.agents.get(otherSessionId)).toBeUndefined()
// The filter row carries the status group alone, whose first chip is the
// unrestricted state; the repeating task is reached by name instead of a
// retired type chip.
const statuses = manager.getByRole('group', { name: 'Task status' })
expect(await statuses.getByRole('button').count()).toBe(3)
await manager.getByRole('button', { name: 'All', exact: true }).click()
await expect.poll(() => tasks.getByRole('listitem').count()).toBe(4)
expect(await tasks.textContent()).toContain('Check exact cadence')
await manager.getByRole('searchbox', { name: 'Search tasks' }).fill('second workspace')
await expect.poll(() => tasks.getByRole('listitem').count()).toBe(1)
await tasks.getByRole('button', { name: other.title, exact: true }).click()
const details = manager.getByRole('complementary', { name: 'Task details' })
expect(await details.textContent()).toContain(otherSessionId)
expect(scaffold.ctx.agents.get(otherSessionId)).toBeUndefined()
await details.getByRole('tab', { name: 'Delivery records', exact: true }).click()
await details.getByText('No delivery record available', { exact: true }).waitFor()
expect(await details.getByRole('region', { name: 'Saved delivery record' }).count()).toBe(0)
expect(scaffold.ctx.agents.get(otherSessionId)).toBeUndefined()
await details.getByRole('tab', { name: 'Rules', exact: true }).click()
await page.setViewportSize({ width: 600, height: 340 })
// The linked Session is a seat in the detail's tab strip, above the rule's
// scroll region, so it stays fully visible however far that region scrolls.
const contextRow = details.locator('[class*="detailContext"]')
const linkedSession = contextRow.getByRole('button', { name: 'Linked session: open original session', exact: true })
expect(await contextRow.getByRole('button').count()).toBe(1)
expect(await linkedSession.textContent()).toContain(otherSessionId)
expect(await contextRow.evaluate(element => element.closest('[class*="detailScroll"]') === null)).toBe(true)
await details.locator('[class*="detailScroll"]').evaluate((element) => { element.scrollTop = element.scrollHeight })
const linkedBounds = await linkedSession.boundingBox()
expect(linkedBounds).not.toBeNull()
expect(linkedBounds!.y).toBeGreaterThanOrEqual(0)
expect(linkedBounds!.y + linkedBounds!.height).toBeLessThanOrEqual(340)
expect(await details.getByRole('button', { name: 'Automation task actions', exact: true }).isVisible()).toBe(true)
await page.setViewportSize({ width: 900, height: 900 })
const moreActions = details.getByRole('button', { name: 'Automation task actions', exact: true })
await moreActions.click()
await page.getByRole('menuitem', { name: 'Delete task', exact: true }).click()
const confirmation = page.getByRole('dialog', { name: 'Delete this task?' })
await confirmation.getByRole('button', { name: 'Cancel', exact: true }).click()
expect((await scaffold.ctx.schedule.catalog()).some(task => task.id === other.id)).toBe(true)
await moreActions.click()
await page.getByRole('menuitem', { name: 'Delete task', exact: true }).click()
await confirmation.getByRole('button', { name: 'Confirm deletion', exact: true }).click()
await expect.poll(async () => (await scaffold.ctx.schedule.catalog()).some(task => task.id === other.id))
.toBe(false)
// A confirmed deletion closes the surface that asked for it and reports the
// outcome through the app-wide toast, so the notice outlives the panel that
// raised it. The Host removed the row with its saved records, so neither the
// rule view nor the records view can read the task again.
await expect.poll(() => details.count()).toBe(0)
await page.getByText('Task deleted.', { exact: true }).waitFor({ timeout: 15_000 })
expect(scaffold.ctx.agents.get(otherSessionId)).toBeUndefined()
await manager.getByRole('searchbox', { name: 'Search tasks' }).fill('Check exact cadence')
await tasks.getByRole('button', { name: 'Check exact cadence', exact: true }).click()
// This Session's catalog row carries no title before the Session is opened,
// so its link shows the Session id and is named without one.
await manager.getByRole('button', { name: 'Linked session: open original session', exact: true }).click()
await liveAgent(scaffold, CATALOG_SESSION_ID)
await page.getByRole('button', { name: '3 reminders', exact: true }).waitFor()
expect(tripwire.pageErrors).toEqual([])
}, 60_000)
it('reads stored tasks without a live Session and deletes them through the catalog', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-schedule-catalog'))
const base = composeEntries([
loadOverlayPatches('Schedule catalog base roster', BASE_PATCH),
@@ -688,34 +853,13 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => {
disabled: false,
})
const catalogRow = page.getByRole('treeitem', { name: new RegExp(CATALOG_TITLE) })
expect(await catalogRow.getByRole('img', { name: ACTIVE_SCHEDULE_LABEL }).count()).toBe(1)
await page.getByRole('button', { name: 'Search sessions' }).click()
const search = page.getByPlaceholder('Search session names', { exact: false })
await search.fill(CATALOG_TITLE)
const result = page.getByRole('tree', { name: 'Search results' })
.getByRole('treeitem', { name: new RegExp(CATALOG_TITLE) })
await result.waitFor({ timeout: 15_000 })
expect(await result.getByRole('img', { name: ACTIVE_SCHEDULE_LABEL }).count()).toBe(1)
expect(await result.getByRole('button').count()).toBe(0)
await page.getByRole('button', { name: 'Clear search' }).click()
await catalogRow.waitFor({ timeout: 15_000 })
await page.getByRole('button', { name: 'View options' }).click()
await page.getByRole('menuitem', { name: 'In one list' }).click()
const flatRow = page.getByRole('treeitem', { name: new RegExp(CATALOG_TITLE) })
await flatRow.waitFor({ timeout: 15_000 })
expect(await flatRow.getByRole('img', { name: ACTIVE_SCHEDULE_LABEL }).count()).toBe(1)
await page.getByRole('button', { name: 'View options' }).click()
await page.getByRole('menuitem', { name: 'WorkSpace', exact: true }).click()
await catalogRow.waitFor({ timeout: 15_000 })
expect(await catalogRow.getByRole('img', { name: ACTIVE_SCHEDULE_LABEL }).count()).toBe(1)
await openSession(page, CATALOG_TITLE)
const parentAgent = await liveAgent(scaffold, CATALOG_SESSION_ID)
await page.getByRole('button', { name: 'Automation tasks', exact: true }).click()
const manager = page.getByTestId('task-manager-page')
await manager.getByRole('button', { name: 'Check exact cadence', exact: true }).click()
await manager.getByRole('button', { name: `Linked session ${CATALOG_TITLE}: open original session`, exact: true }).click()
await liveAgent(scaffold, CATALOG_SESSION_ID)
await page.getByRole('navigation', { name: 'Session hierarchy' })
.getByText(CATALOG_TITLE, { exact: true }).waitFor({ timeout: 15_000 })
const trigger = page.getByRole('button', { name: '3 reminders' })
await trigger.waitFor({ timeout: 15_000 })
@@ -755,13 +899,16 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => {
expect(lightLayout.background).not.toBe('rgba(0, 0, 0, 0)')
const longRow = catalog.getByRole('listitem').filter({ hasText: 'Join release review' })
const cadenceRow = catalog.getByRole('listitem').filter({ hasText: 'Check exact cadence' })
const longPrompt = longRow.locator(':scope > span').nth(1)
// The row's opener is a block box, so its own width is the one a stored name
// can widen: this lane's 19-character seeded name renders inside it without
// pushing the popover past its own width, and a longer stored name wraps
// through the same `overflow-wrap: anywhere` declaration the stylesheet
// states for the title.
const longPrompt = longRow.getByRole('button', { name: 'Open reminder details: Join release review' })
const promptLayout = await longPrompt.evaluate(element => ({
height: element.getBoundingClientRect().height,
clientWidth: element.clientWidth,
scrollWidth: element.scrollWidth,
}))
expect(promptLayout.height).toBeGreaterThan(18)
expect(promptLayout.scrollWidth).toBeLessThanOrEqual(promptLayout.clientWidth)
const [longRowLayout, cadenceRowLayout] = await Promise.all([
longRow.evaluate((element) => {
@@ -785,11 +932,23 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => {
}
expect(longRowLayout.bottom).toBeLessThanOrEqual(cadenceRowLayout.top)
expect(cadenceRowLayout.bottom).toBeGreaterThan(cadenceRowLayout.top)
// The popover caps its height at min(420px, calc(100vh - 140px))
// (ScheduleCatalogAction.module.css). Each of the three rows keeps its 48px
// min-height plus the 6px list padding and two 1px gaps, so the list needs
// at least 152px; a 260px viewport leaves a 120px budget, which must clamp
// the list and make the clip scroll.
const clampedViewportHeight = 260
const clampedHeightCap = Math.min(420, clampedViewportHeight - 140)
const naturalHeight = await catalog.evaluate(element => element.clientHeight)
expect(naturalHeight).toBeGreaterThan(clampedHeightCap)
await page.setViewportSize({ width: 900, height: clampedViewportHeight })
const scrollLayout = await catalog.evaluate(element => ({
clientHeight: element.clientHeight,
scrollHeight: element.scrollHeight,
}))
expect(scrollLayout.clientHeight).toBeLessThan(naturalHeight)
expect(scrollLayout.scrollHeight).toBeGreaterThan(scrollLayout.clientHeight)
await page.setViewportSize({ width: 900, height: 900 })
const evidenceDir = join(REPO_ROOT, '.artifacts')
await mkdir(evidenceDir, { recursive: true })
@@ -812,19 +971,44 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => {
MODE,
)
const sessionRow = page.getByRole('treeitem', { name: new RegExp(CATALOG_TITLE) })
expect(await sessionRow.getByRole('img', { name: ACTIVE_SCHEDULE_LABEL }).count()).toBe(1)
for (const id of Object.values(CATALOG_IDS)) {
parentAgent.session.append('schedule/change', { version: 1, operation: 'delete', id })
// The next-run line states the target as the reader's local clock stamp
// followed by the distance to it in parentheses: the stamp is the instant in
// this device's zone, and only the parenthesized distance ticks.
const nextRunLine = catalog.getByRole('listitem').filter({ hasText: 'Join release review' })
.locator('[class*="nextRun"]')
await expect.poll(() => nextRunLine.count(), { timeout: 15_000 }).toBeGreaterThan(0)
const metadataText = await nextRunLine.first().textContent() ?? ''
const nextRun = await nextRunLine.first().locator('time').getAttribute('dateTime')
if (nextRun === null) throw new Error('the next-run line names no instant')
const nextRunAbsolute = new Intl.DateTimeFormat('en', {
month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit',
hour12: false, timeZone: AT_BROWSER_ZONE,
}).format(new Date(nextRun))
expect(metadataText).toContain(`Next run ${nextRunAbsolute}`)
expect(metadataText).toMatch(/Next run [^)]+\((?:in \d+ \w+|Due now|\d+ .*overdue)\)/)
// The stamp is a local clock time, not a bare duration.
expect(metadataText).toMatch(/\d{1,2}:\d{2}/)
// The delete glyph is an aria-hidden SVG, so the ARIA golden cannot prove it
// still renders; the DOM has to.
const deleteButton = catalog.getByRole('button', { name: /^Delete reminder:/ }).first()
await expect.poll(() => deleteButton.locator('svg').count(), { timeout: 15_000 }).toBe(1)
for (let remaining = 3; remaining > 0; remaining--) {
await catalog.getByRole('button', { name: /^Delete reminder:/ }).first().click()
await expect.poll(() => catalog.getByRole('button', { name: /^Delete reminder:/ }).count(), { timeout: 15_000 })
.toBe(remaining - 1)
}
await expect(scaffold.ctx.sessions.flush(parentAgent.session)).resolves.toBe(true)
await expect.poll(() => page.getByRole('button', { name: REMINDER_TRIGGER_NAME }).count(), {
timeout: 15_000,
}).toBe(0)
expect(await page.getByRole('list', { name: 'Active reminders' }).count()).toBe(0)
await expect.poll(() => sessionRow.getByRole('img', { name: ACTIVE_SCHEDULE_LABEL }).count(), {
timeout: 15_000,
}).toBe(0)
await expectNoReminderEntry(page)
expect(await scaffold.ctx.schedule.list({ sessionId: CATALOG_SESSION_ID })).toEqual([])
await page.reload({ waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
await expectNoReminderEntry(page)
// This authored pin owns its sidecars through the snapshot manifest's
// `header.pin: true` contract, which the session-snapshot corpus enforces
// (`scripts/session-snapshot-corpus.corpus.ts`). The sidecars have no replay
// comparison here: this scenario seeds its fixture and then runs further
// turns, so `assertReplaySession` (enabled only by `replayFixture`) has no
// matching recording to compare against.
await assertFixtureInventory(CATALOG_SNAPSHOT_DIR, [
'catalog.expected.md',
'session.v3.jsonl',
@@ -834,4 +1018,168 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => {
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
}, 60_000)
it('shows the pruning footer only for confirmed cleanup after the final saved page', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-schedule-retention'))
const record = await scaffold.ctx.schedule.create(CATALOG_SESSION_ID, {
prompt: 'Review retained records', title: 'History retention review', at: '2099-09-01T12:00:00.000Z',
})
try {
await page.getByRole('button', { name: 'Automation tasks', exact: true }).click()
const manager = page.getByTestId('task-manager-page')
await manager.getByRole('button', { name: 'All', exact: true }).click()
await manager.getByRole('searchbox', { name: 'Search tasks' }).fill(record.title)
await manager.getByRole('list', { name: 'Task catalog' }).getByRole('button', { name: record.title, exact: true }).click()
const detail = manager.getByRole('complementary', { name: 'Task details' })
const recordsTab = detail.getByRole('tab', { name: 'Delivery records', exact: true })
const rulesTab = detail.getByRole('tab', { name: 'Rules', exact: true })
const notice = detail.getByText('Earlier delivery records have been cleared', { exact: true })
await recordsTab.click()
await detail.getByText('No delivery record available', { exact: true }).waitFor()
expect(await notice.count()).toBe(0)
const domain = scaffold.ctx.storageDomain.get('schedule')
if (domain === undefined) throw new Error('Schedule domain was not opened')
const instant = new Date().toISOString()
const receipt = { scheduledAt: instant, deliveredAt: instant, messageId: MessageId('retention-legacy') }
await rulesTab.click()
await domain.table('tasks').put(record.id, {
sessionId: CATALOG_SESSION_ID, record, status: 'inactive', lastDelivery: receipt,
deliveryHistory: { records: [receipt], earlierRecordsUnavailable: true },
})
await recordsTab.click()
const saved = detail.getByRole('region', { name: 'Saved delivery record' })
await expect.poll(() => saved.count()).toBe(1)
expect(await notice.count()).toBe(0)
await rulesTab.click()
let task: ScheduleTask = {
sessionId: CATALOG_SESSION_ID, record, status: 'inactive',
deliveryHistory: { records: [], earlierRecordsUnavailable: false },
}
for (let index = 0; index < 23; index++) {
const time = new Date(Date.now() - (index === 0 ? 40 * 86_400_000 : (23 - index) * 3_600_000)).toISOString()
task = { ...task, ...appendDelivery(task, {
scheduledAt: time, deliveredAt: time, messageId: MessageId(`retention-${index}`),
}, { days: 30, records: 200 }) }
}
expect(task.deliveryHistory?.records).toHaveLength(22)
expect(task.deliveryHistory?.earlierRecordsPruned).toBe(true)
await domain.table('tasks').put(record.id, task)
await recordsTab.click()
await expect.poll(() => saved.count()).toBe(20)
expect(await notice.count()).toBe(0)
await detail.getByRole('button', { name: 'Load more', exact: true }).click()
await notice.waitFor()
expect(await saved.count()).toBe(22)
const info = detail.getByRole('button', { name: 'Retention rules', exact: true })
await info.click()
const bounds = detail.getByText('Each task keeps up to 200 delivery records from the last 30 days.', { exact: true })
await bounds.waitFor()
expect(await detail.locator('footer').innerText()).toMatchInlineSnapshot(`
"Earlier delivery records have been cleared
Each task keeps up to 200 delivery records from the last 30 days.
When a new record is saved, older records outside these limits are cleared automatically. Clearing records does not stop the task."
`)
await info.click()
expect(await bounds.count()).toBe(0)
const panelBox = await detail.boundingBox()
const footerBox = await detail.locator('footer').boundingBox()
expect(panelBox).not.toBeNull()
expect(footerBox).not.toBeNull()
expect(Math.abs(footerBox!.x - panelBox!.x)).toBeLessThan(2)
expect(Math.abs(footerBox!.y + footerBox!.height - panelBox!.y - panelBox!.height)).toBeLessThan(2)
expect(await detail.locator('footer').evaluate(element => getComputedStyle(element).borderTopWidth)).toBe('0px')
expect(tripwire.pageErrors).toEqual([])
} finally {
await scaffold.ctx.schedule.delete({ sessionId: CATALOG_SESSION_ID, id: record.id })
}
}, 60_000)
it('shows a tool-created daily rule as repeating with its original time zone', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-schedule-daily'))
const agent = await liveAgent(scaffold, CATALOG_SESSION_ID)
const timeZone = 'America/New_York'
const clock = new Intl.DateTimeFormat('en-GB-u-nu-latn', {
timeZone, hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23',
})
const time = clock.format(new Date(Date.now() + 3_600_000))
const prompt = 'Inspect the daily calendar rule'
const result = await scaffold.ctx.tools.execute({
signal: AbortSignal.timeout(10_000), callId: ToolCallId('schedule-daily-catalog-create'),
name: 'schedule_create', arguments: { prompt, title: prompt, daily: { time, time_zone: timeZone } }, agent,
})
const created = (await scaffold.ctx.schedule.catalog()).find(task => (
task.sessionId === CATALOG_SESSION_ID && task.prompt === prompt
))
if (created === undefined) throw new Error('daily tool did not create a stored task')
try {
expect(result.isError).toBe(false)
expect(result.value).toMatchObject({ kind: 'daily', time: `${time}.000`, timeZone })
expect(created).toMatchObject({ kind: 'daily', time: `${time}.000`, timeZone, status: 'active' })
expect(clock.format(new Date(created.scheduledAt))).toBe(time)
await page.clock.setFixedTime(new Date())
await page.getByRole('button', { name: 'Automation tasks', exact: true }).click()
const manager = page.getByTestId('task-manager-page')
await manager.getByRole('button', { name: 'All', exact: true }).click()
await manager.getByRole('searchbox', { name: 'Search tasks' }).fill(prompt)
const tasks = manager.getByRole('list', { name: 'Task catalog' })
const row = tasks.getByRole('button', { name: prompt, exact: true })
await row.waitFor()
// The frequency line names the stored zone by its localized label: the
// current UTC offset and the ICU generic name of that zone. Both parts
// come from the same ICU data the app reads, so the assertion follows the
// runtime's own zone names.
const zoneName = new Intl.DateTimeFormat('en', { timeZone, timeZoneName: 'longGeneric' })
.formatToParts(new Date()).find(part => part.type === 'timeZoneName')?.value
const zoneOffset = new Intl.DateTimeFormat('en', { timeZone, timeZoneName: 'longOffset' })
.formatToParts(new Date()).find(part => part.type === 'timeZoneName')?.value?.replace(/^GMT/, 'UTC')
const frequency = `Daily at ${time.replace(/:00$/, '')} (${zoneOffset} · ${zoneName})`
expect(await row.textContent()).toContain(frequency)
await row.click()
const detail = manager.getByRole('complementary', { name: 'Task details' })
expect(await detail.getByText(frequency, { exact: true }).count()).toBe(1)
expect(await detail.getByText('Once', { exact: true }).count()).toBe(0)
const runTime = detail.getByRole('region', { name: 'Run time', exact: true })
const timeField = runTime.getByRole('button', { name: 'Time', exact: true })
// The Time zone row is a menu trigger button; the zone is chosen, never typed.
const zoneField = runTime.getByRole('button', { name: /Time zone/ })
await zoneField.click()
// Zone rows lead with the current offset and the localized zone name, so
// the browser's own zone is the row marked as the system one.
await page.getByRole('menuitem', { name: /China Standard Time \(system\)$/ }).click()
// The Time row opens three clock columns; a pick stages the clock itself,
// and the stored rule keeps the millisecond precision the picker writes.
await timeField.click()
expect(await timeField.getAttribute('aria-expanded')).toBe('true')
await page.getByRole('listbox', { name: 'Hour', exact: true }).getByRole('option', { name: '12', exact: true }).click()
await page.getByRole('listbox', { name: 'Minute', exact: true }).getByRole('option', { name: '34', exact: true }).click()
await page.getByRole('listbox', { name: 'Second', exact: true }).getByRole('option', { name: '56', exact: true }).click()
await page.keyboard.press('Escape')
expect(await timeField.textContent()).toBe('12:34:56')
await expect.poll(() => detail.getByText('Unsaved changes', { exact: true }).count(), { timeout: 15_000 }).toBe(1)
// The staged draft reaches the Host only through the save bar's Save action.
await detail.getByRole('button', { name: 'Save changes', exact: true }).click()
await expect.poll(async () => (await scaffold.ctx.schedule.catalog()).find(task => task.id === created.id), { timeout: 15_000 })
.toMatchObject({ time: '12:34:56.000', timeZone: 'Asia/Shanghai' })
const saved = (await scaffold.ctx.schedule.catalog()).find(task => task.id === created.id)
expect(saved).toMatchObject({
id: created.id, sessionId: CATALOG_SESSION_ID, prompt, status: 'active',
kind: 'daily', time: '12:34:56.000', timeZone: 'Asia/Shanghai',
})
// A stored zone equal to the browser zone is omitted from the frequency line.
await expect.poll(() => detail.getByText('Daily at 12:34:56', { exact: true }).count(), { timeout: 15_000 }).toBe(1)
expect(await scaffold.ctx.schedule.history({ sessionId: CATALOG_SESSION_ID, id: created.id, limit: 20 }))
.toEqual({ id: created.id, records: [], earlierRecordsUnavailable: false,
earlierRecordsPruned: false, retention: { days: 30, records: 200 } })
// The created rule stays active, so the ended-status filter hides every
// row; the retired type chips no longer exist.
await manager.getByRole('button', { name: 'Inactive', exact: true }).click()
await expect.poll(() => tasks.getByRole('listitem').count()).toBe(0)
expect(tripwire.pageErrors).toEqual([])
} finally {
await scaffold.ctx.schedule.delete({ sessionId: CATALOG_SESSION_ID, id: created.id })
await page.clock.setFixedTime(new Date(CATALOG_NOW))
}
}, 60_000)
})
+9 -3
View File
@@ -52,7 +52,7 @@ describe.skipIf(MODE === 'record')('web e2e: compact Tool details', () => {
it('expands every recorded tool through its keyed renderer', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-tool-details'))
const rows = page.locator('[data-tool]')
expect(await rows.count()).toBe(44)
expect(await rows.count()).toBe(45)
for (const row of await rows.all()) {
await row.getByRole('button', { expanded: false }).first().click()
expect(await row.getByRole('listitem').count()).toBeGreaterThan(0)
@@ -65,8 +65,14 @@ describe.skipIf(MODE === 'record')('web e2e: compact Tool details', () => {
await page.locator('[data-tool="create_goal"]').getByText('Awaiting continuation', { exact: true }).waitFor()
const schema = page.locator('[data-tool="cordis_inspect_list"] pre').first()
await expect.poll(() => schema.textContent()).toContain('"properties"')
const snapshots = await Promise.all(Array.from({ length: 44 }, (_, index) =>
captureStableAria(page, `[data-chat-call-id="details-call-${index + 1}"] [data-tool]`, scaffold.workspaceCwd)))
const callIds = [
...Array.from({ length: 3 }, (_, index) => `details-call-${index + 1}`),
// The authored schedule_update round sits beside its schedule_create sibling.
'details-call-3b',
...Array.from({ length: 41 }, (_, index) => `details-call-${index + 4}`),
]
const snapshots = await Promise.all(callIds.map(id =>
captureStableAria(page, `[data-chat-call-id="${id}"] [data-tool]`, scaffold.workspaceCwd)))
const snapshot = snapshots.join('\n')
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
await page.setViewportSize({ width: 360, height: 800 })
+1
View File
@@ -89,6 +89,7 @@
"tests/onboarding-deepseek-config.e2e.ts",
"tests/onboarding-native.e2e.ts",
"tests/desktop-onboarding.e2e.ts",
"tests/bonus-notice.e2e.ts",
"tests/onboarding-usable-provider.e2e.ts",
"tests/remote-welcome.e2e.ts",
"tests/public-mount.e2e.ts",
+4 -4
View File
@@ -24,11 +24,11 @@
en: 214c3c81c21df6f9
zh: 75d2cdc3c9fc9930
/deepseek-harness-architecture/turn-flow:
en: 0ea92353b5344090
zh: 46ec58a8e6259335
en: e13500f6d5b614d6
zh: 7e6acb96a520ded2
/deepseek-harness-architecture/session-log:
en: 03a4394c86b18d91
zh: f5df5939f9b42990
en: e1e3a44140036f25
zh: 84c70c969e5ad216
/deepseek-harness-architecture/capability-seams:
en: 93d1dc2eeda4aec2
zh: ee7a9da89151ea17
+2 -2
View File
@@ -110,7 +110,7 @@ turn/end
One inbox feeds the driver; injected context waits for a waking message. AgentLoop’s durable `inbox` projection exposes pending input without live Agents.
`agent/pre-step` decides the accepted input. Listeners may rewrite or reject claimed messages; a rejected or empty first claim closes a durable turn without a step. An enter decision may set `startsRequestSeries`: the loop logs a fresh `request/header` (reason `series`, or `change` with `startsSeries: true` when the envelope also changed). Wrapping listeners preserve that declaration with `{ ...decision, messages }`. After assembly and `step/start`, `agent/request` and `prepareCall()` resolve the actual route before the system prompt and accepted users are committed; cancellation during either async phase commits neither. The prepared call capability governs prompt admission, not the preceding `request/context`. Every attempt synchronously reconciles the same rendered assembly, appends users only on the first attempt, logs header/context as needed, and derives and freezes the request before streaming the bound call. Retries do not repeat assembly or `agent/pre-step`. Surface replacements and image-offload decisions after attachment start a new request series, including during the first resumed pre-step; unchanged resume continues the series. The first admitted step reserves the system head before user messages even for an empty prompt (no wire message). The prompt travels only as `system/message` history: an empty rendering clears all active system nodes, leaving no old prompt model-visible; capable routes can append non-empty updates after the cached prefix; incapable routes and new request series consolidate non-empty prompt text at the first system node, with logged empty replacements for non-empty later system nodes ([decision](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md); [decision rule](../packages/core/agent-loop/README.md#understand-the-implementation)).
`agent/pre-step` decides the accepted input. Listeners may rewrite or reject claimed messages; a rejected or empty first claim closes a durable turn without a step. An enter decision may set `startsRequestSeries`: the loop logs a fresh `request/header` (reason `series`, or `change` with `startsSeries: true` when the envelope also changed). Wrapping listeners preserve that declaration with `{ ...decision, messages }`. After assembly and `step/start`, `agent/request` and `prepareCall()` resolve the actual route before the system prompt and accepted users are committed; cancellation during either async phase commits neither. Prepared call capabilities govern prompt admission, not preceding `request/context`. Every attempt synchronously reconciles the same rendered assembly, appends users only on the first attempt, logs header/context as needed, and derives and freezes the request before streaming the bound call. Retries do not repeat assembly or `agent/pre-step`. Surface replacements and image-offload decisions after attachment start a new request series, including during the first resumed pre-step; unchanged resume continues the series. The first admitted step reserves the system head before user messages even for an empty prompt (no wire message). The prompt travels only as `system/message` history: an empty rendering clears all active system nodes, leaving no old prompt model-visible; capable routes append non-empty updates after cached history, including supported tool updates; incapable routes and new request series consolidate non-empty prompt text at the first system node, with logged empty replacements for non-empty later system nodes ([decision](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md); [decision rule](../packages/core/agent-loop/README.md#understand-the-implementation)).
The loop sends immutable requests with live cancellation and reuses freeze evidence for frozen identities; [agent-loop](../packages/core/agent-loop/README.md) owns construction and cancellation causes.
@@ -122,7 +122,7 @@ The session log is the source of the context the model sees. `deriveMessages()`
Session consumers know only the current logical format. Header-only `stat` and `list` rescan each Session directory, select its numerically highest canonical generation, and translate a supported historical header without loading events or publishing a successor. A stored-session `open` selects that same generation, refuses a future version, or decodes and composes the static adjacent migration chain once before returning validated current logical events. A read open uses that in-memory result without publishing a successor; a write open first encodes, verifies, and exclusively publishes the final version-named successor beside the unchanged source. Ordinary repair of an unsealed interrupted tail remains a handle consumer responsibility; migration inserts a missing interrupted `turn/end` only for the bounded released restart already sealed by a later `turn/start`. JSONL v0 uses `session.jsonl[.zstd]`, v1 and later use lowercase `session.vN.jsonl[.zstd]`, and committed generation paths are never renamed, replaced, or deleted. The JSONL provider owns physical framing, compression, generation selection, and exclusive publication, while each adjacent migration package owns exactly one `vN -> vN+1` step ([decision](../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md)).
**Model-visible means logged.** Anything that reaches a model request must be reconstructable from the log, and a runtime invariant asserts it. A new model-visible input requires a session event. Plugins that change existing message content register [pure message projections](subsystems/session.md#plugin-owned-message-projections); detached readers supply the same definitions explicitly.
**Model-visible means logged.** A runtime invariant checks model requests are reconstructable from the log. New model-visible inputs require session events. Plugins changing existing message content register [pure message projections](subsystems/session.md#plugin-owned-message-projections); detached readers supply the same definitions explicitly. Tool changes are capability-independent; [Session tool history](../packages/core/session/README.md) supplies provider declarations.
**Projection seam.** `dsh-session-projection` owns `ctx.sessionProjections`: registered units fold committed events incrementally, host consumers read one typed state with `stateOf()`, and carriers batch cropped client views with `snapshot()`. A host reader either requires this service during activation or fails explicitly when the registry or required key is absent. Contributors may retain `ctx.inject(['sessionProjections'], ...)` registration without silently defaulting a missing host value. The agent loop registers shared `turnBoundary` state for its readers ([decision](../.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.md)).
+2 -2
View File
@@ -114,7 +114,7 @@ turn/end
输入通过同一个 inbox 到达驱动器;注入的上下文等待一条唤醒消息。AgentLoop 的持久 `inbox` 投影使待处理输入在没有活跃 Agent 时仍可读取。
`agent/pre-step` 决定接纳的输入。监听器可以改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,`agent/request` 和 `prepareCall()` 先解析实际路由,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试同步协调同一份已渲染组装结果、仅在首次尝试追加用户消息、按需记录 header/context、派生并冻结请求,再通过绑定调用发起流式请求。重试不重复组装或 `agent/pre-step`。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。首次接纳的步骤在用户消息之前预留系统头节点,即使提示词为空(不产生协议消息)。提示词仅通过 `system/message` 历史传递:空渲染文本清除所有生效的系统节点,模型不再看到旧提示词;具备能力的路由可在缓存前缀之后追加非空更新;不具备能力的路由与新请求序列将非空提示词文本归并到首个系统节点,并为非空的后续系统节点记录空内容替换([决策](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md);[决策规则](../packages/core/agent-loop/README.zh.md#understand-the-implementation))。
`agent/pre-step` 决定接纳的输入。监听器可以改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,`agent/request` 和 `prepareCall()` 先解析实际路由,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试同步协调同一份已渲染组装结果、仅在首次尝试追加用户消息、按需记录 header/context、派生并冻结请求,再通过绑定调用发起流式请求。重试不重复组装或 `agent/pre-step`。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。首次接纳的步骤在用户消息之前预留系统头节点,即使提示词为空(不产生协议消息)。提示词仅通过 `system/message` 历史传递:空渲染文本清除所有生效的系统节点,模型不再看到旧提示词;具备能力的路由可在缓存前缀之后追加非空更新,包括同时发生的受支持工具更新;不具备能力的路由与新请求序列将非空提示词文本归并到首个系统节点,并为非空的后续系统节点记录空内容替换([决策](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md);[决策规则](../packages/core/agent-loop/README.zh.md#understand-the-implementation))。
循环发送不可变请求,同时保持取消有效,仅对完全冻结的对象复用冻结证据;[agent-loop](../packages/core/agent-loop/README.zh.md) 负责请求构建与取消原因。
@@ -126,7 +126,7 @@ turn/end
Session 消费方只了解当前逻辑格式。仅 header 的 `stat` 与 `list` 会重新扫描每个 Session 目录,选择数值最高的规范 generation,并在不加载事件或发布后继的情况下转换受支持的历史 header。已存储 Session 的 `open` 选择同一 generation,拒绝未来版本,或只 Decode 并组合一次构建时静态确定的相邻迁移链,再返回经过校验的当前逻辑事件。只读 open 直接使用这份内存结果,不发布后继;写 open 则先编码、校验并在未改变源的旁边排他发布最终版本命名的后继。未被后续事件封住的普通中断尾部仍由句柄消费方修复;只有在后续 `turn/start` 已经封住一种有限的已发布 restart 时,migration 才会插入缺失的 interrupted `turn/end`。JSONL v0 使用 `session.jsonl[.zstd]`,v1 及后续版本使用小写 `session.vN.jsonl[.zstd]`;已提交 generation 路径绝不重命名、替换或删除。JSONL provider 负责物理 framing、压缩、generation 选择与排他发布,每个相邻迁移包只负责一个 `vN -> vN+1` 步骤([决策](../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md))。
**模型可见即已记录。** 抵达模型请求的一切都必须能从日志重建,并由一项运行时不变量断言这一点。新增模型可见输入需要一个会话事件。修改现有消息内容的插件注册[纯消息投影](subsystems/session.zh.md#plugin-owned-message-projections),独立读取器显式传入相同的处理器。
**模型可见即已记录。** 运行时不变量检查模型请求是否可从日志重建。新增模型可见输入需要会话事件。修改现有消息内容的插件注册[纯消息投影](subsystems/session.zh.md#plugin-owned-message-projections),独立读取器显式传入相同的处理器。 工具变更不依赖能力;[Session 工具历史](../packages/core/session/README.zh.md)提供提供方声明。
**投影 seam。** `dsh-session-projection` 提供 `ctx.sessionProjections`:已注册单元增量折叠已提交事件,host 消费方通过 `stateOf()` 读取单个类型化状态,载体通过 `snapshot()` 批量取得裁剪后的客户端视图。host 读取方要么在激活时要求该服务,要么在注册表或必需 key 缺席时明确失败。贡献方可以保留 `ctx.inject(['sessionProjections'], ...)` 注册,但不能为缺失的 host 值静默提供默认值。agent loop 为读取方注册共享的 `turnBoundary` 状态([决策](../.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.zh.md))。
+2 -2
View File
@@ -6,5 +6,5 @@
en: 41d772192075e133
zh: 5ef3da69b4de1d8a
/capability-seams-and-core-services:
en: ea114a5c950fa88f
zh: 1d42873021ba511d
en: 35a8ab92922009f2
zh: b2d0accdef6b94bd
+4
View File
@@ -174,6 +174,8 @@ flowchart LR
svc_agentLoop["ctx.agentLoop<br/>Concrete loop driver"]
pkg_base["base"]
pkg_sdk_minimal["sdk-minimal"]
pkg_schedule["schedule"]
svc_schedule["ctx.schedule<br/>Host scheduled messages"]
pkg_goal["goal"]
svc_goals["ctx.goals<br/>Same-session goal domain"]
pkg_ssh["ssh"]
@@ -357,6 +359,7 @@ flowchart LR
pkg_sandbox_local --> svc_sandbox
pkg_sandbox_policy --> svc_sandboxPolicy
pkg_sandbox_ssh --> svc_sandbox
pkg_schedule --> svc_schedule
pkg_session --> svc_sessions
pkg_session_log_deepseek --> svc_deepseekLlmApiExtensions
pkg_session_persistence --> svc_sessionPersistence
@@ -626,6 +629,7 @@ flowchart LR
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. |
| `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`api-session-controller`](../packages/api/session-controller), [`headless`](../packages/bundle/headless) | - | Reads the default ModelSelection from volatile Config and saves selections through the profile editor. |
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`base`](../packages/bundle/base), [`sdk-minimal`](../packages/bundle/sdk-minimal) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. |
| `ctx.schedule` | `core` | [`schedule`](../packages/schedule/schedule) | - | - | - | Stores tasks independently of Session activation and queues due messages in the original Session. |
| `ctx.goals` | `core` | [`goal`](../packages/goal/goal) | - | - | - | Folds revisioned objective state from the session log and keeps live continuation activation process-local. |
| `ctx.ssh` | `core` | [`ssh`](../packages/ssh/ssh) | - | [`fs-ssh`](../packages/ssh/fs-ssh), [`subprocess-ssh`](../packages/ssh/subprocess-ssh), [`sandbox-ssh`](../packages/ssh/sandbox-ssh) | - | Owns one authenticated OpenSSH connection, installed helper identity, independent program streams and disconnect cleanup for the paired remote providers. |
| `ctx.subprocess` | `seam` | [`subprocess`](../packages/subprocess/subprocess) | [`subprocess-local`](../packages/subprocess/subprocess-local), [`subprocess-ssh`](../packages/ssh/subprocess-ssh) | [`bash-local`](../packages/shell/bash-local), [`bash-sandbox`](../packages/shell/bash-sandbox), [`terminal-bash`](../packages/terminal/terminal-bash), [`lsp-stdio`](../packages/lsp/lsp-stdio), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | - | The bash executors, the PTY shell backend, the LSP host, and the out-of-process ACP, Codex, and Claude Code subagent backends spawn through ctx.subprocess; the service owns process coordinates, tree/session lifetime, stdio dispositions, terminal mechanics, and kill escalation. |
+4
View File
@@ -176,6 +176,8 @@ flowchart LR
svc_agentLoop["ctx.agentLoop<br/>Concrete loop driver"]
pkg_base["base"]
pkg_sdk_minimal["sdk-minimal"]
pkg_schedule["schedule"]
svc_schedule["ctx.schedule<br/>Host scheduled messages"]
pkg_goal["goal"]
svc_goals["ctx.goals<br/>Same-session goal domain"]
pkg_ssh["ssh"]
@@ -359,6 +361,7 @@ flowchart LR
pkg_sandbox_local --> svc_sandbox
pkg_sandbox_policy --> svc_sandboxPolicy
pkg_sandbox_ssh --> svc_sandbox
pkg_schedule --> svc_schedule
pkg_session --> svc_sessions
pkg_session_log_deepseek --> svc_deepseekLlmApiExtensions
pkg_session_persistence --> svc_sessionPersistence
@@ -628,6 +631,7 @@ flowchart LR
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | - | 拥有实时 Agent 句柄、创建/恢复工厂 seam,以及进程本地的发起方传播。 |
| `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`api-session-controller`](../packages/api/session-controller), [`headless`](../packages/bundle/headless) | - | Reads the default ModelSelection from volatile Config and saves selections through the profile editor. |
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`base`](../packages/bundle/base), [`sdk-minimal`](../packages/bundle/sdk-minimal) | - | 唯一的具体循环插件;扩展包依赖 dsh-agent 的事件和服务,而不依赖此包。 |
| `ctx.schedule` | `core` | [`schedule`](../packages/schedule/schedule) | - | - | - | 独立于 Session 的加载状态存储任务,并将到期消息排入原 Session。 |
| `ctx.goals` | `core` | [`goal`](../packages/goal/goal) | - | - | - | 从会话日志折叠带修订版本的目标状态,并将实时延续激活保留在进程本地。 |
| `ctx.ssh` | `core` | [`ssh`](../packages/ssh/ssh) | - | [`fs-ssh`](../packages/ssh/fs-ssh), [`subprocess-ssh`](../packages/ssh/subprocess-ssh), [`sandbox-ssh`](../packages/ssh/sandbox-ssh) | - | 负责一条经过认证的 OpenSSH 连接、已安装辅助程序身份、独立程序流,以及配套远端提供方的断连清理。 |
| `ctx.subprocess` | `seam` | [`subprocess`](../packages/subprocess/subprocess) | [`subprocess-local`](../packages/subprocess/subprocess-local), [`subprocess-ssh`](../packages/ssh/subprocess-ssh) | [`bash-local`](../packages/shell/bash-local), [`bash-sandbox`](../packages/shell/bash-sandbox), [`terminal-bash`](../packages/terminal/terminal-bash), [`lsp-stdio`](../packages/lsp/lsp-stdio), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | - | Bash 执行器、PTY shell 后端、LSP Host,以及进程外 ACP、Codex 和 Claude Code subagent 后端都通过 ctx.subprocess 执行 spawn;该服务负责进程坐标、进程树/会话生命周期、stdio 处置、终端机制和 kill 升级。 |
+34 -4
View File
@@ -558,12 +558,16 @@ export interface Config extends ContactConfig {
developerTools: Volatile<boolean>
}
/** Questionnaire destination and its supported source option. */
/** Questionnaire destination and bonus notice timings shared by Host and Client. */
export interface ContactConfig {
/** HTTPS questionnaire URL; override for a test form. */
contactFormUrl: string
/** Questionnaire source option; empty until Harness is supported by the form. */
contactSource: string
/** First delay before retrying a failed bonus acknowledgement. */
bonusAckRetryDelayMs: number
/** Ceiling for the acknowledgement retry backoff. */
bonusAckRetryMaxDelayMs: number
}
/** Persisted steps; a native top-up page leaves the durable step at credit. */
@@ -1851,8 +1855,8 @@ export type PiAiThinkingTokenBudgetField = NonNullable<OpenAICompletionsCompat['
## `@deepseek-ai/dsh-llm-replay`
- `inject`: `llm`
- `refs`: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts)
- `source`: [`packages/test-support/llm-replay/src/index.ts:1123`](../packages/test-support/llm-replay/src/index.ts)
- `refs`: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts) · [`ToolUpdate`](../packages/llm/llm/src/index.ts)
- `source`: [`packages/test-support/llm-replay/src/index.ts:1129`](../packages/test-support/llm-replay/src/index.ts)
```ts config-catalog
/** Plugin config: the {@link ReplayConfig} inputs, each defaulting to its `DSH_SNAPSHOT_*` env var in `apply`. */
@@ -1920,6 +1924,8 @@ export interface ReplayModelConfig {
defaultReasoningEffort?: string
/** Optional in-history system prompt replacement for a keyless replay route. */
systemPromptUpdate?: SystemPromptUpdate
/** Optional mid-conversation tool declaration mode for a keyless replay route. */
toolUpdate?: ToolUpdate
}
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-llm-replay -->
@@ -2460,6 +2466,31 @@ export interface Config {
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-sandbox-policy -->
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-schedule -->
<a id="deepseek-aidsh-schedule"></a>
## `@deepseek-ai/dsh-schedule`
- `inject`: `agents` · `sessions` · `tools` · `storageDomain` · `sessionController` · `sessionPersistence`
- `source`: [`packages/schedule/schedule/src/index.ts:73`](../packages/schedule/schedule/src/index.ts)
```ts config-catalog
/** Configuration for the Host Schedule domain. */
export interface Config {
/**
* Delivery-history window retained per task, in days; omission defaults to 30.
* Pruning happens when an acknowledgment is appended, and `lastDelivery` is always retained.
*/
deliveryHistoryDays?: number
/**
* Retained delivery records per task; omission defaults to 200. The older of this
* cap and the window wins, and the newest records survive.
*/
deliveryHistoryRecords?: number
}
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-schedule -->
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-sdk-app -->
<a id="deepseek-aidsh-sdk-app"></a>
@@ -4320,7 +4351,6 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
| `@deepseek-ai/dsh-lsp` | — | [`packages/lsp/lsp/src/index.ts`](../packages/lsp/lsp/src/index.ts) |
| `@deepseek-ai/dsh-mcp-resources` | `tools` | [`packages/mcp/mcp-resources/src/index.ts`](../packages/mcp/mcp-resources/src/index.ts) |
| `@deepseek-ai/dsh-sandbox-ssh` | `ssh` | [`packages/ssh/sandbox-ssh/src/index.ts`](../packages/ssh/sandbox-ssh/src/index.ts) |
| `@deepseek-ai/dsh-schedule` | `agents` · `sessions` · `tools` · `sessionPersistence` | [`packages/schedule/schedule/src/index.ts`](../packages/schedule/schedule/src/index.ts) |
| `@deepseek-ai/dsh-session` | — | [`packages/core/session/src/index.ts`](../packages/core/session/src/index.ts) |
| `@deepseek-ai/dsh-session-checkpoint-policy` | `llm` · `sessionPersistence` · `sessions` · `tools` | [`packages/session/session-checkpoint-policy/src/index.ts`](../packages/session/session-checkpoint-policy/src/index.ts) |
| `@deepseek-ai/dsh-session-projection` | — | [`packages/session/session-projection/src/index.ts`](../packages/session/session-projection/src/index.ts) |
+34 -4
View File
@@ -560,12 +560,16 @@ export interface Config extends ContactConfig {
developerTools: Volatile<boolean>
}
/** Questionnaire destination and its supported source option. */
/** Questionnaire destination and bonus notice timings shared by Host and Client. */
export interface ContactConfig {
/** HTTPS questionnaire URL; override for a test form. */
contactFormUrl: string
/** Questionnaire source option; empty until Harness is supported by the form. */
contactSource: string
/** First delay before retrying a failed bonus acknowledgement. */
bonusAckRetryDelayMs: number
/** Ceiling for the acknowledgement retry backoff. */
bonusAckRetryMaxDelayMs: number
}
/** Persisted steps; a native top-up page leaves the durable step at credit. */
@@ -1853,8 +1857,8 @@ export type PiAiThinkingTokenBudgetField = NonNullable<OpenAICompletionsCompat['
## `@deepseek-ai/dsh-llm-replay`
- `inject`: `llm`
- `refs`: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts)
- `source`: [`packages/test-support/llm-replay/src/index.ts:1123`](../packages/test-support/llm-replay/src/index.ts)
- `refs`: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts) · [`ToolUpdate`](../packages/llm/llm/src/index.ts)
- `source`: [`packages/test-support/llm-replay/src/index.ts:1129`](../packages/test-support/llm-replay/src/index.ts)
```ts config-catalog
/** Plugin config: the {@link ReplayConfig} inputs, each defaulting to its `DSH_SNAPSHOT_*` env var in `apply`. */
@@ -1922,6 +1926,8 @@ export interface ReplayModelConfig {
defaultReasoningEffort?: string
/** Optional in-history system prompt replacement for a keyless replay route. */
systemPromptUpdate?: SystemPromptUpdate
/** Optional mid-conversation tool declaration mode for a keyless replay route. */
toolUpdate?: ToolUpdate
}
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-llm-replay -->
@@ -2462,6 +2468,31 @@ export interface Config {
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-sandbox-policy -->
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-schedule -->
<a id="deepseek-aidsh-schedule"></a>
## `@deepseek-ai/dsh-schedule`
- `inject`: `agents` · `sessions` · `tools` · `storageDomain` · `sessionController` · `sessionPersistence`
- `source`: [`packages/schedule/schedule/src/index.ts:73`](../packages/schedule/schedule/src/index.ts)
```ts config-catalog
/** Configuration for the Host Schedule domain. */
export interface Config {
/**
* Delivery-history window retained per task, in days; omission defaults to 30.
* Pruning happens when an acknowledgment is appended, and `lastDelivery` is always retained.
*/
deliveryHistoryDays?: number
/**
* Retained delivery records per task; omission defaults to 200. The older of this
* cap and the window wins, and the newest records survive.
*/
deliveryHistoryRecords?: number
}
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-schedule -->
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-sdk-app -->
<a id="deepseek-aidsh-sdk-app"></a>
@@ -4322,7 +4353,6 @@ export interface Config {
| `@deepseek-ai/dsh-lsp` | — | [`packages/lsp/lsp/src/index.ts`](../packages/lsp/lsp/src/index.ts) |
| `@deepseek-ai/dsh-mcp-resources` | `tools` | [`packages/mcp/mcp-resources/src/index.ts`](../packages/mcp/mcp-resources/src/index.ts) |
| `@deepseek-ai/dsh-sandbox-ssh` | `ssh` | [`packages/ssh/sandbox-ssh/src/index.ts`](../packages/ssh/sandbox-ssh/src/index.ts) |
| `@deepseek-ai/dsh-schedule` | `agents` · `sessions` · `tools` · `sessionPersistence` | [`packages/schedule/schedule/src/index.ts`](../packages/schedule/schedule/src/index.ts) |
| `@deepseek-ai/dsh-session` | — | [`packages/core/session/src/index.ts`](../packages/core/session/src/index.ts) |
| `@deepseek-ai/dsh-session-checkpoint-policy` | `llm` · `sessionPersistence` · `sessions` · `tools` | [`packages/session/session-checkpoint-policy/src/index.ts`](../packages/session/session-checkpoint-policy/src/index.ts) |
| `@deepseek-ai/dsh-session-projection` | — | [`packages/session/session-projection/src/index.ts`](../packages/session/session-projection/src/index.ts) |
+1 -1
View File
@@ -10,4 +10,4 @@
zh: a8505006244f5619
/event-producer-and-consumer-matrix/non-harness-or-undeclared-event-strings-seen-in-package-source:
en: 96dc0ff1580e5741
zh: f076e3afa8fbc57c
zh: 96dc0ff1580e5741
+10 -9
View File
@@ -12,7 +12,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:71`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` |
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
@@ -20,7 +20,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:611`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:591`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
@@ -41,8 +41,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `cordis/request-run-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:374`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:102`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization), [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform), `remotes` |
| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:90`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` |
| `deepseek-account/model-sign-in-required` | `emit` | [`packages/credentials/deepseek-account/src/types.ts:59`](../packages/credentials/deepseek-account/src/types.ts) | [`deepseek-account`](../packages/credentials/deepseek-account) (`emit`) | `remotes` |
| `deepseek-account/session-expired` | `emit` | [`packages/credentials/deepseek-account/src/types.ts:55`](../packages/credentials/deepseek-account/src/types.ts) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) (`emit`) | `account-controller`, `remotes` |
| `deepseek-account/model-sign-in-required` | `emit` | [`packages/credentials/deepseek-account/src/types.ts:90`](../packages/credentials/deepseek-account/src/types.ts) | [`deepseek-account`](../packages/credentials/deepseek-account) (`emit`) | `remotes` |
| `deepseek-account/session-expired` | `emit` | [`packages/credentials/deepseek-account/src/types.ts:86`](../packages/credentials/deepseek-account/src/types.ts) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) (`emit`) | `account-controller`, `remotes` |
| `deepseek-account/signed-out` | `emit` | [`packages/credentials/deepseek-account/src/index.ts:12`](../packages/credentials/deepseek-account/src/index.ts) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) (`emit`) | [`deepseek-account`](../packages/credentials/deepseek-account) |
| `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace), `workspace-controller` |
| `feedback/committed` | `parallel` | [`packages/feedback/message-feedback/src/index.ts:58`](../packages/feedback/message-feedback/src/index.ts) | [`message-feedback`](../packages/feedback/message-feedback) (`parallel`) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) |
@@ -54,16 +54,17 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `hmr/change` | `emit` | [`packages/boot/hmr/src/index.ts:30`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - |
| `hmr/reload` | `emit` | [`packages/boot/hmr/src/index.ts:35`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - |
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:73`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:75`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
| `permission-presets/catalog-changed` | `emit` | [`packages/interaction/permission-presets/src/types.ts:48`](../packages/interaction/permission-presets/src/types.ts) | [`permission-presets`](../packages/interaction/permission-presets) (`events.dispatch`) | `remotes` |
| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:243`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:249`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:256`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:471`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` |
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:53`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:63`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:75`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:55`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:65`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:77`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:86`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) |
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` |
| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:174`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) |
+11 -10
View File
@@ -14,7 +14,7 @@
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:71`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` |
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
@@ -22,7 +22,7 @@
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:611`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:591`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
@@ -43,8 +43,8 @@
| `cordis/request-run-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:374`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:102`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization), [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform), `remotes` |
| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:90`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` |
| `deepseek-account/model-sign-in-required` | `emit` | [`packages/credentials/deepseek-account/src/types.ts:59`](../packages/credentials/deepseek-account/src/types.ts) | [`deepseek-account`](../packages/credentials/deepseek-account) (`emit`) | `remotes` |
| `deepseek-account/session-expired` | `emit` | [`packages/credentials/deepseek-account/src/types.ts:55`](../packages/credentials/deepseek-account/src/types.ts) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) (`emit`) | `account-controller`, `remotes` |
| `deepseek-account/model-sign-in-required` | `emit` | [`packages/credentials/deepseek-account/src/types.ts:90`](../packages/credentials/deepseek-account/src/types.ts) | [`deepseek-account`](../packages/credentials/deepseek-account) (`emit`) | `remotes` |
| `deepseek-account/session-expired` | `emit` | [`packages/credentials/deepseek-account/src/types.ts:86`](../packages/credentials/deepseek-account/src/types.ts) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) (`emit`) | `account-controller`, `remotes` |
| `deepseek-account/signed-out` | `emit` | [`packages/credentials/deepseek-account/src/index.ts:12`](../packages/credentials/deepseek-account/src/index.ts) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) (`emit`) | [`deepseek-account`](../packages/credentials/deepseek-account) |
| `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace), `workspace-controller` |
| `feedback/committed` | `parallel` | [`packages/feedback/message-feedback/src/index.ts:58`](../packages/feedback/message-feedback/src/index.ts) | [`message-feedback`](../packages/feedback/message-feedback) (`parallel`) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) |
@@ -56,16 +56,17 @@
| `hmr/change` | `emit` | [`packages/boot/hmr/src/index.ts:30`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - |
| `hmr/reload` | `emit` | [`packages/boot/hmr/src/index.ts:35`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - |
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:73`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:75`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
| `permission-presets/catalog-changed` | `emit` | [`packages/interaction/permission-presets/src/types.ts:48`](../packages/interaction/permission-presets/src/types.ts) | [`permission-presets`](../packages/interaction/permission-presets) (`events.dispatch`) | `remotes` |
| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:243`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:249`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:256`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:471`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` |
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:53`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:63`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:75`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:55`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:65`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:77`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:86`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) |
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` |
| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:174`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) |
@@ -107,4 +108,4 @@
| `slots/changed` | `ui-renderer` (`emit`) | - |
<!-- END GENERATED event-producer-consumer:undeclared -->
维护模式:自动生成:Cordis 事件声明及生产方/监听方关系从仓库 TypeScript Program 中解析。
Maintenance mode: generated: Cordis event declarations and producer/listener edges are resolved from the repository TypeScript Program.
+13 -10
View File
@@ -926,15 +926,6 @@ flowchart TD
pkg_agent_preset_registry --> pkg_system_prompt
pkg_agent_preset_registry --> pkg_tools
pkg_agent_preset_registry --> pkg_typert_protocol
pkg_schedule --> pkg_agent
pkg_schedule --> pkg_brand
pkg_schedule --> pkg_invariants
pkg_schedule --> pkg_llm
pkg_schedule --> pkg_session
pkg_schedule --> pkg_session_persistence
pkg_schedule --> pkg_session_projection
pkg_schedule --> pkg_tools
pkg_schedule --> pkg_workspace
pkg_session_checkpoint_policy --> pkg_agent
pkg_session_checkpoint_policy --> pkg_llm
pkg_session_checkpoint_policy --> pkg_session
@@ -1318,6 +1309,18 @@ flowchart TD
pkg_experimental_tool_agent_team --> pkg_session
pkg_experimental_tool_agent_team --> pkg_system_prompt
pkg_experimental_tool_agent_team --> pkg_tools
pkg_schedule --> pkg_agent
pkg_schedule --> pkg_api_session_controller
pkg_schedule --> pkg_brand
pkg_schedule --> pkg_invariants
pkg_schedule --> pkg_llm
pkg_schedule --> pkg_session
pkg_schedule --> pkg_session_persistence
pkg_schedule --> pkg_session_projection
pkg_schedule --> pkg_storage_domain
pkg_schedule --> pkg_tools
pkg_schedule --> pkg_typert_protocol
pkg_schedule --> pkg_workspace
pkg_sdk_client --> pkg_llm
pkg_sdk_client --> pkg_sdk_protocol
pkg_sdk_client --> pkg_session
@@ -1608,7 +1611,6 @@ flowchart TD
| [`tool-lsp`](../packages/lsp/tool-lsp) | `lsp` | [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`mcp-resources`](../packages/mcp/mcp-resources) | `mcp` | [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`agent-preset-registry`](../packages/preset/agent-preset-registry) | `preset` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
| [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools), [`workspace`](../packages/workspace/workspace) |
| [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy) | `session` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) |
| [`session-title-all-prompts-llm`](../packages/session/session-title-all-prompts-llm) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) |
| [`session-title-first-prompt-llm`](../packages/session/session-title-first-prompt-llm) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) |
@@ -1666,6 +1668,7 @@ flowchart TD
| [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session) |
| [`experimental-client-ui-voice-input`](../packages/experimental/client-ui-voice-input) | `experimental` | [`api-gateway`](../packages/api/gateway), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-store`](../packages/client/store), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-plugin-manager`](../packages/client/ui-plugin-manager), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text), [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
| [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) |
| [`sdk-client`](../packages/sdk/client) | `sdk` | [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-hmr`](../packages/client/hmr), [`client-modules`](../packages/client/modules), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web`](../packages/client/web), [`remote-mock`](../packages/test-support/remote-mock), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) |
+13 -10
View File
@@ -928,15 +928,6 @@ flowchart TD
pkg_agent_preset_registry --> pkg_system_prompt
pkg_agent_preset_registry --> pkg_tools
pkg_agent_preset_registry --> pkg_typert_protocol
pkg_schedule --> pkg_agent
pkg_schedule --> pkg_brand
pkg_schedule --> pkg_invariants
pkg_schedule --> pkg_llm
pkg_schedule --> pkg_session
pkg_schedule --> pkg_session_persistence
pkg_schedule --> pkg_session_projection
pkg_schedule --> pkg_tools
pkg_schedule --> pkg_workspace
pkg_session_checkpoint_policy --> pkg_agent
pkg_session_checkpoint_policy --> pkg_llm
pkg_session_checkpoint_policy --> pkg_session
@@ -1320,6 +1311,18 @@ flowchart TD
pkg_experimental_tool_agent_team --> pkg_session
pkg_experimental_tool_agent_team --> pkg_system_prompt
pkg_experimental_tool_agent_team --> pkg_tools
pkg_schedule --> pkg_agent
pkg_schedule --> pkg_api_session_controller
pkg_schedule --> pkg_brand
pkg_schedule --> pkg_invariants
pkg_schedule --> pkg_llm
pkg_schedule --> pkg_session
pkg_schedule --> pkg_session_persistence
pkg_schedule --> pkg_session_projection
pkg_schedule --> pkg_storage_domain
pkg_schedule --> pkg_tools
pkg_schedule --> pkg_typert_protocol
pkg_schedule --> pkg_workspace
pkg_sdk_client --> pkg_llm
pkg_sdk_client --> pkg_sdk_protocol
pkg_sdk_client --> pkg_session
@@ -1610,7 +1613,6 @@ flowchart TD
| [`tool-lsp`](../packages/lsp/tool-lsp) | `lsp` | [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`mcp-resources`](../packages/mcp/mcp-resources) | `mcp` | [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`agent-preset-registry`](../packages/preset/agent-preset-registry) | `preset` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
| [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools), [`workspace`](../packages/workspace/workspace) |
| [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy) | `session` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) |
| [`session-title-all-prompts-llm`](../packages/session/session-title-all-prompts-llm) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) |
| [`session-title-first-prompt-llm`](../packages/session/session-title-first-prompt-llm) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) |
@@ -1668,6 +1670,7 @@ flowchart TD
| [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session) |
| [`experimental-client-ui-voice-input`](../packages/experimental/client-ui-voice-input) | `experimental` | [`api-gateway`](../packages/api/gateway), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-store`](../packages/client/store), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-plugin-manager`](../packages/client/ui-plugin-manager), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text), [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
| [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) |
| [`sdk-client`](../packages/sdk/client) | `sdk` | [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-hmr`](../packages/client/hmr), [`client-modules`](../packages/client/modules), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web`](../packages/client/web), [`remote-mock`](../packages/test-support/remote-mock), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) |
+80 -80
View File
@@ -9,8 +9,8 @@
en: e7c80d1de4d328b4
zh: d9f368c27fd9f998
/session-persistence-event-catalog/persistence-type-fingerprints:
en: 42ad01cbefcc0966
zh: c21832fd903f4d6e
en: 0254fc43535dfaad
zh: 2f592cfae60e10ac
/session-persistence-event-catalog/event-envelope:
en: 7ebb1a2e0dd1f916
zh: 8946f415dda5c1b8
@@ -165,8 +165,8 @@
en: e5b55405bd2b69f5
zh: e5b55405bd2b69f5
/session-persistence-event-catalog/events/schedule/schedule-change-log-only:
en: 1d23d6738a98fbde
zh: 3da1dfebfe2f1d2e
en: 353442ce8925614f
zh: 71e6ab8296c74d2a
/session-persistence-event-catalog/events/session:
en: a1ac8f2660a4f61e
zh: a1ac8f2660a4f61e
@@ -528,8 +528,8 @@
en: 028791baa24a6a0a
zh: 028791baa24a6a0a
/session-persistence-event-catalog/resolved-persistence-types/in-history:
en: 81e6913fffa3c12a
zh: 806ed52e823cc08f
en: 7ee8e3e9b3947c22
zh: 035ceca2d5031800
/session-persistence-event-catalog/resolved-persistence-types/in-progress:
en: 61de2b0c9de8c15b
zh: 61de2b0c9de8c15b
@@ -897,11 +897,8 @@
en: d462b9a5e0855776
zh: d462b9a5e0855776
/session-persistence-event-catalog/resolved-persistence-types/4:
en: 1be6fa10b6117b5c
zh: 1be6fa10b6117b5c
/session-persistence-event-catalog/resolved-persistence-types/afterschedulerecord:
en: fa6f933b0bd06745
zh: b66de9881c47473e
en: 9ec6b8bd755f3e33
zh: 9ec6b8bd755f3e33
/session-persistence-event-catalog/resolved-persistence-types/agentinstructionchange:
en: c79fd9b4b7a0a5ba
zh: d196895911636220
@@ -927,11 +924,8 @@
en: 144fc40c557c39fb
zh: 93edf73620f690bc
/session-persistence-event-catalog/resolved-persistence-types/assistantstreamrecord~2:
en: 92d1cfcb2e6460c7
zh: 709b94be1f8b7270
/session-persistence-event-catalog/resolved-persistence-types/atschedulerecord:
en: 8b2f283ddc7660ac
zh: f449d9579770fae3
en: e393deed01de3665
zh: 0fa98ad93c22ce79
/session-persistence-event-catalog/resolved-persistence-types/commandsource:
en: 86c7e2b4ae099661
zh: 2ffe1bd3f25b96fd
@@ -939,11 +933,11 @@
en: 87f2aaf2b23a5249
zh: 3d6c5ed6603bee31
/session-persistence-event-catalog/resolved-persistence-types/contentblock:
en: e13fed596c512f6c
zh: 516df9e1d246151b
en: d3eb9a17ab27856f
zh: a71b16a1e36a36ac
/session-persistence-event-catalog/resolved-persistence-types/contentblocktype:
en: 7569a19f6b02eaff
zh: aca4414298167b81
en: 27dc8b92bae26f1f
zh: e05ace867fb59ae1
/session-persistence-event-catalog/resolved-persistence-types/contentblock~2:
en: 9207aa0489ccf8db
zh: 850dd92da6769998
@@ -966,11 +960,8 @@
en: d31020fa1fda6fa4
zh: 77984c4bf09443fb
/session-persistence-event-catalog/resolved-persistence-types/everyscheduledispatchchange:
en: 89ddd311d931d6de
zh: 721587f81b40511b
/session-persistence-event-catalog/resolved-persistence-types/everyschedulerecord:
en: 936195016fec40d5
zh: 60b2c64f544a2d2a
en: 54c487394ce0db1d
zh: 48d7c9affbe841cb
/session-persistence-event-catalog/resolved-persistence-types/feedbackrecord:
en: 3b6931556cfe6b9e
zh: ce5a5b1d92ce9eac
@@ -981,8 +972,8 @@
en: 4cab6c6ed2abedd8
zh: a6cdd344e6c36c24
/session-persistence-event-catalog/resolved-persistence-types/finishreason:
en: a140f3c2ab4f213f
zh: 2d90532ca0327934
en: 49fd9834247f3cc2
zh: 4378ead90069d0d6
/session-persistence-event-catalog/resolved-persistence-types/goalblockreason:
en: b1589f9578a585dd
zh: 3783dbbd8793bb37
@@ -1035,8 +1026,20 @@
en: c239aa9c90226c35
zh: 3561e77832c7c8b5
/session-persistence-event-catalog/resolved-persistence-types/jsonvalue~2:
en: 11f46fc45dcbe604
zh: 2661bc7ebf7b6152
en: 6fb52f5c682b7efd
zh: 129b3a4109b617bc
/session-persistence-event-catalog/resolved-persistence-types/legacyafterschedulerecord:
en: 8a1ee52ff092bd30
zh: 10db0a03ecc0ddfd
/session-persistence-event-catalog/resolved-persistence-types/legacyatschedulerecord:
en: 1362c9c357bfd526
zh: 1d22c433bf49ea95
/session-persistence-event-catalog/resolved-persistence-types/legacyeveryschedulerecord:
en: 4db10a2563b18010
zh: cf8d17875d5b9002
/session-persistence-event-catalog/resolved-persistence-types/legacyschedulerecord:
en: d2a3b6594fc71ea0
zh: a5a7df42594fdb57
/session-persistence-event-catalog/resolved-persistence-types/llmcallconfig:
en: aa3a91341bf04b9e
zh: d767b45d45846333
@@ -1080,8 +1083,8 @@
en: 771e9ab3e64076bc
zh: 30d5095a93549d55
/session-persistence-event-catalog/resolved-persistence-types/oneshotscheduledispatchchange:
en: f96aaab863f77802
zh: 3d6f70473794ee06
en: a3300babe57b6b67
zh: 4aaf06a149b399c3
/session-persistence-event-catalog/resolved-persistence-types/oneshotsubagentdescriptordata:
en: 9ba0cfd5ee9d6a38
zh: 1414fd7468c42651
@@ -1104,8 +1107,8 @@
en: 07e04c3e336f880f
zh: 6f8986ede9994fa1
/session-persistence-event-catalog/resolved-persistence-types/replayenvelope:
en: 6d1a36d68e868f6b
zh: 8a4235fb147cd8f4
en: 9a80b6f5eb08eaea
zh: 061c632b8f3e4d87
/session-persistence-event-catalog/resolved-persistence-types/requestcontext:
en: cc8bb7075d546941
zh: 77209abf30744be0
@@ -1113,20 +1116,17 @@
en: 93c4403f94ecdc14
zh: 2b2eb0c400881d71
/session-persistence-event-catalog/resolved-persistence-types/sandboxmode:
en: 15c400d874e75ff1
zh: ebcd2df0ba05f0db
en: 13071a796a3bfcd2
zh: 93e5fbb1cdf78a79
/session-persistence-event-catalog/resolved-persistence-types/schedulechange:
en: 6a8be1d4040fb88c
zh: fe5430a81d577932
en: 16be53be76c16720
zh: 38fd164dde86ccac
/session-persistence-event-catalog/resolved-persistence-types/schedulecreatechange:
en: 2ade6ccef730c278
zh: 9fe4aec379fc45d9
en: c2d3053c1ce07725
zh: 0a3201d7c483c7c1
/session-persistence-event-catalog/resolved-persistence-types/scheduledeletechange:
en: 59231642e68b813a
zh: 80be1173e96d670f
/session-persistence-event-catalog/resolved-persistence-types/schedulerecord:
en: f9867069eed2d3fb
zh: 391a44aba281b0e4
en: 35c0b15c285d045e
zh: 8ccce6ea60dd5742
/session-persistence-event-catalog/resolved-persistence-types/sessionheader:
en: 0f60d736cd9d6c94
zh: 205b8833a4efc5e5
@@ -1155,8 +1155,8 @@
en: ecc15ebadb77afdf
zh: 97ac27d6a4175f6f
/session-persistence-event-catalog/resolved-persistence-types/streamchunk:
en: 8682019620da1958
zh: c4b8ba8f3c432194
en: e865a9a3b7749c1a
zh: 16800bf55135c89e
/session-persistence-event-catalog/resolved-persistence-types/subagentcatalogevent:
en: e9c8f82e0497e5a5
zh: 55ab13b1a5067759
@@ -1203,8 +1203,8 @@
en: 8bf023246cbd0887
zh: dbf7113574c1bf59
/session-persistence-event-catalog/resolved-persistence-types/tokenusage:
en: bdf729b844aa389c
zh: c086d4fbc87c79c0
en: 0aa983637b619157
zh: e0f68258606aae78
/session-persistence-event-catalog/resolved-persistence-types/tooladditionblock:
en: 7ba7c600fc529015
zh: 94b69e9d5bd1fcfd
@@ -1224,8 +1224,8 @@
en: 2258683c3cfa4ce3
zh: 5663aa76c911761b
/session-persistence-event-catalog/resolved-persistence-types/toolschema:
en: 9c8dae9974f1670f
zh: f1f10c58059fc868
en: 6e1e6ebc7c1c38c8
zh: e47a3ca80c4c3b71
/session-persistence-event-catalog/resolved-persistence-types/toolschema~2:
en: 12c14727bb7834e2
zh: b0f3c877873c36db
@@ -1278,8 +1278,8 @@
en: 38211d71372021e4
zh: c0305af81a67e285
/session-persistence-event-catalog/resolved-persistence-types/string:
en: 8f788be68a2720c8
zh: 67ec07a9c8699f6f
en: 052f9ce1988e44f2
zh: 5adaad6c9bca43e2
/session-persistence-event-catalog/resolved-persistence-types/string~2:
en: 0089620bf4638263
zh: 1922bef1713094bb
@@ -1416,11 +1416,11 @@
en: 423077da59c080a5
zh: ea8a92a786603279
/session-persistence-event-catalog/resolved-persistence-types/failure-kind-aborted:
en: f0e715f0fdaf08fa
zh: ab28e85aad591d69
en: 349cd90f2598fd12
zh: 1eda02d520ad5d76
/session-persistence-event-catalog/resolved-persistence-types/failure-kind-error:
en: 821ba2489295bff5
zh: b5682cea9895aff7
en: 51af619ed8532a43
zh: a5ad878c63d0e046
/session-persistence-event-catalog/resolved-persistence-types/header-reason-startsseries:
en: 62ca405eba4d2757
zh: c6a8861533363ae9
@@ -1545,8 +1545,8 @@
en: 7c000bffbf0c567e
zh: b3210306a125820b
/session-persistence-event-catalog/resolved-persistence-types/kind-max-tokens:
en: 1fb4225e5c5c6384
zh: 3489785e7ae800f5
en: 20433ac79d2cf7a7
zh: 8907d87c5179fca8
/session-persistence-event-catalog/resolved-persistence-types/kind-model-selection:
en: a5165486dca91116
zh: b7051ca8167806ef
@@ -1662,8 +1662,8 @@
en: c99d8a67438771ff
zh: 070f6160fc419fa5
/session-persistence-event-catalog/resolved-persistence-types/kind-stop:
en: 01db92f4780227c2
zh: e04e2eeb4af38d5c
en: 09ce6f3196938ccf
zh: 1b8d5540f47fd3aa
/session-persistence-event-catalog/resolved-persistence-types/kind-time-context:
en: 5733addfc8b12a47
zh: aaf60d5396e30971
@@ -1707,8 +1707,8 @@
en: b05115b92a176f3a
zh: 22ba93572bd0209a
/session-persistence-event-catalog/resolved-persistence-types/kind-tool-calls:
en: d09c7fcfd1693606
zh: a04e0bd15f5b87ea
en: 65fb50e399cddd05
zh: 9e713bbcb1b7db35
/session-persistence-event-catalog/resolved-persistence-types/kind-tool-goal:
en: e11ea3169004ae33
zh: 7d6a3a77cb9bf205
@@ -1857,11 +1857,11 @@
en: bb507524bcc39c53
zh: 8314898562437473
/session-persistence-event-catalog/resolved-persistence-types/type-block-end:
en: 51aab19ee8c0d679
zh: 1a6b88cf5a137de4
en: 8c5a05fa6adf3e70
zh: 66768b25d6fb3e07
/session-persistence-event-catalog/resolved-persistence-types/type-block-start:
en: 239cf3971f4cfc3f
zh: 0da0583120e7f271
en: 5caed426cab416ee
zh: 86f47f90f8c440f2
/session-persistence-event-catalog/resolved-persistence-types/type-chunk:
en: 2186ede3e9917aea
zh: 0cfef64045dce95b
@@ -1899,8 +1899,8 @@
en: 14e4b48d856bcf57
zh: 72ba176535dc7f3f
/session-persistence-event-catalog/resolved-persistence-types/type-finish:
en: b3add00734cb9911
zh: 377548efe8ef9a31
en: 21627e72a24990ff
zh: d71375de325ee9f8
/session-persistence-event-catalog/resolved-persistence-types/type-goal-change:
en: aad2516b62d3ee83
zh: e7204c4dc4e8e408
@@ -1932,8 +1932,8 @@
en: 34e5591d75322649
zh: a216839f305c9796
/session-persistence-event-catalog/resolved-persistence-types/type-reasoning-delta:
en: db3a10d417867d7b
zh: 4378e4b03c05067b
en: a18feb76be195fa0
zh: 3db7147ee4c5c963
/session-persistence-event-catalog/resolved-persistence-types/type-request-context:
en: f8addb1a01da9ca1
zh: 4ef11ebddd302c05
@@ -1941,11 +1941,11 @@
en: f34ce1d84ad0fb1e
zh: c1c49f37c1e33f8c
/session-persistence-event-catalog/resolved-persistence-types/type-sandbox-mode:
en: 590152834b153366
zh: 1d051321572572a7
en: 4d2313b10fef1ff0
zh: 3a2cd6bf870ac7d3
/session-persistence-event-catalog/resolved-persistence-types/type-schedule-change:
en: 6c114aa51492ebf0
zh: 1270e031f482c587
en: 40e813f93a0215de
zh: d46d0f7e04e7323b
/session-persistence-event-catalog/resolved-persistence-types/type-session-log-deepseek-delivery-accepted:
en: d4dbca005a27b78b
zh: b5792c3f38a6ba75
@@ -1992,8 +1992,8 @@
en: 1484738718de5312
zh: 3e6fbe9c64068225
/session-persistence-event-catalog/resolved-persistence-types/type-text-delta:
en: f32f07d70ae189ff
zh: 615eae6be0834444
en: 47ae2d57be710450
zh: b072753349ac505e
/session-persistence-event-catalog/resolved-persistence-types/type-todo-write:
en: 2fa7ba77598a95b4
zh: 50972b51d77e9155
@@ -2001,8 +2001,8 @@
en: 32f732b97739e4d2
zh: f288e48e356969c0
/session-persistence-event-catalog/resolved-persistence-types/type-tool-call-delta:
en: e8023bce4c9a3c15
zh: 73308ba59a8ca17a
en: 1df89f427ccac815
zh: 1bec3d025ea1f7e3
/session-persistence-event-catalog/resolved-persistence-types/type-tool-workflow-agent-end:
en: 8eb5e1eb95722bc7
zh: 8dd5980029600648
@@ -2034,8 +2034,8 @@
en: 1b045de96172be4b
zh: 8a15c6f35b576b37
/session-persistence-event-catalog/resolved-persistence-types/type-usage:
en: d15bdc3a5b652790
zh: 6b78c1388fba47d6
en: a01e61480accb34b
zh: 6387a3cc7666aa7e
/session-persistence-event-catalog/resolved-persistence-types/type-user-message:
en: 58d3e616db84f840
zh: 9aed64a03347caad
+118 -115
View File
@@ -50,7 +50,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover
| `event:request/context` | event | `37cbc9cf06d494cfe5c67f078af1605eacb8e4c2c853e9e26455b73de0ee7ccf` | [`{ type: "request/context" }`](#persistence-type-sha256-37cbc9cf06d494cfe5c67f078af1605eacb8e4c2c853e9e26455b73de0ee7ccf) |
| `event:request/header` | event | `4208123b50df5006b181481ab45fcf1cde807b88d3fd4d340090bc2e202fac41` | [`{ type: "request/header" }`](#persistence-type-sha256-4208123b50df5006b181481ab45fcf1cde807b88d3fd4d340090bc2e202fac41) |
| `event:sandbox/mode` | event | `516da4cdd6d2f1e5ce488e648578ca51f40e458f707b803e5b24de870e799415` | [`{ type: "sandbox/mode" }`](#persistence-type-sha256-516da4cdd6d2f1e5ce488e648578ca51f40e458f707b803e5b24de870e799415) |
| `event:schedule/change` | event | `2a7f86849ae54b3398ee49661a757c4fcb59a6192b7036ee2ff514617e13fb42` | [`{ type: "schedule/change" }`](#persistence-type-sha256-2a7f86849ae54b3398ee49661a757c4fcb59a6192b7036ee2ff514617e13fb42) |
| `event:schedule/change` | event | `a0a2e5c42e1c929445ecd1cd70f49be6b66441894ec72c08e8ae332821d4a3cb` | [`{ type: "schedule/change" }`](#persistence-type-sha256-a0a2e5c42e1c929445ecd1cd70f49be6b66441894ec72c08e8ae332821d4a3cb) |
| `event:session-log-deepseek/delivery-accepted` | event | `d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9` | [`{ type: "session-log-deepseek/delivery-accepted" }`](#persistence-type-sha256-d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9) |
| `event:session/end-seed` | event | `5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e` | [`{ type: "session/end-seed" }`](#persistence-type-sha256-5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e) |
| `event:session/title` | event | `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | [`{ type: "session/title" }`](#persistence-type-sha256-1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5) |
@@ -729,7 +729,7 @@ Source: [`packages/core/session/src/types.ts:402`](../packages/core/session/src/
'request/header': {
header: EpochHeader
reason: RequestHeaderReason
/** A changed header also begins a distinct model-message series. */
/** This request begins a distinct model-message series, independently of the header reason. */
startsSeries?: true
}
```
@@ -775,7 +775,7 @@ Source: [`packages/sandbox/sandbox-policy/src/session-mode.ts:33`](../packages/s
Types: [ScheduleChange](subsystems/schedule.md)
Source: [`packages/schedule/schedule/src/types.ts:228`](../packages/schedule/schedule/src/types.ts)
Source: [`packages/schedule/schedule/src/types.ts:358`](../packages/schedule/schedule/src/types.ts)
### `session/*`
@@ -1919,7 +1919,7 @@ SHA-256: `ebb768d85ec87fb68e42fc14fd7d8535b5ac11791b14d04c6770180f0a719bf8`
SHA-256: `f311671e07bc7fa645a0b6597de9966b97147956565296ba76a36543801b66c5`
Sources: [`packages/llm/llm/src/types.ts:397`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:396`](../packages/llm/llm/src/types.ts)
`"in-history"`
@@ -2907,26 +2907,6 @@ SHA-256: `af6e173f7be819bcd0a5c8d77918649c10ce761d8a184ff472f2e01ce64eb2e0`
`4`
<a id="persistence-type-sha256-27e337d3d6b402b221e50c247a3f518a70d0c474cc60beee15fac3e9c5cbfcbe"></a>
<a id="persistence-type-afterschedulerecord"></a>
<a id="persistence-type-packagesscheduleschedulesrctypestsafterschedulerecord"></a>
### `AfterScheduleRecord`
SHA-256: `27e337d3d6b402b221e50c247a3f518a70d0c474cc60beee15fac3e9c5cbfcbe`
Sources: [`packages/schedule/schedule/src/types.ts:15`](../packages/schedule/schedule/src/types.ts)
| Property | Presence | Type |
|---|---|---|
| `afterSeconds` | required | `number` |
| `id` | required | `string` |
| `kind` | required | `"after"` |
| `prompt` | required | `string` |
| `scheduledAt` | required | `string` |
<a id="persistence-type-sha256-83da72c6857601c75da11212ef847f57d457b9c99f0a82246dcdff3e26700239"></a>
<a id="persistence-type-agentinstructionchange"></a>
@@ -3074,25 +3054,6 @@ SHA-256: `84bbfdce7d2eab5b0c53e72ff7406db856c8bfd621b965b72fd86598ba493bf7`
Array of [`AssistantStreamRecord`](#persistence-type-sha256-33341aac09312300de5a178e6cc6f6544a4876f7f710cfea55eda473c8d329ef).
<a id="persistence-type-sha256-658d895d187b26d83f3b53109a3bf77bbfba9f6f9a6d4bc4ee4315b4eb36239e"></a>
<a id="persistence-type-atschedulerecord"></a>
<a id="persistence-type-packagesscheduleschedulesrctypestsatschedulerecord"></a>
### `AtScheduleRecord`
SHA-256: `658d895d187b26d83f3b53109a3bf77bbfba9f6f9a6d4bc4ee4315b4eb36239e`
Sources: [`packages/schedule/schedule/src/types.ts:29`](../packages/schedule/schedule/src/types.ts)
| Property | Presence | Type |
|---|---|---|
| `id` | required | `string` |
| `kind` | required | `"at"` |
| `prompt` | required | `string` |
| `scheduledAt` | required | `string` |
<a id="persistence-type-sha256-ada310bf0bdb8fed51f3b56ea63f6ea6b18bbd587f04fccb63a14ab0b2a24e05"></a>
<a id="persistence-type-commandsource"></a>
@@ -3137,7 +3098,7 @@ Sources: [`packages/compaction/compaction/src/checkpoint.ts:22`](../packages/com
SHA-256: `8064c03c87c357fd1575cb9395ce86e6dcbe2d275bf7717864a02cddf01827d5`
Sources: [`packages/llm/llm/src/types.ts:151`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:150`](../packages/llm/llm/src/types.ts)
One of:
@@ -3159,7 +3120,7 @@ One of:
SHA-256: `7d65acf0a3a43a8f445ce5d3320b42eca492739fc931b2a7516bfb8368dc2c68`
Sources: [`packages/llm/llm/src/types.ts:149`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:148`](../packages/llm/llm/src/types.ts)
One of:
@@ -3298,7 +3259,7 @@ Sources: [`packages/core/session/src/types.ts:240`](../packages/core/session/src
SHA-256: `e6063d0451dc02026fd9b54ad3f77c7b1b6392ad3e872a7737ac84e13ca72344`
Sources: [`packages/schedule/schedule/src/types.ts:95`](../packages/schedule/schedule/src/types.ts)
Sources: [`packages/schedule/schedule/src/types.ts:244`](../packages/schedule/schedule/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -3307,26 +3268,6 @@ Sources: [`packages/schedule/schedule/src/types.ts:95`](../packages/schedule/sch
| `operation` | required | `"dispatch"` |
| `version` | required | `1` |
<a id="persistence-type-sha256-66ece68282d8c06330fb83329a24ae54c386d5ccfab296bbf18369da8beb7d14"></a>
<a id="persistence-type-everyschedulerecord"></a>
<a id="persistence-type-packagesscheduleschedulesrctypestseveryschedulerecord"></a>
### `EveryScheduleRecord`
SHA-256: `66ece68282d8c06330fb83329a24ae54c386d5ccfab296bbf18369da8beb7d14`
Sources: [`packages/schedule/schedule/src/types.ts:41`](../packages/schedule/schedule/src/types.ts)
| Property | Presence | Type |
|---|---|---|
| `everySeconds` | required | `number` |
| `id` | required | `string` |
| `kind` | required | `"every"` |
| `prompt` | required | `string` |
| `scheduledAt` | required | `string` |
<a id="persistence-type-sha256-7d54a5a5e629e2e7783bfac72683bcc5e969e9f43426b9289826aaf294ff6344"></a>
<a id="persistence-type-feedbackrecord"></a>
@@ -3389,7 +3330,7 @@ Sources: [`packages/llm/llm/src/types.ts:98`](../packages/llm/llm/src/types.ts)
SHA-256: `5808e36c95355f0e745201b5db1b6bcec4ebf5adb75d3e82d7d59cc89635ab80`
Sources: [`packages/llm/llm/src/types.ts:166`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:165`](../packages/llm/llm/src/types.ts)
| kind | Form property | Other required fields | Full definition |
|---|---|---|---|
@@ -3725,6 +3666,86 @@ SHA-256: `72ec79127a9c0d0241b1106a74cc2b24c81ce467170f1c3c93f7b71a9496d227`
Array of [`JsonValue`](#persistence-type-sha256-c592ce75aab73fcab19c1d7845684c72cf402b78d2e1f2833a58ecf9f3598ed6).
<a id="persistence-type-sha256-3fdf9582c3b5597df104d32d97dab6c6ee3a93794f3034ec6ada6272404ea27f"></a>
<a id="persistence-type-legacyafterschedulerecord"></a>
<a id="persistence-type-packagesscheduleschedulesrctypestslegacyafterschedulerecord"></a>
### `LegacyAfterScheduleRecord`
SHA-256: `3fdf9582c3b5597df104d32d97dab6c6ee3a93794f3034ec6ada6272404ea27f`
Sources: [`packages/schedule/schedule/src/types.ts:161`](../packages/schedule/schedule/src/types.ts)
| Property | Presence | Type |
|---|---|---|
| `afterSeconds` | required | `number` |
| `id` | required | `string` |
| `kind` | required | `"after"` |
| `prompt` | required | `string` |
| `scheduledAt` | required | `string` |
| `title` | optional | `string` |
<a id="persistence-type-sha256-b63b089a00b3b9357675b3e0c3d3e9aa4fdbda1fba9c91a6b0c59ffdbb89f505"></a>
<a id="persistence-type-legacyatschedulerecord"></a>
<a id="persistence-type-packagesscheduleschedulesrctypestslegacyatschedulerecord"></a>
### `LegacyAtScheduleRecord`
SHA-256: `b63b089a00b3b9357675b3e0c3d3e9aa4fdbda1fba9c91a6b0c59ffdbb89f505`
Sources: [`packages/schedule/schedule/src/types.ts:167`](../packages/schedule/schedule/src/types.ts)
| Property | Presence | Type |
|---|---|---|
| `id` | required | `string` |
| `kind` | required | `"at"` |
| `prompt` | required | `string` |
| `scheduledAt` | required | `string` |
| `title` | optional | `string` |
<a id="persistence-type-sha256-a926dc71814edd01a4f041eb5fc766d153db3407ef4704e98f4c1b6527309ebb"></a>
<a id="persistence-type-legacyeveryschedulerecord"></a>
<a id="persistence-type-packagesscheduleschedulesrctypestslegacyeveryschedulerecord"></a>
### `LegacyEveryScheduleRecord`
SHA-256: `a926dc71814edd01a4f041eb5fc766d153db3407ef4704e98f4c1b6527309ebb`
Sources: [`packages/schedule/schedule/src/types.ts:173`](../packages/schedule/schedule/src/types.ts)
| Property | Presence | Type |
|---|---|---|
| `everySeconds` | required | `number` |
| `id` | required | `string` |
| `kind` | required | `"every"` |
| `prompt` | required | `string` |
| `scheduledAt` | required | `string` |
| `title` | optional | `string` |
<a id="persistence-type-sha256-4b1e632e05776982a2d21c25a50c7c04c199d311c5675218fa73ff9fe30d22cd"></a>
<a id="persistence-type-legacyschedulerecord"></a>
<a id="persistence-type-packagesscheduleschedulesrctypestslegacyschedulerecord"></a>
### `LegacyScheduleRecord`
SHA-256: `4b1e632e05776982a2d21c25a50c7c04c199d311c5675218fa73ff9fe30d22cd`
Sources: [`packages/schedule/schedule/src/types.ts:185`](../packages/schedule/schedule/src/types.ts)
| kind | Form property | Other required fields | Full definition |
|---|---|---|---|
| `after` | not declared | `afterSeconds`: `number`; `id`: `string`; `prompt`: `string`; `scheduledAt`: `string` | [`LegacyAfterScheduleRecord`](#persistence-type-sha256-3fdf9582c3b5597df104d32d97dab6c6ee3a93794f3034ec6ada6272404ea27f) |
| `at` | not declared | `id`: `string`; `prompt`: `string`; `scheduledAt`: `string` | [`LegacyAtScheduleRecord`](#persistence-type-sha256-b63b089a00b3b9357675b3e0c3d3e9aa4fdbda1fba9c91a6b0c59ffdbb89f505) |
| `every` | not declared | `everySeconds`: `number`; `id`: `string`; `prompt`: `string`; `scheduledAt`: `string` | [`LegacyEveryScheduleRecord`](#persistence-type-sha256-a926dc71814edd01a4f041eb5fc766d153db3407ef4704e98f4c1b6527309ebb) |
<a id="persistence-type-sha256-67d5f73ceae82f5265258142ad6cf172de2c1fef93ba551b7f0454e0a93bffe0"></a>
<a id="persistence-type-llmcallconfig"></a>
@@ -4092,7 +4113,7 @@ Sources: [`packages/api/session-controller/src/types.ts:100`](../packages/api/se
SHA-256: `8e8e91646a5cae3fc78f7532a0013ff2088ae47077642c2edd507a508209b8af`
Sources: [`packages/schedule/schedule/src/types.ts:88`](../packages/schedule/schedule/src/types.ts)
Sources: [`packages/schedule/schedule/src/types.ts:237`](../packages/schedule/schedule/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -4231,7 +4252,7 @@ Sources: [`packages/llm/llm/src/types.ts:68`](../packages/llm/llm/src/types.ts)
SHA-256: `0acd94fe8794574c9ae72c7855ee16d29c546096d4af760d16d1225385764555`
Sources: [`packages/llm/llm/src/types.ts:418`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:430`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -4294,7 +4315,7 @@ One of:
- `"read-only"`
- `"workspace-write"`
<a id="persistence-type-sha256-e1d0e52dcd1855ffb803efc18f8f7b776c48d1c25757e8a75c0281fa298c02e1"></a>
<a id="persistence-type-sha256-2e3523541e942ef6cc9fbc5b91f242e0ca4c6ada479b79b96025b797f6e41f3f"></a>
<a id="persistence-type-packagesscheduleschedulesrctypestsschedulechange"></a>
@@ -4302,18 +4323,18 @@ One of:
### `ScheduleChange`
SHA-256: `e1d0e52dcd1855ffb803efc18f8f7b776c48d1c25757e8a75c0281fa298c02e1`
SHA-256: `2e3523541e942ef6cc9fbc5b91f242e0ca4c6ada479b79b96025b797f6e41f3f`
Sources: [`packages/schedule/schedule/src/types.ts:107`](../packages/schedule/schedule/src/types.ts)
Sources: [`packages/schedule/schedule/src/types.ts:256`](../packages/schedule/schedule/src/types.ts)
One of:
- [`EveryScheduleDispatchChange`](#persistence-type-sha256-e6063d0451dc02026fd9b54ad3f77c7b1b6392ad3e872a7737ac84e13ca72344)
- [`ScheduleDeleteChange`](#persistence-type-sha256-18dc21f12bbac8ade8377e6fbb54179e20c3d955d73fba3244c57fb6bf001df0)
- [`OneShotScheduleDispatchChange`](#persistence-type-sha256-8e8e91646a5cae3fc78f7532a0013ff2088ae47077642c2edd507a508209b8af)
- [`ScheduleCreateChange`](#persistence-type-sha256-ca6425edd20afe34d0cedc6ad1abad1ad7b32e2fbc4c1d24db53939e80bf887f)
- [`ScheduleCreateChange`](#persistence-type-sha256-eea687d437880bd5f55fb447a561112800ca34334c0724d9cecb98493059d9e4)
<a id="persistence-type-sha256-ca6425edd20afe34d0cedc6ad1abad1ad7b32e2fbc4c1d24db53939e80bf887f"></a>
<a id="persistence-type-sha256-eea687d437880bd5f55fb447a561112800ca34334c0724d9cecb98493059d9e4"></a>
<a id="persistence-type-packagesscheduleschedulesrctypestsschedulecreatechange"></a>
@@ -4321,14 +4342,14 @@ One of:
### `ScheduleCreateChange`
SHA-256: `ca6425edd20afe34d0cedc6ad1abad1ad7b32e2fbc4c1d24db53939e80bf887f`
SHA-256: `eea687d437880bd5f55fb447a561112800ca34334c0724d9cecb98493059d9e4`
Sources: [`packages/schedule/schedule/src/types.ts:74`](../packages/schedule/schedule/src/types.ts)
Sources: [`packages/schedule/schedule/src/types.ts:223`](../packages/schedule/schedule/src/types.ts)
| Property | Presence | Type |
|---|---|---|
| `operation` | required | `"create"` |
| `schedule` | required | [`ScheduleRecord`](#persistence-type-sha256-d8f868c6b3e7189de6c1ffe2ea04794ea2aa31315b6347d12536e46020afac87) |
| `schedule` | required | [`LegacyScheduleRecord`](#persistence-type-sha256-4b1e632e05776982a2d21c25a50c7c04c199d311c5675218fa73ff9fe30d22cd) |
| `version` | required | `1` |
<a id="persistence-type-sha256-18dc21f12bbac8ade8377e6fbb54179e20c3d955d73fba3244c57fb6bf001df0"></a>
@@ -4341,7 +4362,7 @@ Sources: [`packages/schedule/schedule/src/types.ts:74`](../packages/schedule/sch
SHA-256: `18dc21f12bbac8ade8377e6fbb54179e20c3d955d73fba3244c57fb6bf001df0`
Sources: [`packages/schedule/schedule/src/types.ts:81`](../packages/schedule/schedule/src/types.ts)
Sources: [`packages/schedule/schedule/src/types.ts:230`](../packages/schedule/schedule/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -4349,24 +4370,6 @@ Sources: [`packages/schedule/schedule/src/types.ts:81`](../packages/schedule/sch
| `operation` | required | `"delete"` |
| `version` | required | `1` |
<a id="persistence-type-sha256-d8f868c6b3e7189de6c1ffe2ea04794ea2aa31315b6347d12536e46020afac87"></a>
<a id="persistence-type-packagesscheduleschedulesrctypestsschedulerecord"></a>
<a id="persistence-type-schedulerecord"></a>
### `ScheduleRecord`
SHA-256: `d8f868c6b3e7189de6c1ffe2ea04794ea2aa31315b6347d12536e46020afac87`
Sources: [`packages/schedule/schedule/src/types.ts:71`](../packages/schedule/schedule/src/types.ts)
| kind | Form property | Other required fields | Full definition |
|---|---|---|---|
| `after` | not declared | `afterSeconds`: `number`; `id`: `string`; `prompt`: `string`; `scheduledAt`: `string` | [`AfterScheduleRecord`](#persistence-type-sha256-27e337d3d6b402b221e50c247a3f518a70d0c474cc60beee15fac3e9c5cbfcbe) |
| `at` | not declared | `id`: `string`; `prompt`: `string`; `scheduledAt`: `string` | [`AtScheduleRecord`](#persistence-type-sha256-658d895d187b26d83f3b53109a3bf77bbfba9f6f9a6d4bc4ee4315b4eb36239e) |
| `every` | not declared | `everySeconds`: `number`; `id`: `string`; `prompt`: `string`; `scheduledAt`: `string` | [`EveryScheduleRecord`](#persistence-type-sha256-66ece68282d8c06330fb83329a24ae54c386d5ccfab296bbf18369da8beb7d14) |
<a id="persistence-type-sha256-1a3440e3577382704d42a6263aa463504eb74c566734a55e9503a63efcd02445"></a>
<a id="persistence-type-packagescoresessionsrctypestssessionheader"></a>
@@ -4543,7 +4546,7 @@ Sources: [`packages/skill/skill/src/index.ts:146`](../packages/skill/skill/src/i
SHA-256: `47e9095591ae36e7c39ee62553097772f2bfa16304536a72dbbc3f691f605180`
Sources: [`packages/llm/llm/src/types.ts:440`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:452`](../packages/llm/llm/src/types.ts)
One of:
@@ -4837,7 +4840,7 @@ Array of [`TodoItem`](#persistence-type-sha256-a99f76fd149051c363f960bfeeb5b9509
SHA-256: `fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394`
Sources: [`packages/llm/llm/src/types.ts:176`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:175`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -4967,7 +4970,7 @@ Sources: [`packages/llm/llm/src/message.ts:173`](../packages/llm/llm/src/message
SHA-256: `66ba70f6f068da443f969a0b947a013c3addd776b9b60a0a393759c19825e9a2`
Sources: [`packages/llm/llm/src/types.ts:461`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:473`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -5315,7 +5318,7 @@ Array of `number`.
SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b`
Sources: [`packages/api/session-controller/src/types.ts:398`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:20`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:20`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:32`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:8`](../packages/experimental/agent-team/src/types.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:12`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/webhook/webhook/src/brand.ts:12`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:6`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:9`](../packages/webhook/webhook/src/brand.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
Sources: [`packages/api/session-controller/src/types.ts:398`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:20`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:20`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:32`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:8`](../packages/experimental/agent-team/src/types.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/webhook/webhook/src/brand.ts:12`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:6`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:9`](../packages/webhook/webhook/src/brand.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
`string`
@@ -5936,7 +5939,7 @@ Sources: [`packages/core/session/src/types.ts:375`](../packages/core/session/src
SHA-256: `8e31bb958e9b015e6f8407eaade32217068159e816f3cc99c3ad76eb889732e0`
Sources: [`packages/llm/llm/src/types.ts:161`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:160`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -5949,7 +5952,7 @@ Sources: [`packages/llm/llm/src/types.ts:161`](../packages/llm/llm/src/types.ts)
SHA-256: `693f4952fb5565da96c06bace8924b098ac2fbc915fde0ff8019701183abc057`
Sources: [`packages/llm/llm/src/types.ts:162`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:161`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -6460,7 +6463,7 @@ Sources: [`packages/core/session/src/types.ts:196`](../packages/core/session/src
SHA-256: `ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7`
Sources: [`packages/core/session/src/types.ts:214`](../packages/core/session/src/types.ts) · [`packages/llm/llm/src/types.ts:160`](../packages/llm/llm/src/types.ts)
Sources: [`packages/core/session/src/types.ts:214`](../packages/core/session/src/types.ts) · [`packages/llm/llm/src/types.ts:159`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -6900,7 +6903,7 @@ SHA-256: `28936c614bd4e7e2774806edcbb3cbfd3ee643ac1cbf0bbdff43d915505a8fe3`
SHA-256: `1d76406cbb2ddadb0dbbb51e9d8bc6cf678fed455cf8eb512f36f7c86d37a62f`
Sources: [`packages/llm/llm/src/types.ts:158`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:157`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -7068,7 +7071,7 @@ SHA-256: `49dab019d00e8c26f27ad2ccff7b604d69e323ae4d17e0d2a7b6b8a830b1ade2`
SHA-256: `287211846beea1eccb5f9acee00d20388a38fcb2e95858fa2332d6f58529379a`
Sources: [`packages/llm/llm/src/types.ts:159`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:158`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -7712,7 +7715,7 @@ SHA-256: `1033093edd0db80ff410e00830b523405e00bb0c7684948e531ff65095799625`
SHA-256: `d4b739491689cc6bb13d07ffac1a32239875abda364c0d5edd8f74faf5e9c00d`
Sources: [`packages/llm/llm/src/types.ts:445`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:457`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -7726,7 +7729,7 @@ Sources: [`packages/llm/llm/src/types.ts:445`](../packages/llm/llm/src/types.ts)
SHA-256: `3bec87f9e6ab3da03346f49923d3c51621debc0159df68605e543c3b2a026f90`
Sources: [`packages/llm/llm/src/types.ts:441`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:453`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -7932,7 +7935,7 @@ SHA-256: `b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8`
SHA-256: `a2b49331716ba0a7078b026d145306a70eae64b7313c3e6204bd931e75ec3bb1`
Sources: [`packages/llm/llm/src/types.ts:447`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:459`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -8106,7 +8109,7 @@ Sources: [`packages/llm/llm/src/assistant-stream.ts:28`](../packages/llm/llm/src
SHA-256: `2abb711460e6238d851c30f3e4bdd87a2aa1887ee70575b3d3b7b25610673095`
Sources: [`packages/llm/llm/src/types.ts:443`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:455`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -8162,17 +8165,17 @@ SHA-256: `516da4cdd6d2f1e5ce488e648578ca51f40e458f707b803e5b24de870e799415`
| `time` | required | `number` |
| `type` | required | `"sandbox/mode"` |
<a id="persistence-type-sha256-2a7f86849ae54b3398ee49661a757c4fcb59a6192b7036ee2ff514617e13fb42"></a>
<a id="persistence-type-sha256-a0a2e5c42e1c929445ecd1cd70f49be6b66441894ec72c08e8ae332821d4a3cb"></a>
<a id="persistence-type-eventschedulechange"></a>
### `{ type: "schedule/change" }`
SHA-256: `2a7f86849ae54b3398ee49661a757c4fcb59a6192b7036ee2ff514617e13fb42`
SHA-256: `a0a2e5c42e1c929445ecd1cd70f49be6b66441894ec72c08e8ae332821d4a3cb`
| Property | Presence | Type |
|---|---|---|
| `data` | required | [`ScheduleChange`](#persistence-type-sha256-e1d0e52dcd1855ffb803efc18f8f7b776c48d1c25757e8a75c0281fa298c02e1) |
| `data` | required | [`ScheduleChange`](#persistence-type-sha256-2e3523541e942ef6cc9fbc5b91f242e0ca4c6ada479b79b96025b797f6e41f3f) |
| `ignorable` | optional | `true` |
| `seq` | required | `number` |
| `time` | required | `number` |
@@ -8426,7 +8429,7 @@ Sources: [`packages/llm/llm/src/assistant-stream.ts:21`](../packages/llm/llm/src
SHA-256: `95080295115fbd83544520ba247b68a03790131f6a2eac323d388758cb542f65`
Sources: [`packages/llm/llm/src/types.ts:442`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:454`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -8474,7 +8477,7 @@ Sources: [`packages/llm/llm/src/assistant-stream.ts:35`](../packages/llm/llm/src
SHA-256: `88a610b831be6d92612a464be5c7d0b88ec24d97bd299837bf74bf7672183c9a`
Sources: [`packages/llm/llm/src/types.ts:444`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:456`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -8652,7 +8655,7 @@ SHA-256: `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8`
SHA-256: `f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515`
Sources: [`packages/llm/llm/src/types.ts:446`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:458`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
+118 -115
View File
@@ -52,7 +52,7 @@
| `event:request/context` | event | `37cbc9cf06d494cfe5c67f078af1605eacb8e4c2c853e9e26455b73de0ee7ccf` | [`{ type: "request/context" }`](#persistence-type-sha256-37cbc9cf06d494cfe5c67f078af1605eacb8e4c2c853e9e26455b73de0ee7ccf) |
| `event:request/header` | event | `4208123b50df5006b181481ab45fcf1cde807b88d3fd4d340090bc2e202fac41` | [`{ type: "request/header" }`](#persistence-type-sha256-4208123b50df5006b181481ab45fcf1cde807b88d3fd4d340090bc2e202fac41) |
| `event:sandbox/mode` | event | `516da4cdd6d2f1e5ce488e648578ca51f40e458f707b803e5b24de870e799415` | [`{ type: "sandbox/mode" }`](#persistence-type-sha256-516da4cdd6d2f1e5ce488e648578ca51f40e458f707b803e5b24de870e799415) |
| `event:schedule/change` | event | `2a7f86849ae54b3398ee49661a757c4fcb59a6192b7036ee2ff514617e13fb42` | [`{ type: "schedule/change" }`](#persistence-type-sha256-2a7f86849ae54b3398ee49661a757c4fcb59a6192b7036ee2ff514617e13fb42) |
| `event:schedule/change` | event | `a0a2e5c42e1c929445ecd1cd70f49be6b66441894ec72c08e8ae332821d4a3cb` | [`{ type: "schedule/change" }`](#persistence-type-sha256-a0a2e5c42e1c929445ecd1cd70f49be6b66441894ec72c08e8ae332821d4a3cb) |
| `event:session-log-deepseek/delivery-accepted` | event | `d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9` | [`{ type: "session-log-deepseek/delivery-accepted" }`](#persistence-type-sha256-d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9) |
| `event:session/end-seed` | event | `5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e` | [`{ type: "session/end-seed" }`](#persistence-type-sha256-5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e) |
| `event:session/title` | event | `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | [`{ type: "session/title" }`](#persistence-type-sha256-1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5) |
@@ -731,7 +731,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
'request/header': {
header: EpochHeader
reason: RequestHeaderReason
/** A changed header also begins a distinct model-message series. */
/** This request begins a distinct model-message series, independently of the header reason. */
startsSeries?: true
}
```
@@ -777,7 +777,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
类型:[ScheduleChange](subsystems/schedule.zh.md)
来源:[`packages/schedule/schedule/src/types.ts:228`](../packages/schedule/schedule/src/types.ts)
来源:[`packages/schedule/schedule/src/types.ts:358`](../packages/schedule/schedule/src/types.ts)
### `session/*`
@@ -1921,7 +1921,7 @@ SHA-256: `ebb768d85ec87fb68e42fc14fd7d8535b5ac11791b14d04c6770180f0a719bf8`
SHA-256: `f311671e07bc7fa645a0b6597de9966b97147956565296ba76a36543801b66c5`
来源:[`packages/llm/llm/src/types.ts:397`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:396`](../packages/llm/llm/src/types.ts)
`"in-history"`
@@ -2909,26 +2909,6 @@ SHA-256: `af6e173f7be819bcd0a5c8d77918649c10ce761d8a184ff472f2e01ce64eb2e0`
`4`
<a id="persistence-type-sha256-27e337d3d6b402b221e50c247a3f518a70d0c474cc60beee15fac3e9c5cbfcbe"></a>
<a id="persistence-type-afterschedulerecord"></a>
<a id="persistence-type-packagesscheduleschedulesrctypestsafterschedulerecord"></a>
### `AfterScheduleRecord`
SHA-256: `27e337d3d6b402b221e50c247a3f518a70d0c474cc60beee15fac3e9c5cbfcbe`
来源:[`packages/schedule/schedule/src/types.ts:15`](../packages/schedule/schedule/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
| `afterSeconds` | 必需 | `number` |
| `id` | 必需 | `string` |
| `kind` | 必需 | `"after"` |
| `prompt` | 必需 | `string` |
| `scheduledAt` | 必需 | `string` |
<a id="persistence-type-sha256-83da72c6857601c75da11212ef847f57d457b9c99f0a82246dcdff3e26700239"></a>
<a id="persistence-type-agentinstructionchange"></a>
@@ -3076,25 +3056,6 @@ SHA-256: `84bbfdce7d2eab5b0c53e72ff7406db856c8bfd621b965b72fd86598ba493bf7`
[`AssistantStreamRecord`](#persistence-type-sha256-33341aac09312300de5a178e6cc6f6544a4876f7f710cfea55eda473c8d329ef) 的数组。
<a id="persistence-type-sha256-658d895d187b26d83f3b53109a3bf77bbfba9f6f9a6d4bc4ee4315b4eb36239e"></a>
<a id="persistence-type-atschedulerecord"></a>
<a id="persistence-type-packagesscheduleschedulesrctypestsatschedulerecord"></a>
### `AtScheduleRecord`
SHA-256: `658d895d187b26d83f3b53109a3bf77bbfba9f6f9a6d4bc4ee4315b4eb36239e`
来源:[`packages/schedule/schedule/src/types.ts:29`](../packages/schedule/schedule/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
| `id` | 必需 | `string` |
| `kind` | 必需 | `"at"` |
| `prompt` | 必需 | `string` |
| `scheduledAt` | 必需 | `string` |
<a id="persistence-type-sha256-ada310bf0bdb8fed51f3b56ea63f6ea6b18bbd587f04fccb63a14ab0b2a24e05"></a>
<a id="persistence-type-commandsource"></a>
@@ -3139,7 +3100,7 @@ SHA-256: `31907de9563751822af6d9be02bafaf26faa098bbd1cb412524134d577cc1648`
SHA-256: `8064c03c87c357fd1575cb9395ce86e6dcbe2d275bf7717864a02cddf01827d5`
来源:[`packages/llm/llm/src/types.ts:151`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:150`](../packages/llm/llm/src/types.ts)
以下类型之一:
@@ -3161,7 +3122,7 @@ SHA-256: `8064c03c87c357fd1575cb9395ce86e6dcbe2d275bf7717864a02cddf01827d5`
SHA-256: `7d65acf0a3a43a8f445ce5d3320b42eca492739fc931b2a7516bfb8368dc2c68`
来源:[`packages/llm/llm/src/types.ts:149`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:148`](../packages/llm/llm/src/types.ts)
以下类型之一:
@@ -3300,7 +3261,7 @@ SHA-256: `9a930283b91d493d6347146f2475bb107294b379415ca02162354f46a64164f7`
SHA-256: `e6063d0451dc02026fd9b54ad3f77c7b1b6392ad3e872a7737ac84e13ca72344`
来源:[`packages/schedule/schedule/src/types.ts:95`](../packages/schedule/schedule/src/types.ts)
来源:[`packages/schedule/schedule/src/types.ts:244`](../packages/schedule/schedule/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -3309,26 +3270,6 @@ SHA-256: `e6063d0451dc02026fd9b54ad3f77c7b1b6392ad3e872a7737ac84e13ca72344`
| `operation` | 必需 | `"dispatch"` |
| `version` | 必需 | `1` |
<a id="persistence-type-sha256-66ece68282d8c06330fb83329a24ae54c386d5ccfab296bbf18369da8beb7d14"></a>
<a id="persistence-type-everyschedulerecord"></a>
<a id="persistence-type-packagesscheduleschedulesrctypestseveryschedulerecord"></a>
### `EveryScheduleRecord`
SHA-256: `66ece68282d8c06330fb83329a24ae54c386d5ccfab296bbf18369da8beb7d14`
来源:[`packages/schedule/schedule/src/types.ts:41`](../packages/schedule/schedule/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
| `everySeconds` | 必需 | `number` |
| `id` | 必需 | `string` |
| `kind` | 必需 | `"every"` |
| `prompt` | 必需 | `string` |
| `scheduledAt` | 必需 | `string` |
<a id="persistence-type-sha256-7d54a5a5e629e2e7783bfac72683bcc5e969e9f43426b9289826aaf294ff6344"></a>
<a id="persistence-type-feedbackrecord"></a>
@@ -3391,7 +3332,7 @@ SHA-256: `f2da0f27c1e6add59982c37b15e4a4912c5b33466859cce3b13d503600e24aef`
SHA-256: `5808e36c95355f0e745201b5db1b6bcec4ebf5adb75d3e82d7d59cc89635ab80`
来源:[`packages/llm/llm/src/types.ts:166`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:165`](../packages/llm/llm/src/types.ts)
| kind | form 属性 | 其他必需字段 | 完整定义 |
|---|---|---|---|
@@ -3727,6 +3668,86 @@ SHA-256: `72ec79127a9c0d0241b1106a74cc2b24c81ce467170f1c3c93f7b71a9496d227`
[`JsonValue`](#persistence-type-sha256-c592ce75aab73fcab19c1d7845684c72cf402b78d2e1f2833a58ecf9f3598ed6) 的数组。
<a id="persistence-type-sha256-3fdf9582c3b5597df104d32d97dab6c6ee3a93794f3034ec6ada6272404ea27f"></a>
<a id="persistence-type-legacyafterschedulerecord"></a>
<a id="persistence-type-packagesscheduleschedulesrctypestslegacyafterschedulerecord"></a>
### `LegacyAfterScheduleRecord`
SHA-256: `3fdf9582c3b5597df104d32d97dab6c6ee3a93794f3034ec6ada6272404ea27f`
来源:[`packages/schedule/schedule/src/types.ts:161`](../packages/schedule/schedule/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
| `afterSeconds` | 必需 | `number` |
| `id` | 必需 | `string` |
| `kind` | 必需 | `"after"` |
| `prompt` | 必需 | `string` |
| `scheduledAt` | 必需 | `string` |
| `title` | 可选 | `string` |
<a id="persistence-type-sha256-b63b089a00b3b9357675b3e0c3d3e9aa4fdbda1fba9c91a6b0c59ffdbb89f505"></a>
<a id="persistence-type-legacyatschedulerecord"></a>
<a id="persistence-type-packagesscheduleschedulesrctypestslegacyatschedulerecord"></a>
### `LegacyAtScheduleRecord`
SHA-256: `b63b089a00b3b9357675b3e0c3d3e9aa4fdbda1fba9c91a6b0c59ffdbb89f505`
来源:[`packages/schedule/schedule/src/types.ts:167`](../packages/schedule/schedule/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
| `id` | 必需 | `string` |
| `kind` | 必需 | `"at"` |
| `prompt` | 必需 | `string` |
| `scheduledAt` | 必需 | `string` |
| `title` | 可选 | `string` |
<a id="persistence-type-sha256-a926dc71814edd01a4f041eb5fc766d153db3407ef4704e98f4c1b6527309ebb"></a>
<a id="persistence-type-legacyeveryschedulerecord"></a>
<a id="persistence-type-packagesscheduleschedulesrctypestslegacyeveryschedulerecord"></a>
### `LegacyEveryScheduleRecord`
SHA-256: `a926dc71814edd01a4f041eb5fc766d153db3407ef4704e98f4c1b6527309ebb`
来源:[`packages/schedule/schedule/src/types.ts:173`](../packages/schedule/schedule/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
| `everySeconds` | 必需 | `number` |
| `id` | 必需 | `string` |
| `kind` | 必需 | `"every"` |
| `prompt` | 必需 | `string` |
| `scheduledAt` | 必需 | `string` |
| `title` | 可选 | `string` |
<a id="persistence-type-sha256-4b1e632e05776982a2d21c25a50c7c04c199d311c5675218fa73ff9fe30d22cd"></a>
<a id="persistence-type-legacyschedulerecord"></a>
<a id="persistence-type-packagesscheduleschedulesrctypestslegacyschedulerecord"></a>
### `LegacyScheduleRecord`
SHA-256: `4b1e632e05776982a2d21c25a50c7c04c199d311c5675218fa73ff9fe30d22cd`
来源:[`packages/schedule/schedule/src/types.ts:185`](../packages/schedule/schedule/src/types.ts)
| kind | form 属性 | 其他必需字段 | 完整定义 |
|---|---|---|---|
| `after` | 未声明 | `afterSeconds`: `number`; `id`: `string`; `prompt`: `string`; `scheduledAt`: `string` | [`LegacyAfterScheduleRecord`](#persistence-type-sha256-3fdf9582c3b5597df104d32d97dab6c6ee3a93794f3034ec6ada6272404ea27f) |
| `at` | 未声明 | `id`: `string`; `prompt`: `string`; `scheduledAt`: `string` | [`LegacyAtScheduleRecord`](#persistence-type-sha256-b63b089a00b3b9357675b3e0c3d3e9aa4fdbda1fba9c91a6b0c59ffdbb89f505) |
| `every` | 未声明 | `everySeconds`: `number`; `id`: `string`; `prompt`: `string`; `scheduledAt`: `string` | [`LegacyEveryScheduleRecord`](#persistence-type-sha256-a926dc71814edd01a4f041eb5fc766d153db3407ef4704e98f4c1b6527309ebb) |
<a id="persistence-type-sha256-67d5f73ceae82f5265258142ad6cf172de2c1fef93ba551b7f0454e0a93bffe0"></a>
<a id="persistence-type-llmcallconfig"></a>
@@ -4094,7 +4115,7 @@ SHA-256: `f09e5e7acf5ff5ccd193ff2eeec015fa2ff7c29d98bc465f8c3302058d47e29b`
SHA-256: `8e8e91646a5cae3fc78f7532a0013ff2088ae47077642c2edd507a508209b8af`
来源:[`packages/schedule/schedule/src/types.ts:88`](../packages/schedule/schedule/src/types.ts)
来源:[`packages/schedule/schedule/src/types.ts:237`](../packages/schedule/schedule/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -4233,7 +4254,7 @@ SHA-256: `814d9434e1ddf9078612b3b34322f26e9363b469213335007f7e9ba7fee9464e`
SHA-256: `0acd94fe8794574c9ae72c7855ee16d29c546096d4af760d16d1225385764555`
来源:[`packages/llm/llm/src/types.ts:418`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:430`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -4296,7 +4317,7 @@ SHA-256: `fab2bb09bc5e3b9db0685e8f5500b029af1a2b33e70bc703c1d5d0f596fbc1fd`
- `"read-only"`
- `"workspace-write"`
<a id="persistence-type-sha256-e1d0e52dcd1855ffb803efc18f8f7b776c48d1c25757e8a75c0281fa298c02e1"></a>
<a id="persistence-type-sha256-2e3523541e942ef6cc9fbc5b91f242e0ca4c6ada479b79b96025b797f6e41f3f"></a>
<a id="persistence-type-packagesscheduleschedulesrctypestsschedulechange"></a>
@@ -4304,18 +4325,18 @@ SHA-256: `fab2bb09bc5e3b9db0685e8f5500b029af1a2b33e70bc703c1d5d0f596fbc1fd`
### `ScheduleChange`
SHA-256: `e1d0e52dcd1855ffb803efc18f8f7b776c48d1c25757e8a75c0281fa298c02e1`
SHA-256: `2e3523541e942ef6cc9fbc5b91f242e0ca4c6ada479b79b96025b797f6e41f3f`
来源:[`packages/schedule/schedule/src/types.ts:107`](../packages/schedule/schedule/src/types.ts)
来源:[`packages/schedule/schedule/src/types.ts:256`](../packages/schedule/schedule/src/types.ts)
以下类型之一:
- [`EveryScheduleDispatchChange`](#persistence-type-sha256-e6063d0451dc02026fd9b54ad3f77c7b1b6392ad3e872a7737ac84e13ca72344)
- [`ScheduleDeleteChange`](#persistence-type-sha256-18dc21f12bbac8ade8377e6fbb54179e20c3d955d73fba3244c57fb6bf001df0)
- [`OneShotScheduleDispatchChange`](#persistence-type-sha256-8e8e91646a5cae3fc78f7532a0013ff2088ae47077642c2edd507a508209b8af)
- [`ScheduleCreateChange`](#persistence-type-sha256-ca6425edd20afe34d0cedc6ad1abad1ad7b32e2fbc4c1d24db53939e80bf887f)
- [`ScheduleCreateChange`](#persistence-type-sha256-eea687d437880bd5f55fb447a561112800ca34334c0724d9cecb98493059d9e4)
<a id="persistence-type-sha256-ca6425edd20afe34d0cedc6ad1abad1ad7b32e2fbc4c1d24db53939e80bf887f"></a>
<a id="persistence-type-sha256-eea687d437880bd5f55fb447a561112800ca34334c0724d9cecb98493059d9e4"></a>
<a id="persistence-type-packagesscheduleschedulesrctypestsschedulecreatechange"></a>
@@ -4323,14 +4344,14 @@ SHA-256: `e1d0e52dcd1855ffb803efc18f8f7b776c48d1c25757e8a75c0281fa298c02e1`
### `ScheduleCreateChange`
SHA-256: `ca6425edd20afe34d0cedc6ad1abad1ad7b32e2fbc4c1d24db53939e80bf887f`
SHA-256: `eea687d437880bd5f55fb447a561112800ca34334c0724d9cecb98493059d9e4`
来源:[`packages/schedule/schedule/src/types.ts:74`](../packages/schedule/schedule/src/types.ts)
来源:[`packages/schedule/schedule/src/types.ts:223`](../packages/schedule/schedule/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
| `operation` | 必需 | `"create"` |
| `schedule` | 必需 | [`ScheduleRecord`](#persistence-type-sha256-d8f868c6b3e7189de6c1ffe2ea04794ea2aa31315b6347d12536e46020afac87) |
| `schedule` | 必需 | [`LegacyScheduleRecord`](#persistence-type-sha256-4b1e632e05776982a2d21c25a50c7c04c199d311c5675218fa73ff9fe30d22cd) |
| `version` | 必需 | `1` |
<a id="persistence-type-sha256-18dc21f12bbac8ade8377e6fbb54179e20c3d955d73fba3244c57fb6bf001df0"></a>
@@ -4343,7 +4364,7 @@ SHA-256: `ca6425edd20afe34d0cedc6ad1abad1ad7b32e2fbc4c1d24db53939e80bf887f`
SHA-256: `18dc21f12bbac8ade8377e6fbb54179e20c3d955d73fba3244c57fb6bf001df0`
来源:[`packages/schedule/schedule/src/types.ts:81`](../packages/schedule/schedule/src/types.ts)
来源:[`packages/schedule/schedule/src/types.ts:230`](../packages/schedule/schedule/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -4351,24 +4372,6 @@ SHA-256: `18dc21f12bbac8ade8377e6fbb54179e20c3d955d73fba3244c57fb6bf001df0`
| `operation` | 必需 | `"delete"` |
| `version` | 必需 | `1` |
<a id="persistence-type-sha256-d8f868c6b3e7189de6c1ffe2ea04794ea2aa31315b6347d12536e46020afac87"></a>
<a id="persistence-type-packagesscheduleschedulesrctypestsschedulerecord"></a>
<a id="persistence-type-schedulerecord"></a>
### `ScheduleRecord`
SHA-256: `d8f868c6b3e7189de6c1ffe2ea04794ea2aa31315b6347d12536e46020afac87`
来源:[`packages/schedule/schedule/src/types.ts:71`](../packages/schedule/schedule/src/types.ts)
| kind | form 属性 | 其他必需字段 | 完整定义 |
|---|---|---|---|
| `after` | 未声明 | `afterSeconds`: `number`; `id`: `string`; `prompt`: `string`; `scheduledAt`: `string` | [`AfterScheduleRecord`](#persistence-type-sha256-27e337d3d6b402b221e50c247a3f518a70d0c474cc60beee15fac3e9c5cbfcbe) |
| `at` | 未声明 | `id`: `string`; `prompt`: `string`; `scheduledAt`: `string` | [`AtScheduleRecord`](#persistence-type-sha256-658d895d187b26d83f3b53109a3bf77bbfba9f6f9a6d4bc4ee4315b4eb36239e) |
| `every` | 未声明 | `everySeconds`: `number`; `id`: `string`; `prompt`: `string`; `scheduledAt`: `string` | [`EveryScheduleRecord`](#persistence-type-sha256-66ece68282d8c06330fb83329a24ae54c386d5ccfab296bbf18369da8beb7d14) |
<a id="persistence-type-sha256-1a3440e3577382704d42a6263aa463504eb74c566734a55e9503a63efcd02445"></a>
<a id="persistence-type-packagescoresessionsrctypestssessionheader"></a>
@@ -4545,7 +4548,7 @@ SHA-256: `672c4aa2a9b736e3d6e5da6346d2e2ed5e3573f2d0baafa18e7407c9f3fee3db`
SHA-256: `47e9095591ae36e7c39ee62553097772f2bfa16304536a72dbbc3f691f605180`
来源:[`packages/llm/llm/src/types.ts:440`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:452`](../packages/llm/llm/src/types.ts)
以下类型之一:
@@ -4839,7 +4842,7 @@ SHA-256: `4b7ddaf3710b88ff9b8df66304f5f97bf03688fd19730eedc2e9535203c820ea`
SHA-256: `fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394`
来源:[`packages/llm/llm/src/types.ts:176`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:175`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -4969,7 +4972,7 @@ SHA-256: `4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731`
SHA-256: `66ba70f6f068da443f969a0b947a013c3addd776b9b60a0a393759c19825e9a2`
来源:[`packages/llm/llm/src/types.ts:461`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:473`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -5317,7 +5320,7 @@ SHA-256: `5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78`
SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b`
来源:[`packages/api/session-controller/src/types.ts:398`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:20`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:20`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:32`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:8`](../packages/experimental/agent-team/src/types.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:12`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/webhook/webhook/src/brand.ts:12`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:6`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:9`](../packages/webhook/webhook/src/brand.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
来源:[`packages/api/session-controller/src/types.ts:398`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:20`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:20`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:32`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:8`](../packages/experimental/agent-team/src/types.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/webhook/webhook/src/brand.ts:12`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:6`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:9`](../packages/webhook/webhook/src/brand.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
`string`
@@ -5938,7 +5941,7 @@ SHA-256: `5302e763151277614b97e77f00e3b326694c6c1247b916a20503c344dc970777`
SHA-256: `8e31bb958e9b015e6f8407eaade32217068159e816f3cc99c3ad76eb889732e0`
来源:[`packages/llm/llm/src/types.ts:161`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:160`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -5951,7 +5954,7 @@ SHA-256: `8e31bb958e9b015e6f8407eaade32217068159e816f3cc99c3ad76eb889732e0`
SHA-256: `693f4952fb5565da96c06bace8924b098ac2fbc915fde0ff8019701183abc057`
来源:[`packages/llm/llm/src/types.ts:162`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:161`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -6462,7 +6465,7 @@ SHA-256: `eaa71be0fbf0f702df8ea6c11d0aa2f9782263d45a10d0fb14eda662f4bfe04c`
SHA-256: `ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7`
来源:[`packages/core/session/src/types.ts:214`](../packages/core/session/src/types.ts) · [`packages/llm/llm/src/types.ts:160`](../packages/llm/llm/src/types.ts)
来源:[`packages/core/session/src/types.ts:214`](../packages/core/session/src/types.ts) · [`packages/llm/llm/src/types.ts:159`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -6902,7 +6905,7 @@ SHA-256: `28936c614bd4e7e2774806edcbb3cbfd3ee643ac1cbf0bbdff43d915505a8fe3`
SHA-256: `1d76406cbb2ddadb0dbbb51e9d8bc6cf678fed455cf8eb512f36f7c86d37a62f`
来源:[`packages/llm/llm/src/types.ts:158`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:157`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -7070,7 +7073,7 @@ SHA-256: `49dab019d00e8c26f27ad2ccff7b604d69e323ae4d17e0d2a7b6b8a830b1ade2`
SHA-256: `287211846beea1eccb5f9acee00d20388a38fcb2e95858fa2332d6f58529379a`
来源:[`packages/llm/llm/src/types.ts:159`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:158`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -7714,7 +7717,7 @@ SHA-256: `1033093edd0db80ff410e00830b523405e00bb0c7684948e531ff65095799625`
SHA-256: `d4b739491689cc6bb13d07ffac1a32239875abda364c0d5edd8f74faf5e9c00d`
来源:[`packages/llm/llm/src/types.ts:445`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:457`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -7728,7 +7731,7 @@ SHA-256: `d4b739491689cc6bb13d07ffac1a32239875abda364c0d5edd8f74faf5e9c00d`
SHA-256: `3bec87f9e6ab3da03346f49923d3c51621debc0159df68605e543c3b2a026f90`
来源:[`packages/llm/llm/src/types.ts:441`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:453`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -7934,7 +7937,7 @@ SHA-256: `b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8`
SHA-256: `a2b49331716ba0a7078b026d145306a70eae64b7313c3e6204bd931e75ec3bb1`
来源:[`packages/llm/llm/src/types.ts:447`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:459`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -8108,7 +8111,7 @@ SHA-256: `71bb3254ec8f1e491ce63b762a7b5f18709de23aa4e8abcc8b886f4b6ce051dd`
SHA-256: `2abb711460e6238d851c30f3e4bdd87a2aa1887ee70575b3d3b7b25610673095`
来源:[`packages/llm/llm/src/types.ts:443`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:455`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -8164,17 +8167,17 @@ SHA-256: `516da4cdd6d2f1e5ce488e648578ca51f40e458f707b803e5b24de870e799415`
| `time` | 必需 | `number` |
| `type` | 必需 | `"sandbox/mode"` |
<a id="persistence-type-sha256-2a7f86849ae54b3398ee49661a757c4fcb59a6192b7036ee2ff514617e13fb42"></a>
<a id="persistence-type-sha256-a0a2e5c42e1c929445ecd1cd70f49be6b66441894ec72c08e8ae332821d4a3cb"></a>
<a id="persistence-type-eventschedulechange"></a>
### `{ type: "schedule/change" }`
SHA-256: `2a7f86849ae54b3398ee49661a757c4fcb59a6192b7036ee2ff514617e13fb42`
SHA-256: `a0a2e5c42e1c929445ecd1cd70f49be6b66441894ec72c08e8ae332821d4a3cb`
| 属性 | 存在性 | 类型 |
|---|---|---|
| `data` | 必需 | [`ScheduleChange`](#persistence-type-sha256-e1d0e52dcd1855ffb803efc18f8f7b776c48d1c25757e8a75c0281fa298c02e1) |
| `data` | 必需 | [`ScheduleChange`](#persistence-type-sha256-2e3523541e942ef6cc9fbc5b91f242e0ca4c6ada479b79b96025b797f6e41f3f) |
| `ignorable` | 可选 | `true` |
| `seq` | 必需 | `number` |
| `time` | 必需 | `number` |
@@ -8428,7 +8431,7 @@ SHA-256: `e72269f931559e901e5e8fe1cfcbd0669fb795f43cc038ff39fba62f60403c33`
SHA-256: `95080295115fbd83544520ba247b68a03790131f6a2eac323d388758cb542f65`
来源:[`packages/llm/llm/src/types.ts:442`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:454`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -8476,7 +8479,7 @@ SHA-256: `3860b4757d512a2869be833acb0faf834335b1b056bf3f83df5474dce95f8fa6`
SHA-256: `88a610b831be6d92612a464be5c7d0b88ec24d97bd299837bf74bf7672183c9a`
来源:[`packages/llm/llm/src/types.ts:444`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:456`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -8654,7 +8657,7 @@ SHA-256: `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8`
SHA-256: `f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515`
来源:[`packages/llm/llm/src/types.ts:446`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:458`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -0,0 +1,28 @@
# Bilingual-pair consistency record for 2026-09-18-schedule-optional-title.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 docs/persistence-changes/2026-09-18-schedule-optional-title.md
/:
en: 3d5a424ae7d7e46b
zh: c22a8e4a62d8cc1a
/2026-09-18-schedule-optional-title:
en: dff09041a3682fc5
zh: f27d9f3e7dfe8996
/2026-09-18-schedule-optional-title/summary:
en: cf0b4015528ea944
zh: c1400b008fffe333
/2026-09-18-schedule-optional-title/table-of-contents:
en: 6cf314a069ceb451
zh: 9adff678e9ecab3b
/2026-09-18-schedule-optional-title/declaration:
en: 6907136f28e48c90
zh: 02f8298485f3d8e7
/2026-09-18-schedule-optional-title/compatibility:
en: d9caa582fc9306ee
zh: 3a725f4e16a5bb8e
/2026-09-18-schedule-optional-title/verification:
en: 62bf89a21ce1ff76
zh: 71ede4795dd8a053
/2026-09-18-schedule-optional-title/dev-note:
en: b75c8458928b72b9
zh: 003ba4e17b4b5269
@@ -0,0 +1,48 @@
---
description: "Records a persistence type transition and its compatibility acknowledgement."
kind: persistence-change
---
# 2026-09-18-schedule-optional-title
English | [中文](2026-09-18-schedule-optional-title.zh.md)
## Summary
Makes the stored title optional on the after, at, and every variants of a persisted schedule/change create record.
## Table of Contents
- [Declaration](#declaration)
- [Compatibility](#compatibility)
- [Verification](#verification)
- [Dev Note](#dev-note)
<a id="declaration"></a>
## Declaration
```yaml persistence-change
schemaVersion: 1
id: 2026-09-18-schedule-optional-title
baseline: false
changes:
- root: "event:schedule/change"
previous: "2026-09-11-initial"
after: "a0a2e5c42e1c929445ecd1cd70f49be6b66441894ec72c08e8ae332821d4a3cb"
decision: same-version
```
<a id="compatibility"></a>
## Compatibility
A version-1 Session event written before titles existed has no title member, so those logs decode and fold without one instead of being refused. Readers that require a name treat an absent title as an unnamed task. The Host task record still requires the member: the storage decoder rejects a stored task without it, and creation, update, and the schedule tools still require it.
<a id="verification"></a>
## Verification
pnpm exec vitest run packages/schedule/schedule/tests: 18 files, 789 tests passed.
<a id="dev-note"></a>
## Dev Note
None.
@@ -0,0 +1,302 @@
{
"formatVersion": 1,
"roots": [
{
"key": "event:schedule/change",
"kind": "event",
"event": "schedule/change",
"surface": false,
"digest": "a0a2e5c42e1c929445ecd1cd70f49be6b66441894ec72c08e8ae332821d4a3cb",
"schema": {
"root": 0,
"nodes": [
{
"kind": "object",
"properties": [
{
"name": "data",
"type": 1,
"optional": false
},
{
"name": "ignorable",
"type": 19,
"optional": true
},
{
"name": "seq",
"type": 13,
"optional": false
},
{
"name": "time",
"type": 13,
"optional": false
},
{
"name": "type",
"type": 20,
"optional": false
}
],
"indices": []
},
{
"kind": "union",
"types": [
2,
6,
8,
9
]
},
{
"kind": "object",
"properties": [
{
"name": "acceptedAt",
"type": 3,
"optional": false
},
{
"name": "id",
"type": 3,
"optional": false
},
{
"name": "operation",
"type": 4,
"optional": false
},
{
"name": "version",
"type": 5,
"optional": false
}
],
"indices": []
},
{
"kind": "primitive",
"type": "string"
},
{
"kind": "literal",
"value": "dispatch"
},
{
"kind": "literal",
"value": 1
},
{
"kind": "object",
"properties": [
{
"name": "id",
"type": 3,
"optional": false
},
{
"name": "operation",
"type": 7,
"optional": false
},
{
"name": "version",
"type": 5,
"optional": false
}
],
"indices": []
},
{
"kind": "literal",
"value": "delete"
},
{
"kind": "object",
"properties": [
{
"name": "id",
"type": 3,
"optional": false
},
{
"name": "operation",
"type": 4,
"optional": false
},
{
"name": "version",
"type": 5,
"optional": false
}
],
"indices": []
},
{
"kind": "object",
"properties": [
{
"name": "operation",
"type": 10,
"optional": false
},
{
"name": "schedule",
"type": 11,
"optional": false
},
{
"name": "version",
"type": 5,
"optional": false
}
],
"indices": []
},
{
"kind": "literal",
"value": "create"
},
{
"kind": "union",
"types": [
12,
15,
17
]
},
{
"kind": "object",
"properties": [
{
"name": "afterSeconds",
"type": 13,
"optional": false
},
{
"name": "id",
"type": 3,
"optional": false
},
{
"name": "kind",
"type": 14,
"optional": false
},
{
"name": "prompt",
"type": 3,
"optional": false
},
{
"name": "scheduledAt",
"type": 3,
"optional": false
},
{
"name": "title",
"type": 3,
"optional": true
}
],
"indices": []
},
{
"kind": "primitive",
"type": "number"
},
{
"kind": "literal",
"value": "after"
},
{
"kind": "object",
"properties": [
{
"name": "everySeconds",
"type": 13,
"optional": false
},
{
"name": "id",
"type": 3,
"optional": false
},
{
"name": "kind",
"type": 16,
"optional": false
},
{
"name": "prompt",
"type": 3,
"optional": false
},
{
"name": "scheduledAt",
"type": 3,
"optional": false
},
{
"name": "title",
"type": 3,
"optional": true
}
],
"indices": []
},
{
"kind": "literal",
"value": "every"
},
{
"kind": "object",
"properties": [
{
"name": "id",
"type": 3,
"optional": false
},
{
"name": "kind",
"type": 18,
"optional": false
},
{
"name": "prompt",
"type": 3,
"optional": false
},
{
"name": "scheduledAt",
"type": 3,
"optional": false
},
{
"name": "title",
"type": 3,
"optional": true
}
],
"indices": []
},
{
"kind": "literal",
"value": "at"
},
{
"kind": "literal",
"value": true
},
{
"kind": "literal",
"value": "schedule/change"
}
]
}
}
],
"types": []
}
@@ -0,0 +1,48 @@
---
description: "记录持久化类型更改及其兼容性确认。"
kind: persistence-change
---
# 2026-09-18-schedule-optional-title
[English](2026-09-18-schedule-optional-title.md) | 中文
## 概述
将持久化的 schedule/change create 记录中 after、at、every 三个变体的已存 title 改为可选。
## 目录
- [声明](#declaration)
- [兼容性](#compatibility)
- [验证](#verification)
- [开发备注](#dev-note)
<a id="declaration"></a>
## 声明
```yaml persistence-change
schemaVersion: 1
id: 2026-09-18-schedule-optional-title
baseline: false
changes:
- root: "event:schedule/change"
previous: "2026-09-11-initial"
after: "a0a2e5c42e1c929445ecd1cd70f49be6b66441894ec72c08e8ae332821d4a3cb"
decision: same-version
```
<a id="compatibility"></a>
## 兼容性
在 title 出现之前写入的版本 1 Session 事件没有 title 成员,因此这些日志可以在没有该成员的情况下解码与折叠,而不会被拒绝。需要名称的读取方将缺失的 title 视为未命名任务。宿主任务记录仍然要求该成员:存储解码器拒绝没有它的已存任务,创建、更新与 schedule 工具也仍然要求它。
<a id="verification"></a>
## 验证
pnpm exec vitest run packages/schedule/schedule/tests:18 个文件、789 个测试通过。
<a id="dev-note"></a>
## 开发备注
无。
+1089 -999
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -3,5 +3,5 @@
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/README.md
/subsystems:
en: abd1088109b69d49
zh: 7564a0579de5e142
en: 58466f6fd60755eb
zh: ab46a374f5a3c3ed
+1 -1
View File
@@ -13,7 +13,7 @@ One page per subsystem of the DeepSeek Harness: what it is, the data structures
| [scope.md](scope.md) | scoped registration identity, dispatch carriers, and the owned `Scope` context |
| [typert.md](typert.md) | Remote invocation descriptors, lookup/Context declarations, Typert registries, and the Host Gateway/Client API boundaries |
| [goal.md](goal.md) | persisted goal identity, lifecycle snapshots, activation, change records, and round attribution |
| [schedule.md](schedule.md) | Session-local reminder records, durable transitions, active views, and ordinary-conversation delivery |
| [schedule.md](schedule.md) | Host-owned reminder records, durable transitions, active and ended views, and ordinary-conversation delivery |
| [todo.md](todo.md) | the todo package's whole-list item type, durable event ownership, projection, and open-turn invariant |
| [deliverables.md](deliverables.md) | what a turn hands to the user: `PresentedFile` deliveries from `present` and the Host-served `WorkspaceChangesSummary` of changed files from git snapshots |
| [commands.md](commands.md) | the human-command registry service: definitions, adapter discovery, direct invocation, results, and parsing views |
Loaded 100 of 460 files, more files were not shown because too many files have changed in this diff. Show more