Merge remote-tracking branch 'origin/master' into feat/unify-code-highlight-languages
This commit is contained in:
commit
ba88134117
109 files changed
+2763
-300
No files matched your search
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-18-session-pin-and-sidebar-archive.md
|
||||
2026-09-18-session-pin-and-sidebar-archive.md: 262f7b76d6ac7eee21a646eedc93b7d936c86675
|
||||
2026-09-18-session-pin-and-sidebar-archive.zh.md: 86ddf4b58781c5974b93562396c3205cfdd0f9dc
|
||||
2026-09-18-session-pin-and-sidebar-archive.md: ac580979b9419e3f00af860e2dce2655fec51908
|
||||
2026-09-18-session-pin-and-sidebar-archive.zh.md: d4719fe75bb8aa1e2ebe9b89d4d80182a7597447
|
||||
@@ -30,7 +30,7 @@ The existing [mode-switch and provisional-New-Session rules](../simplification/2
|
||||
|
||||
`WorkspaceRegistry.unarchiveSession` removes one id from the global archive set on the same serialized operation chain as archive and other registry writes. An absent id is an idempotent no-op with no durable write or notification. Restoration preserves Workspace membership and Session logs, and it neither restores a pin nor changes saved ordering.
|
||||
|
||||
Unarchive performs no Session-existence probe: removing an id cannot introduce an unknown referent, so even an entry whose Session is gone can be removed without listing persistence. The Remote returns the complete archive set, and the existing `archived` increment carries the same state to other Clients. A Client installs a unary reply only while it remains the latest archive-set request; a newer request, follow increment, or replacement baseline supersedes it. Failed writes leave the set unchanged, and the sidebar logs the rejection while retaining the row.
|
||||
Unarchive performs no Session-existence probe: removing an id cannot introduce an unknown referent, so even an entry whose Session is gone can be removed without listing persistence. The Remote returns the complete archive set, and the existing `archived` increment carries the same state to other Clients. A Client installs a unary reply only while it remains the latest archive-set request; a newer request, follow increment, or replacement baseline supersedes it. Failed writes leave the set unchanged, and the sidebar logs the rejection while retaining the row, except the running-work refusal, which opens the stop-and-archive confirmation ([archiving a Session with running work](2026-09-21-archive-stops-running-session-work.md)).
|
||||
|
||||
### Archived visibility and row motion
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ Status: implemented
|
||||
|
||||
`WorkspaceRegistry.unarchiveSession` 与归档及其他注册表写入共用串行操作链,从全局归档集合移除一个 id。id 不在集合中时是幂等空操作,不产生持久写入或通知。恢复保留 Workspace 成员关系与 Session 日志,不恢复置顶,也不改变保存的顺序。
|
||||
|
||||
取消归档不探测 Session 是否存在:移除 id 不会引入未知引用,因此即使 Session 已不存在,也能在不列举持久化记录的情况下移除归档项。Remote 返回完整归档集合,既有 `archived` 增量把同一状态传给其他 Client。Client 只在一元响应仍属于最新归档集合请求时安装它;更新的请求、follow 增量或替换基线会覆盖旧响应的资格。写入失败不改变集合,侧边栏记录拒绝原因并保留原行。
|
||||
取消归档不探测 Session 是否存在:移除 id 不会引入未知引用,因此即使 Session 已不存在,也能在不列举持久化记录的情况下移除归档项。Remote 返回完整归档集合,既有 `archived` 增量把同一状态传给其他 Client。Client 只在一元响应仍属于最新归档集合请求时安装它;更新的请求、follow 增量或替换基线会覆盖旧响应的资格。写入失败不改变集合,侧边栏记录拒绝原因并保留原行;唯一例外是“仍有工作在跑”的拒绝,它会打开停止并归档的确认([归档仍有工作在跑的 Session](2026-09-21-archive-stops-running-session-work.zh.md))。
|
||||
|
||||
### 归档显隐与行动画
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-21-archive-stops-running-session-work.md
|
||||
2026-09-21-archive-stops-running-session-work.md: 2df18c00e6a561e3d85820cbf472bacf5acd9b1d
|
||||
2026-09-21-archive-stops-running-session-work.zh.md: 423a01bd2feab61c62fa0f4b4b4296674e6b24e4
|
||||
@@ -0,0 +1,41 @@
|
||||
# Agent Note: Archiving a Session with running work
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-21-archive-stops-running-session-work.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Archiving was a pure visibility write: `WorkspaceRegistry.archiveSession` added the id to the registry-global archive set and nothing else. A Session archived in the middle of a turn kept running out of sight — the agent, its tool subprocess, and its model requests continued to completion — and the archived row deliberately shows no status dot, so nothing told the user an agent was still spending tokens. A background subagent, an owned job, or a due reminder could likewise keep working or start a new turn in a hidden Session. The Web surface never disposes an Agent, so the exposure lasted until the Host process exited.
|
||||
|
||||
## Decision
|
||||
|
||||
Archive never hides running work. The registry owns the rule and declares one capability seam for it: `workspace/session-activity` (waterfall) asks providers what still runs for a Session; `workspace/session-stop` (parallel) asks them to stop it. Providers register on the root the way any plugin registers a listener, so the workspace package knows no agent, job, or schedule vocabulary. The `SessionActivityKindMap` is empty in the workspace package: each provider merges its own family (`turn` from the Agent registry, `job`, `subagent`, `schedule`) from a module both its faces import, and a consumer that renders them imports those modules for its cases and falls through to a generic line for any other key.
|
||||
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Refuse only, and make the user stop each item.** Reasonix desktop does this. It keeps archive non-destructive, but a stopped Session with a background subagent still running would be refused, and the user would have to hunt down subagents, jobs, and reminders by hand; reminders have no Web control at all. The confirmation dialog states the destruction, which was the only reason to prefer refusal.
|
||||
|
||||
**Wait for settlement before writing.** Codex bounds the wait at ten seconds and proceeds; MiniMax refuses when a turn lock survives its budget. Both keep the user's click pending on the slowest cancel. The stops here are cheap synchronous requests, the pre-step gate guarantees no hidden model step regardless of settlement, and a failed cleanup is acceptable, so the wait moved out of the request path entirely.
|
||||
|
||||
**Stop from the client before archiving.** OpenCode hides in the client and never stops, and its running Sessions vanish from the list while their status events continue. A client-owned semantic reaches one client; the registry-owned seam reaches every caller.
|
||||
|
||||
**Grey the archive action from a client-side activity mirror.** It would need its own derivation of running turns, running children, owned jobs, and active reminders from the list projections, and it could disagree with the Host (an unowned job, a child without a Host Agent). One refused round trip yields the Host's own list instead.
|
||||
|
||||
**Give the workspace or schedule package a dependency on the agent registry.** The waterfall and parallel events let the Agent registry, the job registry seam, the Subagent runtime, and the Schedule plugin provide their families without new dependency edges into the entity registry; each gained an optional type-only edge to `dsh-workspace` for the event types, in the natural feature-to-entity direction (`dsh-workspace` depends on none of them, so no cycle).
|
||||
|
||||
**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.
|
||||
|
||||
## 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.
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Agent Note:归档仍有工作在跑的 Session
|
||||
|
||||
状态:implemented
|
||||
|
||||
[English](2026-09-21-archive-stops-running-session-work.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
归档原本是一次纯可见性写入:`WorkspaceRegistry.archiveSession` 把 id 加进注册表全局的归档集合,别的什么都不做。在回合中途被归档的 Session 会在看不见的地方继续跑——agent、它的工具子进程、它的模型请求一直跑到结束——而归档行刻意不画状态点,用户无从得知还有 agent 在消耗 token。后台子代理、所属任务或到期提醒同样可能继续工作,或在隐藏的 Session 里开启新回合。Web 面从不销毁 Agent,这种暴露会持续到宿主进程退出。
|
||||
|
||||
## 决策
|
||||
|
||||
归档绝不隐藏正在运行的工作。规则归注册表所有,并为此声明一个能力接缝:`workspace/session-activity`(waterfall)向提供方询问某 Session 还有什么在跑;`workspace/session-stop`(parallel)请它们停止。提供方像任何插件注册监听器一样注册在根上,因此 workspace 包不认识任何 agent、任务或提醒的词汇。`SessionActivityKindMap` 在 workspace 包里是空的:每个提供方从其两个面都导入的模块合并自己的族(Agent 注册表的 `turn`、`job`、`subagent`、`schedule`),渲染它们的消费方为其分支导入这些模块,遇到其他任何键时落到通用行。
|
||||
|
||||
- **默认拒绝。** `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 从一份正常收尾的日志继续,取消归档不会带回任何被停止的东西。
|
||||
- **已归档即不跑模型步。** API Session Controller 的 `agent/pre-step` 监听器拒绝为已归档 Session 或其子代理子孙提出的步骤,loop 以 `blocked` 收口且不发请求。这封住了迟到的唤醒投递——子代理结算、排队的 follow-up、在活动作答与写入之间开始的回合——跑出隐藏回合的窗口,也让无视 cancel 的子代理无法替一个谁也看不见的父级干活;取消归档即为整条血缘解除门禁。
|
||||
- **拒绝即确认。** 侧栏先发普通归档;静止的 Session 一如既往无对话框直接归档。`workspace/session-active` 拒绝会打开"停止并归档"对话框,其清单来自宿主的答案而非客户端对运行状态的镜像,因此对话框列出的正是宿主将要停止的内容。确认后带 `stopActivity` 重发;提示提供撤销,撤销只恢复 Session,不会让被停止的工作继续。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**只拒绝,让用户逐项手动停止。** Reasonix 桌面端如此。它保住了归档的非破坏性,但已停止的 Session 若还有后台子代理在跑仍会被拒,用户得自己去追子代理、任务和提醒;提醒在 Web 上根本没有控件。确认对话框已经写明了破坏性,而这本是偏向拒绝的唯一理由。
|
||||
|
||||
**写入前等待收敛。** Codex 把等待限定在十秒然后继续;MiniMax 在回合锁超出预算仍存活时拒绝。两者都让用户的点击悬在最慢的那个取消上。这里的停止是廉价的同步请求,pre-step 门禁无论是否收敛都保证不跑隐藏模型步,清理失败也可接受,因此等待整个移出了请求路径。
|
||||
|
||||
**归档前在客户端停止。** OpenCode 在客户端隐藏且从不停止,它运行中的 Session 从列表消失而状态事件继续。客户端拥有的语义只覆盖一个客户端;注册表拥有的接缝覆盖每一个调用方。
|
||||
|
||||
**用客户端的活动镜像把归档动作置灰。** 那需要从列表投影自行推导运行中的回合、运行中的子代理、所属任务和活动提醒,且可能与宿主不一致(无主任务、没有宿主 Agent 的子代理)。一次被拒绝的往返就能拿到宿主自己的清单。
|
||||
|
||||
**让 workspace 或 schedule 包依赖 agent 注册表。** waterfall 与 parallel 事件让 Agent 注册表、任务注册表接缝、Subagent runtime 和 Schedule 插件无需向实体注册表新增依赖边就能提供各自的族;每一方都为事件类型新增了指向 `dsh-workspace` 的可选纯类型边,方向是自然的"特性依赖实体"(`dsh-workspace` 不依赖它们中的任何一个,因此不成环)。
|
||||
|
||||
**由 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 保证空闲:它要么本来就静止,要么其工作已按用户停止的方式停止,且在恢复之前不会为它跑任何一步。置顶与归档决策里的"归档无需确认"仅对静止 Session 成立;运行中的情形会先询问。停止的收敛是尽力而为:无视 kill 的任务或无视 cancel 的子代理会被记录,pre-step 门禁在此期间阻止该 Session 消耗模型请求。活动检查与归档写入是两步,其间开始的回合会被隐藏,pre-step 先于该写入的每一步连同其工具调用照常执行,直到写入之后的第一步被阻断。先写后停与门禁留下两处残余。在归档写入与 Schedule 插件的删除屏障之间崩溃,会留下一个提醒仍被记录的已归档 Session;取消归档时它们再次出现,与从未请求过停止时一样。冷的已归档 Session 不报告提醒,而恢复其 Agent 的操作——重命名、模型、队列编辑都按 Session 寻址而不取消归档——会重新武装它们:到期的一次性提醒把 follow-up 投递进一个被门禁以 `blocked` 收口的回合,该次触发因此在没有模型步的情况下被消耗,取消归档后也不会重放。
|
||||
|
||||
为此决策调研的同行产品:Codex 与 Kimi Code 先停止再归档;Claude Code 通过让远端会话无法写入来停止;Reasonix 与 MiniMax 拒绝忙碌的 Session,MiniMax 在有界取消之后才拒绝;Pi 没有归档但在每次切换会话前 abort;OpenCode 既不拒绝也不停止。每一个会停止的产品都走自己的常规取消路径,以便日志正常收尾。
|
||||
@@ -0,0 +1,9 @@
|
||||
- dialog "Stop and archive this session?":
|
||||
- heading "Stop and archive this session?" [level=2]
|
||||
- button "Close"
|
||||
- paragraph: “ARCHIVE_ACTIVE run the long command” still has work in progress. Archiving stops it first; you can restore the session later from “Show archived” in the sidebar, and the stopped work will not resume on its own.
|
||||
- list "Work that will be stopped":
|
||||
- listitem: The turn in progress
|
||||
- listitem: "1 background job: node -e \"const fs=require('node:fs'); fs.writeFileSync('started.txt', 'started'); setInterval(() => { fs.appendFileSync('heartbeat.txt', '.') }, 200); setTimeout(() => { fs.writeFileSync('finished.txt', 'finished') }, 60000)\""
|
||||
- button "Cancel"
|
||||
- button "Stop and archive"
|
||||
@@ -258,7 +258,11 @@ describe('web e2e: resident question composer round trip', () => {
|
||||
const originalRow = page.locator('[role="treeitem"]')
|
||||
.filter({ hasText: 'Use the ask_user_question tool' }).first()
|
||||
await page.getByRole('button', { name: 'New session', exact: true }).last().click()
|
||||
await page.getByText('New Session', { exact: true }).waitFor({ timeout: 15_000 })
|
||||
// Scope to the tree: the wide sidebar's New Session button carries the
|
||||
// same visible label, and an unscoped match would either settle on the
|
||||
// button before the row exists or trip strict mode once it does.
|
||||
await page.getByRole('tree', { name: 'Sessions' })
|
||||
.getByText('New Session', { exact: true }).waitFor({ timeout: 15_000 })
|
||||
await expect.poll(() => composer.count(), { timeout: 10_000 }).toBe(0)
|
||||
await originalRow.click()
|
||||
await composer.waitFor({ timeout: 15_000 })
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
// Web e2e: archiving a Session whose turn is still running. The Host refuses
|
||||
// the plain archive, the sidebar turns that refusal into the stop-and-archive
|
||||
// confirmation, and confirming stops the turn the way the stop button does —
|
||||
// the aborted Bash call settles with a tool/result and the turn ends as
|
||||
// cancelled — before the row hides. Unarchiving restores a Session that
|
||||
// continues the conversation. Keyless: the model is a replay override whose
|
||||
// first stream calls Bash with a long-running command, and the second answers
|
||||
// the continuation prompt after the restore.
|
||||
import { mkdtemp, rm, stat, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { Browser, Locator, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { ToolCallId, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import type { ReplayEntry } from '@deepseek-ai/dsh-llm-replay'
|
||||
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, launchWebScaffold, readPersistedEvents,
|
||||
watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot, writeComposerDraft } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/session-archive-active', import.meta.url))
|
||||
const DIALOG_EXPECTED = join(SNAPSHOT_DIR, 'dialog.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
const PROMPT = 'ARCHIVE_ACTIVE run the long command'
|
||||
const CONTINUE_PROMPT = 'ARCHIVE_ACTIVE continue after the restore'
|
||||
const CONTINUED = 'ARCHIVE_ACTIVE_CONTINUED'
|
||||
// Long enough for the archive to land mid-call under CI load; the stop must
|
||||
// end the call well before it would finish on its own.
|
||||
const WAIT_MS = 60_000
|
||||
const HEARTBEAT_MS = 200
|
||||
/** The child the Bash call runs: announce, heartbeat, and only finish long after the archive. */
|
||||
const WAIT_COMMAND = `node -e "const fs=require('node:fs'); fs.writeFileSync('started.txt', 'started'); setInterval(() => { fs.appendFileSync('heartbeat.txt', '.') }, ${HEARTBEAT_MS}); setTimeout(() => { fs.writeFileSync('finished.txt', 'finished') }, ${WAIT_MS})"`
|
||||
|
||||
/**
|
||||
* First stream: one Bash call whose child announces itself, then beats on a
|
||||
* file until it finishes; second stream: the continuation answer. The
|
||||
* heartbeat file is the liveness evidence: a pid is not comparable across the
|
||||
* Linux sandbox's PID namespace, a growing file is.
|
||||
*/
|
||||
function script(): ReplayEntry[] {
|
||||
const args = JSON.stringify({ command: WAIT_COMMAND, description: 'Wait a long time' })
|
||||
const callId = ToolCallId('call_wait')
|
||||
const first: StreamChunk[] = [
|
||||
{ type: 'block-start', index: 0, blockType: 'tool-call' },
|
||||
{ type: 'tool-call-delta', index: 0, id: callId, name: 'bash', argumentsDelta: args },
|
||||
{ type: 'block-end', index: 0, block: { type: 'tool-call', id: callId, name: 'bash', arguments: args } },
|
||||
{ type: 'usage', usage: { inputTokens: 10, outputTokens: 10 } },
|
||||
{ type: 'finish', reason: { kind: 'tool-calls' } },
|
||||
]
|
||||
const second: StreamChunk[] = [
|
||||
{ type: 'block-start', index: 0, blockType: 'text' },
|
||||
{ type: 'text-delta', index: 0, text: CONTINUED },
|
||||
{ type: 'block-end', index: 0, block: { type: 'text', text: CONTINUED } },
|
||||
{ type: 'usage', usage: { inputTokens: 10, outputTokens: 5 } },
|
||||
{ type: 'finish', reason: { kind: 'stop' } },
|
||||
]
|
||||
return [{ kind: 'chunks', chunks: first }, { kind: 'chunks', chunks: second }]
|
||||
}
|
||||
|
||||
async function exists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await stat(path)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function heartbeats(cwd: string): Promise<number> {
|
||||
try {
|
||||
return (await stat(join(cwd, 'heartbeat.txt'))).size
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the Bash child is still beating: its file grows across one settle window. */
|
||||
async function childAlive(cwd: string): Promise<boolean> {
|
||||
const before = await heartbeats(cwd)
|
||||
await new Promise((resolve) => { setTimeout(resolve, HEARTBEAT_MS * 6) })
|
||||
return (await heartbeats(cwd)) > before
|
||||
}
|
||||
|
||||
describe.skipIf(MODE === 'record')('web e2e: archiving a running Session stops it first', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let replayDir: string
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
let sessionId: SessionId
|
||||
|
||||
/**
|
||||
* Reveal and click a row action, re-hovering if a projection update replaces
|
||||
* the row before its hover-only button becomes visible.
|
||||
* @param row - the row owning the action.
|
||||
* @param name - accessible name of the action button.
|
||||
*/
|
||||
async function clickHoverAction(row: Locator, name: string): Promise<void> {
|
||||
const button = row.getByRole('button', { name })
|
||||
await expect.poll(async () => {
|
||||
await row.hover()
|
||||
return await button.isVisible()
|
||||
}, { timeout: 10_000 }).toBe(true)
|
||||
await button.click()
|
||||
}
|
||||
|
||||
/** The one Session row of the fresh workspace group (the group row carries no actions). */
|
||||
function sessionRow(): Locator {
|
||||
return page.locator('[role="treeitem"]').filter({ has: page.locator('button[aria-label^="Session actions for "]') })
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
replayDir = await mkdtemp(join(tmpdir(), 'dsh-session-archive-active-'))
|
||||
const replayOverride = join(replayDir, 'replay.override.json')
|
||||
await writeFile(replayOverride, JSON.stringify(script()))
|
||||
scaffold = await launchWebScaffold({
|
||||
replayFixture: join(replayDir, 'override-only.jsonl'),
|
||||
replayOverride,
|
||||
})
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await connectFreshWorkspace(page, scaffold.workspaceCwd)
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
const failures: unknown[] = []
|
||||
await browser?.close().catch((error: unknown) => failures.push(error))
|
||||
await scaffold?.close().catch((error: unknown) => failures.push(error))
|
||||
if (replayDir !== undefined) {
|
||||
await rm(replayDir, { recursive: true, force: true }).catch((error: unknown) => failures.push(error))
|
||||
}
|
||||
if (failures.length === 1) throw failures[0]
|
||||
if (failures.length > 1) throw new AggregateError(failures, 'session-archive-active e2e cleanup failed')
|
||||
})
|
||||
|
||||
it('refuses the plain archive of a running Session and asks to stop it instead', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-session-archive-active-ask'))
|
||||
const cwd = join(scaffold.workspaceCwd, 'workspace')
|
||||
const input = page.locator('[data-composer-input]').first()
|
||||
await input.waitFor({ timeout: 10_000 })
|
||||
sessionId = await page.evaluate(() => {
|
||||
const selected = JSON.parse(localStorage.getItem('dsh.sessions.current')!) as { sessionId: string }
|
||||
return selected.sessionId
|
||||
}) as SessionId
|
||||
await writeComposerDraft(page, input, PROMPT)
|
||||
await page.getByRole('button', { name: 'Send message', exact: true }).click()
|
||||
await expect.poll(() => exists(join(cwd, 'started.txt')), { timeout: 20_000 }).toBe(true)
|
||||
expect(await childAlive(cwd)).toBe(true)
|
||||
expect(scaffold.ctx.agents.get(sessionId)?.status).toBe('running')
|
||||
|
||||
await clickHoverAction(sessionRow(), 'Session actions for ')
|
||||
await page.getByRole('menuitem', { name: 'Archive session' }).click()
|
||||
const dialog = page.getByRole('dialog', { name: 'Stop and archive this session?' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
// A foreground Bash command is a registered job from its start, so the
|
||||
// Host reports it beside the turn; stopping the turn kills it.
|
||||
await expect.poll(() => dialog.getByRole('list', { name: 'Work that will be stopped' }).locator('li').allTextContents())
|
||||
.toEqual(['The turn in progress', `1 background job: ${WAIT_COMMAND}`])
|
||||
// Asking is not archiving: the Host set is untouched, the turn and its
|
||||
// Bash child keep running, and the row stays in the sidebar.
|
||||
expect([...scaffold.ctx.workspaceRegistry.archivedSessionIds]).toEqual([])
|
||||
expect(scaffold.ctx.agents.get(sessionId)?.status).toBe('running')
|
||||
expect(await childAlive(cwd)).toBe(true)
|
||||
await expect.poll(() => sessionRow().count()).toBe(1)
|
||||
// The Bash command is a running job while the dialog is open, by design.
|
||||
const snapshot = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd, { runningJobs: 'keep' })
|
||||
await compareOrRefreshGolden(DIALOG_EXPECTED, snapshot, MODE)
|
||||
|
||||
// Cancel keeps everything as it was.
|
||||
await dialog.getByRole('button', { name: 'Cancel', exact: true }).click()
|
||||
await expect.poll(() => page.getByRole('dialog').count()).toBe(0)
|
||||
expect(scaffold.ctx.agents.get(sessionId)?.status).toBe('running')
|
||||
}, 60_000)
|
||||
|
||||
it('confirming stops the turn like the stop button, then hides the row', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-session-archive-active-confirm'))
|
||||
const agent = scaffold.ctx.agents.get(sessionId)
|
||||
if (agent === undefined) throw new Error('the running Session lost its Agent')
|
||||
const settled = scaffold.whenTurnSettled(30_000)
|
||||
await clickHoverAction(sessionRow(), 'Session actions for ')
|
||||
await page.getByRole('menuitem', { name: 'Archive session' }).click()
|
||||
const dialog = page.getByRole('dialog', { name: 'Stop and archive this session?' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
await dialog.getByRole('button', { name: 'Stop and archive', exact: true }).click()
|
||||
|
||||
await expect.poll(() => [...scaffold.ctx.workspaceRegistry.archivedSessionIds].map(String), { timeout: 10_000 })
|
||||
.toEqual([String(sessionId)])
|
||||
await expect.poll(() => page.getByRole('dialog').count()).toBe(0)
|
||||
await expect.poll(() => sessionRow().count(), { timeout: 10_000 }).toBe(0)
|
||||
await expect.poll(() => page.getByText('Session stopped and archived.', { exact: false }).count(), { timeout: 10_000 })
|
||||
.toBe(1)
|
||||
|
||||
// The stop is the user's cancel: the Bash child dies, the turn settles as
|
||||
// cancelled, and the log closes every open call before the turn ends.
|
||||
expect(await settled).toBe(sessionId)
|
||||
const cwd = join(scaffold.workspaceCwd, 'workspace')
|
||||
await expect.poll(() => childAlive(cwd), { timeout: 10_000, interval: 50 }).toBe(false)
|
||||
expect(agent.status).toBe('idle')
|
||||
const live = agent.session.snapshotEvents().filter(event => event.seq >= agent.session.firstLiveSeq)
|
||||
const calls = live.filter((event): event is SessionEvent<'tool/call'> => event.type === 'tool/call')
|
||||
const results = live.filter((event): event is SessionEvent<'tool/result'> => event.type === 'tool/result')
|
||||
expect(calls.map(event => event.data.callId)).toEqual(['call_wait'])
|
||||
expect(results.map(event => event.data.message.toolCallId)).toEqual(['call_wait'])
|
||||
expect(results[0]?.data.error?.code).toBe('ABORTED')
|
||||
const turnEnd = live.at(-1)
|
||||
expect(turnEnd?.type).toBe('turn/end')
|
||||
expect(turnEnd?.type === 'turn/end' && turnEnd.data.reason).toEqual({ kind: 'aborted', reason: { kind: 'user' } })
|
||||
expect(await exists(join(scaffold.workspaceCwd, 'workspace', 'finished.txt'))).toBe(false)
|
||||
}, 60_000)
|
||||
|
||||
it('the restored Session continues the conversation from a regular log', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-session-archive-active-restore'))
|
||||
await page.getByRole('button', { name: 'View options' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Show archived', exact: true }).click()
|
||||
await expect.poll(() => sessionRow().count(), { timeout: 10_000 }).toBe(1)
|
||||
await clickHoverAction(sessionRow(), 'Session actions for ')
|
||||
await page.getByRole('menuitem', { name: 'Unarchive session' }).click()
|
||||
await expect.poll(() => [...scaffold.ctx.workspaceRegistry.archivedSessionIds], { timeout: 10_000 }).toEqual([])
|
||||
await sessionRow().click()
|
||||
await expect.poll(() => sessionRow().getAttribute('aria-selected'), { timeout: 10_000 }).toBe('true')
|
||||
|
||||
const composer = page.locator('[data-composer-input]').last()
|
||||
await writeComposerDraft(page, composer, CONTINUE_PROMPT)
|
||||
const settled = scaffold.whenTurnSettled(30_000)
|
||||
await page.getByRole('button', { name: 'Send message', exact: true }).click()
|
||||
await page.getByText(CONTINUED, { exact: false }).last().waitFor({ timeout: 15_000 })
|
||||
expect(await settled).toBe(sessionId)
|
||||
|
||||
// The persisted log is what a later cold resume would read: two turns,
|
||||
// each closed, with the cancelled call settled inside the first.
|
||||
const events = await readPersistedEvents(scaffold, sessionId)
|
||||
const turns = events.filter(event => event.type === 'turn/start').map(event => event.data.turn)
|
||||
expect(turns).toEqual([1, 2])
|
||||
const ends = events.filter((event): event is SessionEvent<'turn/end'> => event.type === 'turn/end')
|
||||
expect(ends.map(event => event.data.reason.kind)).toEqual(['aborted', 'completed'])
|
||||
const calls = events.filter(event => event.type === 'tool/call').length
|
||||
const results = events.filter(event => event.type === 'tool/result').length
|
||||
expect(results).toBe(calls)
|
||||
}, 60_000)
|
||||
|
||||
it('kept the console clean and the fixture inventory closed', async () => {
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['dialog.expected.md'])
|
||||
})
|
||||
})
|
||||
@@ -81,6 +81,7 @@
|
||||
"tests/workspace-management.e2e.ts",
|
||||
"tests/workspace-recency.e2e.ts",
|
||||
"tests/session-unarchive.e2e.ts",
|
||||
"tests/session-archive-active.e2e.ts",
|
||||
"tests/replay-round-trip.e2e.ts",
|
||||
"tests/hmr-live.e2e.ts",
|
||||
"tests/preview-boot.e2e.ts",
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/config-catalog.md
|
||||
config-catalog.md: 52e14bfa0d654fc2a98f309f96362c6b2ac5bc7c
|
||||
config-catalog.zh.md: 3adbcf982e3ab014e6d82f2bdc8377ebbe89e89b
|
||||
config-catalog.md: 4f175a62402879af68c30dc0e2e8ee3e7a844c6f
|
||||
config-catalog.zh.md: 58f15a2d939202cea0611e6302328ae541712d97
|
||||
@@ -228,7 +228,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/api/session-controller/src/index.ts:78`](../packages/api/session-controller/src/index.ts)
|
||||
Source: [`packages/api/session-controller/src/index.ts:79`](../packages/api/session-controller/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-api-settings-controller"></a>
|
||||
|
||||
@@ -2860,7 +2860,7 @@ export interface Config {
|
||||
|
||||
Depends on: `Volatile` (`@deepseek-ai/cordis`)
|
||||
|
||||
Source: [`packages/subagent/subagent/src/index.ts:191`](../packages/subagent/subagent/src/index.ts)
|
||||
Source: [`packages/subagent/subagent/src/index.ts:192`](../packages/subagent/subagent/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-subagent-acp"></a>
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
来源: [`packages/api/session-controller/src/index.ts:78`](../packages/api/session-controller/src/index.ts)
|
||||
来源: [`packages/api/session-controller/src/index.ts:79`](../packages/api/session-controller/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-api-settings-controller"></a>
|
||||
|
||||
@@ -2862,7 +2862,7 @@ export interface Config {
|
||||
|
||||
Depends on: `Volatile` (`@deepseek-ai/cordis`)
|
||||
|
||||
来源: [`packages/subagent/subagent/src/index.ts:191`](../packages/subagent/subagent/src/index.ts)
|
||||
来源: [`packages/subagent/subagent/src/index.ts:192`](../packages/subagent/subagent/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-subagent-acp"></a>
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
|
||||
event-producer-consumer.md: 50995814a2f4124f9d77d4c1689e28950476f81f
|
||||
event-producer-consumer.zh.md: 6b2c4738859b6526508f73b7aca5cedf27a3ee55
|
||||
event-producer-consumer.md: beee1c62b63e7f637a1f159f0dcce6450e53dd59
|
||||
event-producer-consumer.zh.md: c80ecba3ba427cb2fbf898f14c2253b10f1f68c2
|
||||
@@ -16,7 +16,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `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) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
|
||||
| `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-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/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` |
|
||||
@@ -62,10 +62,10 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `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) |
|
||||
| `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:173`](../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) |
|
||||
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:147`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:153`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:164`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) |
|
||||
| `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) |
|
||||
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:148`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:154`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:165`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) |
|
||||
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
|
||||
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:208`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
@@ -82,6 +82,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `workflow/log` | `emit` | [`packages/workflow/workflow/src/index.ts:58`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
|
||||
| `workflow/phase` | `emit` | [`packages/workflow/workflow/src/index.ts:51`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
|
||||
| `workflow/start` | `emit` | [`packages/workflow/workflow/src/index.ts:43`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`workflow`](../packages/workflow/workflow) |
|
||||
| `workspace/session-activity` | `waterfall` | [`packages/workspace/workspace/src/index.ts:129`](../packages/workspace/workspace/src/index.ts) | [`workspace`](../packages/workspace/workspace) (`waterfall`) | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`schedule`](../packages/schedule/schedule), [`subagent`](../packages/subagent/subagent) |
|
||||
| `workspace/session-stop` | `parallel` | [`packages/workspace/workspace/src/index.ts:147`](../packages/workspace/workspace/src/index.ts) | [`workspace`](../packages/workspace/workspace) (`parallel`) | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`schedule`](../packages/schedule/schedule), [`subagent`](../packages/subagent/subagent) |
|
||||
|
||||
## Non-harness or undeclared event strings seen in package source
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
| `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) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
|
||||
| `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-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/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` |
|
||||
@@ -64,10 +64,10 @@
|
||||
| `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) |
|
||||
| `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:173`](../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) |
|
||||
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:147`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:153`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:164`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) |
|
||||
| `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) |
|
||||
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:148`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:154`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:165`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) |
|
||||
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
|
||||
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:208`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
@@ -84,6 +84,8 @@
|
||||
| `workflow/log` | `emit` | [`packages/workflow/workflow/src/index.ts:58`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
|
||||
| `workflow/phase` | `emit` | [`packages/workflow/workflow/src/index.ts:51`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
|
||||
| `workflow/start` | `emit` | [`packages/workflow/workflow/src/index.ts:43`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`workflow`](../packages/workflow/workflow) |
|
||||
| `workspace/session-activity` | `waterfall` | [`packages/workspace/workspace/src/index.ts:129`](../packages/workspace/workspace/src/index.ts) | [`workspace`](../packages/workspace/workspace) (`waterfall`) | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`schedule`](../packages/schedule/schedule), [`subagent`](../packages/subagent/subagent) |
|
||||
| `workspace/session-stop` | `parallel` | [`packages/workspace/workspace/src/index.ts:147`](../packages/workspace/workspace/src/index.ts) | [`workspace`](../packages/workspace/workspace) (`parallel`) | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`schedule`](../packages/schedule/schedule), [`subagent`](../packages/subagent/subagent) |
|
||||
|
||||
## Non-harness or undeclared event strings seen in package source
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/module-graph.md
|
||||
module-graph.md: 8fb280f3d3139d955ffb5c71f0cc23260534e390
|
||||
module-graph.zh.md: 4e789df0900072020350122b3aa1e3983fecf40a
|
||||
module-graph.md: 18c751e2a6db6706a9a3ed8fc6a8864db61c6174
|
||||
module-graph.zh.md: 0fcb03b1c8819f2ed9f7de2accea6db3074c3e6f
|
||||
+62
-58
@@ -503,14 +503,6 @@ flowchart TD
|
||||
pkg_settings --> pkg_session
|
||||
pkg_session_snapshot --> pkg_http_proxy
|
||||
pkg_session_snapshot --> pkg_session
|
||||
pkg_agent --> pkg_invariants
|
||||
pkg_agent --> pkg_llm
|
||||
pkg_agent --> pkg_scope
|
||||
pkg_agent --> pkg_session
|
||||
pkg_agent --> pkg_session_projection
|
||||
pkg_agent --> pkg_system_prompt
|
||||
pkg_agent --> pkg_typert_protocol
|
||||
pkg_agent --> pkg_util_values
|
||||
pkg_fs --> pkg_brand
|
||||
pkg_fs --> pkg_invariants
|
||||
pkg_fs --> pkg_llm
|
||||
@@ -561,6 +553,51 @@ flowchart TD
|
||||
pkg_llm_pi_ai --> pkg_launch_environment
|
||||
pkg_llm_pi_ai --> pkg_llm
|
||||
pkg_llm_pi_ai --> pkg_timeout
|
||||
pkg_agent --> pkg_invariants
|
||||
pkg_agent --> pkg_llm
|
||||
pkg_agent --> pkg_scope
|
||||
pkg_agent --> pkg_session
|
||||
pkg_agent --> pkg_session_projection
|
||||
pkg_agent --> pkg_system_prompt
|
||||
pkg_agent --> pkg_typert_protocol
|
||||
pkg_agent --> pkg_util_values
|
||||
pkg_agent --> pkg_workspace
|
||||
pkg_fs_local --> pkg_fs
|
||||
pkg_fs_observation_policy --> pkg_fs
|
||||
pkg_skill_filesystem --> pkg_fs
|
||||
pkg_skill_filesystem --> pkg_home_paths
|
||||
pkg_skill_filesystem --> pkg_skill
|
||||
pkg_hook_protocol --> pkg_invariants
|
||||
pkg_hook_protocol --> pkg_session
|
||||
pkg_hook_protocol --> pkg_shell
|
||||
pkg_api_workspace_controller --> pkg_api_gateway
|
||||
pkg_api_workspace_controller --> pkg_client_connection
|
||||
pkg_api_workspace_controller --> pkg_host_directory_picker
|
||||
pkg_api_workspace_controller --> pkg_session
|
||||
pkg_api_workspace_controller --> pkg_storage_domain
|
||||
pkg_api_workspace_controller --> pkg_typert_protocol
|
||||
pkg_api_workspace_controller --> pkg_workspace
|
||||
pkg_config_editor --> pkg_app_boot
|
||||
pkg_config_editor --> pkg_hmr
|
||||
pkg_experimental_api_speech_to_text --> pkg_experimental_speech_to_text
|
||||
pkg_experimental_api_speech_to_text --> pkg_typert_protocol
|
||||
pkg_experimental_ptc_runtime_python --> pkg_ptc_runtime
|
||||
pkg_experimental_ptc_runtime_python --> pkg_timeout
|
||||
pkg_experimental_ptc_runtime_python --> pkg_util_values
|
||||
pkg_experimental_speech_to_text_sensevoice --> pkg_experimental_speech_to_text
|
||||
pkg_experimental_speech_to_text_sensevoice --> pkg_subprocess
|
||||
pkg_lsp_stdio --> pkg_brand
|
||||
pkg_lsp_stdio --> pkg_fs
|
||||
pkg_lsp_stdio --> pkg_llm
|
||||
pkg_lsp_stdio --> pkg_lsp
|
||||
pkg_lsp_stdio --> pkg_subprocess
|
||||
pkg_lsp_stdio --> pkg_timeout
|
||||
pkg_bash_local --> pkg_shell
|
||||
pkg_bash_local --> pkg_subprocess
|
||||
pkg_bash_local --> pkg_timeout
|
||||
pkg_pwsh_local --> pkg_shell
|
||||
pkg_pwsh_local --> pkg_subprocess
|
||||
pkg_pwsh_local --> pkg_timeout
|
||||
pkg_llm_retry --> pkg_agent
|
||||
pkg_llm_retry --> pkg_brand
|
||||
pkg_llm_retry --> pkg_invariants
|
||||
@@ -578,28 +615,11 @@ flowchart TD
|
||||
pkg_goal --> pkg_session
|
||||
pkg_goal --> pkg_session_projection
|
||||
pkg_goal --> pkg_typert_protocol
|
||||
pkg_fs_local --> pkg_fs
|
||||
pkg_fs_observation_policy --> pkg_fs
|
||||
pkg_skill_filesystem --> pkg_fs
|
||||
pkg_skill_filesystem --> pkg_home_paths
|
||||
pkg_skill_filesystem --> pkg_skill
|
||||
pkg_web_search_deepseek --> pkg_agent
|
||||
pkg_web_search_deepseek --> pkg_credentials
|
||||
pkg_web_search_deepseek --> pkg_launch_environment
|
||||
pkg_web_search_deepseek --> pkg_session
|
||||
pkg_web_search_deepseek --> pkg_web
|
||||
pkg_hook_protocol --> pkg_invariants
|
||||
pkg_hook_protocol --> pkg_session
|
||||
pkg_hook_protocol --> pkg_shell
|
||||
pkg_api_workspace_controller --> pkg_api_gateway
|
||||
pkg_api_workspace_controller --> pkg_client_connection
|
||||
pkg_api_workspace_controller --> pkg_host_directory_picker
|
||||
pkg_api_workspace_controller --> pkg_session
|
||||
pkg_api_workspace_controller --> pkg_storage_domain
|
||||
pkg_api_workspace_controller --> pkg_typert_protocol
|
||||
pkg_api_workspace_controller --> pkg_workspace
|
||||
pkg_config_editor --> pkg_app_boot
|
||||
pkg_config_editor --> pkg_hmr
|
||||
pkg_file_reference --> pkg_agent
|
||||
pkg_time_context --> pkg_agent
|
||||
pkg_time_context --> pkg_invariants
|
||||
@@ -610,13 +630,6 @@ flowchart TD
|
||||
pkg_tmux_context --> pkg_session
|
||||
pkg_tmux_context --> pkg_session_projection
|
||||
pkg_tmux_context --> pkg_shell
|
||||
pkg_experimental_api_speech_to_text --> pkg_experimental_speech_to_text
|
||||
pkg_experimental_api_speech_to_text --> pkg_typert_protocol
|
||||
pkg_experimental_ptc_runtime_python --> pkg_ptc_runtime
|
||||
pkg_experimental_ptc_runtime_python --> pkg_timeout
|
||||
pkg_experimental_ptc_runtime_python --> pkg_util_values
|
||||
pkg_experimental_speech_to_text_sensevoice --> pkg_experimental_speech_to_text
|
||||
pkg_experimental_speech_to_text_sensevoice --> pkg_subprocess
|
||||
pkg_commands --> pkg_agent
|
||||
pkg_commands --> pkg_attachment
|
||||
pkg_commands --> pkg_brand
|
||||
@@ -639,12 +652,7 @@ flowchart TD
|
||||
pkg_jobs --> pkg_brand
|
||||
pkg_jobs --> pkg_invariants
|
||||
pkg_jobs --> pkg_session
|
||||
pkg_lsp_stdio --> pkg_brand
|
||||
pkg_lsp_stdio --> pkg_fs
|
||||
pkg_lsp_stdio --> pkg_llm
|
||||
pkg_lsp_stdio --> pkg_lsp
|
||||
pkg_lsp_stdio --> pkg_subprocess
|
||||
pkg_lsp_stdio --> pkg_timeout
|
||||
pkg_jobs --> pkg_workspace
|
||||
pkg_sandbox_policy --> pkg_agent
|
||||
pkg_sandbox_policy --> pkg_invariants
|
||||
pkg_sandbox_policy --> pkg_sandbox
|
||||
@@ -659,12 +667,6 @@ flowchart TD
|
||||
pkg_session_title --> pkg_llm
|
||||
pkg_session_title --> pkg_session
|
||||
pkg_session_title --> pkg_session_projection
|
||||
pkg_bash_local --> pkg_shell
|
||||
pkg_bash_local --> pkg_subprocess
|
||||
pkg_bash_local --> pkg_timeout
|
||||
pkg_pwsh_local --> pkg_shell
|
||||
pkg_pwsh_local --> pkg_subprocess
|
||||
pkg_pwsh_local --> pkg_timeout
|
||||
pkg_terminal --> pkg_agent
|
||||
pkg_terminal --> pkg_brand
|
||||
pkg_loader_smoke --> pkg_agent
|
||||
@@ -913,6 +915,7 @@ flowchart TD
|
||||
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
|
||||
@@ -1084,6 +1087,7 @@ flowchart TD
|
||||
pkg_subagent --> pkg_typert_protocol
|
||||
pkg_subagent --> pkg_user_approval
|
||||
pkg_subagent --> pkg_util_time
|
||||
pkg_subagent --> pkg_workspace
|
||||
pkg_session_query_sqlite --> pkg_session
|
||||
pkg_session_query_sqlite --> pkg_session_persistence
|
||||
pkg_session_query_sqlite --> pkg_session_query
|
||||
@@ -1479,7 +1483,6 @@ flowchart TD
|
||||
| [`session-projection`](../packages/session/session-projection) | `session` | [`session`](../packages/core/session) |
|
||||
| [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`session-snapshot`](../packages/test-support/session-snapshot) | `test-support` | [`http-proxy`](../packages/util/http-proxy), [`session`](../packages/core/session) |
|
||||
| [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol), [`util-values`](../packages/util/values) |
|
||||
| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
|
||||
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`spill`](../packages/spill/spill) |
|
||||
| [`session-log-export`](../packages/session-query/session-log-export) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
|
||||
@@ -1495,32 +1498,33 @@ flowchart TD
|
||||
| [`workspace`](../packages/workspace/workspace) | `workspace` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage`](../packages/storage/storage), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`atomic-write`](../packages/util/atomic-write), [`attachment`](../packages/attachment/attachment), [`credentials`](../packages/credentials/credentials), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout) |
|
||||
| [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`attachment`](../packages/attachment/attachment), [`authorization`](../packages/credentials/authorization), [`credentials`](../packages/credentials/credentials), [`fs`](../packages/fs/fs), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout) |
|
||||
| [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`timeout`](../packages/util/timeout) |
|
||||
| [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm) |
|
||||
| [`goal`](../packages/goal/goal) | `goal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol), [`util-values`](../packages/util/values), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs) |
|
||||
| [`fs-observation-policy`](../packages/fs/fs-observation-policy) | `fs` | [`fs`](../packages/fs/fs) |
|
||||
| [`skill-filesystem`](../packages/skill/skill-filesystem) | `skill` | [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`skill`](../packages/skill/skill) |
|
||||
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`web`](../packages/web/web) |
|
||||
| [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`shell`](../packages/shell/shell) |
|
||||
| [`api-workspace-controller`](../packages/api/workspace-controller) | `api` | [`api-gateway`](../packages/api/gateway), [`client-connection`](../packages/client/connection), [`host-directory-picker`](../packages/host/directory-picker), [`session`](../packages/core/session), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`config-editor`](../packages/boot/config-editor) | `boot` | [`app-boot`](../packages/boot/app-boot), [`hmr`](../packages/boot/hmr) |
|
||||
| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent) |
|
||||
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
|
||||
| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell) |
|
||||
| [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text) | `experimental` | [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`experimental-ptc-runtime-python`](../packages/experimental/ptc-runtime-python) | `experimental` | [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`timeout`](../packages/util/timeout), [`util-values`](../packages/util/values) |
|
||||
| [`experimental-speech-to-text-sensevoice`](../packages/experimental/speech-to-text-sensevoice) | `experimental` | [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
| [`lsp-stdio`](../packages/lsp/lsp-stdio) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`bash-local`](../packages/shell/bash-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`timeout`](../packages/util/timeout) |
|
||||
| [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm) |
|
||||
| [`goal`](../packages/goal/goal) | `goal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`web`](../packages/web/web) |
|
||||
| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent) |
|
||||
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
|
||||
| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell) |
|
||||
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
|
||||
| [`jobs`](../packages/jobs/jobs) | `jobs` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`lsp-stdio`](../packages/lsp/lsp-stdio) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`jobs`](../packages/jobs/jobs) | `jobs` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`session-telemetry`](../packages/session/session-telemetry) | `session` | [`agent`](../packages/core/agent), [`session`](../packages/core/session) |
|
||||
| [`session-title`](../packages/session/session-title) | `session` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
|
||||
| [`bash-local`](../packages/shell/bash-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand) |
|
||||
| [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`http-proxy`](../packages/util/http-proxy), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||
| [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||
@@ -1569,7 +1573,7 @@ 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) |
|
||||
| [`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) |
|
||||
@@ -1598,7 +1602,7 @@ flowchart TD
|
||||
| [`subprocess-ssh`](../packages/ssh/subprocess-ssh) | `ssh` | [`ssh`](../packages/ssh/ssh), [`subprocess`](../packages/subprocess/subprocess), [`subprocess-local`](../packages/subprocess/subprocess-local) |
|
||||
| [`agent-loop-testkit`](../packages/test-support/agent-loop-testkit) | `test-support` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`webhook`](../packages/webhook/webhook) | `webhook` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time) |
|
||||
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
|
||||
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
|
||||
| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) |
|
||||
|
||||
+62
-58
@@ -505,14 +505,6 @@ flowchart TD
|
||||
pkg_settings --> pkg_session
|
||||
pkg_session_snapshot --> pkg_http_proxy
|
||||
pkg_session_snapshot --> pkg_session
|
||||
pkg_agent --> pkg_invariants
|
||||
pkg_agent --> pkg_llm
|
||||
pkg_agent --> pkg_scope
|
||||
pkg_agent --> pkg_session
|
||||
pkg_agent --> pkg_session_projection
|
||||
pkg_agent --> pkg_system_prompt
|
||||
pkg_agent --> pkg_typert_protocol
|
||||
pkg_agent --> pkg_util_values
|
||||
pkg_fs --> pkg_brand
|
||||
pkg_fs --> pkg_invariants
|
||||
pkg_fs --> pkg_llm
|
||||
@@ -563,6 +555,51 @@ flowchart TD
|
||||
pkg_llm_pi_ai --> pkg_launch_environment
|
||||
pkg_llm_pi_ai --> pkg_llm
|
||||
pkg_llm_pi_ai --> pkg_timeout
|
||||
pkg_agent --> pkg_invariants
|
||||
pkg_agent --> pkg_llm
|
||||
pkg_agent --> pkg_scope
|
||||
pkg_agent --> pkg_session
|
||||
pkg_agent --> pkg_session_projection
|
||||
pkg_agent --> pkg_system_prompt
|
||||
pkg_agent --> pkg_typert_protocol
|
||||
pkg_agent --> pkg_util_values
|
||||
pkg_agent --> pkg_workspace
|
||||
pkg_fs_local --> pkg_fs
|
||||
pkg_fs_observation_policy --> pkg_fs
|
||||
pkg_skill_filesystem --> pkg_fs
|
||||
pkg_skill_filesystem --> pkg_home_paths
|
||||
pkg_skill_filesystem --> pkg_skill
|
||||
pkg_hook_protocol --> pkg_invariants
|
||||
pkg_hook_protocol --> pkg_session
|
||||
pkg_hook_protocol --> pkg_shell
|
||||
pkg_api_workspace_controller --> pkg_api_gateway
|
||||
pkg_api_workspace_controller --> pkg_client_connection
|
||||
pkg_api_workspace_controller --> pkg_host_directory_picker
|
||||
pkg_api_workspace_controller --> pkg_session
|
||||
pkg_api_workspace_controller --> pkg_storage_domain
|
||||
pkg_api_workspace_controller --> pkg_typert_protocol
|
||||
pkg_api_workspace_controller --> pkg_workspace
|
||||
pkg_config_editor --> pkg_app_boot
|
||||
pkg_config_editor --> pkg_hmr
|
||||
pkg_experimental_api_speech_to_text --> pkg_experimental_speech_to_text
|
||||
pkg_experimental_api_speech_to_text --> pkg_typert_protocol
|
||||
pkg_experimental_ptc_runtime_python --> pkg_ptc_runtime
|
||||
pkg_experimental_ptc_runtime_python --> pkg_timeout
|
||||
pkg_experimental_ptc_runtime_python --> pkg_util_values
|
||||
pkg_experimental_speech_to_text_sensevoice --> pkg_experimental_speech_to_text
|
||||
pkg_experimental_speech_to_text_sensevoice --> pkg_subprocess
|
||||
pkg_lsp_stdio --> pkg_brand
|
||||
pkg_lsp_stdio --> pkg_fs
|
||||
pkg_lsp_stdio --> pkg_llm
|
||||
pkg_lsp_stdio --> pkg_lsp
|
||||
pkg_lsp_stdio --> pkg_subprocess
|
||||
pkg_lsp_stdio --> pkg_timeout
|
||||
pkg_bash_local --> pkg_shell
|
||||
pkg_bash_local --> pkg_subprocess
|
||||
pkg_bash_local --> pkg_timeout
|
||||
pkg_pwsh_local --> pkg_shell
|
||||
pkg_pwsh_local --> pkg_subprocess
|
||||
pkg_pwsh_local --> pkg_timeout
|
||||
pkg_llm_retry --> pkg_agent
|
||||
pkg_llm_retry --> pkg_brand
|
||||
pkg_llm_retry --> pkg_invariants
|
||||
@@ -580,28 +617,11 @@ flowchart TD
|
||||
pkg_goal --> pkg_session
|
||||
pkg_goal --> pkg_session_projection
|
||||
pkg_goal --> pkg_typert_protocol
|
||||
pkg_fs_local --> pkg_fs
|
||||
pkg_fs_observation_policy --> pkg_fs
|
||||
pkg_skill_filesystem --> pkg_fs
|
||||
pkg_skill_filesystem --> pkg_home_paths
|
||||
pkg_skill_filesystem --> pkg_skill
|
||||
pkg_web_search_deepseek --> pkg_agent
|
||||
pkg_web_search_deepseek --> pkg_credentials
|
||||
pkg_web_search_deepseek --> pkg_launch_environment
|
||||
pkg_web_search_deepseek --> pkg_session
|
||||
pkg_web_search_deepseek --> pkg_web
|
||||
pkg_hook_protocol --> pkg_invariants
|
||||
pkg_hook_protocol --> pkg_session
|
||||
pkg_hook_protocol --> pkg_shell
|
||||
pkg_api_workspace_controller --> pkg_api_gateway
|
||||
pkg_api_workspace_controller --> pkg_client_connection
|
||||
pkg_api_workspace_controller --> pkg_host_directory_picker
|
||||
pkg_api_workspace_controller --> pkg_session
|
||||
pkg_api_workspace_controller --> pkg_storage_domain
|
||||
pkg_api_workspace_controller --> pkg_typert_protocol
|
||||
pkg_api_workspace_controller --> pkg_workspace
|
||||
pkg_config_editor --> pkg_app_boot
|
||||
pkg_config_editor --> pkg_hmr
|
||||
pkg_file_reference --> pkg_agent
|
||||
pkg_time_context --> pkg_agent
|
||||
pkg_time_context --> pkg_invariants
|
||||
@@ -612,13 +632,6 @@ flowchart TD
|
||||
pkg_tmux_context --> pkg_session
|
||||
pkg_tmux_context --> pkg_session_projection
|
||||
pkg_tmux_context --> pkg_shell
|
||||
pkg_experimental_api_speech_to_text --> pkg_experimental_speech_to_text
|
||||
pkg_experimental_api_speech_to_text --> pkg_typert_protocol
|
||||
pkg_experimental_ptc_runtime_python --> pkg_ptc_runtime
|
||||
pkg_experimental_ptc_runtime_python --> pkg_timeout
|
||||
pkg_experimental_ptc_runtime_python --> pkg_util_values
|
||||
pkg_experimental_speech_to_text_sensevoice --> pkg_experimental_speech_to_text
|
||||
pkg_experimental_speech_to_text_sensevoice --> pkg_subprocess
|
||||
pkg_commands --> pkg_agent
|
||||
pkg_commands --> pkg_attachment
|
||||
pkg_commands --> pkg_brand
|
||||
@@ -641,12 +654,7 @@ flowchart TD
|
||||
pkg_jobs --> pkg_brand
|
||||
pkg_jobs --> pkg_invariants
|
||||
pkg_jobs --> pkg_session
|
||||
pkg_lsp_stdio --> pkg_brand
|
||||
pkg_lsp_stdio --> pkg_fs
|
||||
pkg_lsp_stdio --> pkg_llm
|
||||
pkg_lsp_stdio --> pkg_lsp
|
||||
pkg_lsp_stdio --> pkg_subprocess
|
||||
pkg_lsp_stdio --> pkg_timeout
|
||||
pkg_jobs --> pkg_workspace
|
||||
pkg_sandbox_policy --> pkg_agent
|
||||
pkg_sandbox_policy --> pkg_invariants
|
||||
pkg_sandbox_policy --> pkg_sandbox
|
||||
@@ -661,12 +669,6 @@ flowchart TD
|
||||
pkg_session_title --> pkg_llm
|
||||
pkg_session_title --> pkg_session
|
||||
pkg_session_title --> pkg_session_projection
|
||||
pkg_bash_local --> pkg_shell
|
||||
pkg_bash_local --> pkg_subprocess
|
||||
pkg_bash_local --> pkg_timeout
|
||||
pkg_pwsh_local --> pkg_shell
|
||||
pkg_pwsh_local --> pkg_subprocess
|
||||
pkg_pwsh_local --> pkg_timeout
|
||||
pkg_terminal --> pkg_agent
|
||||
pkg_terminal --> pkg_brand
|
||||
pkg_loader_smoke --> pkg_agent
|
||||
@@ -915,6 +917,7 @@ flowchart TD
|
||||
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
|
||||
@@ -1086,6 +1089,7 @@ flowchart TD
|
||||
pkg_subagent --> pkg_typert_protocol
|
||||
pkg_subagent --> pkg_user_approval
|
||||
pkg_subagent --> pkg_util_time
|
||||
pkg_subagent --> pkg_workspace
|
||||
pkg_session_query_sqlite --> pkg_session
|
||||
pkg_session_query_sqlite --> pkg_session_persistence
|
||||
pkg_session_query_sqlite --> pkg_session_query
|
||||
@@ -1481,7 +1485,6 @@ flowchart TD
|
||||
| [`session-projection`](../packages/session/session-projection) | `session` | [`session`](../packages/core/session) |
|
||||
| [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`session-snapshot`](../packages/test-support/session-snapshot) | `test-support` | [`http-proxy`](../packages/util/http-proxy), [`session`](../packages/core/session) |
|
||||
| [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol), [`util-values`](../packages/util/values) |
|
||||
| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
|
||||
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`spill`](../packages/spill/spill) |
|
||||
| [`session-log-export`](../packages/session-query/session-log-export) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
|
||||
@@ -1497,32 +1500,33 @@ flowchart TD
|
||||
| [`workspace`](../packages/workspace/workspace) | `workspace` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage`](../packages/storage/storage), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`atomic-write`](../packages/util/atomic-write), [`attachment`](../packages/attachment/attachment), [`credentials`](../packages/credentials/credentials), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout) |
|
||||
| [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`attachment`](../packages/attachment/attachment), [`authorization`](../packages/credentials/authorization), [`credentials`](../packages/credentials/credentials), [`fs`](../packages/fs/fs), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout) |
|
||||
| [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`timeout`](../packages/util/timeout) |
|
||||
| [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm) |
|
||||
| [`goal`](../packages/goal/goal) | `goal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol), [`util-values`](../packages/util/values), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs) |
|
||||
| [`fs-observation-policy`](../packages/fs/fs-observation-policy) | `fs` | [`fs`](../packages/fs/fs) |
|
||||
| [`skill-filesystem`](../packages/skill/skill-filesystem) | `skill` | [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`skill`](../packages/skill/skill) |
|
||||
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`web`](../packages/web/web) |
|
||||
| [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`shell`](../packages/shell/shell) |
|
||||
| [`api-workspace-controller`](../packages/api/workspace-controller) | `api` | [`api-gateway`](../packages/api/gateway), [`client-connection`](../packages/client/connection), [`host-directory-picker`](../packages/host/directory-picker), [`session`](../packages/core/session), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`config-editor`](../packages/boot/config-editor) | `boot` | [`app-boot`](../packages/boot/app-boot), [`hmr`](../packages/boot/hmr) |
|
||||
| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent) |
|
||||
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
|
||||
| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell) |
|
||||
| [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text) | `experimental` | [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`experimental-ptc-runtime-python`](../packages/experimental/ptc-runtime-python) | `experimental` | [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`timeout`](../packages/util/timeout), [`util-values`](../packages/util/values) |
|
||||
| [`experimental-speech-to-text-sensevoice`](../packages/experimental/speech-to-text-sensevoice) | `experimental` | [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
| [`lsp-stdio`](../packages/lsp/lsp-stdio) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`bash-local`](../packages/shell/bash-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`timeout`](../packages/util/timeout) |
|
||||
| [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm) |
|
||||
| [`goal`](../packages/goal/goal) | `goal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`web`](../packages/web/web) |
|
||||
| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent) |
|
||||
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
|
||||
| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell) |
|
||||
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
|
||||
| [`jobs`](../packages/jobs/jobs) | `jobs` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`lsp-stdio`](../packages/lsp/lsp-stdio) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`jobs`](../packages/jobs/jobs) | `jobs` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`session-telemetry`](../packages/session/session-telemetry) | `session` | [`agent`](../packages/core/agent), [`session`](../packages/core/session) |
|
||||
| [`session-title`](../packages/session/session-title) | `session` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
|
||||
| [`bash-local`](../packages/shell/bash-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand) |
|
||||
| [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`http-proxy`](../packages/util/http-proxy), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||
| [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||
@@ -1571,7 +1575,7 @@ 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) |
|
||||
| [`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) |
|
||||
@@ -1600,7 +1604,7 @@ flowchart TD
|
||||
| [`subprocess-ssh`](../packages/ssh/subprocess-ssh) | `ssh` | [`ssh`](../packages/ssh/ssh), [`subprocess`](../packages/subprocess/subprocess), [`subprocess-local`](../packages/subprocess/subprocess-local) |
|
||||
| [`agent-loop-testkit`](../packages/test-support/agent-loop-testkit) | `test-support` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`webhook`](../packages/webhook/webhook) | `webhook` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time) |
|
||||
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
|
||||
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
|
||||
| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) |
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/persistence-catalog.md
|
||||
persistence-catalog.md: d771378c6899d2c55c30407a9e87ebe3689f9838
|
||||
persistence-catalog.zh.md: 2ca7c9940efdbeb8309010891025d5bd73030da3
|
||||
persistence-catalog.md: bb3ee2c998189533ed87e149422eae8123fb07b1
|
||||
persistence-catalog.zh.md: 3dabca40d2975e090935d471cd187def826ea4df
|
||||
+14
-14
@@ -180,7 +180,7 @@ Sources: [`packages/core/session/src/types.ts:431`](../packages/core/session/src
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:87`](../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:96`](../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent-preset/*`
|
||||
|
||||
@@ -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:219`](../packages/schedule/schedule/src/types.ts)
|
||||
Source: [`packages/schedule/schedule/src/types.ts:228`](../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
### `session/*`
|
||||
|
||||
@@ -2917,7 +2917,7 @@ SHA-256: `af6e173f7be819bcd0a5c8d77918649c10ce761d8a184ff472f2e01ce64eb2e0`
|
||||
|
||||
SHA-256: `27e337d3d6b402b221e50c247a3f518a70d0c474cc60beee15fac3e9c5cbfcbe`
|
||||
|
||||
Sources: [`packages/schedule/schedule/src/types.ts:13`](../packages/schedule/schedule/src/types.ts)
|
||||
Sources: [`packages/schedule/schedule/src/types.ts:15`](../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
@@ -3084,7 +3084,7 @@ Array of [`AssistantStreamRecord`](#persistence-type-sha256-33341aac09312300de5a
|
||||
|
||||
SHA-256: `658d895d187b26d83f3b53109a3bf77bbfba9f6f9a6d4bc4ee4315b4eb36239e`
|
||||
|
||||
Sources: [`packages/schedule/schedule/src/types.ts:27`](../packages/schedule/schedule/src/types.ts)
|
||||
Sources: [`packages/schedule/schedule/src/types.ts:29`](../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
@@ -3298,7 +3298,7 @@ Sources: [`packages/core/session/src/types.ts:240`](../packages/core/session/src
|
||||
|
||||
SHA-256: `e6063d0451dc02026fd9b54ad3f77c7b1b6392ad3e872a7737ac84e13ca72344`
|
||||
|
||||
Sources: [`packages/schedule/schedule/src/types.ts:93`](../packages/schedule/schedule/src/types.ts)
|
||||
Sources: [`packages/schedule/schedule/src/types.ts:95`](../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
@@ -3317,7 +3317,7 @@ Sources: [`packages/schedule/schedule/src/types.ts:93`](../packages/schedule/sch
|
||||
|
||||
SHA-256: `66ece68282d8c06330fb83329a24ae54c386d5ccfab296bbf18369da8beb7d14`
|
||||
|
||||
Sources: [`packages/schedule/schedule/src/types.ts:39`](../packages/schedule/schedule/src/types.ts)
|
||||
Sources: [`packages/schedule/schedule/src/types.ts:41`](../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
@@ -3689,7 +3689,7 @@ Array of [`ImageOffloadTarget`](#persistence-type-sha256-1cde1bedde79c5e8a56aee9
|
||||
|
||||
SHA-256: `96f4a9c81fc0f940ef71528a8cc66731ae4cf1f40d79680c5fe3c39f96d7723c`
|
||||
|
||||
Sources: [`packages/core/agent/src/types.ts:30`](../packages/core/agent/src/types.ts)
|
||||
Sources: [`packages/core/agent/src/types.ts:39`](../packages/core/agent/src/types.ts)
|
||||
|
||||
One of:
|
||||
|
||||
@@ -4092,7 +4092,7 @@ Sources: [`packages/api/session-controller/src/types.ts:100`](../packages/api/se
|
||||
|
||||
SHA-256: `8e8e91646a5cae3fc78f7532a0013ff2088ae47077642c2edd507a508209b8af`
|
||||
|
||||
Sources: [`packages/schedule/schedule/src/types.ts:86`](../packages/schedule/schedule/src/types.ts)
|
||||
Sources: [`packages/schedule/schedule/src/types.ts:88`](../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
@@ -4304,7 +4304,7 @@ One of:
|
||||
|
||||
SHA-256: `e1d0e52dcd1855ffb803efc18f8f7b776c48d1c25757e8a75c0281fa298c02e1`
|
||||
|
||||
Sources: [`packages/schedule/schedule/src/types.ts:105`](../packages/schedule/schedule/src/types.ts)
|
||||
Sources: [`packages/schedule/schedule/src/types.ts:107`](../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
One of:
|
||||
|
||||
@@ -4323,7 +4323,7 @@ One of:
|
||||
|
||||
SHA-256: `ca6425edd20afe34d0cedc6ad1abad1ad7b32e2fbc4c1d24db53939e80bf887f`
|
||||
|
||||
Sources: [`packages/schedule/schedule/src/types.ts:72`](../packages/schedule/schedule/src/types.ts)
|
||||
Sources: [`packages/schedule/schedule/src/types.ts:74`](../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
@@ -4341,7 +4341,7 @@ Sources: [`packages/schedule/schedule/src/types.ts:72`](../packages/schedule/sch
|
||||
|
||||
SHA-256: `18dc21f12bbac8ade8377e6fbb54179e20c3d955d73fba3244c57fb6bf001df0`
|
||||
|
||||
Sources: [`packages/schedule/schedule/src/types.ts:79`](../packages/schedule/schedule/src/types.ts)
|
||||
Sources: [`packages/schedule/schedule/src/types.ts:81`](../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
@@ -4359,7 +4359,7 @@ Sources: [`packages/schedule/schedule/src/types.ts:79`](../packages/schedule/sch
|
||||
|
||||
SHA-256: `d8f868c6b3e7189de6c1ffe2ea04794ea2aa31315b6347d12536e46020afac87`
|
||||
|
||||
Sources: [`packages/schedule/schedule/src/types.ts:69`](../packages/schedule/schedule/src/types.ts)
|
||||
Sources: [`packages/schedule/schedule/src/types.ts:71`](../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
| kind | Form property | Other required fields | Full definition |
|
||||
|---|---|---|---|
|
||||
@@ -5315,7 +5315,7 @@ Array of `number`.
|
||||
|
||||
SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b`
|
||||
|
||||
Sources: [`packages/api/session-controller/src/types.ts:396`](../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:10`](../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:396`](../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)
|
||||
|
||||
`string`
|
||||
|
||||
@@ -6071,7 +6071,7 @@ Sources: [`packages/core/session/src/types.ts:427`](../packages/core/session/src
|
||||
|
||||
SHA-256: `dc4bd7ac827f4b1f9236f9a157a4b4cc171a0588064a611e43edd95f62c68130`
|
||||
|
||||
Sources: [`packages/core/agent/src/types.ts:87`](../packages/core/agent/src/types.ts)
|
||||
Sources: [`packages/core/agent/src/types.ts:96`](../packages/core/agent/src/types.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
|
||||
@@ -182,7 +182,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/core/agent/src/types.ts:87`](../packages/core/agent/src/types.ts)
|
||||
来源:[`packages/core/agent/src/types.ts:96`](../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent-preset/*`
|
||||
|
||||
@@ -777,7 +777,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
||||
|
||||
类型:[ScheduleChange](subsystems/schedule.zh.md)
|
||||
|
||||
来源:[`packages/schedule/schedule/src/types.ts:219`](../packages/schedule/schedule/src/types.ts)
|
||||
来源:[`packages/schedule/schedule/src/types.ts:228`](../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
### `session/*`
|
||||
|
||||
@@ -2919,7 +2919,7 @@ SHA-256: `af6e173f7be819bcd0a5c8d77918649c10ce761d8a184ff472f2e01ce64eb2e0`
|
||||
|
||||
SHA-256: `27e337d3d6b402b221e50c247a3f518a70d0c474cc60beee15fac3e9c5cbfcbe`
|
||||
|
||||
来源:[`packages/schedule/schedule/src/types.ts:13`](../packages/schedule/schedule/src/types.ts)
|
||||
来源:[`packages/schedule/schedule/src/types.ts:15`](../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
@@ -3086,7 +3086,7 @@ SHA-256: `84bbfdce7d2eab5b0c53e72ff7406db856c8bfd621b965b72fd86598ba493bf7`
|
||||
|
||||
SHA-256: `658d895d187b26d83f3b53109a3bf77bbfba9f6f9a6d4bc4ee4315b4eb36239e`
|
||||
|
||||
来源:[`packages/schedule/schedule/src/types.ts:27`](../packages/schedule/schedule/src/types.ts)
|
||||
来源:[`packages/schedule/schedule/src/types.ts:29`](../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
@@ -3300,7 +3300,7 @@ SHA-256: `9a930283b91d493d6347146f2475bb107294b379415ca02162354f46a64164f7`
|
||||
|
||||
SHA-256: `e6063d0451dc02026fd9b54ad3f77c7b1b6392ad3e872a7737ac84e13ca72344`
|
||||
|
||||
来源:[`packages/schedule/schedule/src/types.ts:93`](../packages/schedule/schedule/src/types.ts)
|
||||
来源:[`packages/schedule/schedule/src/types.ts:95`](../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
@@ -3319,7 +3319,7 @@ SHA-256: `e6063d0451dc02026fd9b54ad3f77c7b1b6392ad3e872a7737ac84e13ca72344`
|
||||
|
||||
SHA-256: `66ece68282d8c06330fb83329a24ae54c386d5ccfab296bbf18369da8beb7d14`
|
||||
|
||||
来源:[`packages/schedule/schedule/src/types.ts:39`](../packages/schedule/schedule/src/types.ts)
|
||||
来源:[`packages/schedule/schedule/src/types.ts:41`](../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
@@ -3691,7 +3691,7 @@ SHA-256: `9e2c06d8653585b33d72d5c1a80da79c8a24a88c284409bca15cc0f0ead5ebfb`
|
||||
|
||||
SHA-256: `96f4a9c81fc0f940ef71528a8cc66731ae4cf1f40d79680c5fe3c39f96d7723c`
|
||||
|
||||
来源:[`packages/core/agent/src/types.ts:30`](../packages/core/agent/src/types.ts)
|
||||
来源:[`packages/core/agent/src/types.ts:39`](../packages/core/agent/src/types.ts)
|
||||
|
||||
以下类型之一:
|
||||
|
||||
@@ -4094,7 +4094,7 @@ SHA-256: `f09e5e7acf5ff5ccd193ff2eeec015fa2ff7c29d98bc465f8c3302058d47e29b`
|
||||
|
||||
SHA-256: `8e8e91646a5cae3fc78f7532a0013ff2088ae47077642c2edd507a508209b8af`
|
||||
|
||||
来源:[`packages/schedule/schedule/src/types.ts:86`](../packages/schedule/schedule/src/types.ts)
|
||||
来源:[`packages/schedule/schedule/src/types.ts:88`](../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
@@ -4306,7 +4306,7 @@ SHA-256: `fab2bb09bc5e3b9db0685e8f5500b029af1a2b33e70bc703c1d5d0f596fbc1fd`
|
||||
|
||||
SHA-256: `e1d0e52dcd1855ffb803efc18f8f7b776c48d1c25757e8a75c0281fa298c02e1`
|
||||
|
||||
来源:[`packages/schedule/schedule/src/types.ts:105`](../packages/schedule/schedule/src/types.ts)
|
||||
来源:[`packages/schedule/schedule/src/types.ts:107`](../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
以下类型之一:
|
||||
|
||||
@@ -4325,7 +4325,7 @@ SHA-256: `e1d0e52dcd1855ffb803efc18f8f7b776c48d1c25757e8a75c0281fa298c02e1`
|
||||
|
||||
SHA-256: `ca6425edd20afe34d0cedc6ad1abad1ad7b32e2fbc4c1d24db53939e80bf887f`
|
||||
|
||||
来源:[`packages/schedule/schedule/src/types.ts:72`](../packages/schedule/schedule/src/types.ts)
|
||||
来源:[`packages/schedule/schedule/src/types.ts:74`](../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
@@ -4343,7 +4343,7 @@ SHA-256: `ca6425edd20afe34d0cedc6ad1abad1ad7b32e2fbc4c1d24db53939e80bf887f`
|
||||
|
||||
SHA-256: `18dc21f12bbac8ade8377e6fbb54179e20c3d955d73fba3244c57fb6bf001df0`
|
||||
|
||||
来源:[`packages/schedule/schedule/src/types.ts:79`](../packages/schedule/schedule/src/types.ts)
|
||||
来源:[`packages/schedule/schedule/src/types.ts:81`](../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
@@ -4361,7 +4361,7 @@ SHA-256: `18dc21f12bbac8ade8377e6fbb54179e20c3d955d73fba3244c57fb6bf001df0`
|
||||
|
||||
SHA-256: `d8f868c6b3e7189de6c1ffe2ea04794ea2aa31315b6347d12536e46020afac87`
|
||||
|
||||
来源:[`packages/schedule/schedule/src/types.ts:69`](../packages/schedule/schedule/src/types.ts)
|
||||
来源:[`packages/schedule/schedule/src/types.ts:71`](../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
| kind | form 属性 | 其他必需字段 | 完整定义 |
|
||||
|---|---|---|---|
|
||||
@@ -5317,7 +5317,7 @@ SHA-256: `5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78`
|
||||
|
||||
SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b`
|
||||
|
||||
来源:[`packages/api/session-controller/src/types.ts:396`](../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:10`](../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:396`](../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)
|
||||
|
||||
`string`
|
||||
|
||||
@@ -6073,7 +6073,7 @@ SHA-256: `17d1afb770d9941936130996da00dc86782cfef731d8d6526162c301256a4ac3`
|
||||
|
||||
SHA-256: `dc4bd7ac827f4b1f9236f9a157a4b4cc171a0588064a611e43edd95f62c68130`
|
||||
|
||||
来源:[`packages/core/agent/src/types.ts:87`](../packages/core/agent/src/types.ts)
|
||||
来源:[`packages/core/agent/src/types.ts:96`](../packages/core/agent/src/types.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
|
||||
Generated
+12
-12
@@ -29438,7 +29438,7 @@
|
||||
"packages/schedule/schedule/src/types.ts#ScheduleDeleteChange"
|
||||
],
|
||||
"sources": [
|
||||
"packages/schedule/schedule/src/types.ts:79"
|
||||
"packages/schedule/schedule/src/types.ts:81"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -32684,7 +32684,7 @@
|
||||
"packages/schedule/schedule/src/types.ts#AfterScheduleRecord"
|
||||
],
|
||||
"sources": [
|
||||
"packages/schedule/schedule/src/types.ts:13"
|
||||
"packages/schedule/schedule/src/types.ts:15"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -44624,7 +44624,7 @@
|
||||
"packages/llm/llm/src/brand.ts:31",
|
||||
"packages/llm/llm/src/brand.ts:43",
|
||||
"packages/llm/llm/src/brand.ts:67",
|
||||
"packages/schedule/schedule/src/types.ts:10",
|
||||
"packages/schedule/schedule/src/types.ts:12",
|
||||
"packages/session/session-title/src/index.ts:42",
|
||||
"packages/session/session-title/src/types.ts:17",
|
||||
"packages/webhook/webhook/src/brand.ts:12",
|
||||
@@ -54375,7 +54375,7 @@
|
||||
"packages/schedule/schedule/src/types.ts#AtScheduleRecord"
|
||||
],
|
||||
"sources": [
|
||||
"packages/schedule/schedule/src/types.ts:27"
|
||||
"packages/schedule/schedule/src/types.ts:29"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -54617,7 +54617,7 @@
|
||||
"packages/schedule/schedule/src/types.ts#EveryScheduleRecord"
|
||||
],
|
||||
"sources": [
|
||||
"packages/schedule/schedule/src/types.ts:39"
|
||||
"packages/schedule/schedule/src/types.ts:41"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -63758,7 +63758,7 @@
|
||||
"packages/schedule/schedule/src/types.ts#OneShotScheduleDispatchChange"
|
||||
],
|
||||
"sources": [
|
||||
"packages/schedule/schedule/src/types.ts:86"
|
||||
"packages/schedule/schedule/src/types.ts:88"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -64330,7 +64330,7 @@
|
||||
"packages/core/agent/src/types.ts#InboxTarget"
|
||||
],
|
||||
"sources": [
|
||||
"packages/core/agent/src/types.ts:30"
|
||||
"packages/core/agent/src/types.ts:39"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -74172,7 +74172,7 @@
|
||||
"packages/schedule/schedule/src/types.ts#ScheduleCreateChange"
|
||||
],
|
||||
"sources": [
|
||||
"packages/schedule/schedule/src/types.ts:72"
|
||||
"packages/schedule/schedule/src/types.ts:74"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -78164,7 +78164,7 @@
|
||||
"packages/schedule/schedule/src/types.ts#ScheduleRecord"
|
||||
],
|
||||
"sources": [
|
||||
"packages/schedule/schedule/src/types.ts:69"
|
||||
"packages/schedule/schedule/src/types.ts:71"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -81193,7 +81193,7 @@
|
||||
},
|
||||
"names": [],
|
||||
"sources": [
|
||||
"packages/core/agent/src/types.ts:87"
|
||||
"packages/core/agent/src/types.ts:96"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -81796,7 +81796,7 @@
|
||||
"packages/schedule/schedule/src/types.ts#ScheduleChange"
|
||||
],
|
||||
"sources": [
|
||||
"packages/schedule/schedule/src/types.ts:105"
|
||||
"packages/schedule/schedule/src/types.ts:107"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -82985,7 +82985,7 @@
|
||||
"packages/schedule/schedule/src/types.ts#EveryScheduleDispatchChange"
|
||||
],
|
||||
"sources": [
|
||||
"packages/schedule/schedule/src/types.ts:93"
|
||||
"packages/schedule/schedule/src/types.ts:95"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/workspace.md
|
||||
workspace.md: 8a3369778f0eac10d5fe077686d336eb47f6b41e
|
||||
workspace.zh.md: f1e7e96f1fb5e983de30bd8e2f9fe08ed0635beb
|
||||
workspace.md: 18c1bf3d8b3528a446c4193f33c405d6ae91b7e7
|
||||
workspace.zh.md: c2674737dbc68e994983ce88e7d38aad3e7672ee
|
||||
@@ -129,6 +129,39 @@ The controller's [transport types](../../packages/api/workspace-controller/src/t
|
||||
|
||||
The controller's [transport types](../../packages/api/workspace-controller/src/types.ts) define `WorkspacePinSessionRequest` and `WorkspaceUnpinSessionRequest`, each carrying one `sessionId`. Both operations return `WorkspacePinValue`: the complete `pinnedSessionIds` array of Session ids, most recently pinned first. Pinning requires a known, unarchived Session; unpinning an id that is not pinned succeeds without changing the set. Archiving removes the Session's pin in the same durable write, and unarchiving does not restore it.
|
||||
|
||||
## Archive admission
|
||||
|
||||
Archiving is a registry-global durable set, and the registry refuses to hide running work behind it. The rule is a capability seam over two Host events the package declares and dispatches ([events](#workspace-events)): `workspace/session-activity` (waterfall) asks the composed providers what still runs for a Session, and `workspace/session-stop` (parallel) asks them to stop it. Each provider registers on the root like any listener, so the package knows no agent, job, or schedule vocabulary; the families are keys of a merge-extensible map.
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Activity families a `workspace/session-activity` listener may report. This
|
||||
* package declares none: each provider merges its own key from a module both
|
||||
* its Host and Client faces import, so a consumer that renders the families
|
||||
* sees exactly the keys its program compiled and falls through to a generic
|
||||
* description for any other. The shipped providers merge `turn` (the Agent
|
||||
* registry), `job` (the job registry seam), `subagent` (the Subagent
|
||||
* runtime), and `schedule` (the Schedule plugin).
|
||||
*/
|
||||
interface SessionActivityKindMap {}
|
||||
```
|
||||
|
||||
`SessionActivityKind` is `keyof SessionActivityKindMap`, so a program that compiled no provider sees no key at all. The shipped keys live in client-importable type modules: `turn` in the Agent registry's `types.ts`, `job` in the job registry seam's `view.ts`, `subagent` in the Subagent runtime's `control-types.ts`, `schedule` in the Schedule plugin's `types.ts`; a consumer that renders the families imports those modules for its cases and keeps a generic line for any other key. A provider answers the waterfall by prepending its `SessionActivity` entries to the result of `next()`; the registry's innermost callback returns an empty list, so a composition without providers archives freely.
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One reason a session counts as active for archive admission. Families with
|
||||
* per-item identity list their items so a caller can name what must stop.
|
||||
*/
|
||||
interface SessionActivity {
|
||||
readonly kind: SessionActivityKind
|
||||
/** Active items of the family; absent for a family without per-item identity (`turn`). */
|
||||
readonly items?: readonly SessionActivityItem[]
|
||||
}
|
||||
```
|
||||
|
||||
`SessionActivityItem` carries the family-specific `id` (a session, job, or schedule id) and an optional display `label`. `archiveSession(sessionId)` asks the waterfall once, after the existence check, and rejects a non-empty answer with `WorkspaceActiveSessionError` (`sessionId`, `activity`) without writing; the controller maps it to the `workspace/session-active` error, whose details carry the same two fields. `archiveSession(sessionId, { stopActivity: true })` — the `ArchiveSessionOptions` field the transport request exposes as `stopActivity` — skips the check, writes the archive, then dispatches `workspace/session-stop`; a rejecting provider is logged and the archive stays, and the stopped work is never awaited to settlement. An already archived id neither asks nor stops. The shipped providers, what they stop, and the `agent/pre-step` gate that keeps an archived Session from running a model step are documented with the [registry package](../../packages/workspace/workspace/README.md#api-behavior); the decision record is the [archive-stops-running-work Agent Note](../../.agents/notes/implemented/feature/2026-09-21-archive-stops-running-session-work.md).
|
||||
|
||||
## Consumers
|
||||
|
||||
[`dsh-workspace-controller`](../../packages/api/workspace-controller) serves workspace CRUD to GUI clients over `ctx.workspaceRegistry`, and [`dsh-session-controller`](../../packages/api/session-controller) performs the create-session-then-attach flow above. [dsh-agent-instructions](../../packages/context/agent-instructions) is **not** a consumer despite the name: it discovers AGENTS.md-style instruction files under an agent's own cwd and never touches `ctx.workspaceRegistry` — the shared word refers to the user's working directory, not to this registry's entities.
|
||||
@@ -507,13 +540,21 @@ insertBefore(id: WorkspaceId, beforeId?: WorkspaceId): Promise<readonly Workspac
|
||||
/**
|
||||
* Archive one session durably. The session must exist (live or in session
|
||||
* persistence); its workspace accounting — or lack of one — is irrelevant.
|
||||
* Archiving drops the session's pin in the same durable write (pinning and
|
||||
* archival are mutually exclusive). An already archived id resolves without
|
||||
* writing.
|
||||
* Without `stopActivity` the session must also be inactive: the
|
||||
* `workspace/session-activity` waterfall is asked once, and any reported
|
||||
* activity rejects with {@link WorkspaceActiveSessionError} before anything
|
||||
* is written. With `stopActivity` the archive is written without an
|
||||
* activity check, and the `workspace/session-stop` providers are then asked
|
||||
* to stop the session's work: the durable archive set is what a provider's
|
||||
* `agent/pre-step` gate reads, so every wake the stops induce is already
|
||||
* blocked. Archiving drops the session's pin in the same durable write
|
||||
* (pinning and archival are mutually exclusive). An already archived id
|
||||
* resolves without writing, asking, or stopping.
|
||||
* @param sessionId - The session to archive.
|
||||
* @returns resolution after durability.
|
||||
* @param options - Whether running work is stopped instead of refusing.
|
||||
* @returns resolution after durability and, with `stopActivity`, after every stop request was issued.
|
||||
*/
|
||||
archiveSession(sessionId: SessionId): Promise<void>
|
||||
archiveSession(sessionId: SessionId, options: ArchiveSessionOptions = {}): Promise<void>
|
||||
|
||||
/**
|
||||
* Unarchive one session durably by dropping it from the registry-global
|
||||
@@ -558,5 +599,57 @@ async resolveByPath(path: string): Promise<Workspace | undefined>
|
||||
|
||||
Types: [SessionId](core.md)
|
||||
|
||||
Source: [`packages/workspace/workspace/src/index.ts`](../../packages/workspace/workspace/src/index.ts)
|
||||
|
||||
<a id="workspace-events"></a>
|
||||
|
||||
### `workspace/*` events
|
||||
|
||||
<a id="workspacesession-activity--waterfall"></a>
|
||||
|
||||
#### `workspace/session-activity` — waterfall
|
||||
|
||||
Ask the composed providers what still runs for a session before it is archived. A listener prepends its own SessionActivity entries to the result of `next()`; the registry's innermost callback returns an empty list, so a composition without providers archives freely. Any non-empty result refuses the archive without a write.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Ask the composed providers what still runs for a session before it is
|
||||
* archived. A listener prepends its own {@link SessionActivity} entries to
|
||||
* the result of `next()`; the registry's innermost callback returns an
|
||||
* empty list, so a composition without providers archives freely. Any
|
||||
* non-empty result refuses the archive without a write.
|
||||
* @param request - the session about to be archived.
|
||||
* @param next - delegate to the remaining providers.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'workspace/session-activity'( request: SessionActivityRequest, next: () => Promise<readonly SessionActivity[]>, ): Promise<readonly SessionActivity[]>
|
||||
```
|
||||
|
||||
Source: [`packages/workspace/workspace/src/index.ts`](../../packages/workspace/workspace/src/index.ts)
|
||||
|
||||
<a id="workspacesession-stop--parallel"></a>
|
||||
|
||||
#### `workspace/session-stop` — parallel
|
||||
|
||||
Stop a session's running work because the caller archived it with `stopActivity`; the archive set is durable when this dispatches. Each provider stops its own families — cancelling a turn, its subagent descendants, owned jobs, or active schedules — through the same cancel paths the user's own stop actions use, so the session log ends every open turn regularly and a later unarchive can continue the conversation. Listeners issue their stop requests without waiting for running work to settle; a listener may await its own durability barrier. A rejection is logged by the registry and does not undo the archive.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Stop a session's running work because the caller archived it with
|
||||
* `stopActivity`; the archive set is durable when this dispatches. Each
|
||||
* provider stops its own families — cancelling a turn, its subagent
|
||||
* descendants, owned jobs, or active schedules — through the same cancel
|
||||
* paths the user's own stop actions use, so the session log ends every
|
||||
* open turn regularly and a later unarchive can continue the
|
||||
* conversation. Listeners issue their stop requests without waiting for
|
||||
* running work to settle; a listener may await its own durability
|
||||
* barrier. A rejection is logged by the registry and does not undo the
|
||||
* archive.
|
||||
* @param request - the session being archived.
|
||||
* @mode parallel
|
||||
*/
|
||||
'workspace/session-stop'(request: SessionActivityRequest): Promise<void> | void
|
||||
```
|
||||
|
||||
Source: [`packages/workspace/workspace/src/index.ts`](../../packages/workspace/workspace/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
@@ -129,6 +129,39 @@ interface Workspace {
|
||||
|
||||
控制器的[传输类型](../../packages/api/workspace-controller/src/types.ts)定义了 `WorkspacePinSessionRequest` 和 `WorkspaceUnpinSessionRequest`,两者都携带一个 `sessionId`。两个操作都返回 `WorkspacePinValue`:完整的会话 id 数组 `pinnedSessionIds`,最近置顶的会话排在前面。置顶要求会话已知且未归档;对未置顶的 id 取消置顶会成功,且不改变集合。归档在同一次持久化写入中移除该会话的置顶,取消归档不会恢复置顶。
|
||||
|
||||
## 归档准入
|
||||
|
||||
归档是一个注册表全局的持久化集合,注册表拒绝把正在运行的工作藏在它后面。这条规则是本包声明并派发的两个宿主事件之上的能力接缝([事件](#workspace-events)):`workspace/session-activity`(waterfall)向已组合的提供方询问某会话还有什么在跑,`workspace/session-stop`(parallel)请它们停止这些工作。每个提供方像任何监听器一样在根上注册,因此本包不认识 agent、job 或 schedule 的词汇;各族是一个可合并扩展的 map 的键。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Activity families a `workspace/session-activity` listener may report. This
|
||||
* package declares none: each provider merges its own key from a module both
|
||||
* its Host and Client faces import, so a consumer that renders the families
|
||||
* sees exactly the keys its program compiled and falls through to a generic
|
||||
* description for any other. The shipped providers merge `turn` (the Agent
|
||||
* registry), `job` (the job registry seam), `subagent` (the Subagent
|
||||
* runtime), and `schedule` (the Schedule plugin).
|
||||
*/
|
||||
interface SessionActivityKindMap {}
|
||||
```
|
||||
|
||||
`SessionActivityKind` 即 `keyof SessionActivityKindMap`,因此一个没有编译任何提供方的程序看不到任何键。随附的键放在各自 client 可导入的类型模块里:`turn` 在 Agent 注册表的 `types.ts`,`job` 在任务注册表接缝的 `view.ts`,`subagent` 在 Subagent runtime 的 `control-types.ts`,`schedule` 在 Schedule 插件的 `types.ts`;渲染各族的消费方为其分支导入这些模块,并为其他任何键保留一条通用文案。提供方通过把自己的 `SessionActivity` 条目前置到 `next()` 的结果来回答 waterfall;注册表最内层的回调返回空列表,因此没有提供方的组合可自由归档。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One reason a session counts as active for archive admission. Families with
|
||||
* per-item identity list their items so a caller can name what must stop.
|
||||
*/
|
||||
interface SessionActivity {
|
||||
readonly kind: SessionActivityKind
|
||||
/** Active items of the family; absent for a family without per-item identity (`turn`). */
|
||||
readonly items?: readonly SessionActivityItem[]
|
||||
}
|
||||
```
|
||||
|
||||
`SessionActivityItem` 携带各族自己的 `id`(会话、任务或提醒 id)和可选的展示 `label`。`archiveSession(sessionId)` 在存在性检查之后只询问 waterfall 一次,对非空答案以 `WorkspaceActiveSessionError`(`sessionId`、`activity`)拒绝且不写入;控制器把它映射为 `workspace/session-active` 错误,其 details 携带同样的两个字段。`archiveSession(sessionId, { stopActivity: true })`——`ArchiveSessionOptions` 的这个字段由传输请求以 `stopActivity` 暴露——跳过检查、先写入归档、再派发 `workspace/session-stop`;提供方抛错只记日志,归档保留,被停止的工作从不等待收敛。已归档的 id 既不询问也不停止。随附的提供方、它们停止什么,以及让已归档会话不跑模型步的 `agent/pre-step` 门禁,记录在[注册表包](../../packages/workspace/workspace/README.zh.md#api-behavior);决策记录见 [archive-stops-running-work Agent Note](../../.agents/notes/implemented/feature/2026-09-21-archive-stops-running-session-work.zh.md)。
|
||||
|
||||
## 消费方
|
||||
|
||||
[`dsh-workspace-controller`](../../packages/api/workspace-controller) 经 `ctx.workspaceRegistry` 向 GUI 客户端提供工作区 CRUD,[`dsh-session-controller`](../../packages/api/session-controller) 执行上文「先建会话再 attach」的流程。[dsh-agent-instructions](../../packages/context/agent-instructions) 尽管名字如此,却**不是**消费方:它在 agent 自己的 cwd 下发现 AGENTS.md 风格的指令文件,从不触碰 `ctx.workspaceRegistry`——两者共用的这个词指的是用户的工作目录,而非本注册表的实体。
|
||||
@@ -507,13 +540,21 @@ insertBefore(id: WorkspaceId, beforeId?: WorkspaceId): Promise<readonly Workspac
|
||||
/**
|
||||
* Archive one session durably. The session must exist (live or in session
|
||||
* persistence); its workspace accounting — or lack of one — is irrelevant.
|
||||
* Archiving drops the session's pin in the same durable write (pinning and
|
||||
* archival are mutually exclusive). An already archived id resolves without
|
||||
* writing.
|
||||
* Without `stopActivity` the session must also be inactive: the
|
||||
* `workspace/session-activity` waterfall is asked once, and any reported
|
||||
* activity rejects with {@link WorkspaceActiveSessionError} before anything
|
||||
* is written. With `stopActivity` the archive is written without an
|
||||
* activity check, and the `workspace/session-stop` providers are then asked
|
||||
* to stop the session's work: the durable archive set is what a provider's
|
||||
* `agent/pre-step` gate reads, so every wake the stops induce is already
|
||||
* blocked. Archiving drops the session's pin in the same durable write
|
||||
* (pinning and archival are mutually exclusive). An already archived id
|
||||
* resolves without writing, asking, or stopping.
|
||||
* @param sessionId - The session to archive.
|
||||
* @returns resolution after durability.
|
||||
* @param options - Whether running work is stopped instead of refusing.
|
||||
* @returns resolution after durability and, with `stopActivity`, after every stop request was issued.
|
||||
*/
|
||||
archiveSession(sessionId: SessionId): Promise<void>
|
||||
archiveSession(sessionId: SessionId, options: ArchiveSessionOptions = {}): Promise<void>
|
||||
|
||||
/**
|
||||
* Unarchive one session durably by dropping it from the registry-global
|
||||
@@ -558,5 +599,57 @@ async resolveByPath(path: string): Promise<Workspace | undefined>
|
||||
|
||||
Types: [SessionId](core.zh.md)
|
||||
|
||||
Source: [`packages/workspace/workspace/src/index.ts`](../../packages/workspace/workspace/src/index.ts)
|
||||
|
||||
<a id="workspace-events"></a>
|
||||
|
||||
### `workspace/*` events
|
||||
|
||||
<a id="workspacesession-activity--waterfall"></a>
|
||||
|
||||
#### `workspace/session-activity` — waterfall
|
||||
|
||||
Ask the composed providers what still runs for a session before it is archived. A listener prepends its own SessionActivity entries to the result of `next()`; the registry's innermost callback returns an empty list, so a composition without providers archives freely. Any non-empty result refuses the archive without a write.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Ask the composed providers what still runs for a session before it is
|
||||
* archived. A listener prepends its own {@link SessionActivity} entries to
|
||||
* the result of `next()`; the registry's innermost callback returns an
|
||||
* empty list, so a composition without providers archives freely. Any
|
||||
* non-empty result refuses the archive without a write.
|
||||
* @param request - the session about to be archived.
|
||||
* @param next - delegate to the remaining providers.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'workspace/session-activity'( request: SessionActivityRequest, next: () => Promise<readonly SessionActivity[]>, ): Promise<readonly SessionActivity[]>
|
||||
```
|
||||
|
||||
Source: [`packages/workspace/workspace/src/index.ts`](../../packages/workspace/workspace/src/index.ts)
|
||||
|
||||
<a id="workspacesession-stop--parallel"></a>
|
||||
|
||||
#### `workspace/session-stop` — parallel
|
||||
|
||||
Stop a session's running work because the caller archived it with `stopActivity`; the archive set is durable when this dispatches. Each provider stops its own families — cancelling a turn, its subagent descendants, owned jobs, or active schedules — through the same cancel paths the user's own stop actions use, so the session log ends every open turn regularly and a later unarchive can continue the conversation. Listeners issue their stop requests without waiting for running work to settle; a listener may await its own durability barrier. A rejection is logged by the registry and does not undo the archive.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Stop a session's running work because the caller archived it with
|
||||
* `stopActivity`; the archive set is durable when this dispatches. Each
|
||||
* provider stops its own families — cancelling a turn, its subagent
|
||||
* descendants, owned jobs, or active schedules — through the same cancel
|
||||
* paths the user's own stop actions use, so the session log ends every
|
||||
* open turn regularly and a later unarchive can continue the
|
||||
* conversation. Listeners issue their stop requests without waiting for
|
||||
* running work to settle; a listener may await its own durability
|
||||
* barrier. A rejection is logged by the registry and does not undo the
|
||||
* archive.
|
||||
* @param request - the session being archived.
|
||||
* @mode parallel
|
||||
*/
|
||||
'workspace/session-stop'(request: SessionActivityRequest): Promise<void> | void
|
||||
```
|
||||
|
||||
Source: [`packages/workspace/workspace/src/index.ts`](../../packages/workspace/workspace/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/api/session-controller/README.md
|
||||
README.md: 684616e7e6011c125e8c4dd4b087d551522ab339
|
||||
README.zh.md: 50b37df21b360a02edbbc099a433fc74e07ba3bd
|
||||
README.md: 0fd4ea07c6575a9f75167b294c6da5dab81496de
|
||||
README.zh.md: 8ce6bb6e70a8f2f766c96c2cc103e93348a4d98a
|
||||
@@ -60,6 +60,8 @@ Queue edits replace pending content with non-empty text only.
|
||||
|
||||
Attachment authorization reads declared content fields of built-in Session events and completed assistant stream blocks, including flat V4 tool-role messages. Unknown event payloads and unrelated fields cannot authorize attachment reads.
|
||||
|
||||
The controller composes `ArchivedSessionGate` through `ctx.plugin`: it loads once the Agent registry, Session store, and Workspace registry exist and unwinds with the controller. Its `agent/pre-step` listener rejects a step proposed for an archived Session or for a subagent descendant of one — read from the Session header's lineage fields, never a fork — so a late waking delivery ends its turn as `blocked` without a model request; unarchiving lifts the gate for the whole lineage. The work an archived Session still runs is reported and stopped by its owners through the Workspace registry's archive admission ([seam](../../workspace/workspace/README.md)): the running turn by the [Agent registry](../../core/agent/README.md), owned jobs by the [job registry seam](../../jobs/jobs/README.md), subagent descendants by the [Subagent](../../subagent/subagent/README.md) runtime, reminders by the [Schedule](../../schedule/schedule/README.md) plugin; this controller reports nothing itself.
|
||||
|
||||
<a id="client-references"></a>
|
||||
## Client references
|
||||
|
||||
|
||||
@@ -60,6 +60,8 @@ Fork 复制 `atSeq` 所选的精确事件前缀,包含切点事件,允许在
|
||||
|
||||
附件授权读取内置 Session 事件声明的内容字段与已完成的 assistant 流块,包括扁平的 V4 tool 角色消息。未知事件载荷与无关字段不能授权附件读取。
|
||||
|
||||
本控制器通过 `ctx.plugin` 组合 `ArchivedSessionGate`:在 Agent 注册表、Session store 与 Workspace 注册表就绪后加载,随控制器一起释放。它的 `agent/pre-step` 监听器会拒绝为已归档会话或其子代理子孙——从 Session header 的血缘字段读出,从不包括 fork——提出的步骤,因此迟到的唤醒投递会让该回合以 `blocked` 收口而不发出模型请求;取消归档即为整条血缘解除门禁。已归档会话仍在跑的工作由各自的 owner 通过 Workspace 注册表的归档准入([接缝](../../workspace/workspace/README.zh.md))报告与停止:运行中的回合由 [Agent 注册表](../../core/agent/README.zh.md)负责,所属任务由[任务注册表接缝](../../jobs/jobs/README.zh.md)负责,子代理子孙由 [Subagent](../../subagent/subagent/README.zh.md) runtime 负责,提醒由 [Schedule](../../schedule/schedule/README.zh.md) 插件负责;本控制器自己不报告任何内容。
|
||||
|
||||
<a id="client-references"></a>
|
||||
## Client 引用
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* The controller's admission gate for archived Sessions: an archived
|
||||
* Session, or a subagent descendant of one, must not run a model step until
|
||||
* it is restored. The work a Session still runs is reported and stopped by
|
||||
* its owners — the Agent registry (`turn`), the job registry seam (`job`),
|
||||
* the Subagent runtime (`subagent`), and the Schedule plugin (`schedule`) —
|
||||
* through the Workspace registry's archive-admission events.
|
||||
*/
|
||||
|
||||
import type { Context, Plugin } from '@deepseek-ai/cordis'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type {} from '@deepseek-ai/dsh-workspace'
|
||||
|
||||
/**
|
||||
* The gate as a plugin for `ctx.plugin(...)`: it loads once the Agent
|
||||
* registry, Session store, and Workspace registry are available and unwinds
|
||||
* with the plugin's fiber. A late waking delivery to an archived Session — a
|
||||
* subagent settlement, a queued follow-up — proposes a step the gate rejects,
|
||||
* which the loop ends as `blocked` without a request; unarchiving lifts the
|
||||
* gate for the whole lineage.
|
||||
*/
|
||||
export const ArchivedSessionGate: Plugin.Object<void> = {
|
||||
name: 'archived-session-gate',
|
||||
inject: ['agents', 'sessions', 'workspaceRegistry'],
|
||||
apply(ctx: Context): void {
|
||||
ctx.on('agent/pre-step', (payload, next) =>
|
||||
underArchivedSession(ctx, payload.agent)
|
||||
? Promise.resolve({ kind: 'reject' as const })
|
||||
: next())
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the Agent's Session, or a Session above it in its subagent lineage,
|
||||
* is archived. Lineage follows the durable header fields through
|
||||
* subagent-origin Sessions only: a fork of an archived Session is an
|
||||
* independent conversation.
|
||||
* @param ctx - Host context.
|
||||
* @param agent - the Agent proposing a step.
|
||||
* @returns whether an archived Session owns the step.
|
||||
*/
|
||||
export function underArchivedSession(ctx: Context, agent: Agent): boolean {
|
||||
const archived = ctx.workspaceRegistry.archivedSessionIds
|
||||
let header = agent.session.header
|
||||
const visited = new Set<SessionId>()
|
||||
while (!visited.has(header.id)) {
|
||||
if (archived.includes(header.id)) return true
|
||||
visited.add(header.id)
|
||||
if (header.origin !== 'subagent' || header.parentSession === undefined) return false
|
||||
const parent = ctx.sessions.get(header.parentSession)
|
||||
if (parent === undefined) return archived.includes(header.parentSession)
|
||||
header = parent.header
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import { buildModelCatalog } from './catalog.ts'
|
||||
import { installModelSelectionProjection } from './model-selection-projection.ts'
|
||||
import { SessionSkillCatalog } from './skill-catalog.ts'
|
||||
import { SessionMediaReferences } from './media-references.ts'
|
||||
import { ArchivedSessionGate } from './archived-session-gate.ts'
|
||||
import type {
|
||||
ModelCatalog,
|
||||
SessionWorkspacePathApplication,
|
||||
@@ -158,6 +159,10 @@ export class SessionController extends TypertRemoteService {
|
||||
ctx.plugin(SessionFileReferences)
|
||||
ctx.plugin(SessionMediaReferences)
|
||||
ctx.plugin(SessionSkillCatalog)
|
||||
// An archived Session, or a subagent descendant of one, runs no model step
|
||||
// until it is restored; what it still runs is stopped by the owners that
|
||||
// answer the Workspace registry's archive-admission events.
|
||||
ctx.plugin(ArchivedSessionGate)
|
||||
|
||||
ctx.on('session/created', (session) => {
|
||||
ctx.emit('api-session/added', this.listState.summaryFor(session))
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import AgentRegistry from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { unsupportedInbox } from '@deepseek-ai/dsh-agent-loop-testkit'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { Session } from '@deepseek-ai/dsh-session'
|
||||
import type {} from '@deepseek-ai/dsh-workspace'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { ArchivedSessionGate } from '../src/archived-session-gate.ts'
|
||||
|
||||
interface Harness {
|
||||
ctx: Context
|
||||
archived: SessionId[]
|
||||
}
|
||||
|
||||
async function harness(): Promise<Harness> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
const archived: SessionId[] = []
|
||||
ctx.provide('workspaceRegistry', { get archivedSessionIds() { return archived } } as never)
|
||||
await ctx.plugin(ArchivedSessionGate)
|
||||
return { ctx, archived }
|
||||
}
|
||||
|
||||
/** A registered idle Agent double: the gate reads only its Session header. */
|
||||
async function liveAgent(ctx: Context, session: Session): Promise<Agent> {
|
||||
const agent: Agent = {
|
||||
id: session.id,
|
||||
options: {},
|
||||
session,
|
||||
inbox: unsupportedInbox(),
|
||||
status: 'idle',
|
||||
ctx,
|
||||
send: () => {},
|
||||
followup: () => {},
|
||||
steer: () => {},
|
||||
inject: () => {},
|
||||
cancel: () => {},
|
||||
runMaintenance: task => task(new AbortController().signal),
|
||||
whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
await ctx.agents.register(agent)
|
||||
return agent
|
||||
}
|
||||
|
||||
/** Propose one model step for the Agent; the innermost decision enters. */
|
||||
const proposed = (ctx: Context, agent: Agent) => ctx.waterfall(
|
||||
'agent/pre-step',
|
||||
{ agent, messages: [], turn: 1, step: 1, signal: new AbortController().signal },
|
||||
() => Promise.resolve({ kind: 'enter' as const, messages: [] }),
|
||||
)
|
||||
|
||||
describe('Archived-session gate', () => {
|
||||
it('rejects a proposed step for an archived session and lets others through', async () => {
|
||||
const { ctx, archived } = await harness()
|
||||
const hidden = await liveAgent(ctx, ctx.sessions.create(SessionId('hidden')))
|
||||
const visible = await liveAgent(ctx, ctx.sessions.create(SessionId('visible')))
|
||||
archived.push(hidden.id)
|
||||
expect(await proposed(ctx, hidden)).toEqual({ kind: 'reject' })
|
||||
expect(await proposed(ctx, visible)).toEqual({ kind: 'enter', messages: [] })
|
||||
archived.length = 0
|
||||
expect(await proposed(ctx, hidden)).toEqual({ kind: 'enter', messages: [] })
|
||||
})
|
||||
|
||||
it('rejects steps of subagent descendants of an archived session, at any depth, but not of its forks', async () => {
|
||||
const { ctx, archived } = await harness()
|
||||
const root = await liveAgent(ctx, ctx.sessions.create(SessionId('archived-root')))
|
||||
const child = await liveAgent(ctx, ctx.sessions.create(SessionId('woken-child'), {
|
||||
meta: { parentSession: root.id, origin: 'subagent' },
|
||||
}))
|
||||
// A settlement from its own child wakes this continuable child after the
|
||||
// root was archived; the gate must see the archived root above it.
|
||||
const grandchild = await liveAgent(ctx, ctx.sessions.create(SessionId('woken-grandchild'), {
|
||||
meta: { parentSession: child.id, origin: 'subagent' },
|
||||
}))
|
||||
const fork = await liveAgent(ctx, ctx.sessions.create(SessionId('fork-of-archived'), {
|
||||
meta: { parentSession: root.id },
|
||||
}))
|
||||
// A child whose parent Session is not resident is judged by the parent id alone.
|
||||
const orphan = await liveAgent(ctx, ctx.sessions.create(SessionId('orphan-child'), {
|
||||
meta: { parentSession: SessionId('gone-parent'), origin: 'subagent' },
|
||||
}))
|
||||
archived.push(root.id, SessionId('gone-parent'))
|
||||
|
||||
expect(await proposed(ctx, child)).toEqual({ kind: 'reject' })
|
||||
expect(await proposed(ctx, grandchild)).toEqual({ kind: 'reject' })
|
||||
expect(await proposed(ctx, orphan)).toEqual({ kind: 'reject' })
|
||||
expect(await proposed(ctx, fork)).toEqual({ kind: 'enter', messages: [] })
|
||||
archived.length = 0
|
||||
expect(await proposed(ctx, grandchild)).toEqual({ kind: 'enter', messages: [] })
|
||||
})
|
||||
|
||||
it('terminates on a damaged lineage that loops', async () => {
|
||||
const { ctx, archived } = await harness()
|
||||
const first = ctx.sessions.create(SessionId('gate-loop-a'), { meta: { parentSession: SessionId('gate-loop-b'), origin: 'subagent' } })
|
||||
const second = ctx.sessions.create(SessionId('gate-loop-b'), { meta: { parentSession: first.id, origin: 'subagent' } })
|
||||
const agent = await liveAgent(ctx, first)
|
||||
await liveAgent(ctx, second)
|
||||
expect(await proposed(ctx, agent)).toEqual({ kind: 'enter', messages: [] })
|
||||
archived.push(second.id)
|
||||
expect(await proposed(ctx, agent)).toEqual({ kind: 'reject' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Archived-session gate: lifetime', () => {
|
||||
it('stops rejecting once the plugin fiber is disposed', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
const archived: SessionId[] = []
|
||||
ctx.provide('workspaceRegistry', { get archivedSessionIds() { return archived } } as never)
|
||||
const agent = await liveAgent(ctx, ctx.sessions.create(SessionId('watched')))
|
||||
archived.push(agent.id)
|
||||
const fiber = await ctx.plugin(ArchivedSessionGate)
|
||||
expect(await proposed(ctx, agent)).toEqual({ kind: 'reject' })
|
||||
await fiber.dispose()
|
||||
expect(await proposed(ctx, agent)).toEqual({ kind: 'enter', messages: [] })
|
||||
})
|
||||
})
|
||||
@@ -33,7 +33,7 @@ async function composed(workspaces: readonly Workspace[] = []): Promise<Context>
|
||||
await ctx.plugin(SystemPrompt, { personaPrefix: '' })
|
||||
await ctx.plugin(AgentRegistry)
|
||||
installSessionReadTestServices(ctx)
|
||||
ctx.provide('workspaceRegistry', { list: () => workspaces } as never)
|
||||
ctx.provide('workspaceRegistry', { list: () => workspaces, archivedSessionIds: [] } as never)
|
||||
ctx.agents.setFactory({
|
||||
createAgent: async (ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle> => {
|
||||
const session = ctx.sessions.create(options.sessionId, {
|
||||
@@ -106,7 +106,7 @@ describe('sessions.fork', () => {
|
||||
textResponse('reply'), textResponse('checkpoint'), textResponse('continued'),
|
||||
])
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
ctx.provide('workspaceRegistry', { list: () => [] } as never)
|
||||
ctx.provide('workspaceRegistry', { list: () => [], archivedSessionIds: [] } as never)
|
||||
const source = await harness.create(sid('manual-compaction-source'), { provider: 'mock', model: 'mock' })
|
||||
const originalPrompt = 'older conversation history '.repeat(60)
|
||||
source.followup(message(originalPrompt))
|
||||
@@ -145,7 +145,7 @@ describe('sessions.fork', () => {
|
||||
const harness = await mountAgentLoopTestHarness(ctx)
|
||||
const adapter = new MockAdapter(Array.from({ length: 4 }, () => textResponse('reply')))
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
ctx.provide('workspaceRegistry', { list: () => [] } as never)
|
||||
ctx.provide('workspaceRegistry', { list: () => [], archivedSessionIds: [] } as never)
|
||||
const source = await harness.create(sid('source'), { provider: 'mock', model: 'mock' })
|
||||
source.followup(message('A'))
|
||||
await source.whenIdle()
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"src/list.ts",
|
||||
"src/media-references.ts",
|
||||
"src/model-selection-projection.ts",
|
||||
"src/archived-session-gate.ts",
|
||||
"src/skill-catalog.ts"
|
||||
],
|
||||
"references": [
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/api/workspace-controller/README.md
|
||||
README.md: 27053331007681eb373b4537514bbe701dad660d
|
||||
README.zh.md: d0726496de5dcf623fedb531ad988b3c608fc2ac
|
||||
README.md: b1ca97465625817c28615cbff1badb564ccc29fe
|
||||
README.zh.md: 7f7dbd49c0ef0ebbcb49dfeb6d736dc03e96565c
|
||||
@@ -22,9 +22,9 @@ English | [中文](README.zh.md)
|
||||
<a id="use-this-package"></a>
|
||||
## Use this package
|
||||
|
||||
The Host controller serializes mutations whose correctness depends on current registry state and throws `RemoteError` with a stable error code for expected failures. Its `follow()` stream synchronously attaches to durable Workspace changes, emits one complete baseline first, then emits ordered `upsert`, `remove`, `order`, `archived`, and `pinned` increments. Archive and pin sets are Session id arrays, with the most recently pinned id first in the pin array. A reconnect starts another generation with a replacement baseline, so consumers do not depend on receiving every increment while disconnected.
|
||||
The Host controller serializes mutations whose correctness depends on current registry state and throws `RemoteError` with a stable error code for expected failures. Its `follow()` stream synchronously attaches to durable Workspace changes, emits one complete baseline first, then emits ordered `upsert`, `remove`, `order`, `archived`, and `pinned` increments. Archive and pin sets are Session id arrays, with the most recently pinned id first in the pin array. A reconnect starts another generation with a replacement baseline, so consumers do not depend on receiving every increment while disconnected. `archiveSession` without `stopActivity` refuses a Session with running work as `workspace/session-active`, whose details list that work by family (`turn`, `subagent`, `job`, `schedule`) with item ids and labels; with `stopActivity: true` the registry's providers stop the work first and the response arrives once the archive set is durable, while the stops settle in the background.
|
||||
|
||||
The Client entry provides `ClientWorkspaceModel` and `createWorkspaceStateStream()`. The model owns Workspace rows, registry order, archived and pinned Session identities, unary mutation echoes, and stream/unary race resolution. A newer Host row wins by `updatedAt`; a committed stream order outranks an older unary response; a removed Workspace id cannot be resurrected by delayed data. Pin snapshots change only when their identities or order change. The package exposes framework-neutral snapshots and subscriptions, leaving navigation policy and React hooks to the UI owner.
|
||||
The Client entry provides `ClientWorkspaceModel` and `createWorkspaceStateStream()`. The model owns Workspace rows, registry order, archived and pinned Session identities, unary mutation echoes, and stream/unary race resolution. A newer Host row wins by `updatedAt`; a committed stream order outranks an older unary response; a removed Workspace id cannot be resurrected by delayed data. Pin snapshots change only when their identities or order change. The package exposes framework-neutral snapshots and subscriptions, leaving navigation policy and React hooks to the UI owner. `WorkspaceController.archiveSession(sessionId, { stopActivity })` throws `WorkspaceArchiveError` with the Host's `rpcError`, so a surface can tell the running-work refusal from a missing Session or a carrier fault and offer to stop the work.
|
||||
|
||||
<a id="first-use-workspace"></a>
|
||||
### First-use Workspace
|
||||
|
||||
@@ -22,9 +22,9 @@ kind: "package-reference"
|
||||
<a id="use-this-package"></a>
|
||||
## 使用本包
|
||||
|
||||
Host 控制器会串行执行正确性取决于当前注册表状态的变更,并为预期失败抛出带有稳定错误码的 `RemoteError`。它的 `follow()` 流会同步订阅持久 Workspace 变更,先发出一份完整 baseline,再按顺序发出 `upsert`、`remove`、`order`、`archived` 和 `pinned` 增量。归档与置顶集合都是会话 id 数组,置顶数组把最近置顶的 id 放在前面。重连会以替换 baseline 开始新一代,因此消费方不依赖收到断线期间的每个增量。
|
||||
Host 控制器会串行执行正确性取决于当前注册表状态的变更,并为预期失败抛出带有稳定错误码的 `RemoteError`。它的 `follow()` 流会同步订阅持久 Workspace 变更,先发出一份完整 baseline,再按顺序发出 `upsert`、`remove`、`order`、`archived` 和 `pinned` 增量。归档与置顶集合都是会话 id 数组,置顶数组把最近置顶的 id 放在前面。重连会以替换 baseline 开始新一代,因此消费方不依赖收到断线期间的每个增量。不带 `stopActivity` 的 `archiveSession` 会以 `workspace/session-active` 拒绝仍有工作在跑的会话,其 details 按族(`turn`、`subagent`、`job`、`schedule`)列出这些工作及各项的 id 与名称;带 `stopActivity: true` 时注册表的提供方先停止这些工作,归档集合持久化后即返回响应,停止在后台收敛。
|
||||
|
||||
Client 入口提供 `ClientWorkspaceModel` 和 `createWorkspaceStateStream()`。该模型拥有 Workspace 行、registry 顺序、归档与置顶会话身份、一元变更回显,以及流与一元调用的竞态处理。较新的 Host 行按 `updatedAt` 获胜;已提交的流顺序优先于较旧的一元响应;已经移除的 Workspace id 不会被延迟数据复活。置顶快照仅在会话身份或顺序变化时更新。该包公开与框架无关的快照和订阅,把导航策略与 React 钩子留给 UI owner。
|
||||
Client 入口提供 `ClientWorkspaceModel` 和 `createWorkspaceStateStream()`。该模型拥有 Workspace 行、registry 顺序、归档与置顶会话身份、一元变更回显,以及流与一元调用的竞态处理。较新的 Host 行按 `updatedAt` 获胜;已提交的流顺序优先于较旧的一元响应;已经移除的 Workspace id 不会被延迟数据复活。置顶快照仅在会话身份或顺序变化时更新。该包公开与框架无关的快照和订阅,把导航策略与 React 钩子留给 UI owner。`WorkspaceController.archiveSession(sessionId, { stopActivity })` 抛出携带 Host `rpcError` 的 `WorkspaceArchiveError`,界面因此能区分"仍有工作在跑"的拒绝与会话缺失或载体故障,并提议停止这些工作。
|
||||
|
||||
<a id="first-use-workspace"></a>
|
||||
### 首次使用工作区
|
||||
|
||||
@@ -15,9 +15,12 @@ export { ClientWorkspaceModel } from './model.ts'
|
||||
export type {
|
||||
WorkspaceFollowSink, WorkspaceListPhase, WorkspaceRemote, WorkspaceSnapshot,
|
||||
} from './model.ts'
|
||||
export { WorkspaceController, WorkspaceCreateError } from './service.ts'
|
||||
export { WorkspaceArchiveError, WorkspaceController, WorkspaceCreateError } from './service.ts'
|
||||
export type { IWorkspaces, WorkspaceSource } from './service.ts'
|
||||
export type { WorkspaceId, WorkspaceInitializeDefaultRequest, WorkspaceView } from '../types.ts'
|
||||
export type {
|
||||
SessionActivity, SessionActivityItem, SessionActivityKind, SessionActivityKindMap, WorkspaceId,
|
||||
WorkspaceInitializeDefaultRequest, WorkspaceView,
|
||||
} from '../types.ts'
|
||||
|
||||
type WorkspaceBaselineFrame = Extract<WorkspaceFollowFrame, { type: 'baseline' }>
|
||||
|
||||
|
||||
@@ -189,13 +189,18 @@ export class ClientWorkspaceModel implements WorkspaceFollowSink {
|
||||
* Archive one Session and install the returned complete archive set.
|
||||
* A reply superseded by a later archive request or a pushed set installs nothing.
|
||||
* @param sessionId - Session to archive.
|
||||
* @param options - Whether the Host stops the Session's running work instead of refusing.
|
||||
* @returns generated Remote result.
|
||||
*/
|
||||
async archiveSession(
|
||||
sessionId: WorkspaceArchiveSessionRequest['sessionId'],
|
||||
options: Pick<WorkspaceArchiveSessionRequest, 'stopActivity'> = {},
|
||||
): Promise<RemoteResult<WorkspaceArchiveValue>> {
|
||||
const requestSeq = ++this.archiveRequestSeq
|
||||
const result = await this.remote.archiveSession({ sessionId })
|
||||
const result = await this.remote.archiveSession({
|
||||
sessionId,
|
||||
...(options.stopActivity === true ? { stopActivity: true } : {}),
|
||||
})
|
||||
if (result.ok && requestSeq === this.archiveRequestSeq) {
|
||||
this.installArchived(result.value.archivedSessionIds)
|
||||
// The Host drops an archived session's pin in the same durable write;
|
||||
|
||||
@@ -17,6 +17,20 @@ export class WorkspaceCreateError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive failed on the Host. `rpcError.code` distinguishes the active-session
|
||||
* refusal (`workspace/session-active`, whose details name what still runs)
|
||||
* from a missing session or a carrier fault.
|
||||
*/
|
||||
export class WorkspaceArchiveError extends Error {
|
||||
override readonly name = 'WorkspaceArchiveError'
|
||||
|
||||
/** @param rpcError - Host business or folded carrier failure. */
|
||||
constructor(readonly rpcError: RemoteFailure) {
|
||||
super(`workspace session archive failed: ${rpcError.code}: ${rpcError.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/** Bare observable source for the Workspace Controller snapshot. */
|
||||
export interface WorkspaceSource {
|
||||
/** Read the identity-stable current snapshot. */
|
||||
@@ -67,8 +81,11 @@ export interface IWorkspaces {
|
||||
/**
|
||||
* Archive a Session from Workspace grouping surfaces.
|
||||
* @param sessionId - Session to archive.
|
||||
* @param options - `stopActivity` asks the Host to stop the Session's running work instead of refusing.
|
||||
* @throws {WorkspaceArchiveError} when the Host refuses; without `stopActivity` a Session with
|
||||
* running work fails as `workspace/session-active`, its details naming what runs.
|
||||
*/
|
||||
archiveSession(sessionId: SessionId): Promise<void>
|
||||
archiveSession(sessionId: SessionId, options?: { readonly stopActivity?: boolean }): Promise<void>
|
||||
/**
|
||||
* Unarchive a Session from the archived Session list.
|
||||
* @param sessionId - Session to unarchive.
|
||||
@@ -139,9 +156,9 @@ export class WorkspaceController extends Service implements IWorkspaces {
|
||||
if (!result.ok) throw commandError('reorder', result.error)
|
||||
}
|
||||
|
||||
async archiveSession(sessionId: SessionId): Promise<void> {
|
||||
const result = await this.model.archiveSession(sessionId)
|
||||
if (!result.ok) throw commandError('session archive', result.error)
|
||||
async archiveSession(sessionId: SessionId, options: { readonly stopActivity?: boolean } = {}): Promise<void> {
|
||||
const result = await this.model.archiveSession(sessionId, options)
|
||||
if (!result.ok) throw new WorkspaceArchiveError(result.error)
|
||||
}
|
||||
|
||||
async unarchiveSession(sessionId: SessionId): Promise<void> {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { Workspace } from '@deepseek-ai/dsh-workspace'
|
||||
import {
|
||||
WorkspaceActiveSessionError,
|
||||
WorkspaceArchivedSessionPinError,
|
||||
WorkspaceId,
|
||||
WorkspaceMoveInvalidError,
|
||||
@@ -151,16 +152,32 @@ export class WorkspaceCommands {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add one known Session to the registry-global archive set.
|
||||
* @param request - Session identity to archive.
|
||||
* Add one known Session to the registry-global archive set. Without
|
||||
* `stopActivity` a Session with running work is refused as
|
||||
* `workspace/session-active` with the activity the registry's providers
|
||||
* reported; with it, the providers stop that work first.
|
||||
* @param request - Session identity to archive and whether to stop its work.
|
||||
* @returns the complete resulting archive set.
|
||||
*/
|
||||
async archiveSession(request: WorkspaceArchiveSessionRequest): Promise<WorkspaceArchiveValue> {
|
||||
try {
|
||||
await this.ctx.workspaceRegistry.archiveSession(request.sessionId)
|
||||
await this.ctx.workspaceRegistry.archiveSession(
|
||||
request.sessionId,
|
||||
request.stopActivity === true ? { stopActivity: true } : {},
|
||||
)
|
||||
} catch (error) {
|
||||
if (!(error instanceof WorkspaceUnknownSessionError)) throw error
|
||||
throw new RemoteError('session/not-found', error.message, { sessionId: request.sessionId }, { cause: error })
|
||||
if (error instanceof WorkspaceUnknownSessionError) {
|
||||
throw new RemoteError('session/not-found', error.message, { sessionId: request.sessionId }, { cause: error })
|
||||
}
|
||||
if (error instanceof WorkspaceActiveSessionError) {
|
||||
throw new RemoteError(
|
||||
'workspace/session-active',
|
||||
error.message,
|
||||
{ sessionId: request.sessionId, activity: error.activity },
|
||||
{ cause: error },
|
||||
)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
return { archivedSessionIds: [...this.ctx.workspaceRegistry.archivedSessionIds] }
|
||||
}
|
||||
|
||||
@@ -6,9 +6,12 @@
|
||||
*/
|
||||
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
|
||||
import type { SessionActivity, WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
|
||||
|
||||
export type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
|
||||
export type {
|
||||
SessionActivity, SessionActivityItem, SessionActivityKind, SessionActivityKindMap,
|
||||
} from '@deepseek-ai/dsh-workspace/types'
|
||||
export type { DirectoryEntry, DirectoryListing } from '@deepseek-ai/dsh-host-directory-picker/types'
|
||||
|
||||
/** One durable Workspace projected for browser consumers. */
|
||||
@@ -32,6 +35,15 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
|
||||
'workspace/invalid-path': { readonly path: string }
|
||||
/** Another Workspace already uses the requested name. */
|
||||
'workspace/name-conflict': { readonly name: string }
|
||||
/**
|
||||
* The Session still has running work — its own turn, a subagent, a
|
||||
* background job, or an active schedule — so archiving was refused
|
||||
* without a write; `activity` names what must stop first.
|
||||
*/
|
||||
'workspace/session-active': {
|
||||
readonly sessionId: SessionId
|
||||
readonly activity: readonly SessionActivity[]
|
||||
}
|
||||
/** The Session or its anchor is not in the Workspace's manual order. */
|
||||
'workspace/move-invalid': {
|
||||
readonly workspaceId: WorkspaceId
|
||||
@@ -110,6 +122,14 @@ export interface WorkspaceInsertSessionBeforeRequest {
|
||||
/** Session requested for archival from Workspace grouping surfaces. */
|
||||
export interface WorkspaceArchiveSessionRequest {
|
||||
readonly sessionId: SessionId
|
||||
/**
|
||||
* Stop the Session's running work — its turn, subagent descendants, owned
|
||||
* background jobs, and active schedules — instead of refusing the archive
|
||||
* as `workspace/session-active`. The stops are requested before the
|
||||
* archive write and are not awaited; the response arrives once the archive
|
||||
* set is durable.
|
||||
*/
|
||||
readonly stopActivity?: boolean
|
||||
}
|
||||
|
||||
/** Session requested for restoration from the archived Session list. */
|
||||
|
||||
@@ -13,6 +13,7 @@ import { createClientTest, type TestClient, webApp } from '@deepseek-ai/dsh-clie
|
||||
import {
|
||||
ClientWorkspaceModel,
|
||||
createWorkspaceStateStream,
|
||||
WorkspaceArchiveError,
|
||||
WorkspaceController,
|
||||
WorkspaceCreateError,
|
||||
type WorkspaceFollowSink,
|
||||
@@ -348,7 +349,8 @@ describe('WorkspaceController', () => {
|
||||
expect(mock.log.requests('workspace/rename')).toEqual([{ workspaceId: 'one', title: 'renamed' }])
|
||||
expect(mock.log.requests('workspace/insertBefore')).toEqual([{ workspaceId: 'one' }])
|
||||
expect(mock.log.requests('workspace/insertSessionBefore')).toEqual([{ workspaceId: 'one', sessionId: 'session' }])
|
||||
expect(mock.log.requests('workspace/archiveSession')).toEqual([{ sessionId: 'session' }])
|
||||
await expect(controller.archiveSession(sid('session'), { stopActivity: true })).resolves.toBeUndefined()
|
||||
expect(mock.log.requests('workspace/archiveSession')).toEqual([{ sessionId: 'session' }, { sessionId: 'session', stopActivity: true }])
|
||||
expect(mock.log.requests('workspace/unarchiveSession')).toEqual([{ sessionId: 'session' }])
|
||||
expect(mock.log.requests('workspace/pinSession')).toEqual([{ sessionId: 'session' }])
|
||||
expect(mock.log.requests('workspace/unpinSession')).toEqual([{ sessionId: 'session' }])
|
||||
@@ -376,8 +378,19 @@ describe('WorkspaceController', () => {
|
||||
mock.remote.workspace.insertBefore.mockResolvedValueOnce(err(missingWorkspace))
|
||||
await expect(controller.insertBefore(wid('missing'))).rejects.toThrow('workspace reorder failed: workspace/not-found: gone')
|
||||
mock.remote.workspace.archiveSession.mockResolvedValueOnce(err(missingSession))
|
||||
await expect(controller.archiveSession(sid('session')))
|
||||
.rejects.toThrow('workspace session archive failed: session/not-found: missing session')
|
||||
const archiveMissing = controller.archiveSession(sid('session'))
|
||||
await expect(archiveMissing).rejects.toBeInstanceOf(WorkspaceArchiveError)
|
||||
await expect(archiveMissing).rejects.toThrow('workspace session archive failed: session/not-found: missing session')
|
||||
// The active-session refusal keeps its structured details so a surface
|
||||
// can name what still runs.
|
||||
const active = new RemoteError('workspace/session-active', 'session is active', {
|
||||
sessionId: sid('session'), activity: [{ kind: 'probe' }, { kind: 'probe-items', items: [{ id: 'item-1' }] }],
|
||||
})
|
||||
mock.remote.workspace.archiveSession.mockResolvedValueOnce(err(active))
|
||||
await expect(controller.archiveSession(sid('session'))).rejects.toMatchObject({
|
||||
name: 'WorkspaceArchiveError',
|
||||
rpcError: { code: 'workspace/session-active', details: { activity: [{ kind: 'probe' }, { kind: 'probe-items', items: [{ id: 'item-1' }] }] } },
|
||||
})
|
||||
mock.remote.workspace.unarchiveSession.mockResolvedValueOnce(err(missingSession))
|
||||
await expect(controller.unarchiveSession(sid('session')))
|
||||
.rejects.toThrow('workspace session unarchive failed: session/not-found: missing session')
|
||||
@@ -407,3 +420,11 @@ describe('WorkspaceController', () => {
|
||||
expect(mock.log.calls('workspace/create').map(call => call.state)).toEqual(['failed'])
|
||||
})
|
||||
})
|
||||
|
||||
// The wire relays whatever families the Host's providers report; this suite merges its own.
|
||||
declare module '@deepseek-ai/dsh-workspace/types' {
|
||||
interface SessionActivityKindMap {
|
||||
probe: true
|
||||
'probe-items': true
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,14 @@ import { WorkspaceFeed } from '../src/feed.ts'
|
||||
import type { WorkspaceFollowFrame } from '../src/types.ts'
|
||||
import { MemoryStorageBackend } from '../../../storage/storage-domain/tests/helpers/memory-backend.ts'
|
||||
|
||||
// The controller relays whatever families the providers report; this suite merges its own.
|
||||
declare module '@deepseek-ai/dsh-workspace/types' {
|
||||
interface SessionActivityKindMap {
|
||||
probe: true
|
||||
'probe-items': true
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/dsh-typert-protocol' {
|
||||
interface RemoteErrorDetailsMap {
|
||||
'fixture/failure': {}
|
||||
@@ -232,6 +240,28 @@ describe('WorkspaceController commands', () => {
|
||||
sessionId: session.id,
|
||||
})).rejects.toMatchObject({ code: 'workspace/not-found' })
|
||||
|
||||
// A session reported active by the registry's activity waterfall is a
|
||||
// stable business failure carrying what still runs, and nothing is written.
|
||||
const activity = [{ kind: 'probe' as const }, { kind: 'probe-items' as const, items: [{ id: 'item-1', label: 'build' }] }]
|
||||
const stopReporting = ctx.on('workspace/session-activity', async ({ sessionId }, next) =>
|
||||
sessionId === session.id ? [...activity, ...(await next())] : next())
|
||||
await expect(controller.archiveSession({ sessionId: session.id })).rejects.toMatchObject({
|
||||
code: 'workspace/session-active',
|
||||
details: { sessionId: session.id, activity },
|
||||
})
|
||||
expect([...ctx.workspaceRegistry.archivedSessionIds]).toEqual([])
|
||||
// Asking to stop the work archives the still-active Session and reaches
|
||||
// the stop providers first.
|
||||
const stops: string[] = []
|
||||
const stopListening = ctx.on('workspace/session-stop', ({ sessionId }) => { stops.push(String(sessionId)) })
|
||||
await expect(controller.archiveSession({ sessionId: session.id, stopActivity: true }))
|
||||
.resolves.toEqual({ archivedSessionIds: [session.id] })
|
||||
expect(stops).toEqual([String(session.id)])
|
||||
stopListening()
|
||||
await expect(controller.unarchiveSession({ sessionId: session.id }))
|
||||
.resolves.toEqual({ archivedSessionIds: [] })
|
||||
stopReporting()
|
||||
|
||||
await expect(controller.archiveSession({ sessionId: session.id }))
|
||||
.resolves.toEqual({ archivedSessionIds: [session.id] })
|
||||
await expect(controller.archiveSession({ sessionId: SessionId('unknown') }))
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-workspace/README.md
|
||||
README.md: 3fd9308f0f47a561a08f831f7d923aad2863a25b
|
||||
README.zh.md: 97bcac6456bb68606a4747984c24f5f618fa9bb4
|
||||
README.md: 7dcff13bc1767051c301174240979873444d4291
|
||||
README.zh.md: 1b3e83c71f700976799ff5b93b6fe4c60f896583
|
||||
@@ -49,7 +49,7 @@ Collapsed search is one header action beside the view and add actions: activatin
|
||||
|
||||
The Session row's Rename action opens a dialog prefilled with the row's display title; confirming an unchanged title is deliberately allowed — it pins the current automatic title against regeneration. Double-clicking a title also opens Rename; for an unarchived Session, the preceding clicks open its conversation. Rename uses a temporary `workspaceOperation` reference and awaits its initial history opening. The row's Fork action forks at the source's last completed turn and increments the inherited persisted title through Session Controller without retaining the child, opening its history, or changing selection. Workspace Delete opens a confirmation that states the retention boundary; success removes the group while its Sessions remain under Ungrouped. Pin, Rename, Fork, and Archive are themselves entries of the `sidebar.workspaces.session.menu.item` list (pin and archive also of `sidebar.workspaces.session.row.action`), so a client plugin's action takes whatever position its `order` gives it.
|
||||
|
||||
Archive commits without a confirmation dialog and retains the Session's account position. View options control visibility: the default hides archived Sessions, Show archived includes them, and Archived only hides ordinary Sessions. Visible archived rows are grayed and carry an accessible explanation that they cannot be opened until restored; Rename, Fork, and Unarchive remain available. A successful archive shows a notice with an undo action and a "show archived" action that switches the filter directly. Unarchive removes the archive mark without restoring a pin or changing the saved position.
|
||||
Archive commits without a confirmation dialog for a quiet Session and retains the Session's account position. A Session with running work is the one case that asks first: the Host refuses the plain archive and names that work, and the sidebar opens a stop-and-archive dialog listing it by family — the turn in progress, running subagents, background jobs, scheduled reminders, each with its labels — with the restore path stated; confirming asks the Host to stop the work the way the stop button does and archives once the archive set is durable, while the stops settle in the background, and cancelling leaves the Session running and visible. View options control visibility: the default hides archived Sessions, Show archived includes them, and Archived only hides ordinary Sessions. Visible archived rows are grayed and carry an accessible explanation that they cannot be opened until restored; Rename, Fork, and Unarchive remain available. A successful archive shows a notice with an undo action and a "show archived" action that switches the filter directly; a stop-and-archive shows the same notice under its own wording, and undo restores the Session without resuming the stopped work. Unarchive removes the archive mark without restoring a pin or changing the saved position.
|
||||
|
||||
A Session title wider than its row is clipped with an ellipsis at rest. Hovering the row scrolls the title to its far edge — the incremented title of a fork, for example — and reveals it without the ellipsis; leaving the row returns the title to its start.
|
||||
|
||||
@@ -91,7 +91,7 @@ Each registration declares a **directory-flow child hole** (`single` kind: `conv
|
||||
|
||||
The Session row's "..." menu and its hover buttons are two `list` slots declared by the WorkspaceBrowser entry: `sidebar.workspaces.session.menu.item` and `sidebar.workspaces.session.row.action`. Every menu row and every hover button is an entry, this package's own actions included: `apply` registers `pin` (menu 100, button 200), `rename` (200), `fork` (300), and `archive` (menu 400, button 100) the way a client plugin registers its actions, so a plugin action lands wherever its `order` says, and reusing a shipped id at another `priority` shadows that action.
|
||||
|
||||
An entry receives only the row identity (`sessionId`, `displayTitle`) and owns everything else. It reads the Host state it cares about through its own injected hooks (the pin and archive sets, as Sets derived once per Workspace snapshot), decides its own visibility (pin renders nothing on an archived row, because the Host keeps the two sets exclusive), carries its whole behavior in its registration's `inject` face (a pin also fronts the Session in its saved orders; an archive raises the notice), and raises its own surfaces: the rename dialog and the row-action notice are this package's `shell.overlay` entries, fed by the requests the actions inject. A menu entry renders one `role="menuitem"` button (this package's rows use `MenuItemButton` from ui-primitives, which adds the host styling and `separatorBefore` for a group start; the hairline comes and goes with the row) and dismisses the menu through the slot-level `useMenuOpenState` hook, the menu's own open state bound from the row's render occurrence; a hover button entry renders one icon button, and the strip keeps its clicks from opening the row. The browser passes no action callbacks to its rows; its remaining verbs are the search results' restore button and the title double-click, which raises the same rename request.
|
||||
An entry receives only the row identity (`sessionId`, `displayTitle`) and owns everything else. It reads the Host state it cares about through its own injected hooks (the pin and archive sets, as Sets derived once per Workspace snapshot), decides its own visibility (pin renders nothing on an archived row, because the Host keeps the two sets exclusive), carries its whole behavior in its registration's `inject` face (a pin also fronts the Session in its saved orders; an archive raises the notice), and raises its own surfaces: the rename dialog, the stop-and-archive dialog, and the row-action notice are this package's `shell.overlay` entries, fed by the requests the actions inject. A menu entry renders one `role="menuitem"` button (this package's rows use `MenuItemButton` from ui-primitives, which adds the host styling and `separatorBefore` for a group start; the hairline comes and goes with the row) and dismisses the menu through the slot-level `useMenuOpenState` hook, the menu's own open state bound from the row's render occurrence; a hover button entry renders one icon button, and the strip keeps its clicks from opening the row. The browser passes no action callbacks to its rows; its remaining verbs are the search results' restore button and the title double-click, which raises the same rename request.
|
||||
|
||||
#### Packaged client plugin
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ kind: "package-reference"
|
||||
|
||||
Session 行内的 Rename 操作打开一个以该行显示标题预填的对话框;确认未修改的标题是有意允许的——这正是把当前自动标题钉住、不再被重新生成覆盖的手势。双击标题也会打开 Rename;对于未归档 Session,先发生的点击会打开其对话。Rename 使用临时 `workspaceOperation` reference,并等待首次历史打开。行内 Fork 在源会话最后一个已完成轮次处 fork,通过 Session Controller 递增继承的持久化标题,不 retain 子会话、不打开其历史,也不改变选择。Workspace 行内的 Delete 操作会打开确认框,说明保留边界;成功后该分组被移除,其 Session 则留在 Ungrouped 下。Pin、Rename、Fork、Archive 本身就是 `sidebar.workspaces.session.menu.item` 列表的条目(pin 与 archive 同时也是 `sidebar.workspaces.session.row.action` 的条目),因此客户端插件的 action 由其 `order` 决定落在哪个位置。
|
||||
|
||||
Archive 不经确认对话框直接提交,并保留 Session 的记账位置。视图选项控制显隐:默认隐藏已归档 Session,显示已归档会将其纳入列表,仅显示已归档则隐藏普通 Session。可见的归档行置灰,并提供无障碍说明,告知取消归档后才能打开;Rename、Fork 与取消归档仍然可用。归档成功后的提示提供"撤销"和"筛选已归档会话"两个动作,后者直接把筛选切到显示已归档。取消归档移除归档标记,但不恢复置顶,也不改变保存的位置。
|
||||
对静止的 Session,Archive 不经确认对话框直接提交,并保留 Session 的记账位置。仍有工作在跑的 Session 是唯一会先询问的情形:Host 拒绝普通归档并列出这些工作,侧栏随即打开"停止并归档"对话框,按族列出——进行中的回合、运行中的子代理、后台任务、定时提醒,各带名称——并写明恢复路径;确认后请 Host 按停止按钮同样的方式停止这些工作,归档集合持久化后即完成归档,停止在后台收敛;取消则让 Session 继续运行并保持可见。视图选项控制显隐:默认隐藏已归档 Session,显示已归档会将其纳入列表,仅显示已归档则隐藏普通 Session。可见的归档行置灰,并提供无障碍说明,告知取消归档后才能打开;Rename、Fork 与取消归档仍然可用。归档成功后的提示提供"撤销"和"筛选已归档会话"两个动作,后者直接把筛选切到显示已归档;停止并归档显示同样的提示但措辞不同,撤销只恢复 Session,不会让被停止的工作继续。取消归档移除归档标记,但不恢复置顶,也不改变保存的位置。
|
||||
|
||||
标题宽于所在行时,静止状态以省略号裁切。把指针停在行上,标题会滚动到远端——例如 fork 递增后的标题——并在揭示时不显示省略号;指针离开后标题回到开头。
|
||||
|
||||
@@ -91,7 +91,7 @@ Client 在启动时按其语言选择初始目录名和标题:中文使用 `
|
||||
|
||||
Session 行的 "..." 菜单和行尾悬停按钮是 WorkspaceBrowser 注册项声明的两个 `list` slot:`sidebar.workspaces.session.menu.item` 与 `sidebar.workspaces.session.row.action`。每一个菜单行、每一个悬停按钮都是条目,本包自己的 action 也不例外:`apply` 以客户端插件注册自己 action 的同一方式注册 `pin`(菜单 100、按钮 200)、`rename`(200)、`fork`(300)、`archive`(菜单 400、按钮 100),因此插件 action 落在其 `order` 所指的位置,以另一个 `priority` 复用内置 id 则遮蔽该 action。
|
||||
|
||||
条目只接收行身份(`sessionId`、`displayTitle`),其余一切自己负责:用自己注入的 hook 读自己关心的 Host 状态(置顶与归档集合,以每次 Workspace 快照只派生一次的 Set 形式),自己决定是否显示(Host 规定归档与置顶互斥,所以 pin 在归档行上不渲染),整套行为放在注册项自己的 `inject` face 里(置顶成功后顺带把会话推到保存顺序最前,归档成功后发提示),浮层也自己带——重命名对话框和行 action 的提示是本包注册在 `shell.overlay` 的条目,由 action 注入的请求驱动。菜单条目渲染一个 `role="menuitem"` 的按钮(本包自己的行用 ui-primitives 的 `MenuItemButton`,它带宿主样式,开启新分组的行加 `separatorBefore`,分隔线随行一起出现和消失),并通过 slot 级 `useMenuOpenState` hook(菜单自身的打开状态,从该行的渲染出现处绑定)关闭菜单;悬停按钮条目渲染一个图标按钮,按钮条会拦住点击、不让它打开该行。browser 不再向行传任何 action 回调,它剩下的动作只有搜索结果里的恢复按钮和标题双击,后者发出的是同一个重命名请求。
|
||||
条目只接收行身份(`sessionId`、`displayTitle`),其余一切自己负责:用自己注入的 hook 读自己关心的 Host 状态(置顶与归档集合,以每次 Workspace 快照只派生一次的 Set 形式),自己决定是否显示(Host 规定归档与置顶互斥,所以 pin 在归档行上不渲染),整套行为放在注册项自己的 `inject` face 里(置顶成功后顺带把会话推到保存顺序最前,归档成功后发提示),浮层也自己带——重命名对话框、停止并归档对话框和行 action 的提示是本包注册在 `shell.overlay` 的条目,由 action 注入的请求驱动。菜单条目渲染一个 `role="menuitem"` 的按钮(本包自己的行用 ui-primitives 的 `MenuItemButton`,它带宿主样式,开启新分组的行加 `separatorBefore`,分隔线随行一起出现和消失),并通过 slot 级 `useMenuOpenState` hook(菜单自身的打开状态,从该行的渲染出现处绑定)关闭菜单;悬停按钮条目渲染一个图标按钮,按钮条会拦住点击、不让它打开该行。browser 不再向行传任何 action 回调,它剩下的动作只有搜索结果里的恢复按钮和标题双击,后者发出的是同一个重命名请求。
|
||||
|
||||
#### 打包客户端插件
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-remotes": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-session-controller": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-workspace-controller": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
@@ -66,6 +67,7 @@
|
||||
"@deepseek-ai/dsh-client-ui-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-sidebar": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-jobs": "workspace:^",
|
||||
"@deepseek-ai/dsh-schedule": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:^",
|
||||
|
||||
@@ -42,7 +42,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
|
||||
import type { SessionSearchResultItem } from '@deepseek-ai/dsh-api-session-controller/client'
|
||||
import type { RemoteHostFacts } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { WorkspaceId, WorkspaceView } from '@deepseek-ai/dsh-api-workspace-controller/client'
|
||||
import type { SessionActivity, WorkspaceId, WorkspaceView } from '@deepseek-ai/dsh-api-workspace-controller/client'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type { createWorkspaceViewStore } from '../stores.ts'
|
||||
|
||||
@@ -241,6 +241,7 @@ export type SessionRowActionProps<Injected extends object = object> =
|
||||
/** One transient Workspace notice rendered by the overlay toast entry. */
|
||||
export type RowToast =
|
||||
| { kind: 'archived'; sessionId: SessionId }
|
||||
| { kind: 'stoppedAndArchived'; sessionId: SessionId }
|
||||
| { kind: 'pinFailed' }
|
||||
| { kind: 'unpinFailed' }
|
||||
| { kind: 'archivedNotOpenable' }
|
||||
@@ -276,8 +277,9 @@ export interface PinSessionInjected {
|
||||
|
||||
/**
|
||||
* Archive action share (menu row and hover button). The callbacks carry the
|
||||
* whole behavior: the Host call, the notice a success raises, and the
|
||||
* diagnostics for a rejection.
|
||||
* whole behavior: the Host call, the notice a success raises, the
|
||||
* stop-and-archive confirmation a Host refusal for running work raises, and
|
||||
* the diagnostics for any other rejection.
|
||||
*/
|
||||
export interface ArchiveSessionInjected {
|
||||
hooks: {
|
||||
@@ -288,12 +290,46 @@ export interface ArchiveSessionInjected {
|
||||
* Archive a Session into the registry-global set: the row keeps its
|
||||
* account position and shows per the archived filter; archiving the
|
||||
* current session clears the selection into the New Session view state.
|
||||
* A Session with running work is not archived by this call: the Host's
|
||||
* refusal opens the stop-and-archive confirmation instead.
|
||||
*/
|
||||
archiveSession: (sessionId: SessionId) => void
|
||||
/** Remove a Session from the registry-global archived set. */
|
||||
unarchiveSession: (sessionId: SessionId) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* A stop-and-archive confirmation the archive action asked for: the Host
|
||||
* refused the plain archive because this work still runs.
|
||||
*/
|
||||
export interface SessionArchiveConfirmRequest {
|
||||
/** Session to stop and archive. */
|
||||
sessionId: SessionId
|
||||
/** The row's display title, named in the dialog. */
|
||||
displayTitle: string
|
||||
/** What the Host reported running, in family order. */
|
||||
activity: readonly SessionActivity[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop-and-archive dialog share: the pending confirmation, its settlement,
|
||||
* and the archive hop that asks the Host to stop the work first.
|
||||
*/
|
||||
export interface SessionArchiveConfirmInjected {
|
||||
hooks: {
|
||||
/** The confirmation asked for, until the dialog consumes or cancels it. */
|
||||
archiveRequest: HostObservable<SessionArchiveConfirmRequest | null>
|
||||
}
|
||||
/** Consume or cancel the pending confirmation. */
|
||||
settleSessionArchive: () => void
|
||||
/**
|
||||
* Archive a Session after the Host stops its running work; resolves once
|
||||
* the archive set is durable (the stops settle in the background) and
|
||||
* raises the stopped-and-archived notice.
|
||||
*/
|
||||
stopAndArchiveSession: (sessionId: SessionId) => Promise<void>
|
||||
}
|
||||
|
||||
/** Fork action share. */
|
||||
export interface ForkSessionInjected {
|
||||
/** Fork a Session at its last completed turn; the child arrives through the Host list. */
|
||||
@@ -347,6 +383,13 @@ export type SessionRenameDialogProps =
|
||||
& Omit<SessionRenameDialogInjected, 'hooks'>
|
||||
& PropsHooks<SessionRenameDialogInjected['hooks']>
|
||||
|
||||
/** Props of the stop-and-archive dialog entry in `shell.overlay`. */
|
||||
export type SessionArchiveConfirmProps =
|
||||
PropsRuntime<'shell.overlay'>
|
||||
& PropsLocale<'workspace'>
|
||||
& Omit<SessionArchiveConfirmInjected, 'hooks'>
|
||||
& PropsHooks<SessionArchiveConfirmInjected['hooks']>
|
||||
|
||||
/** Props of the row toast entry in `shell.overlay`. */
|
||||
export type RowToastProps =
|
||||
PropsRuntime<'shell.overlay'>
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { RemoteHostFacts } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client'
|
||||
import type { IWorkspaces, WorkspaceSnapshot } from '@deepseek-ai/dsh-api-workspace-controller/client'
|
||||
import type {
|
||||
IWorkspaces, SessionActivity, WorkspaceArchiveError, WorkspaceSnapshot,
|
||||
} from '@deepseek-ai/dsh-api-workspace-controller/client'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
import type { HostObservable, SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
@@ -31,13 +33,14 @@ import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-session/client'
|
||||
import {
|
||||
type ArchiveSessionInjected, type ForkSessionInjected, menuOpenStateFactory, type PinSessionInjected,
|
||||
type SessionArchiveConfirmInjected, type SessionArchiveConfirmRequest,
|
||||
type RenameSessionInjected, type RowToast, type RowToastInjected, type RowToastState, type SessionRenameDialogInjected,
|
||||
type SessionRenameTarget, type WorkspaceBrowserInjected, type WorkspacePickerInjected,
|
||||
} from './contract/slots.ts'
|
||||
import { UiWorkspaceService } from './navigation.ts'
|
||||
import { createWorkspaceViewStore } from './stores.ts'
|
||||
import { WorkspaceBrowser } from './rows/WorkspaceBrowser.tsx'
|
||||
import { ArchiveSessionMenuItem, ArchiveSessionRowButton } from './session-actions/ArchiveSession.tsx'
|
||||
import { ArchiveSessionMenuItem, ArchiveSessionRowButton, SessionArchiveConfirmDialog } from './session-actions/ArchiveSession.tsx'
|
||||
import { derive } from './session-actions/derived.ts'
|
||||
import { ForkSessionMenuItem } from './session-actions/ForkSession.tsx'
|
||||
import { PinSessionMenuItem, PinSessionRowButton } from './session-actions/PinSession.tsx'
|
||||
@@ -141,6 +144,7 @@ export function apply(ctx: Context): void {
|
||||
// writes through its own injected callback and the surface reads through
|
||||
// its bound hook.
|
||||
const renameRequest = createSnapshotStore<SessionRenameTarget | null>(null)
|
||||
const archiveRequest = createSnapshotStore<SessionArchiveConfirmRequest | null>(null)
|
||||
const requestSessionRename = (sessionId: SessionId, currentTitle: string): void => {
|
||||
renameRequest.set({ sessionId, currentTitle })
|
||||
}
|
||||
@@ -170,17 +174,33 @@ export function apply(ctx: Context): void {
|
||||
})
|
||||
const archiveInjected = (): ArchiveSessionInjected => ({
|
||||
hooks: { archived: archivedSet },
|
||||
// Archive preserves the log and the account position, so it needs no
|
||||
// confirmation; the notice offers undo and the archived filter.
|
||||
// Archive preserves the log and the account position, so a quiet Session
|
||||
// needs no confirmation; the notice offers undo and the archived filter.
|
||||
// The Host's refusal for running work is the one case that asks first:
|
||||
// the confirmation names that work and offers to stop it.
|
||||
archiveSession: (sessionId) => {
|
||||
uiWorkspace.archiveSession(sessionId).then(() => {
|
||||
notify({ kind: 'archived', sessionId })
|
||||
}).catch((reason: unknown) => {
|
||||
console.warn('session archive rejected:', reason)
|
||||
const activity = activeSessionRefusal(reason)
|
||||
if (activity === undefined) {
|
||||
console.warn('session archive rejected:', reason)
|
||||
return
|
||||
}
|
||||
const displayTitle = sessions.list.getSnapshot().byId[sessionId]?.displayTitle ?? sessionId
|
||||
archiveRequest.set({ sessionId, displayTitle, activity })
|
||||
})
|
||||
},
|
||||
unarchiveSession,
|
||||
})
|
||||
const archiveConfirmInjected = (): SessionArchiveConfirmInjected => ({
|
||||
hooks: { archiveRequest },
|
||||
settleSessionArchive: () => { archiveRequest.set(null) },
|
||||
stopAndArchiveSession: async (sessionId) => {
|
||||
await uiWorkspace.archiveSession(sessionId, { stopActivity: true })
|
||||
notify({ kind: 'stoppedAndArchived', sessionId })
|
||||
},
|
||||
})
|
||||
const forkInjected = (): ForkSessionInjected => ({
|
||||
forkSession: (sessionId) => {
|
||||
uiWorkspace.forkSession(sessionId).catch(() => {
|
||||
@@ -263,6 +283,9 @@ export function apply(ctx: Context): void {
|
||||
yield ctx.slots.register({
|
||||
name: 'shell.overlay', id: 'workspace.session-rename', locale: NS, inject: renameDialogInjected,
|
||||
}, SessionRenameDialog)
|
||||
yield ctx.slots.register({
|
||||
name: 'shell.overlay', id: 'workspace.session-archive', locale: NS, inject: archiveConfirmInjected,
|
||||
}, SessionArchiveConfirmDialog)
|
||||
yield ctx.slots.register({
|
||||
name: 'shell.overlay', id: 'workspace.row-toast', locale: NS, inject: rowToastInjected,
|
||||
}, RowActionToast)
|
||||
@@ -277,3 +300,14 @@ export function apply(ctx: Context): void {
|
||||
WorkspacePicker,
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* The activity a Host `workspace/session-active` refusal reported, or nothing
|
||||
* for any other failure. The class identity check goes by name: client plugin
|
||||
* bundles do not share error-class identity.
|
||||
*/
|
||||
function activeSessionRefusal(reason: unknown): readonly SessionActivity[] | undefined {
|
||||
if (!(reason instanceof Error) || reason.name !== 'WorkspaceArchiveError') return undefined
|
||||
const { rpcError } = reason as WorkspaceArchiveError
|
||||
return rpcError.code === 'workspace/session-active' ? rpcError.details.activity : undefined
|
||||
}
|
||||
@@ -57,6 +57,22 @@ export const zh = {
|
||||
'row.pinned': '已置顶',
|
||||
'toast.archivedNotOpenable': '已归档对话暂时无法查看,请取消归档后查看',
|
||||
'toast.archived': '会话已归档,可',
|
||||
'toast.stoppedAndArchived': '已停止并归档,可',
|
||||
'archive.confirm.title': '停止并归档此会话?',
|
||||
'archive.confirm.desc': '“{title}”仍有正在进行的工作。归档会先停止这些工作;之后可在侧栏“显示已归档”中恢复会话,被停止的工作不会自动继续。',
|
||||
'archive.confirm.activity': '将被停止的工作',
|
||||
'archive.confirm.turn': '进行中的回合',
|
||||
'archive.confirm.subagents.one': '{n} 个运行中的子代理:{names}',
|
||||
'archive.confirm.subagents.other': '{n} 个运行中的子代理:{names}',
|
||||
'archive.confirm.jobs.one': '{n} 个后台任务:{names}',
|
||||
'archive.confirm.jobs.other': '{n} 个后台任务:{names}',
|
||||
'archive.confirm.schedules.one': '{n} 条定时提醒:{names}',
|
||||
'archive.confirm.schedules.other': '{n} 条定时提醒:{names}',
|
||||
'archive.confirm.other.one': '{n} 项其他工作({kind})',
|
||||
'archive.confirm.other.other': '{n} 项其他工作({kind})',
|
||||
'archive.confirm.listSeparator': '、',
|
||||
'archive.confirm.action': '停止并归档',
|
||||
'archive.confirm.pending': '正在停止并归档…',
|
||||
'toast.archivedUndo': '撤销',
|
||||
'toast.archivedOr': '或',
|
||||
'toast.archivedFilter': '筛选已归档会话',
|
||||
@@ -153,6 +169,22 @@ export const en = {
|
||||
'row.pinned': 'Pinned',
|
||||
'toast.archivedNotOpenable': 'Archived sessions cannot be opened. Unarchive it to view.',
|
||||
'toast.archived': 'Session archived. You can ',
|
||||
'toast.stoppedAndArchived': 'Session stopped and archived. You can ',
|
||||
'archive.confirm.title': 'Stop and archive this session?',
|
||||
'archive.confirm.desc': '“{title}” still has work in progress. Archiving stops it first; you can restore the session later from “Show archived” in the sidebar, and the stopped work will not resume on its own.',
|
||||
'archive.confirm.activity': 'Work that will be stopped',
|
||||
'archive.confirm.turn': 'The turn in progress',
|
||||
'archive.confirm.subagents.one': '{n} running subagent: {names}',
|
||||
'archive.confirm.subagents.other': '{n} running subagents: {names}',
|
||||
'archive.confirm.jobs.one': '{n} background job: {names}',
|
||||
'archive.confirm.jobs.other': '{n} background jobs: {names}',
|
||||
'archive.confirm.schedules.one': '{n} scheduled reminder: {names}',
|
||||
'archive.confirm.schedules.other': '{n} scheduled reminders: {names}',
|
||||
'archive.confirm.other.one': '{n} other item of work ({kind})',
|
||||
'archive.confirm.other.other': '{n} other items of work ({kind})',
|
||||
'archive.confirm.listSeparator': ', ',
|
||||
'archive.confirm.action': 'Stop and archive',
|
||||
'archive.confirm.pending': 'Stopping and archiving…',
|
||||
'toast.archivedUndo': 'undo',
|
||||
'toast.archivedOr': ' or ',
|
||||
'toast.archivedFilter': 'filter archived sessions',
|
||||
|
||||
@@ -64,8 +64,9 @@ export interface UiWorkspace {
|
||||
/**
|
||||
* Archive a Session and clear it when it is the current selection.
|
||||
* @param sessionId - Session to archive.
|
||||
* @param options - `stopActivity` asks the Host to stop the Session's running work instead of refusing.
|
||||
*/
|
||||
archiveSession(sessionId: SessionId): Promise<void>
|
||||
archiveSession(sessionId: SessionId, options?: { readonly stopActivity?: boolean }): Promise<void>
|
||||
/**
|
||||
* Unarchive a Session, restoring it to its recorded Workspace position.
|
||||
* @param sessionId - Session to unarchive.
|
||||
@@ -239,8 +240,8 @@ class UiWorkspaceService extends Service implements UiWorkspace {
|
||||
)
|
||||
}
|
||||
|
||||
async archiveSession(sessionId: SessionId): Promise<void> {
|
||||
await this.workspaces.archiveSession(sessionId)
|
||||
async archiveSession(sessionId: SessionId, options: { readonly stopActivity?: boolean } = {}): Promise<void> {
|
||||
await this.workspaces.archiveSession(sessionId, options)
|
||||
if (this.mainReference?.sessionId === sessionId) this.clearMain()
|
||||
}
|
||||
|
||||
|
||||
@@ -556,6 +556,19 @@
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
/* The stop-and-archive dialog's list of work that will be stopped. */
|
||||
.archiveActivity {
|
||||
margin: 0 0 8px;
|
||||
padding-left: 18px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.archiveActivity li {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wide {
|
||||
animation: none;
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
/**
|
||||
* The archive action: a `sidebar.workspaces.session.menu.item` row and a
|
||||
* `sidebar.workspaces.session.row.action` button over one injected behavior.
|
||||
* The same entries restore an archived row; the notice a successful archive
|
||||
* raises and the diagnostics for Host rejections live in the injected
|
||||
* callbacks, not here.
|
||||
* `sidebar.workspaces.session.row.action` button over one injected behavior,
|
||||
* plus the `shell.overlay` dialog that confirms stopping a Session's running
|
||||
* work before archiving it. The same entries restore an archived row; the
|
||||
* notice a successful archive raises and the diagnostics for Host rejections
|
||||
* live in the injected callbacks, not here.
|
||||
*/
|
||||
import { useState } from 'react'
|
||||
import type { SessionActivity } from '@deepseek-ai/dsh-api-workspace-controller/client'
|
||||
// Type-only: the family keys each provider merges; a key this program did not compile takes the generic line.
|
||||
import type {} from '@deepseek-ai/dsh-agent/types'
|
||||
import type {} from '@deepseek-ai/dsh-jobs/view'
|
||||
import type {} from '@deepseek-ai/dsh-schedule/client'
|
||||
import type {} from '@deepseek-ai/dsh-subagent/client'
|
||||
import {
|
||||
IconArchiveOutlineRegular, IconUnarchiveOutlineRegular, MenuItemButton, Tooltip,
|
||||
Button, IconArchiveOutlineRegular, IconUnarchiveOutlineRegular, MenuItemButton, Modal, Tooltip,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { ArchiveSessionInjected, SessionMenuItemProps, SessionRowActionProps } from '../contract/slots.ts'
|
||||
import type {
|
||||
ArchiveSessionInjected, SessionArchiveConfirmInjected, SessionArchiveConfirmProps, SessionArchiveConfirmRequest,
|
||||
SessionMenuItemProps, SessionRowActionProps,
|
||||
} from '../contract/slots.ts'
|
||||
import css from '../rows/Rows.module.css'
|
||||
import browserCss from '../rows/WorkspaceBrowser.module.css'
|
||||
|
||||
/**
|
||||
* Menu row (order 400): archive, or restore an archived row.
|
||||
@@ -56,3 +68,102 @@ export function ArchiveSessionRowButton({
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The `shell.overlay` entry: nothing while no confirmation is pending,
|
||||
* otherwise one dialog per request (keyed by the Session). Confirming asks
|
||||
* the Host to stop the listed work and archive; cancelling leaves the
|
||||
* Session running and visible.
|
||||
* @param props - the request hook, its settlement, the stop-and-archive hop, and the locale seat.
|
||||
* @returns the open dialog, or null.
|
||||
*/
|
||||
export function SessionArchiveConfirmDialog({
|
||||
useArchiveRequest, settleSessionArchive, stopAndArchiveSession, t,
|
||||
}: SessionArchiveConfirmProps) {
|
||||
const request = useArchiveRequest(pending => pending)
|
||||
if (request === null) return null
|
||||
return (
|
||||
<ArchiveConfirmForm
|
||||
key={request.sessionId}
|
||||
request={request}
|
||||
stopAndArchiveSession={stopAndArchiveSession}
|
||||
onSettle={settleSessionArchive}
|
||||
t={t}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/** One request's dialog: in-flight and error state die with it. */
|
||||
function ArchiveConfirmForm({ request, stopAndArchiveSession, onSettle, t }: {
|
||||
request: SessionArchiveConfirmRequest
|
||||
stopAndArchiveSession: SessionArchiveConfirmInjected['stopAndArchiveSession']
|
||||
onSettle: () => void
|
||||
t: SessionArchiveConfirmProps['t']
|
||||
}) {
|
||||
const [archiving, setArchiving] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const close = () => {
|
||||
if (archiving) return
|
||||
onSettle()
|
||||
}
|
||||
const confirm = () => {
|
||||
setArchiving(true)
|
||||
setError(null)
|
||||
stopAndArchiveSession(request.sessionId).then(() => {
|
||||
setArchiving(false)
|
||||
onSettle()
|
||||
}).catch((reason: unknown) => {
|
||||
setArchiving(false)
|
||||
setError(reason instanceof Error ? reason.message : String(reason))
|
||||
})
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
onClose={close}
|
||||
closeLabel={t('close')}
|
||||
title={t('archive.confirm.title')}
|
||||
description={t('archive.confirm.desc', { title: request.displayTitle })}
|
||||
footer={(
|
||||
<>
|
||||
<Button variant="outline" disabled={archiving} onClick={close}>{t('cancel')}</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={browserCss.deleteAction}
|
||||
disabled={archiving}
|
||||
onClick={confirm}
|
||||
>
|
||||
{t('archive.confirm.action')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<ul className={browserCss.archiveActivity} aria-label={t('archive.confirm.activity')}>
|
||||
{request.activity.map((entry, index) => (
|
||||
<li key={`${entry.kind}-${String(index)}`}>{activityLine(entry, t)}</li>
|
||||
))}
|
||||
</ul>
|
||||
{archiving && <div className={browserCss.deleteStatus} role="status">{t('archive.confirm.pending')}</div>}
|
||||
{error !== null && <div className={browserCss.renameError} role="alert">{error}</div>}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* One family's line: its count and the items' labels (ids when a family
|
||||
* carries no label). A family this dictionary does not know — a provider
|
||||
* merged into the kind map — falls through to the generic line.
|
||||
*/
|
||||
function activityLine(entry: SessionActivity, t: SessionArchiveConfirmProps['t']): string {
|
||||
const items = entry.items ?? []
|
||||
const n = items.length
|
||||
const names = items.map(item => item.label ?? item.id).join(t('archive.confirm.listSeparator'))
|
||||
const plural = n === 1 ? 'one' : 'other'
|
||||
switch (entry.kind) {
|
||||
case 'turn': return t('archive.confirm.turn')
|
||||
case 'subagent': return t(`archive.confirm.subagents.${plural}`, { n, names })
|
||||
case 'job': return t(`archive.confirm.jobs.${plural}`, { n, names })
|
||||
case 'schedule': return t(`archive.confirm.schedules.${plural}`, { n, names })
|
||||
default: return t(`archive.confirm.other.${plural}`, { kind: entry.kind, n })
|
||||
}
|
||||
}
|
||||
@@ -14,22 +14,22 @@ import type { RowToastProps, RowToastState } from '../contract/slots.ts'
|
||||
const LONG_TOAST_HOLD_MS = 6000
|
||||
|
||||
/**
|
||||
* Render the current notice: the archived notice with its undo
|
||||
* and show-archived actions on a 6 s hold, a refused Session creation with
|
||||
* the Host's reason on the same hold, or a plain warning for a failed
|
||||
* pin, an archived row that was clicked, or default Workspace creation.
|
||||
* Render the current notice: the archived and stopped-and-archived notices
|
||||
* with their undo and show-archived actions on a 6 s hold, a refused Session
|
||||
* creation with the Host's reason on the same hold, or a plain warning for a
|
||||
* failed pin, an archived row that was clicked, or default Workspace creation.
|
||||
* @param props - the notice hook, its dismissal, the two archived-notice actions, and the locale seat.
|
||||
* @returns the notice on display, or null.
|
||||
*/
|
||||
export function RowActionToast({ useToast, dismissToast, undoArchive, showArchived, t }: RowToastProps) {
|
||||
const toast = useToast(current => current)
|
||||
if (toast === null) return null
|
||||
if (toast.kind === 'archived') {
|
||||
if (toast.kind === 'archived' || toast.kind === 'stoppedAndArchived') {
|
||||
const { sessionId } = toast
|
||||
return (
|
||||
<Toast
|
||||
key={`toast-${String(toast.seq)}`}
|
||||
text={t('toast.archived')}
|
||||
text={t(toast.kind === 'archived' ? 'toast.archived' : 'toast.stoppedAndArchived')}
|
||||
tone="success"
|
||||
holdMs={LONG_TOAST_HOLD_MS}
|
||||
actions={[
|
||||
@@ -63,7 +63,7 @@ export function RowActionToast({ useToast, dismissToast, undoArchive, showArchiv
|
||||
|
||||
/** The copy of one plain warning, keyed by the notice kind the union closes over. */
|
||||
function plainNoticeText(
|
||||
toast: Exclude<RowToastState, { kind: 'archived' | 'createFailed' }>,
|
||||
toast: Exclude<RowToastState, { kind: 'archived' | 'stoppedAndArchived' | 'createFailed' }>,
|
||||
t: RowToastProps['t'],
|
||||
): string {
|
||||
switch (toast.kind) {
|
||||
|
||||
@@ -13,10 +13,11 @@ import { apply, inject } from '@deepseek-ai/dsh-client-ui-workspace/client'
|
||||
import type { WorkspaceBrowserInjected, WorkspacePickerInjected } from '@deepseek-ai/dsh-client-ui-workspace/client'
|
||||
import {
|
||||
type ArchiveSessionInjected, type ForkSessionInjected, menuOpenStateFactory, type PinSessionInjected,
|
||||
type RenameSessionInjected, type RowToastInjected, type SessionRenameDialogInjected, type WorkspaceViewStoreHandle,
|
||||
type RenameSessionInjected, type RowToastInjected, type SessionArchiveConfirmInjected, type SessionRenameDialogInjected,
|
||||
type WorkspaceViewStoreHandle,
|
||||
} from '../src/client/contract/slots.ts'
|
||||
import { WorkspaceBrowser } from '../src/client/rows/WorkspaceBrowser.tsx'
|
||||
import { ArchiveSessionMenuItem, ArchiveSessionRowButton } from '../src/client/session-actions/ArchiveSession.tsx'
|
||||
import { ArchiveSessionMenuItem, ArchiveSessionRowButton, SessionArchiveConfirmDialog } from '../src/client/session-actions/ArchiveSession.tsx'
|
||||
import { ForkSessionMenuItem } from '../src/client/session-actions/ForkSession.tsx'
|
||||
import { PinSessionMenuItem, PinSessionRowButton } from '../src/client/session-actions/PinSession.tsx'
|
||||
import { RenameSessionMenuItem, SessionRenameDialog } from '../src/client/session-actions/RenameSession.tsx'
|
||||
@@ -216,7 +217,7 @@ describe('ui-workspace apply', () => {
|
||||
// The row actions follow the browser's own declaration, whenever it lands.
|
||||
expect(after.slots.entries(MENU_ITEM)).toHaveLength(4)
|
||||
expect(after.slots.entries(ROW_ACTION)).toHaveLength(2)
|
||||
expect(after.slots.entries('shell.overlay')).toHaveLength(2)
|
||||
expect(after.slots.entries('shell.overlay')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('declares the two Session row lists and registers the shipped actions and overlay surfaces into them', async () => {
|
||||
@@ -244,6 +245,7 @@ describe('ui-workspace apply', () => {
|
||||
])
|
||||
expect(rows('shell.overlay')).toEqual([
|
||||
['workspace.session-rename', undefined, SessionRenameDialog, 'workspace'],
|
||||
['workspace.session-archive', undefined, SessionArchiveConfirmDialog, 'workspace'],
|
||||
['workspace.row-toast', undefined, RowActionToast, 'workspace'],
|
||||
])
|
||||
// Only the browser declares the viewing store; its handle hands out the
|
||||
@@ -388,6 +390,44 @@ describe('ui-workspace apply', () => {
|
||||
expect(toast.hooks.toast.getSnapshot()).toEqual({ kind: 'archived', sessionId: 'one', seq: 1 })
|
||||
})
|
||||
|
||||
it('turns the Host\'s running-work refusal into the stop-and-archive confirmation, which archives with stopActivity', async () => {
|
||||
const b = await bench()
|
||||
b.setSessions(sessionState([{ ...summary('busy', 3), displayTitle: 'Busy session' }]))
|
||||
declare(b.slots, 'sidebar.workspaces', 'shell.overlay')
|
||||
await b.ctx.plugin({ inject: [...inject], apply }).await()
|
||||
const activity = [{ kind: 'turn' as const }, { kind: 'job' as const, items: [{ id: 'bash-1', label: 'pnpm run build' }] }]
|
||||
const refusal = Object.assign(new Error('workspace session archive failed: workspace/session-active: active'), {
|
||||
name: 'WorkspaceArchiveError',
|
||||
rpcError: new RemoteError('workspace/session-active', 'active', { sessionId: sid('busy'), activity }),
|
||||
})
|
||||
const archiveSession = vi.spyOn(b.ctx.uiWorkspace, 'archiveSession')
|
||||
.mockRejectedValueOnce(refusal)
|
||||
.mockResolvedValueOnce(undefined)
|
||||
const archive = faceOf(entry(b.slots, ROW_ACTION, 'archive')) as ArchiveSessionInjected
|
||||
const confirm = faceOf(entry(b.slots, 'shell.overlay', 'workspace.session-archive')) as SessionArchiveConfirmInjected
|
||||
const toast = faceOf(entry(b.slots, 'shell.overlay', 'workspace.row-toast')) as RowToastInjected
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
try {
|
||||
archive.archiveSession(sid('busy'))
|
||||
await vi.waitFor(() => {
|
||||
expect(confirm.hooks.archiveRequest.getSnapshot()).toEqual({ sessionId: 'busy', displayTitle: 'Busy session', activity })
|
||||
})
|
||||
// The refusal is a question, not a diagnostic, and nothing is archived yet.
|
||||
expect(warn).not.toHaveBeenCalled()
|
||||
expect(toast.hooks.toast.getSnapshot()).toBeNull()
|
||||
expect(archiveSession).toHaveBeenCalledWith('busy')
|
||||
|
||||
// Cancelling settles the request; confirming asks the Host to stop the work.
|
||||
confirm.settleSessionArchive()
|
||||
expect(confirm.hooks.archiveRequest.getSnapshot()).toBeNull()
|
||||
await confirm.stopAndArchiveSession(sid('busy'))
|
||||
expect(archiveSession).toHaveBeenLastCalledWith('busy', { stopActivity: true })
|
||||
expect(toast.hooks.toast.getSnapshot()).toEqual({ kind: 'stoppedAndArchived', sessionId: 'busy', seq: 1 })
|
||||
} finally {
|
||||
warn.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
it('the notice share takes the notice down, undoes an archive, and shows the archived rows', async () => {
|
||||
const b = await bench()
|
||||
declare(b.slots, 'sidebar.workspaces', 'shell.overlay')
|
||||
@@ -551,7 +591,7 @@ describe('ui-workspace apply', () => {
|
||||
await fiber.await()
|
||||
expect(b.slots.entries(MENU_ITEM)).toHaveLength(4)
|
||||
expect(b.slots.entries(ROW_ACTION)).toHaveLength(2)
|
||||
expect(b.slots.entries('shell.overlay')).toHaveLength(2)
|
||||
expect(b.slots.entries('shell.overlay')).toHaveLength(3)
|
||||
await fiber.dispose()
|
||||
expect(b.slots.entries('sidebar.workspaces')).toHaveLength(0)
|
||||
expect(b.slots.entries('conversation.hero.workspace')).toHaveLength(0)
|
||||
|
||||
@@ -17,22 +17,27 @@ import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
import type { GlobalStandardProps, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { bindSnapshotSelector, makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { en as commonEn } from '@deepseek-ai/dsh-client-locale/src/locales/en.ts'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
import type {
|
||||
MenuOpenState, RowToast, RowToastState, SessionRenameDialogInjected, SessionRenameTarget,
|
||||
MenuOpenState, RowToast, RowToastState, SessionArchiveConfirmInjected, SessionArchiveConfirmRequest,
|
||||
SessionRenameDialogInjected, SessionRenameTarget,
|
||||
} from '../src/client/contract/slots.ts'
|
||||
import { ArchiveSessionMenuItem, ArchiveSessionRowButton } from '../src/client/session-actions/ArchiveSession.tsx'
|
||||
import {
|
||||
ArchiveSessionMenuItem, ArchiveSessionRowButton, SessionArchiveConfirmDialog,
|
||||
} from '../src/client/session-actions/ArchiveSession.tsx'
|
||||
import { ForkSessionMenuItem } from '../src/client/session-actions/ForkSession.tsx'
|
||||
import { PinSessionMenuItem, PinSessionRowButton } from '../src/client/session-actions/PinSession.tsx'
|
||||
import { RenameSessionMenuItem, SessionRenameDialog } from '../src/client/session-actions/RenameSession.tsx'
|
||||
import { RowActionToast } from '../src/client/session-actions/RowActionToast.tsx'
|
||||
import { zh } from '../src/client/locales.ts'
|
||||
import { en, zh } from '../src/client/locales.ts'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
// The seat's key domain is workspace ∪ common; the stub mirrors the real
|
||||
// lookup chain (namespace, then common vocabulary, then the key).
|
||||
const t: PropsLocale<'workspace'>['t'] = makeTranslate(zh, commonZh)
|
||||
const tEn: PropsLocale<'workspace'>['t'] = makeTranslate(en, commonEn)
|
||||
|
||||
const sid = (id: string) => id as SessionId
|
||||
/** Selector hook over one fixed snapshot: how the renderer binds a standard or injected `hooks` source. */
|
||||
@@ -334,6 +339,122 @@ describe('SessionRenameDialog', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('SessionArchiveConfirmDialog', () => {
|
||||
/** The dialog over a test-owned request source; settling clears the request the way apply does. */
|
||||
function archiveDialog(stopAndArchiveSession: SessionArchiveConfirmInjected['stopAndArchiveSession'], translate = t) {
|
||||
const request = createSnapshotStore<SessionArchiveConfirmRequest | null>(null)
|
||||
const settleSessionArchive = vi.fn(() => { request.set(null) })
|
||||
render(
|
||||
<SessionArchiveConfirmDialog
|
||||
{...overlay}
|
||||
t={translate}
|
||||
useArchiveRequest={bindSnapshotSelector(request)}
|
||||
settleSessionArchive={settleSessionArchive}
|
||||
stopAndArchiveSession={stopAndArchiveSession}
|
||||
/>,
|
||||
)
|
||||
const ask = (activity: SessionArchiveConfirmRequest['activity']): void => {
|
||||
act(() => { request.set({ sessionId: sid('one'), displayTitle: 'Busy session', activity }) })
|
||||
}
|
||||
return { settleSessionArchive, ask }
|
||||
}
|
||||
|
||||
it('renders nothing until a confirmation is requested', () => {
|
||||
archiveDialog(vi.fn(async () => {}))
|
||||
expect(screen.queryByRole('dialog')).toBeNull()
|
||||
})
|
||||
|
||||
it('names the session and lists every reported family with its items, then stops and archives on confirm', async () => {
|
||||
const pending = Promise.withResolvers<undefined>()
|
||||
const stopAndArchiveSession = vi.fn(() => pending.promise)
|
||||
const { settleSessionArchive, ask } = archiveDialog(stopAndArchiveSession)
|
||||
ask([
|
||||
{ kind: 'turn' },
|
||||
{ kind: 'subagent', items: [{ id: 'child-1', label: 'reviewer' }, { id: 'child-2' }] },
|
||||
{ kind: 'job', items: [{ id: 'bash-1', label: 'pnpm run build' }] },
|
||||
{ kind: 'schedule', items: [{ id: 'schedule-1', label: 'check the build' }] },
|
||||
])
|
||||
const dialog = screen.getByRole('dialog', { name: '停止并归档此会话?' })
|
||||
expect(dialog.textContent).toContain('“Busy session”仍有正在进行的工作')
|
||||
const lines = [...screen.getByRole('list', { name: '将被停止的工作' }).querySelectorAll('li')].map(li => li.textContent)
|
||||
expect(lines).toEqual([
|
||||
'进行中的回合',
|
||||
'2 个运行中的子代理:reviewer、child-2',
|
||||
'1 个后台任务:pnpm run build',
|
||||
'1 条定时提醒:check the build',
|
||||
])
|
||||
fireEvent.click(screen.getByRole('button', { name: '停止并归档' }))
|
||||
expect(stopAndArchiveSession).toHaveBeenCalledWith(sid('one'))
|
||||
// While the Host call is pending, closing is blocked and the status shows.
|
||||
expect(screen.getByRole('status').textContent).toBe('正在停止并归档…')
|
||||
fireEvent.click(screen.getByRole('button', { name: '取消' }))
|
||||
expect(settleSessionArchive).not.toHaveBeenCalled()
|
||||
await act(async () => { pending.resolve(undefined) })
|
||||
expect(settleSessionArchive).toHaveBeenCalledOnce()
|
||||
expect(screen.queryByRole('dialog')).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps the dialog open with a rejection surfaced, and Cancel settles without archiving', async () => {
|
||||
const stopAndArchiveSession = vi.fn<SessionArchiveConfirmInjected['stopAndArchiveSession']>()
|
||||
.mockRejectedValueOnce(new Error('stop exploded'))
|
||||
const { settleSessionArchive, ask } = archiveDialog(stopAndArchiveSession)
|
||||
ask([{ kind: 'turn' }])
|
||||
fireEvent.click(screen.getByRole('button', { name: '停止并归档' }))
|
||||
await waitFor(() => { expect(screen.getByRole('alert').textContent).toBe('stop exploded') })
|
||||
expect(settleSessionArchive).not.toHaveBeenCalled()
|
||||
fireEvent.click(screen.getByRole('button', { name: '取消' }))
|
||||
expect(settleSessionArchive).toHaveBeenCalledOnce()
|
||||
expect(stopAndArchiveSession).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('ignores Escape while the Host call is pending and reports a non-Error reason as text', async () => {
|
||||
const pending = Promise.withResolvers<undefined>()
|
||||
const stopAndArchiveSession = vi.fn<SessionArchiveConfirmInjected['stopAndArchiveSession']>()
|
||||
.mockReturnValueOnce(pending.promise)
|
||||
const { settleSessionArchive, ask } = archiveDialog(stopAndArchiveSession)
|
||||
ask([{ kind: 'turn' }])
|
||||
fireEvent.click(screen.getByRole('button', { name: '停止并归档' }))
|
||||
fireEvent.keyDown(document, { key: 'Escape' })
|
||||
expect(settleSessionArchive).not.toHaveBeenCalled()
|
||||
await act(async () => { pending.reject('plain failure') })
|
||||
await waitFor(() => { expect(screen.getByRole('alert').textContent).toBe('plain failure') })
|
||||
fireEvent.keyDown(document, { key: 'Escape' })
|
||||
expect(settleSessionArchive).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('describes a family merged by another provider with the generic line', () => {
|
||||
const { ask } = archiveDialog(vi.fn(async () => {}))
|
||||
ask([{ kind: 'probe', items: [{ id: 'probe-1' }] }])
|
||||
const lines = [...screen.getByRole('list', { name: '将被停止的工作' }).querySelectorAll('li')].map(li => li.textContent)
|
||||
expect(lines).toEqual(['1 项其他工作(probe)'])
|
||||
})
|
||||
|
||||
it('selects the singular or plural English line by item count', () => {
|
||||
const { ask } = archiveDialog(vi.fn(async () => {}), tEn)
|
||||
ask([
|
||||
{ kind: 'subagent', items: [{ id: 'child-1', label: 'reviewer' }] },
|
||||
{ kind: 'job', items: [{ id: 'bash-1' }, { id: 'bash-2' }] },
|
||||
{ kind: 'schedule', items: [{ id: 'schedule-1', label: 'check the build' }, { id: 'schedule-2', label: 'stand-up' }] },
|
||||
{ kind: 'probe', items: [{ id: 'probe-1' }] },
|
||||
])
|
||||
const lines = [...screen.getByRole('list', { name: 'Work that will be stopped' }).querySelectorAll('li')].map(li => li.textContent)
|
||||
expect(lines).toEqual([
|
||||
'1 running subagent: reviewer',
|
||||
'2 background jobs: bash-1, bash-2',
|
||||
'2 scheduled reminders: check the build, stand-up',
|
||||
'1 other item of work (probe)',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
// A provider outside this package may merge its own family into the kind map;
|
||||
// the dialog must describe it without knowing its copy.
|
||||
declare module '@deepseek-ai/dsh-workspace/types' {
|
||||
interface SessionActivityKindMap {
|
||||
probe: true
|
||||
}
|
||||
}
|
||||
|
||||
describe('RowActionToast', () => {
|
||||
/** The notice surface over a test-owned notice source; dismissal clears the notice the way apply does. */
|
||||
function toastSurface() {
|
||||
@@ -363,6 +484,14 @@ describe('RowActionToast', () => {
|
||||
expect(document.body.textContent).toBe('')
|
||||
})
|
||||
|
||||
it('the stopped-and-archived notice offers the same undo and filter actions under its own wording', () => {
|
||||
const { undoArchive, notify } = toastSurface()
|
||||
notify({ kind: 'stoppedAndArchived', sessionId: sid('one') })
|
||||
expect(screen.getByRole('alert').textContent).toBe('已停止并归档,可撤销或筛选已归档会话')
|
||||
fireEvent.click(screen.getByRole('button', { name: '撤销' }))
|
||||
expect(undoArchive).toHaveBeenCalledWith(sid('one'))
|
||||
})
|
||||
|
||||
it('the archived notice takes itself down, then undoes the archive or shows the archived rows', () => {
|
||||
const { dismissToast, undoArchive, showArchived, notify } = toastSurface()
|
||||
notify({ kind: 'archived', sessionId: sid('one') })
|
||||
|
||||
@@ -35,12 +35,18 @@
|
||||
{
|
||||
"path": "../store"
|
||||
},
|
||||
{
|
||||
"path": "../../core/agent"
|
||||
},
|
||||
{
|
||||
"path": "../../core/session"
|
||||
},
|
||||
{
|
||||
"path": "../../subagent/subagent"
|
||||
},
|
||||
{
|
||||
"path": "../../jobs/jobs"
|
||||
},
|
||||
{
|
||||
"path": "../../schedule/schedule"
|
||||
},
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/core/agent/README.md
|
||||
README.md: 8ba3dc730790acb0f792173e9ee01d2dc954957e
|
||||
README.zh.md: 0718ae5f3f73b1fd0bb5b600de3a08c50e67421e
|
||||
README.md: be003d7b899306b58c4ddc88e720c18dde9ce33b
|
||||
README.zh.md: e360ba975dddc381e25b69766b1df82f6e45b3b4
|
||||
@@ -101,11 +101,14 @@ The package is built on one separation: the public `Agent` surface and registry
|
||||
| [`src/consumed-work.ts`](src/consumed-work.ts) | `foldConsumedWork(events)`: what the log's consumed work became |
|
||||
| [`src/model-selection.ts`](src/model-selection.ts) | `installModelSelection`: coupling one selection to assembly and routing |
|
||||
| [`src/invariant.ts`](src/invariant.ts) | Invariant companion: no-op `agent/status` transitions fail |
|
||||
| [`src/archive-admission.ts`](src/archive-admission.ts) | The `turn` family of the Workspace registry's archive admission: a running turn and its user-cause cancel |
|
||||
|
||||
### Registry and lifecycle
|
||||
|
||||
`AgentRegistry` keeps one entry per live agent with its carrier and creator relation. Await `register()` to finish serial creation listeners with source `startup` before using an already-constructed agent; the async factory uses the split `enter()`/`announce()` pair so setup and initialization stay rollback-covered. A detach requested during creation waits for every awaited listener to settle, and each detach is bound to the exact entry, so a stale disposer cannot remove a later same-id replacement. Teardown stops and drains the loop, unwinds the scope, detaches the agent, then detaches the session; the id becomes reusable after private cleanup.
|
||||
|
||||
The registry also answers the Workspace registry's archive admission ([seam](../../workspace/workspace/README.md)) for every Agent it publishes: `workspace/session-activity` reports the `turn` family while the Session's Agent is running, a turn waiting for an approval or an answer included, and `workspace/session-stop` cancels that turn the way the user's own stop does — `agent.cancel({ kind: 'user' })`, but without the stop button's `keepInbox`, so queued input is discarded with a logged inbox splice instead of waking the archived Session later. Nothing is awaited to settlement; a Session without a live Agent has no turn that could run. The `turn` key is merged into `SessionActivityKindMap` from [`src/types.ts`](src/types.ts), so a Client that renders the families imports `@deepseek-ai/dsh-agent/types` for it.
|
||||
|
||||
### Initiator scope
|
||||
|
||||
Each driver runs its complete lifetime inside `ctx.agents.withInitiator(agent, ...)`, so inherited async chains observe that agent; `withoutInitiator()` hides it for unrelated process-local work such as shared timers. The boundary is process-local attribution only — ambient presence is neither liveness proof nor authorization, and explicit identity stays authoritative at worker, process, persistence, and wire boundaries. Teardown rejects new boundaries, lets returned-Promise boundaries drain, then disables the underlying storage. The [initiator-scope decision](../../../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md) owns the detailed contract.
|
||||
|
||||
@@ -101,11 +101,14 @@ await handle.agent.whenIdle()
|
||||
| [`src/consumed-work.ts`](src/consumed-work.ts) | `foldConsumedWork(events)`:日志消费掉的工作最终怎样了 |
|
||||
| [`src/model-selection.ts`](src/model-selection.ts) | `installModelSelection`:把一个选择耦合到组装与路由 |
|
||||
| [`src/invariant.ts`](src/invariant.ts) | 不变式配套:无操作的 `agent/status` 转换会失败 |
|
||||
| [`src/archive-admission.ts`](src/archive-admission.ts) | Workspace 注册表归档准入中的 `turn` 族:运行中的回合及其用户原因取消 |
|
||||
|
||||
### 注册表与生命周期
|
||||
|
||||
`AgentRegistry` 为每个实时 agent 保留一个条目,含其载体与创建者关系。使用已构造的 agent 前,等待 `register()` 以 `startup` 来源完成串行创建监听器;异步工厂使用拆分的 `enter()`/`announce()` 对,使 setup 与初始化始终受回滚保护。创建期间请求的 detach 会等待所有已调用的异步监听器结算,且每次 detach 都绑定到确切条目,因此陈旧 disposer 无法移除之后出现的同 id 替代项。Teardown 停止并排空循环、撤销作用域、detach agent,再 detach 会话;私有清理完成后该 id 即可复用。
|
||||
|
||||
注册表还为它发布的每个 Agent 回答 Workspace 注册表的归档准入([接缝](../../workspace/workspace/README.zh.md)):`workspace/session-activity` 在会话的 Agent 运行时报告 `turn` 族(等待审批或回答的回合也算),`workspace/session-stop` 以用户自己的停止方式取消该回合——`agent.cancel({ kind: 'user' })`,但不带停止按钮的 `keepInbox`,因此排队输入被丢弃并记录一次收件箱拼接,而不会在之后唤醒已归档会话。任何一步都不等待收敛;没有活 Agent 的会话没有可运行的回合。`turn` 键从 [`src/types.ts`](src/types.ts) 合并进 `SessionActivityKindMap`,渲染各族的 Client 为此导入 `@deepseek-ai/dsh-agent/types`。
|
||||
|
||||
### 发起方作用域
|
||||
|
||||
每个驱动器在 `ctx.agents.withInitiator(agent, ...)` 内运行其完整生命周期,因此继承的异步链会观察到该 agent;`withoutInitiator()` 为共享定时器等无关的进程本地工作隐藏它。该边界只是进程本地归因——环境中的身份既不是存活证明,也不是授权,显式身份在 worker、进程、持久化与 wire 边界保持权威。Teardown 拒绝新边界,让返回 Promise 的边界排空,然后禁用底层存储。[发起方作用域决策](../../../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md) 拥有详细约定。
|
||||
|
||||
@@ -45,8 +45,14 @@
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/dsh-typert-protocol": "workspace:^",
|
||||
"@deepseek-ai/dsh-util-values": "workspace:^",
|
||||
"@deepseek-ai/dsh-workspace": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@deepseek-ai/dsh-workspace": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
@@ -57,6 +63,7 @@
|
||||
"@deepseek-ai/dsh-typert-protocol": "workspace:^",
|
||||
"@deepseek-ai/dsh-typert-registry": "workspace:^",
|
||||
"@deepseek-ai/dsh-util-values": "workspace:^",
|
||||
"@deepseek-ai/dsh-workspace": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* The `turn` family of the Workspace registry's archive admission: whether a
|
||||
* Session's own Agent is inside a turn, and its cancel when the Session is
|
||||
* archived with its work. Installed by the registry's constructor, so it
|
||||
* answers for every Agent the registry publishes.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-agent
|
||||
*/
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionActivity } from '@deepseek-ai/dsh-workspace'
|
||||
import type { Agent } from './types.ts'
|
||||
|
||||
/**
|
||||
* Answer `workspace/session-activity` with the `turn` family while the
|
||||
* Session's Agent is running (a turn waiting for an approval or an answer
|
||||
* included), and `workspace/session-stop` by cancelling that turn the way
|
||||
* the user's own stop does — `agent.cancel({ kind: 'user' })`, but without
|
||||
* the stop button's `keepInbox`, so queued input is discarded with a logged
|
||||
* inbox splice instead of waking the archived Session later. Nothing is
|
||||
* awaited to settlement. Both listeners live as long as `ctx`'s fiber.
|
||||
* @param ctx - the registry's registration context.
|
||||
* @param lookup - the registry's live-Agent lookup by Session id.
|
||||
*/
|
||||
export function installTurnArchiveAdmission(ctx: Context, lookup: (sessionId: SessionId) => Agent | undefined): void {
|
||||
ctx.on('workspace/session-activity', async ({ sessionId }, next) => {
|
||||
const running = lookup(sessionId)?.status === 'running'
|
||||
const rest = await next()
|
||||
if (!running) return rest
|
||||
const own: SessionActivity = { kind: 'turn' }
|
||||
return [own, ...rest]
|
||||
})
|
||||
ctx.on('workspace/session-stop', ({ sessionId }) => {
|
||||
const agent = lookup(sessionId)
|
||||
if (agent?.status === 'running') agent.cancel({ kind: 'user' })
|
||||
})
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { isPromise } from 'node:util/types'
|
||||
import { scopeTarget } from '@deepseek-ai/dsh-scope'
|
||||
import type { Scoped } from '@deepseek-ai/dsh-scope'
|
||||
import type { SessionEvent, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session'
|
||||
import { installTurnArchiveAdmission } from './archive-admission.ts'
|
||||
import type { Agent } from './types.ts'
|
||||
import type { AgentOptions, SessionStartSource } from './runtime-types.ts'
|
||||
|
||||
@@ -276,6 +277,9 @@ export class AgentRegistry extends Service {
|
||||
yield () => this.disposeInitiators()
|
||||
yield () => { this.closeInitiators() }
|
||||
}.bind(this), 'agents.initiatorLifecycle()')
|
||||
// Archive admission: the Workspace registry asks what still runs for a
|
||||
// Session before hiding it; a running turn answers here, for every Agent.
|
||||
installTurnArchiveAdmission(ctx, sessionId => this.get(sessionId))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
*/
|
||||
|
||||
import type { UserMessage } from '@deepseek-ai/dsh-llm/types'
|
||||
// Type-only: the Workspace registry's archive-admission family map this registry merges `turn` into.
|
||||
import type {} from '@deepseek-ai/dsh-workspace/types'
|
||||
import type { OptionalSessionSeq, SessionId, SessionSeq } from '@deepseek-ai/dsh-session/types'
|
||||
import type { TypertContext, TypertLookup } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
|
||||
@@ -15,6 +17,13 @@ export interface Agent {
|
||||
readonly id: SessionId
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/dsh-workspace/types' {
|
||||
interface SessionActivityKindMap {
|
||||
/** The session's own Agent is inside a turn, including one waiting for an approval or an answer. */
|
||||
turn: true
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/dsh-typert-protocol' {
|
||||
interface TypertLookupMap {
|
||||
agent: TypertLookup<Agent, SessionId>
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import AgentRegistry from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent, AgentCancelCause, AgentStatus, CancelOptions } from '@deepseek-ai/dsh-agent'
|
||||
import type { SessionActivity } from '@deepseek-ai/dsh-workspace'
|
||||
|
||||
type CancelCall = [AgentCancelCause, CancelOptions | undefined]
|
||||
|
||||
/** A registered Agent double whose status the test flips and whose cancels it records. */
|
||||
type LiveAgent = Agent & { cancels: CancelCall[]; setStatus(status: AgentStatus): void }
|
||||
|
||||
async function liveAgent(ctx: Context, rawId: string, initial: AgentStatus): Promise<LiveAgent> {
|
||||
const id = SessionId(rawId)
|
||||
const cancels: CancelCall[] = []
|
||||
let status = initial
|
||||
const agent: LiveAgent = {
|
||||
id,
|
||||
options: {},
|
||||
session: Session.create(id),
|
||||
inbox: { nextTurn: [], nextStep: [] } as never,
|
||||
get status() { return status },
|
||||
ctx,
|
||||
send: () => {},
|
||||
followup: () => {},
|
||||
steer: () => ({ outcome: Promise.resolve({ status: 'rejected' as const }) }),
|
||||
inject: () => {},
|
||||
cancel: (cause, options) => { cancels.push([cause, options]) },
|
||||
runMaintenance: task => task(new AbortController().signal),
|
||||
whenIdle: () => Promise.resolve(),
|
||||
cancels,
|
||||
setStatus: (next) => { status = next },
|
||||
}
|
||||
await ctx.agents.register(agent)
|
||||
return agent
|
||||
}
|
||||
|
||||
function ask(ctx: Context, sessionId: SessionId): Promise<readonly SessionActivity[]> {
|
||||
return ctx.waterfall('workspace/session-activity', { sessionId }, () => Promise.resolve([]))
|
||||
}
|
||||
|
||||
function stop(ctx: Context, sessionId: SessionId): Promise<void> {
|
||||
return ctx.parallel('workspace/session-stop', { sessionId })
|
||||
}
|
||||
|
||||
describe('Turn archive admission', () => {
|
||||
it('reports the turn family only while the session\'s Agent is running, ahead of later providers', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(AgentRegistry)
|
||||
const agent = await liveAgent(ctx, 'busy', 'running')
|
||||
await liveAgent(ctx, 'quiet', 'idle')
|
||||
ctx.on('workspace/session-activity', async (_request, next) => [...(await next()), { kind: 'probe' }])
|
||||
|
||||
expect(await ask(ctx, agent.id)).toEqual([{ kind: 'turn' }, { kind: 'probe' }])
|
||||
expect(await ask(ctx, SessionId('quiet'))).toEqual([{ kind: 'probe' }])
|
||||
// A Session without a live Agent has no turn that could run.
|
||||
expect(await ask(ctx, SessionId('cold-or-unknown'))).toEqual([{ kind: 'probe' }])
|
||||
agent.setStatus('idle')
|
||||
expect(await ask(ctx, agent.id)).toEqual([{ kind: 'probe' }])
|
||||
})
|
||||
|
||||
it('cancels a running turn with the user cause and without keeping the inbox, and leaves the rest alone', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(AgentRegistry)
|
||||
const running = await liveAgent(ctx, 'stop-me', 'running')
|
||||
const idle = await liveAgent(ctx, 'already-idle', 'idle')
|
||||
|
||||
await stop(ctx, running.id)
|
||||
await stop(ctx, idle.id)
|
||||
await stop(ctx, SessionId('nobody'))
|
||||
expect(running.cancels).toEqual([[{ kind: 'user' }, undefined]])
|
||||
expect(idle.cancels).toEqual([])
|
||||
})
|
||||
|
||||
it('stops answering once the registry fiber is disposed', async () => {
|
||||
const ctx = new Context()
|
||||
const fiber = await ctx.plugin(AgentRegistry)
|
||||
const agent = await liveAgent(ctx, 'watched', 'running')
|
||||
expect(await ask(ctx, agent.id)).toEqual([{ kind: 'turn' }])
|
||||
await fiber.dispose()
|
||||
expect(await ask(ctx, agent.id)).toEqual([])
|
||||
await stop(ctx, agent.id)
|
||||
expect(agent.cancels).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
// The registry knows only its own family; this suite merges a second one to observe ordering.
|
||||
declare module '@deepseek-ai/dsh-workspace/types' {
|
||||
interface SessionActivityKindMap {
|
||||
probe: true
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,9 @@
|
||||
},
|
||||
{
|
||||
"path": "../../typert/protocol"
|
||||
},
|
||||
{
|
||||
"path": "../../workspace/workspace"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -365,9 +365,9 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
parameters: [{ name: 'workspaceId', description: 'explicit target; absent inherits the current or most recent Workspace.' }],
|
||||
},
|
||||
{
|
||||
signature: 'archiveSession(sessionId: SessionId): Promise<void>',
|
||||
signature: 'archiveSession(sessionId: SessionId, options?: { readonly stopActivity?: boolean }): Promise<void>',
|
||||
description: 'Archive a Session and clear it when it is the current selection.',
|
||||
parameters: [{ name: 'sessionId', description: 'Session to archive.' }],
|
||||
parameters: [{ name: 'sessionId', description: 'Session to archive.' }, { name: 'options', description: '`stopActivity` asks the Host to stop the Session\'s running work instead of refusing.' }],
|
||||
},
|
||||
{
|
||||
signature: 'unarchiveSession(sessionId: SessionId): Promise<void>',
|
||||
@@ -417,9 +417,10 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
parameters: [{ name: 'workspaceId', description: 'target Workspace.' }],
|
||||
},
|
||||
{
|
||||
signature: 'archiveSession(sessionId: SessionId): Promise<void>',
|
||||
signature: 'archiveSession(sessionId: SessionId, options?: { readonly stopActivity?: boolean }): Promise<void>',
|
||||
description: 'Archive a Session from Workspace grouping surfaces.',
|
||||
parameters: [{ name: 'sessionId', description: 'Session to archive.' }],
|
||||
parameters: [{ name: 'sessionId', description: 'Session to archive.' }, { name: 'options', description: '`stopActivity` asks the Host to stop the Session\'s running work instead of refusing.' }],
|
||||
throws: ['{WorkspaceArchiveError} when the Host refuses; without `stopActivity` a Session with running work fails as `workspace/session-active`, its details naming what runs.'],
|
||||
},
|
||||
{
|
||||
signature: 'unarchiveSession(sessionId: SessionId): Promise<void>',
|
||||
|
||||
@@ -2736,6 +2736,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
declaredBy: 'an entry in \'root\' (client-ui-layout), so it exists while that entry is mounted',
|
||||
occupants: [
|
||||
'client-ui-workspace SessionRenameDialog id \'workspace.session-rename\'',
|
||||
'client-ui-workspace SessionArchiveConfirmDialog id \'workspace.session-archive\'',
|
||||
'client-ui-workspace RowActionToast id \'workspace.row-toast\'',
|
||||
],
|
||||
replaceRisk: 'none',
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('Client Cordis inspect catalog', () => {
|
||||
'create(input: { path: string }): Promise<WorkspaceView>',
|
||||
'rename(workspaceId: WorkspaceId, title: string): Promise<WorkspaceView>',
|
||||
'delete(workspaceId: WorkspaceId): Promise<void>',
|
||||
'archiveSession(sessionId: SessionId): Promise<void>',
|
||||
'archiveSession(sessionId: SessionId, options?: { readonly stopActivity?: boolean }): Promise<void>',
|
||||
'unarchiveSession(sessionId: SessionId): Promise<void>',
|
||||
'insertSessionBefore( workspaceId: WorkspaceId, sessionId: SessionId, beforeSessionId?: SessionId, ): Promise<WorkspaceView>',
|
||||
])
|
||||
@@ -27,7 +27,7 @@ describe('Client Cordis inspect catalog', () => {
|
||||
'forkSession(sessionId: SessionId): Promise<void>',
|
||||
'connectWorkspace(workspaceId: WorkspaceId): Promise<SessionId>',
|
||||
'startSession(workspaceId?: WorkspaceId): void',
|
||||
'archiveSession(sessionId: SessionId): Promise<void>',
|
||||
'archiveSession(sessionId: SessionId, options?: { readonly stopActivity?: boolean }): Promise<void>',
|
||||
'unarchiveSession(sessionId: SessionId): Promise<void>',
|
||||
'pickDirectory(): Promise<string | null>',
|
||||
'listDirectory(path?: string, signal?: AbortSignal): Promise<DirectoryListing>',
|
||||
|
||||
@@ -3491,10 +3491,10 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
returns: 'the complete committed workspace order.',
|
||||
},
|
||||
{
|
||||
signature: 'archiveSession(sessionId: SessionId): Promise<void>',
|
||||
description: 'Archive one session durably. The session must exist (live or in session persistence); its workspace accounting — or lack of one — is irrelevant. Archiving drops the session\'s pin in the same durable write (pinning and archival are mutually exclusive). An already archived id resolves without writing.',
|
||||
parameters: [{ name: 'sessionId', description: 'The session to archive.' }],
|
||||
returns: 'resolution after durability.',
|
||||
signature: 'archiveSession(sessionId: SessionId, options: ArchiveSessionOptions = {}): Promise<void>',
|
||||
description: 'Archive one session durably. The session must exist (live or in session persistence); its workspace accounting — or lack of one — is irrelevant. Without `stopActivity` the session must also be inactive: the `workspace/session-activity` waterfall is asked once, and any reported activity rejects with WorkspaceActiveSessionError before anything is written. With `stopActivity` the archive is written without an activity check, and the `workspace/session-stop` providers are then asked to stop the session\'s work: the durable archive set is what a provider\'s `agent/pre-step` gate reads, so every wake the stops induce is already blocked. Archiving drops the session\'s pin in the same durable write (pinning and archival are mutually exclusive). An already archived id resolves without writing, asking, or stopping.',
|
||||
parameters: [{ name: 'sessionId', description: 'The session to archive.' }, { name: 'options', description: 'Whether running work is stopped instead of refusing.' }],
|
||||
returns: 'resolution after durability and, with `stopActivity`, after every stop request was issued.',
|
||||
},
|
||||
{
|
||||
signature: 'unarchiveSession(sessionId: SessionId): Promise<void>',
|
||||
@@ -4126,6 +4126,22 @@ export const EVENT_API: readonly EventApiEntry[] = [
|
||||
description: 'A workflow run started — the script\'s meta block validated, the body about to execute. Paired with Events[\'workflow/end\'].',
|
||||
parameters: [{ name: 'info', description: 'the run\'s identity snapshot (id + meta).' }],
|
||||
},
|
||||
{
|
||||
name: 'workspace/session-activity',
|
||||
mode: 'waterfall',
|
||||
signature: '\'workspace/session-activity\'( request: SessionActivityRequest, next: () => Promise<readonly SessionActivity[]>, ): Promise<readonly SessionActivity[]>',
|
||||
summary: 'Ask the composed providers what still runs for a session before it is archived.',
|
||||
description: 'Ask the composed providers what still runs for a session before it is archived. A listener prepends its own SessionActivity entries to the result of `next()`; the registry\'s innermost callback returns an empty list, so a composition without providers archives freely. Any non-empty result refuses the archive without a write.',
|
||||
parameters: [{ name: 'request', description: 'the session about to be archived.' }, { name: 'next', description: 'delegate to the remaining providers.' }],
|
||||
},
|
||||
{
|
||||
name: 'workspace/session-stop',
|
||||
mode: 'parallel',
|
||||
signature: '\'workspace/session-stop\'(request: SessionActivityRequest): Promise<void> | void',
|
||||
summary: 'Stop a session\'s running work because the caller archived it with `stopActivity`; the archive set is durable when this dispatches.',
|
||||
description: 'Stop a session\'s running work because the caller archived it with `stopActivity`; the archive set is durable when this dispatches. Each provider stops its own families — cancelling a turn, its subagent descendants, owned jobs, or active schedules — through the same cancel paths the user\'s own stop actions use, so the session log ends every open turn regularly and a later unarchive can continue the conversation. Listeners issue their stop requests without waiting for running work to settle; a listener may await its own durability barrier. A rejection is logged by the registry and does not undo the archive.',
|
||||
parameters: [{ name: 'request', description: 'the session being archived.' }],
|
||||
},
|
||||
]
|
||||
|
||||
/** Shapes of every exported type the Service and Event signatures reference (transitively), sorted by name. */
|
||||
@@ -4218,6 +4234,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'ApprovalRequestEvent',
|
||||
declaration: 'export interface ApprovalRequestEvent {\n readonly agent: Agent;\n readonly toolName: string;\n readonly callId?: ToolCallId;\n readonly reason?: string;\n readonly signal?: AbortSignal;\n}',
|
||||
},
|
||||
{
|
||||
name: 'ArchiveSessionOptions',
|
||||
declaration: 'export interface ArchiveSessionOptions {\n readonly stopActivity?: boolean;\n}',
|
||||
},
|
||||
{
|
||||
name: 'AskUserQuestionAnswer',
|
||||
declaration: 'export interface AskUserQuestionAnswer {\n answers: AskUserQuestionAnswerItem[];\n}',
|
||||
@@ -5926,6 +5946,26 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'SessionAccess',
|
||||
declaration: 'export type SessionAccess = \'read\' | \'write\';',
|
||||
},
|
||||
{
|
||||
name: 'SessionActivity',
|
||||
declaration: 'export interface SessionActivity {\n readonly kind: SessionActivityKind;\n readonly items?: readonly SessionActivityItem[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionActivityItem',
|
||||
declaration: 'export interface SessionActivityItem {\n readonly id: string;\n readonly label?: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionActivityKind',
|
||||
declaration: 'export type SessionActivityKind = keyof SessionActivityKindMap;',
|
||||
},
|
||||
{
|
||||
name: 'SessionActivityKindMap',
|
||||
declaration: 'export interface SessionActivityKindMap {\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionActivityRequest',
|
||||
declaration: 'export interface SessionActivityRequest {\n readonly sessionId: SessionId;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionAddress',
|
||||
declaration: 'export type SessionAddress = {\n readonly kind: \'session\';\n readonly sessionId: SessionId;\n} | {\n readonly kind: \'subagent\';\n readonly parentSessionId: SessionId;\n readonly childSessionId: SessionId;\n readonly mode: \'one-shot\' | \'continuable\' | \'unknown\';\n};',
|
||||
@@ -7444,7 +7484,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'WorkspaceArchiveSessionRequest',
|
||||
declaration: 'export interface WorkspaceArchiveSessionRequest {\n readonly sessionId: SessionId;\n}',
|
||||
declaration: 'export interface WorkspaceArchiveSessionRequest {\n readonly sessionId: SessionId;\n readonly stopActivity?: boolean;\n}',
|
||||
},
|
||||
{
|
||||
name: 'WorkspaceArchiveValue',
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/jobs/jobs/README.md
|
||||
README.md: 1c7a933d6fa1bda03b37bf3768ac9798cb7d1325
|
||||
README.zh.md: a36924fe3ffbfa83e565e0aeecfcb6ffbeb0519d
|
||||
README.md: 38454678cdaabb650ee02c55f60f71c6a9cd0e37
|
||||
README.zh.md: ab130743816a175d06976cad17a022e7e9a06421
|
||||
@@ -80,12 +80,15 @@ This section explains the design decisions behind the contract and points at the
|
||||
| [`src/types.ts`](src/types.ts) | Shared vocabulary: `JobSpec`, `JobHandle`, `JobHooks`, `JobOutcome`, `JobEvent`, and the read results |
|
||||
| [`src/view.ts`](src/view.ts) | Client-safe leaf: `JobView`, `JobChunk`, `JobStatus`, and the merge-extensible `JobKindMap` |
|
||||
| [`src/brand.ts`](src/brand.ts) | `JobId` branded identifier, importable without the agent dependency |
|
||||
| [`src/archive-admission.ts`](src/archive-admission.ts) | The `job` family of the Workspace registry's archive admission, installed by the seam's constructor for every implementation |
|
||||
| [`src/invariant.ts`](src/invariant.ts) | Invariant companion: checks the announced event protocol per job (registered first, one settlement, removal last) and each announced projection against the registry's own read |
|
||||
|
||||
### Service operations
|
||||
|
||||
Each read or control operation accepts an optional caller `SessionId`; omitting it permits only unowned jobs: `list` and `get` return fresh projections, `read` advances the model's cursor and hands out the producer's result once after settlement, `readAt` reads retained chunks at an absolute offset without consuming anything, `kill` invokes producer cancellation before changing status and records the reason for the terminal `detail`, `wait` blocks up to a timeout, `remove` drops a settled record a caller collected through its own wait and never handed out, and `start()` preflights access, validation, and admission before invoking the producer's `run()` once while refusing any owner no attached controller serves; `events.subscribe` delivers registration, progress, stopping, settlement, removal, and output commits at owner, scope, or process granularity.
|
||||
|
||||
Every implementation also answers the Workspace registry's archive admission ([seam](../../workspace/workspace/README.md)), installed by the seam's constructor through the abstract `list` and `kill` alone: `workspace/session-activity` reports the running or stopping jobs the asked Session owns as the `job` family, one item per job with its label; `workspace/session-stop` kills each of them with the reason `session archived`, one at a time, so a producer that throws on cancel is logged while the Session's other jobs still stop. Unowned jobs belong to nobody and are never reported or killed for a Session.
|
||||
|
||||
</details>
|
||||
|
||||
-----
|
||||
|
||||
@@ -80,12 +80,15 @@ kind: "package-reference"
|
||||
| [`src/types.ts`](src/types.ts) | 共享词汇:`JobSpec`、`JobHandle`、`JobHooks`、`JobOutcome`、`JobEvent` 与读取结果 |
|
||||
| [`src/view.ts`](src/view.ts) | 客户端安全叶子:`JobView`、`JobChunk`、`JobStatus` 与可合并扩展的 `JobKindMap` |
|
||||
| [`src/brand.ts`](src/brand.ts) | `JobId` 带类型标记的标识符,无需 agent 依赖即可导入 |
|
||||
| [`src/archive-admission.ts`](src/archive-admission.ts) | Workspace 注册表归档准入中的 `job` 族,由接缝构造函数为每个实现安装 |
|
||||
| [`src/invariant.ts`](src/invariant.ts) | 不变式伴生插件:校验每个 job 的事件协议(先 registered、恰一次结算、最后 removed)以及每个通告的投影与注册表自身读取的一致性 |
|
||||
|
||||
### 服务操作
|
||||
|
||||
每个读取或控制操作接收可选的调用方 `SessionId`,省略时仅允许访问无主 job:`list` 与 `get` 返回全新投影,`read` 推进模型游标并在结算后把生产方的 result 交出一次,`readAt` 按绝对偏移读取保留块且不消耗任何东西,`kill` 在改变状态前调用生产方取消并为终态 `detail` 记录原因,`wait` 阻塞至超时,`remove` 丢弃调用方经自己的等待收走且从未交出的已结算记录,`start()` 在调用生产方 `run()` 一次之前预检访问、校验与准入,同时拒绝任何没有已附加控制器服务的所有者;`events.subscribe` 按所有者、scope 或进程粒度投递注册、进度、停止中、结算、移除与输出提交。
|
||||
|
||||
每个实现还会回答 Workspace 注册表的归档准入([接缝](../../workspace/workspace/README.zh.md)),由接缝的构造函数只通过抽象的 `list` 与 `kill` 安装:`workspace/session-activity` 把被询问会话拥有的运行中或停止中任务作为 `job` 族报告,每个任务一项、附其 label;`workspace/session-stop` 以原因 `session archived` 逐个 kill 它们,因此一个在取消时抛错的生产方只记日志,该会话的其他任务仍会停止。无主任务不属于任何会话,绝不会为某个会话被报告或 kill。
|
||||
|
||||
</details>
|
||||
|
||||
-----
|
||||
|
||||
@@ -45,13 +45,20 @@
|
||||
"@deepseek-ai/dsh-brand": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-workspace": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@deepseek-ai/dsh-workspace": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-brand": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-workspace": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* The `job` family of the Workspace registry's archive admission: the
|
||||
* background jobs a Session owns that have not settled, and their kill when
|
||||
* the Session is archived with its work. Installed by every registry
|
||||
* implementation through the seam's constructor, so it holds for each of
|
||||
* them through the abstract `list` and `kill` alone.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-jobs
|
||||
*/
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionActivity } from '@deepseek-ai/dsh-workspace'
|
||||
import type { JobRegistry } from './index.ts'
|
||||
import type { JobView } from './view.ts'
|
||||
|
||||
/**
|
||||
* Answer `workspace/session-activity` with the running or stopping jobs the
|
||||
* asked Session owns, and `workspace/session-stop` by killing each of them.
|
||||
* Both listeners live as long as `ctx`'s fiber — the registry's own.
|
||||
* @param ctx - the registry's registration context.
|
||||
* @param registry - the registry whose `list` and `kill` answer.
|
||||
*/
|
||||
export function installJobArchiveAdmission(ctx: Context, registry: JobRegistry): void {
|
||||
ctx.on('workspace/session-activity', async ({ sessionId }, next) => {
|
||||
const jobs = runningJobs(registry, sessionId)
|
||||
const rest = await next()
|
||||
if (jobs.length === 0) return rest
|
||||
const own: SessionActivity = { kind: 'job', items: jobs.map(job => ({ id: job.id, label: job.label })) }
|
||||
return [own, ...rest]
|
||||
})
|
||||
ctx.on('workspace/session-stop', ({ sessionId }) => {
|
||||
for (const job of runningJobs(registry, sessionId)) {
|
||||
try {
|
||||
registry.kill(job.id, sessionId, 'session archived')
|
||||
} catch (error: unknown) {
|
||||
// A producer throwing on cancel must not keep the Session's other jobs running once it is archived.
|
||||
ctx.logger.warn(`jobs: killing "${job.id}" for an archived Session failed: ${String(error)}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** The jobs the Session owns that have not settled; unowned jobs in the same listing belong to nobody. */
|
||||
function runningJobs(registry: JobRegistry, owner: SessionId): JobView[] {
|
||||
return registry.list(owner)
|
||||
.filter(job => job.owner === owner && (job.status === 'running' || job.status === 'stopping'))
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
import { Context, Service } from '@deepseek-ai/cordis'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { installJobArchiveAdmission } from './archive-admission.ts'
|
||||
import type { JobEvents, JobId, JobOutputRead, JobRead, JobSpec, JobView } from './types.ts'
|
||||
|
||||
export { JobId } from './types.ts'
|
||||
@@ -90,6 +91,9 @@ export abstract class JobRegistry extends Service {
|
||||
throw new Error('@deepseek-ai/dsh-jobs is the abstract job registry seam; load an implementation such as @deepseek-ai/dsh-jobs-local instead')
|
||||
}
|
||||
super(ctx, 'jobs')
|
||||
// Archive admission: the Workspace registry asks what still runs for a
|
||||
// Session before hiding it; owned jobs answer here for every implementation.
|
||||
installJobArchiveAdmission(ctx, this)
|
||||
}
|
||||
|
||||
/** Lifecycle and output events, filtered per subscription. */
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
*/
|
||||
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
// Type-only: the Workspace registry's archive-admission family map this seam merges `job` into.
|
||||
import type {} from '@deepseek-ai/dsh-workspace/types'
|
||||
import type { JobId } from './brand.ts'
|
||||
|
||||
/**
|
||||
@@ -16,6 +18,13 @@ import type { JobId } from './brand.ts'
|
||||
*/
|
||||
export type JobStatus = 'running' | 'stopping' | 'completed' | 'killed' | 'failed'
|
||||
|
||||
declare module '@deepseek-ai/dsh-workspace/types' {
|
||||
interface SessionActivityKindMap {
|
||||
/** A background job owned by this session is running or stopping. */
|
||||
job: true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Producer-defined job kinds. Plugins extend this map by declaration merging;
|
||||
* the registry treats every value as an opaque id namespace.
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { JobId, JobRegistry } from '@deepseek-ai/dsh-jobs'
|
||||
import type { JobEvents, JobOutputRead, JobRead, JobSpec, JobStatus, JobView } from '@deepseek-ai/dsh-jobs'
|
||||
import type { SessionActivity } from '@deepseek-ai/dsh-workspace'
|
||||
|
||||
interface Row {
|
||||
readonly id: string
|
||||
readonly label: string
|
||||
readonly owner?: SessionId
|
||||
status: JobStatus
|
||||
}
|
||||
|
||||
/**
|
||||
* A registry whose listing the test scripts: the seam's archive-admission
|
||||
* listeners are driven through the abstract `list` and `kill` alone.
|
||||
*/
|
||||
class ScriptedJobRegistry extends JobRegistry {
|
||||
readonly rows: Row[] = []
|
||||
readonly kills: [JobId, SessionId | undefined, string | undefined][] = []
|
||||
refusing: string | undefined
|
||||
|
||||
readonly events: JobEvents = { subscribe: () => () => {} }
|
||||
|
||||
start(_spec: JobSpec): JobId {
|
||||
throw new Error('not scripted')
|
||||
}
|
||||
|
||||
list(caller?: SessionId): JobView[] {
|
||||
return this.rows
|
||||
.filter(row => row.owner === undefined || row.owner === caller)
|
||||
.map(row => this.view(row))
|
||||
}
|
||||
|
||||
get(id: JobId): JobView {
|
||||
const row = this.rows.find(candidate => candidate.id === id)
|
||||
if (row === undefined) throw new Error(`unknown job ${id}`)
|
||||
return this.view(row)
|
||||
}
|
||||
|
||||
read(id: JobId): JobRead {
|
||||
return { chunks: [], lossy: false, job: this.get(id) }
|
||||
}
|
||||
|
||||
readAt(_id: JobId, from: number): JobOutputRead {
|
||||
return { chunks: [], next: from, lossy: false }
|
||||
}
|
||||
|
||||
kill(id: JobId, caller?: SessionId, reason?: string): 'requested' | 'already-finished' {
|
||||
if (id === this.refusing) throw new Error('producer refuses cancel')
|
||||
this.kills.push([id, caller, reason])
|
||||
const row = this.rows.find(candidate => candidate.id === id)
|
||||
if (row !== undefined) row.status = 'stopping'
|
||||
return 'requested'
|
||||
}
|
||||
|
||||
wait(id: JobId): Promise<JobView> {
|
||||
return Promise.resolve(this.get(id))
|
||||
}
|
||||
|
||||
remove(): void {}
|
||||
|
||||
attachController(): () => void {
|
||||
return () => {}
|
||||
}
|
||||
|
||||
private view(row: Row): JobView {
|
||||
return {
|
||||
id: JobId(row.id),
|
||||
kind: 'bash',
|
||||
label: row.label,
|
||||
...row.owner === undefined ? {} : { owner: row.owner },
|
||||
status: row.status,
|
||||
startedAt: 0,
|
||||
output: { total: 0, earliest: 0 },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function harness(): Promise<{ ctx: Context; jobs: ScriptedJobRegistry }> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(ScriptedJobRegistry)
|
||||
return { ctx, jobs: ctx.jobs as ScriptedJobRegistry }
|
||||
}
|
||||
|
||||
function ask(ctx: Context, sessionId: SessionId): Promise<readonly SessionActivity[]> {
|
||||
return ctx.waterfall('workspace/session-activity', { sessionId }, () => Promise.resolve([]))
|
||||
}
|
||||
|
||||
function stop(ctx: Context, sessionId: SessionId): Promise<void> {
|
||||
return ctx.parallel('workspace/session-stop', { sessionId })
|
||||
}
|
||||
|
||||
const owner = SessionId('owner')
|
||||
const other = SessionId('other')
|
||||
|
||||
describe('Job archive admission', () => {
|
||||
it('reports the running or stopping jobs the session owns, never unowned, foreign, or settled ones', async () => {
|
||||
const { ctx, jobs } = await harness()
|
||||
jobs.rows.push(
|
||||
{ id: 'bash-1', label: 'pnpm run build', owner, status: 'running' },
|
||||
{ id: 'bash-2', label: 'pnpm test', owner, status: 'stopping' },
|
||||
{ id: 'bash-3', label: 'done', owner, status: 'completed' },
|
||||
{ id: 'bash-4', label: 'theirs', owner: other, status: 'running' },
|
||||
{ id: 'bash-5', label: 'unowned', status: 'running' },
|
||||
)
|
||||
ctx.on('workspace/session-activity', async (_request, next) => [...(await next()), { kind: 'turn' }])
|
||||
expect(await ask(ctx, owner)).toEqual([
|
||||
{ kind: 'job', items: [{ id: 'bash-1', label: 'pnpm run build' }, { id: 'bash-2', label: 'pnpm test' }] },
|
||||
{ kind: 'turn' },
|
||||
])
|
||||
expect(await ask(ctx, SessionId('nobody'))).toEqual([{ kind: 'turn' }])
|
||||
})
|
||||
|
||||
it('kills each owned live job with the archive reason and keeps going when one producer refuses', async () => {
|
||||
const { ctx, jobs } = await harness()
|
||||
jobs.rows.push(
|
||||
{ id: 'bash-1', label: 'stubborn', owner, status: 'running' },
|
||||
{ id: 'bash-2', label: 'quiet', owner, status: 'running' },
|
||||
{ id: 'bash-3', label: 'theirs', owner: other, status: 'running' },
|
||||
)
|
||||
jobs.refusing = 'bash-1'
|
||||
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {})
|
||||
await stop(ctx, owner)
|
||||
expect(jobs.kills).toEqual([[JobId('bash-2'), owner, 'session archived']])
|
||||
expect(warn.mock.calls.map(([message]) => String(message))).toEqual([expect.stringContaining('killing "bash-1"')])
|
||||
expect(jobs.get(JobId('bash-3')).status).toBe('running')
|
||||
// A second stop finds the survivor already stopping and asks it again; the refusal is logged again.
|
||||
await stop(ctx, owner)
|
||||
expect(jobs.kills).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('stops answering once the registry fiber is disposed', async () => {
|
||||
const ctx = new Context()
|
||||
const fiber = await ctx.plugin(ScriptedJobRegistry)
|
||||
const jobs = ctx.jobs as ScriptedJobRegistry
|
||||
jobs.rows.push({ id: 'bash-1', label: 'pnpm run build', owner, status: 'running' })
|
||||
expect(await ask(ctx, owner)).toEqual([{ kind: 'job', items: [{ id: 'bash-1', label: 'pnpm run build' }] }])
|
||||
await fiber.dispose()
|
||||
expect(await ask(ctx, owner)).toEqual([])
|
||||
await stop(ctx, owner)
|
||||
expect(jobs.kills).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -25,6 +25,9 @@
|
||||
},
|
||||
{
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
},
|
||||
{
|
||||
"path": "../../workspace/workspace"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/schedule/schedule/README.md
|
||||
README.md: 962c343111849ef20fd085dbe8ba454e57a78f8b
|
||||
README.zh.md: 8913bc53d68ad2eb7406f1ce6780d69ec522b956
|
||||
README.md: 55a4c86b88ae5a28219fd927930ec68cc780185e
|
||||
README.zh.md: 4670803e88b61501a03e9635f6b260948c98610f
|
||||
@@ -53,7 +53,7 @@ Input that cannot become a reminder — an empty prompt, more than one selector,
|
||||
|
||||
### When reminders fire
|
||||
|
||||
Due reminders appear as ordinary follow-up messages after the conversation becomes idle; the agent never interrupts a running turn. An already-live idle agent can claim maintenance and deliver immediately without another resume. One-time reminders fire before any repeating batch, and several repeating reminders due at once arrive together in one message ordered by time. If the session is closed or cold when a reminder comes due, it stays overdue until a future live root agent resumes the session — nothing is sent outside the session. A repeating reminder that missed intervals while the session was away presents only its latest due occurrence, not a backlog. The optional Web catalog shows only active records and is not a delivery receipt; dispatch means the follow-up was queued and recorded, not that the model succeeded or the user read the answer.
|
||||
Due reminders appear as ordinary follow-up messages after the conversation becomes idle; the agent never interrupts a running turn. An already-live idle agent can claim maintenance and deliver immediately without another resume. One-time reminders fire before any repeating batch, and several repeating reminders due at once arrive together in one message ordered by time. If the session is closed or cold when a reminder comes due, it stays overdue until a future live root agent resumes the session — nothing is sent outside the session. A repeating reminder that missed intervals while the session was away presents only its latest due occurrence, not a backlog. The optional Web catalog shows only active records and is not a delivery receipt; dispatch means the follow-up was queued and recorded, not that the model succeeded or the user read the answer. Archiving a live session with active reminders is refused until they stop, and choosing to stop them deletes every active reminder; unarchiving does not bring them back.
|
||||
|
||||
-----
|
||||
|
||||
@@ -122,6 +122,8 @@ The owner splits long waits into bounded timer segments and rereads the wall clo
|
||||
|
||||
An overdue reminder first checkpoints persistence, then claims the Agent's idle maintenance phase through `runMaintenance()`; if a turn or another maintenance task owns the Agent, the claim is rejected, the record stays active, and the owner retries after `whenIdle()`. A successful maintenance task refolds, samples one decision time, builds the fixed framing, synchronously queues `followup()`, and appends the dispatch before releasing the phase. Dispatch means the follow-up was queued and recorded, not that the model succeeded or the user read the answer. Framing or synchronous follow-up failure writes no dispatch; an append failure faults the owner because the message may already be queued; a barrier rejection leaves dispatch pending for a later ordinary preflight. Agent or plugin disposal cancels timers and stops new work without deleting durable records.
|
||||
|
||||
The plugin answers the Workspace registry's archive admission ([seam](../../workspace/workspace/README.md)) for every session it owns a runtime for, from that owner's own fold of the live log — the projection registry is a Client view and is not read here: `workspace/session-activity` reports the active records of the session's own suffix as the `schedule` family, one item per reminder with its prompt as label, and `workspace/session-stop` is a management delete in the [same serialized transaction and barriers](#management-pipeline) as the tool: it awaits `ctx.sessions.flush(session)` before reading the fold, appends the same `delete` change the `schedule_delete` tool records for each active reminder, asks the owner to redrive so its timers clear, then awaits a second barrier after the appends. A failed barrier rejects the stop; the registry logs it and keeps the archive, and the reminders stay for the archived-session `agent/pre-step` gate to block when they fire. The registry writes the archive before it dispatches the stop, so a crash between that write and the delete barrier leaves an archived session whose reminders are still recorded; the next unarchive shows them again, exactly as if the stop had never been requested. A session without a live agent, or a live agent without an owned runtime (published before this plugin loaded, or a runtime that stopped or faulted), reports nothing and has nothing to stop, because nothing of it is armed to fire.
|
||||
|
||||
</details>
|
||||
|
||||
-----
|
||||
|
||||
@@ -53,7 +53,7 @@ dsh web --patch apps/cli/config/examples/schedule/cordis.yml
|
||||
|
||||
### 提醒何时触发
|
||||
|
||||
到期提醒会在会话空闲后作为普通 follow-up 消息出现;agent 绝不会中断正在运行的轮次。已经 live 且空闲的 agent 可以认领 maintenance 并立即交付,无需再次恢复。一次性提醒先于任何重复批次触发;同时到期的多条重复提醒会按时间顺序合并为一条消息。如果会话在提醒到期时已关闭或 cold,提醒会保持逾期,直到未来的 live 根 agent 恢复会话——会话之外不会发送任何内容。错过若干间隔的重复提醒只展示最新一个到期发生时点,不展示积压。可选 Web 目录只显示活动记录,并不充当交付回执;dispatch 表示 follow-up 已入队并被记录,不表示模型成功或用户已读取回答。
|
||||
到期提醒会在会话空闲后作为普通 follow-up 消息出现;agent 绝不会中断正在运行的轮次。已经 live 且空闲的 agent 可以认领 maintenance 并立即交付,无需再次恢复。一次性提醒先于任何重复批次触发;同时到期的多条重复提醒会按时间顺序合并为一条消息。如果会话在提醒到期时已关闭或 cold,提醒会保持逾期,直到未来的 live 根 agent 恢复会话——会话之外不会发送任何内容。错过若干间隔的重复提醒只展示最新一个到期发生时点,不展示积压。可选 Web 目录只显示活动记录,并不充当交付回执;dispatch 表示 follow-up 已入队并被记录,不表示模型成功或用户已读取回答。归档仍有活动提醒的活会话会被拒绝,直到这些提醒停止;选择停止它们会删除全部活动提醒,取消归档不会把它们带回来。
|
||||
|
||||
-----
|
||||
|
||||
@@ -110,6 +110,7 @@ projection 只携带持久记录。它不持久化或传输 scheduled/overdue
|
||||
|
||||
日历规范化是确定性的。夏令时缺口内的本地时间会被拒绝;重叠时选择第一次出现的较早时刻。Schedule 的时间校验不会读取浏览器、Session header 中的时区字段、模型 time-context、连接或进程时区,因此回放永不依赖环境时区状态。
|
||||
|
||||
<a id="management-pipeline"></a>
|
||||
### 管理流水线
|
||||
|
||||
一条 agent 范围的队列把每项已接纳的管理事务与 live owner 的到期事务从 preflight 到任何 post-append barrier 全程串行化。`schedule_create` 建立检查点、分配永不复用的 id、追加 create 事件,再次建立检查点;被取消的调用方在追加前停止。每次成功的管理 preflight 还会要求 live owner 重新计算,这会在先前的 post-append barrier 返回 `persistence_uncertain` 后恢复所保留的 create 或 delete 批次。
|
||||
@@ -122,6 +123,8 @@ owner 把长等待拆分为有界的 timer 段,并在每次唤醒后重新读
|
||||
|
||||
逾期提醒首先为持久化建立检查点,然后通过 `runMaintenance()` 认领 agent 的 idle maintenance phase;如果某个轮次或另一项 maintenance task 已占用 agent,认领会失败,记录保持活动,owner 在 `whenIdle()` 后重试。获准的 maintenance task 会重新折叠、采样一个决策时点、构造固定 framing、同步将 `followup()` 入队,并在释放 phase 前追加 dispatch。dispatch 表示 follow-up 已入队并被记录,不表示模型成功或用户已读取回答。framing 构造或同步 follow-up 失败不会写入 dispatch;追加失败会使 owner 进入故障状态,因为消息可能已经入队;barrier 拒绝则把 dispatch 留给后续普通 preflight。agent 或插件执行资源释放时取消 timer 并停止新工作,但不删除持久记录。
|
||||
|
||||
本插件为每个它拥有 runtime 的会话回答 Workspace 注册表的归档准入([接缝](../../workspace/workspace/README.zh.md)),答案来自该 owner 对活日志的自有 fold——投影注册表是 Client 视图,这里不读它:`workspace/session-activity` 把会话自身后缀中的活动记录作为 `schedule` 族报告,每条提醒一项、以其 prompt 作名称;`workspace/session-stop` 是与工具处于[同一串行事务与屏障](#management-pipeline)之下的管理删除:先 await `ctx.sessions.flush(session)` 再读取 fold,为每条活动提醒追加与 `schedule_delete` 工具所记录的相同的 `delete` 变更,请求 owner 重新驱动以清除其 timer,然后在追加之后 await 第二道屏障。屏障失败会让该 stop 拒绝;注册表记录日志并保留归档,这些提醒留给已归档会话的 `agent/pre-step` 门禁在触发时拦下。注册表先写入归档再派发 stop,因此在该写入与删除屏障之间崩溃会留下一个提醒仍被记录的已归档会话;下次取消归档时它们会再次出现,与从未请求过 stop 时完全一样。没有活 agent 的会话,或没有归属 runtime 的活 agent(在本插件加载前发布,或其 runtime 已停止、已故障),不报告任何内容也没有可停的东西,因为它没有任何已武装、可触发的提醒。
|
||||
|
||||
</details>
|
||||
|
||||
-----
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"@deepseek-ai/dsh-session-persistence": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-projection": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/dsh-workspace": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -64,6 +65,7 @@
|
||||
"@deepseek-ai/dsh-session-projection": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/dsh-workspace": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,18 @@
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type {} from '@deepseek-ai/dsh-session-persistence'
|
||||
// Type-only: resolves ctx.sessionProjections for the optional projection child.
|
||||
import type {} from '@deepseek-ai/dsh-session-projection'
|
||||
// Type-only: the Workspace registry's archive-admission events this plugin answers.
|
||||
import type { SessionActivity } from '@deepseek-ai/dsh-workspace'
|
||||
import { flushSchedulePersistence } from './persistence.ts'
|
||||
import { scheduleProjectionDefinition } from './projection.ts'
|
||||
import { ScheduleRuntime } from './runtime.ts'
|
||||
import { registerScheduleTools } from './tools.ts'
|
||||
import { runScheduleTransaction } from './transaction.ts'
|
||||
import type { ScheduleRecord } from './types.ts'
|
||||
|
||||
export type * from './types.ts'
|
||||
export {
|
||||
@@ -39,14 +45,71 @@ export const inject = ['agents', 'sessions', 'tools', 'sessionPersistence']
|
||||
|
||||
type OwnerCleanup = () => void | Promise<void>
|
||||
|
||||
/** Install Schedule only for root agents published after this plugin loads. */
|
||||
interface OwnedRuntime {
|
||||
readonly runtime: ScheduleRuntime
|
||||
readonly cleanup: OwnerCleanup
|
||||
}
|
||||
|
||||
/**
|
||||
* Install Schedule only for root agents published after this plugin loads,
|
||||
* and answer the Workspace registry's archive admission for every session
|
||||
* this plugin owns a runtime for. The owner answers from its own fold of the
|
||||
* live log, so a session without a live agent or without an owned runtime
|
||||
* — nothing armed that could fire — reports nothing and has nothing to stop.
|
||||
* Active reminders count as activity. A stop request is a management delete
|
||||
* in the agent's transaction queue, serialized with the tools and the
|
||||
* owner's due transaction: it awaits the shared persistence barrier before
|
||||
* reading the fold, appends the same durable `schedule/change` delete the
|
||||
* `schedule_delete` tool records for every active reminder, and awaits a
|
||||
* second barrier after the appends. A failed barrier rejects the stop, which
|
||||
* the registry logs, leaving the reminders for the archived-session gate to
|
||||
* block when they fire.
|
||||
*/
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.inject(['sessionProjections'], (projectionCtx) => {
|
||||
projectionCtx.sessionProjections.register(scheduleProjectionDefinition)
|
||||
})
|
||||
|
||||
const runtimes = new Map<Agent, OwnerCleanup>()
|
||||
const runtimes = new Map<Agent, OwnedRuntime>()
|
||||
let stopping = false
|
||||
/** The owned runtime's active records for a session id; none without a live agent or an owned runtime. */
|
||||
const activeReminders = (sessionId: SessionId): { agent: Agent; active: readonly ScheduleRecord[] } | undefined => {
|
||||
const agent = ctx.agents.get(sessionId)
|
||||
const active = agent === undefined ? undefined : runtimes.get(agent)?.runtime.activeRecords()
|
||||
return agent === undefined || active === undefined ? undefined : { agent, active }
|
||||
}
|
||||
|
||||
ctx.effect(() => {
|
||||
const stopActivity = ctx.on('workspace/session-activity', async ({ sessionId }, next) => {
|
||||
const reminders = activeReminders(sessionId)
|
||||
const rest = await next()
|
||||
if (reminders === undefined || reminders.active.length === 0) return rest
|
||||
const own: SessionActivity = {
|
||||
kind: 'schedule',
|
||||
items: reminders.active.map(record => ({ id: record.id, label: record.prompt })),
|
||||
}
|
||||
return [own, ...rest]
|
||||
})
|
||||
const stopStop = ctx.on('workspace/session-stop', async ({ sessionId }) => {
|
||||
const owned = activeReminders(sessionId)
|
||||
if (owned === undefined) return
|
||||
const { agent } = owned
|
||||
await runScheduleTransaction(agent, async () => {
|
||||
await flushSchedulePersistence(ctx, agent.session)
|
||||
const active = activeReminders(sessionId)?.active ?? []
|
||||
if (active.length === 0) return
|
||||
for (const record of active) {
|
||||
agent.session.append('schedule/change', { version: 1, operation: 'delete', id: record.id })
|
||||
}
|
||||
runtimes.get(agent)?.runtime.requestDrive()
|
||||
await flushSchedulePersistence(ctx, agent.session)
|
||||
})
|
||||
})
|
||||
return () => {
|
||||
stopStop()
|
||||
stopActivity()
|
||||
}
|
||||
}, 'schedule.archiveAdmission()')
|
||||
|
||||
ctx.effect(() => {
|
||||
const stopCreated = ctx.on('agent/created', ({ agent }) => {
|
||||
@@ -67,19 +130,19 @@ export function apply(ctx: Context): void {
|
||||
try {
|
||||
await runtime.dispose()
|
||||
} finally {
|
||||
if (runtimes.get(agent) === cleanup) runtimes.delete(agent)
|
||||
if (runtimes.get(agent)?.cleanup === cleanup) runtimes.delete(agent)
|
||||
}
|
||||
}
|
||||
}, 'schedule.runtime()')
|
||||
runtimes.set(agent, cleanup)
|
||||
runtimes.set(agent, { runtime, cleanup })
|
||||
})
|
||||
|
||||
return async () => {
|
||||
stopping = true
|
||||
stopCreated()
|
||||
const cleanups = [...runtimes.values()]
|
||||
const owned = [...runtimes.values()]
|
||||
runtimes.clear()
|
||||
await Promise.allSettled(cleanups.map(cleanup => Promise.resolve(cleanup())))
|
||||
await Promise.allSettled(owned.map(({ cleanup }) => Promise.resolve(cleanup())))
|
||||
}
|
||||
}, 'schedule.lifecycle()')
|
||||
}
|
||||
@@ -13,7 +13,7 @@ declare module '@deepseek-ai/dsh-llm' {
|
||||
}
|
||||
}
|
||||
|
||||
import type { EveryScheduleRecord, OneShotScheduleRecord } from './types.ts'
|
||||
import type { EveryScheduleRecord, OneShotScheduleRecord, ScheduleRecord } from './types.ts'
|
||||
import {
|
||||
foldScheduleEvents,
|
||||
renderEveryReminderBatchFraming,
|
||||
@@ -106,6 +106,16 @@ export class ScheduleRuntime {
|
||||
this.requestDrive()
|
||||
}
|
||||
|
||||
/**
|
||||
* Active records of the exact runtime suffix, folded from the live log the
|
||||
* way every drive folds it: the owner answers for what its timers can fire.
|
||||
* @returns the active records in create order, or nothing while this runtime is stopping, faulted, or reading a corrupt stream.
|
||||
*/
|
||||
activeRecords(): readonly ScheduleRecord[] | undefined {
|
||||
if (this.stopping || this.faulted) return undefined
|
||||
return this.readFolded()?.active
|
||||
}
|
||||
|
||||
/** Recompute the live projection after a committed mutation or idle transition. */
|
||||
requestDrive(): void {
|
||||
if (this.stopping || this.faulted) return
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
import type { Branded } from '@deepseek-ai/dsh-brand'
|
||||
import type {} from '@deepseek-ai/dsh-session/types'
|
||||
// Type-only: the Workspace registry's archive-admission family map this plugin merges `schedule` into.
|
||||
import type {} from '@deepseek-ai/dsh-workspace/types'
|
||||
|
||||
/** Stable reminder identity that is unique and never reused within one session. */
|
||||
export type ScheduleId = Branded<'ScheduleId'>
|
||||
@@ -210,6 +212,13 @@ export type ScheduleDeleteResult =
|
||||
/** Canonical `schedule_delete` value. */
|
||||
export type ScheduleDeleteValue = ScheduleDeleteResult | ScheduleToolError
|
||||
|
||||
declare module '@deepseek-ai/dsh-workspace/types' {
|
||||
interface SessionActivityKindMap {
|
||||
/** A scheduled follow-up for this session is still active. */
|
||||
schedule: true
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/dsh-session/types' {
|
||||
interface SessionEventMap {
|
||||
/**
|
||||
|
||||
@@ -143,6 +143,114 @@ describe('Schedule plugin composition', () => {
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('reports active reminders as archive activity and deletes them on a stop request', async () => {
|
||||
const ctx = await harness()
|
||||
// Published before the plugin: no owned runtime, so nothing of its log is armed.
|
||||
const existing = await ctx.agents.create({ sessionId: SessionId('schedule-archive-existing') })
|
||||
const plugin = await ctx.plugin(toolSchedule)
|
||||
existing.agent.session.append('schedule/change', {
|
||||
version: 1,
|
||||
operation: 'create',
|
||||
schedule: toolSchedule.createAfterScheduleRecord(toolSchedule.ScheduleId('schedule-1'), 'orphaned', 3_600, Date.now()),
|
||||
})
|
||||
const askExisting = () => ctx.waterfall(
|
||||
'workspace/session-activity', { sessionId: existing.agent.id }, () => Promise.resolve([]),
|
||||
)
|
||||
expect(await askExisting()).toEqual([])
|
||||
await ctx.parallel('workspace/session-stop', { sessionId: existing.agent.id })
|
||||
expect(existing.agent.session.snapshotEvents().filter(event => event.type === 'schedule/change')).toHaveLength(1)
|
||||
|
||||
const root = await ctx.agents.create({ sessionId: SessionId('schedule-archive') })
|
||||
const ask = () => ctx.waterfall(
|
||||
'workspace/session-activity', { sessionId: root.agent.id }, () => Promise.resolve([]),
|
||||
)
|
||||
const listeners = (event: 'workspace/session-activity' | 'workspace/session-stop') =>
|
||||
ctx.events._hooks[event]?.length ?? 0
|
||||
const changes = () => root.agent.session.snapshotEvents().filter(event => event.type === 'schedule/change')
|
||||
// How many schedule/change events the log held at each persistence barrier of the root session.
|
||||
const barriers: number[] = []
|
||||
const stopFlush = ctx.on('session/flush', (session) => {
|
||||
if (session === root.agent.session) barriers.push(changes().length)
|
||||
})
|
||||
expect(await ask()).toEqual([])
|
||||
expect(barriers).toEqual([])
|
||||
|
||||
const created = await ctx.agents.withInitiator(root.agent, () => ctx.tools.execute({
|
||||
signal: new AbortController().signal,
|
||||
callId: ToolCallId('schedule-archive-create'),
|
||||
name: 'schedule_create',
|
||||
arguments: { prompt: 'check the build', after_seconds: 3_600 },
|
||||
agent: root.agent,
|
||||
}))
|
||||
expect(created.isError).toBe(false)
|
||||
expect(await ask()).toEqual([
|
||||
{ kind: 'schedule', items: [{ id: 'schedule-1', label: 'check the build' }] },
|
||||
])
|
||||
// A session without a live agent has nothing that could fire.
|
||||
expect(await ctx.waterfall(
|
||||
'workspace/session-activity', { sessionId: SessionId('schedule-cold') }, () => Promise.resolve([]),
|
||||
)).toEqual([])
|
||||
|
||||
barriers.length = 0
|
||||
await ctx.parallel('workspace/session-stop', { sessionId: root.agent.id })
|
||||
// The delete is the same durable change the schedule_delete tool records,
|
||||
// between the same two barriers: one before the fold is read, one after the
|
||||
// append. The runtime re-drive the delete triggers adds its own preflight
|
||||
// barrier, so two barriers see the appended delete.
|
||||
expect(changes().map(event => event.data)).toEqual([
|
||||
expect.objectContaining({ operation: 'create' }),
|
||||
{ version: 1, operation: 'delete', id: 'schedule-1' },
|
||||
])
|
||||
expect(barriers).toEqual([1, 2, 2])
|
||||
expect(await ask()).toEqual([])
|
||||
// A second stop still checkpoints before reading, then has nothing left to delete and records nothing.
|
||||
barriers.length = 0
|
||||
await ctx.parallel('workspace/session-stop', { sessionId: root.agent.id })
|
||||
expect(changes()).toHaveLength(2)
|
||||
expect(barriers).toEqual([2])
|
||||
// A stop for a session without a live agent neither checkpoints nor appends.
|
||||
barriers.length = 0
|
||||
await ctx.parallel('workspace/session-stop', { sessionId: SessionId('schedule-cold') })
|
||||
expect(barriers).toEqual([])
|
||||
// A fold that turns unreadable at the barrier faults the owner, which then answers nothing to delete.
|
||||
const again = await ctx.agents.withInitiator(root.agent, () => ctx.tools.execute({
|
||||
signal: new AbortController().signal,
|
||||
callId: ToolCallId('schedule-archive-create-2'),
|
||||
name: 'schedule_create',
|
||||
arguments: { prompt: 'check the deploy', after_seconds: 3_600 },
|
||||
agent: root.agent,
|
||||
}))
|
||||
expect(again.isError).toBe(false)
|
||||
const corruptAtBarrier = ctx.on('session/flush', (session) => {
|
||||
if (session !== root.agent.session) return
|
||||
Object.defineProperty(root.agent.session, 'snapshotEvents', {
|
||||
configurable: true,
|
||||
value: () => { throw new Error('unreadable at the barrier') },
|
||||
})
|
||||
})
|
||||
barriers.length = 0
|
||||
await ctx.parallel('workspace/session-stop', { sessionId: root.agent.id })
|
||||
corruptAtBarrier()
|
||||
delete (root.agent.session as { snapshotEvents?: unknown }).snapshotEvents
|
||||
expect(barriers).toEqual([3])
|
||||
expect(changes()).toHaveLength(3)
|
||||
|
||||
// One listener per event is the composed Agent registry's own `turn` family.
|
||||
expect(listeners('workspace/session-activity')).toBe(2)
|
||||
expect(listeners('workspace/session-stop')).toBe(2)
|
||||
await plugin.dispose()
|
||||
// Disposal withdraws both listeners with the plugin: a stop no longer checkpoints the session.
|
||||
expect(listeners('workspace/session-activity')).toBe(1)
|
||||
expect(listeners('workspace/session-stop')).toBe(1)
|
||||
barriers.length = 0
|
||||
await ctx.parallel('workspace/session-stop', { sessionId: root.agent.id })
|
||||
expect(barriers).toEqual([])
|
||||
stopFlush()
|
||||
await root.dispose()
|
||||
await existing.dispose()
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('does not checkpoint unrelated idle sessions', async () => {
|
||||
const ctx = await harness()
|
||||
const plugin = await ctx.plugin(toolSchedule)
|
||||
|
||||
@@ -614,6 +614,32 @@ describe('Schedule runtime failure and teardown boundaries', () => {
|
||||
event.type === 'schedule/change' && event.data.operation === 'dispatch')).toEqual([])
|
||||
})
|
||||
|
||||
it('answers activeRecords from its own fold and withholds it once stopping, faulted, or unreadable', async () => {
|
||||
const test = await harness()
|
||||
appendAfter(test, 'schedule-1', 3_600)
|
||||
const runtime = runtimeFor(test)
|
||||
runtime.start()
|
||||
await settle()
|
||||
expect(runtime.activeRecords()?.map(record => record.id)).toEqual(['schedule-1'])
|
||||
// A fold that turns unreadable faults the runtime, as a drive would, and answers nothing afterwards.
|
||||
Object.defineProperty(test.agent.session, 'snapshotEvents', {
|
||||
configurable: true,
|
||||
value: () => { throw new Error('unreadable log') },
|
||||
})
|
||||
expect(runtime.activeRecords()).toBeUndefined()
|
||||
delete (test.agent.session as { snapshotEvents?: unknown }).snapshotEvents
|
||||
expect(runtime.activeRecords()).toBeUndefined()
|
||||
await runtime.dispose()
|
||||
|
||||
const stopped = await harness()
|
||||
appendAfter(stopped, 'schedule-1', 3_600)
|
||||
const stoppedRuntime = runtimeFor(stopped)
|
||||
stoppedRuntime.start()
|
||||
await settle()
|
||||
await stoppedRuntime.dispose()
|
||||
expect(stoppedRuntime.activeRecords()).toBeUndefined()
|
||||
})
|
||||
|
||||
it('faults on corrupt or unreadable durable state after preflight', async () => {
|
||||
const corrupt = await harness()
|
||||
Object.defineProperty(corrupt.agent.session, 'snapshotEvents', {
|
||||
|
||||
@@ -38,6 +38,9 @@
|
||||
{
|
||||
"path": "../../session/session-projection"
|
||||
},
|
||||
{
|
||||
"path": "../../workspace/workspace"
|
||||
},
|
||||
{
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/subagent/subagent/README.md
|
||||
README.md: 4ed03104d3d8ab70816ee0cff7334a253196aef2
|
||||
README.zh.md: 92acf7498f282cdefea4f440d59e17f6dfffc81a
|
||||
README.md: adffd0edfdc55177afee0390c8d1c2f7fbee7999
|
||||
README.zh.md: 79d39ca879c8a15385e1e97c6556178b04897fca
|
||||
@@ -100,6 +100,7 @@ This section explains how the service is built and where the observable behavior
|
||||
| [`src/list-children.ts`](src/list-children.ts) | Direct parent-catalog reads and complete descendant-corpus reads |
|
||||
| [`src/control.ts`](src/control.ts) | Browser control request validation and stable failure codes |
|
||||
| [`src/control-types.ts`](src/control-types.ts) | Client-safe catalog row, control requests, receipts, and failures |
|
||||
| [`src/archive-admission.ts`](src/archive-admission.ts) | The `subagent` family of the Workspace registry's archive admission: running descendants and their parent-cause cancel |
|
||||
|
||||
### One-shot flow
|
||||
|
||||
@@ -117,6 +118,7 @@ Successful local child creation appends a `subagent/catalog` fact to the parent
|
||||
- **Registration is effect-scoped** — removing a provider blocks new starts but never revokes accepted runs.
|
||||
- **Agent-message authority is exact adjacency** — `sendMessage()` requires the exact live sender; every sender may target a direct continuable child, while only a sender with a resident continuable Activation may target its direct parent.
|
||||
- **The descriptor is log-only** — a session event absent from model history and retained across compaction; a continuable descriptor records the resolved child provider, model, and reasoning effort explicitly for cold resume.
|
||||
- **This runtime answers archive admission for children** ([seam](../../workspace/workspace/README.md)) — `workspace/session-activity` reports the live subagent descendants inside a turn as the `subagent` family, found by the durable lineage this package records (`parentSession` with the subagent origin, any depth, never a fork) and labelled from each child's descriptor through a live Session observation when the Session query service is composed, otherwise by id; `workspace/session-stop` cancels each of them with the parent cause, one at a time, so one child refusing its cancel is logged while its siblings still stop. The parent's own turn, its jobs, and the archived-lineage step gate belong to the API Session Controller.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ kind: "package-reference"
|
||||
| [`src/list-children.ts`](src/list-children.ts) | 直接 parent 目录读取与完整后代语料读取 |
|
||||
| [`src/control.ts`](src/control.ts) | 浏览器控制请求校验与稳定失败分码 |
|
||||
| [`src/control-types.ts`](src/control-types.ts) | client-safe 的目录行、控制面请求、回执与失败 |
|
||||
| [`src/archive-admission.ts`](src/archive-admission.ts) | Workspace 注册表归档准入中的 `subagent` 族:运行中的子孙及其父级取消 |
|
||||
|
||||
### 一次性流程
|
||||
|
||||
@@ -117,6 +118,7 @@ kind: "package-reference"
|
||||
- **注册受 effect 作用域约束**——移除提供方会阻止新启动,但绝不撤销已接受的运行。
|
||||
- **Agent 消息权限基于确切相邻关系**——`sendMessage()` 要求确切在线 sender;每个 sender 都可以指定直接可继续 child,只有具备驻留可继续 Activation 的 sender 可以指定自己的直接 parent。
|
||||
- **描述符仅进日志**——它是会话事件,不进入模型历史,并跨压缩(compaction)保留;可继续描述符会显式记录解析后的子级提供方、模型与推理强度,用于冷恢复。
|
||||
- **本 runtime 为子代理回答归档准入**([接缝](../../workspace/workspace/README.zh.md))——`workspace/session-activity` 把回合中的在线子代理子孙作为 `subagent` 族报告:按本包记录的持久化血缘查找(带 subagent 来源的 `parentSession`,任意深度,从不包括 fork),组合了 Session query 服务时经一次活会话 observation 从各 child 的描述符取名称,否则只报 id;`workspace/session-stop` 以父级原因逐个取消它们,一个拒绝取消的 child 只记日志,其兄弟仍会停止。父级自身的回合、它的任务以及已归档血缘的步骤门禁归 API Session Controller。
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -80,7 +80,8 @@
|
||||
"@deepseek-ai/dsh-typert-protocol": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-approval": "workspace:^",
|
||||
"@deepseek-ai/dsh-util-time": "workspace:^",
|
||||
"@deepseek-ai/dsh-permission-presets": "workspace:^"
|
||||
"@deepseek-ai/dsh-permission-presets": "workspace:^",
|
||||
"@deepseek-ai/dsh-workspace": "workspace:^"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@deepseek-ai/dsh-agent-preset-registry": {
|
||||
@@ -107,6 +108,9 @@
|
||||
"@deepseek-ai/dsh-session-query": {
|
||||
"optional": true
|
||||
},
|
||||
"@deepseek-ai/dsh-workspace": {
|
||||
"optional": true
|
||||
},
|
||||
"@deepseek-ai/dsh-jobs": {
|
||||
"optional": true
|
||||
},
|
||||
@@ -142,6 +146,7 @@
|
||||
"@deepseek-ai/dsh-typert-protocol": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-approval": "workspace:^",
|
||||
"@deepseek-ai/dsh-util-time": "workspace:^",
|
||||
"@deepseek-ai/dsh-permission-presets": "workspace:^"
|
||||
"@deepseek-ai/dsh-permission-presets": "workspace:^",
|
||||
"@deepseek-ai/dsh-workspace": "workspace:^"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* The `subagent` family of the Workspace registry's archive admission: which
|
||||
* subagent descendants of a Session are still inside a turn, and how they
|
||||
* stop when the Session is archived with its work.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-subagent
|
||||
*/
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type {} from '@deepseek-ai/dsh-session-query'
|
||||
import type { SessionActivity, SessionActivityItem } from '@deepseek-ai/dsh-workspace'
|
||||
import { foldSubagentDescriptor } from './descriptor.ts'
|
||||
|
||||
/**
|
||||
* Answer `workspace/session-activity` with the running subagent descendants of
|
||||
* the asked Session, and `workspace/session-stop` by cancelling each of them
|
||||
* as their parent would. Both listeners live as long as `ctx`'s fiber.
|
||||
* @param ctx - context carrying the Agent registry; the Session query service is optional and only supplies labels.
|
||||
*/
|
||||
export function installSubagentArchiveAdmission(ctx: Context): void {
|
||||
ctx.on('workspace/session-activity', async ({ sessionId }, next) => {
|
||||
const running = runningDescendants(ctx, sessionId)
|
||||
const rest = await next()
|
||||
if (running.length === 0) return rest
|
||||
const own: SessionActivity = { kind: 'subagent', items: await Promise.all(running.map(child => describe(ctx, child))) }
|
||||
return [own, ...rest]
|
||||
})
|
||||
ctx.on('workspace/session-stop', ({ sessionId }) => {
|
||||
for (const child of runningDescendants(ctx, sessionId)) {
|
||||
try {
|
||||
child.cancel({ kind: 'parent' })
|
||||
} catch (error: unknown) {
|
||||
// One child refusing its cancel must not keep its siblings running for an archived parent.
|
||||
ctx.logger.warn(`subagent: cancelling "${child.id}" for an archived Session failed: ${String(error)}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Live subagent descendants inside a turn, by durable lineage: a child whose
|
||||
* header names its parent and carries the subagent origin this package
|
||||
* records, at any depth. A fork shares the lineage field without the origin
|
||||
* and is an independent conversation, so it never holds its source. Lineage
|
||||
* is read as data, so a damaged header chain that loops is visited once.
|
||||
*/
|
||||
function runningDescendants(ctx: Context, rootId: SessionId): Agent[] {
|
||||
const childrenOf = new Map<SessionId, Agent[]>()
|
||||
for (const agent of ctx.agents.list()) {
|
||||
const { parentSession, origin } = agent.session.header
|
||||
if (parentSession === undefined || origin !== 'subagent') continue
|
||||
const siblings = childrenOf.get(parentSession) ?? []
|
||||
siblings.push(agent)
|
||||
childrenOf.set(parentSession, siblings)
|
||||
}
|
||||
const running: Agent[] = []
|
||||
const pending = [rootId]
|
||||
const visited = new Set<SessionId>()
|
||||
while (pending.length > 0) {
|
||||
const parentId = pending.shift() as SessionId
|
||||
if (visited.has(parentId)) continue
|
||||
visited.add(parentId)
|
||||
for (const child of childrenOf.get(parentId) ?? []) {
|
||||
if (child.status === 'running') running.push(child)
|
||||
pending.push(child.id)
|
||||
}
|
||||
}
|
||||
return running
|
||||
}
|
||||
|
||||
/**
|
||||
* The child's activity item: its id, plus the durable creation label its
|
||||
* descriptor carries, read through a live Session observation. Without the
|
||||
* Session query service, or with a descriptor that is absent or unreadable,
|
||||
* the item names the child by id alone.
|
||||
*/
|
||||
async function describe(ctx: Context, child: Agent): Promise<SessionActivityItem> {
|
||||
const query = ctx.get('sessionQuery')
|
||||
if (query === undefined) return { id: child.id }
|
||||
try {
|
||||
using observation = await query.observeSession(child.id, { projectionMode: 'none' })
|
||||
const label = foldSubagentDescriptor(observation.events.slice(observation.inheritedEventCount))?.label
|
||||
return label === undefined ? { id: child.id } : { id: child.id, label }
|
||||
} catch {
|
||||
// A damaged descriptor is data damage in the child; the report still names the child.
|
||||
return { id: child.id }
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import type { PromptContentPart } from '@deepseek-ai/dsh-attachment/types'
|
||||
import type { Branded } from '@deepseek-ai/dsh-brand'
|
||||
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
// Type-only: the Workspace registry's archive-admission family map this runtime merges `subagent` into.
|
||||
import type {} from '@deepseek-ai/dsh-workspace/types'
|
||||
|
||||
/**
|
||||
* Client-minted identity of one browser prompt, persisted on the exact accepted
|
||||
@@ -117,6 +119,13 @@ export interface SubagentInterruptReceipt {
|
||||
* Failure details the control surface answers with. Prompts and interrupts
|
||||
* share these failures with the Client Remote result.
|
||||
*/
|
||||
declare module '@deepseek-ai/dsh-workspace/types' {
|
||||
interface SessionActivityKindMap {
|
||||
/** A subagent session delegated from this session (at any depth) is inside a turn. */
|
||||
subagent: true
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/dsh-typert-protocol' {
|
||||
interface RemoteErrorDetailsMap {
|
||||
/** A browser-supplied zone is neither UTC nor a canonical IANA name. */
|
||||
|
||||
@@ -73,6 +73,7 @@ import SubagentContinuationManager from './continuation.ts'
|
||||
import type { SubagentDelivery } from './inbox.ts'
|
||||
import { listChildren as listSubagentChildren, listDescendants as listSubagentDescendants } from './list-children.ts'
|
||||
import type { SubagentDescendantListEntry } from './list-children.ts'
|
||||
import { installSubagentArchiveAdmission } from './archive-admission.ts'
|
||||
import { snapshotSubagentDescriptor } from './descriptor.ts'
|
||||
import { subagentIdentityProjectionDefinition, subagentTimingProjectionDefinition } from './projection.ts'
|
||||
import { establishCatalogChild, subagentCatalogProjectionDefinition } from './catalog.ts'
|
||||
@@ -230,6 +231,9 @@ export class SubagentRuntime extends TypertRemoteService {
|
||||
projections.register(subagentTimingProjectionDefinition)
|
||||
projections.register(subagentIdentityProjectionDefinition)
|
||||
})
|
||||
// Archive admission: this runtime is the owner that knows which live
|
||||
// children descend from a Session and how a parent stops them.
|
||||
ctx.inject(['agents'], (agentsCtx: Context) => { installSubagentArchiveAdmission(agentsCtx) })
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import AgentRegistry from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent, AgentCancelCause, AgentStatus, CancelOptions } from '@deepseek-ai/dsh-agent'
|
||||
import { unsupportedInbox } from '@deepseek-ai/dsh-agent-loop-testkit'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { Session } from '@deepseek-ai/dsh-session'
|
||||
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
|
||||
import type { SessionActivity } from '@deepseek-ai/dsh-workspace'
|
||||
import SubagentRuntime, { SUBAGENT_DESCRIPTOR_VERSION } from '../src/index.ts'
|
||||
import { TestSessionQuery } from './test-session-query.ts'
|
||||
|
||||
type CancelCall = [AgentCancelCause, CancelOptions | undefined]
|
||||
|
||||
/** A registered Agent double whose status the test flips and whose cancels it records. */
|
||||
type LiveAgent = Agent & { cancels: CancelCall[]; setStatus(status: AgentStatus): void }
|
||||
|
||||
async function harness(options: { sessionQuery?: boolean } = {}): Promise<Context> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SessionProjectionRegistry)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
if (options.sessionQuery !== false) await ctx.plugin(TestSessionQuery)
|
||||
await ctx.plugin(SubagentRuntime)
|
||||
return ctx
|
||||
}
|
||||
|
||||
async function liveAgent(ctx: Context, session: Session, initial: AgentStatus): Promise<LiveAgent> {
|
||||
const cancels: CancelCall[] = []
|
||||
let status = initial
|
||||
const agent: LiveAgent = {
|
||||
id: session.id,
|
||||
options: {},
|
||||
session,
|
||||
inbox: unsupportedInbox(),
|
||||
get status() { return status },
|
||||
ctx,
|
||||
send: () => {},
|
||||
followup: () => {},
|
||||
steer: () => {},
|
||||
inject: () => {},
|
||||
cancel: (cause, options) => { cancels.push([cause, options]) },
|
||||
runMaintenance: task => task(new AbortController().signal),
|
||||
whenIdle: () => Promise.resolve(),
|
||||
cancels,
|
||||
setStatus: (next) => { status = next },
|
||||
}
|
||||
await ctx.agents.register(agent)
|
||||
return agent
|
||||
}
|
||||
|
||||
/** A child session in the durable lineage this package records at delegation. */
|
||||
function childOf(ctx: Context, id: string, parent: SessionId, label?: string): Session {
|
||||
const child = ctx.sessions.create(SessionId(id), { meta: { parentSession: parent, origin: 'subagent' } })
|
||||
if (label !== undefined) {
|
||||
child.append('subagent/descriptor', { version: SUBAGENT_DESCRIPTOR_VERSION, mode: 'continuable', provider: 'spawn', label })
|
||||
}
|
||||
return child
|
||||
}
|
||||
|
||||
function ask(ctx: Context, sessionId: SessionId): Promise<readonly SessionActivity[]> {
|
||||
return ctx.waterfall('workspace/session-activity', { sessionId }, () => Promise.resolve([]))
|
||||
}
|
||||
|
||||
function stop(ctx: Context, sessionId: SessionId): Promise<void> {
|
||||
return ctx.parallel('workspace/session-stop', { sessionId })
|
||||
}
|
||||
|
||||
describe('Subagent archive admission: reporting', () => {
|
||||
it('reports running descendants by durable lineage at any depth, with their labels, but never forks', async () => {
|
||||
const ctx = await harness()
|
||||
const parent = ctx.sessions.create(SessionId('parent'))
|
||||
await liveAgent(ctx, parent, 'idle')
|
||||
const child = childOf(ctx, 'child', parent.id, 'reviewer')
|
||||
const childAgent = await liveAgent(ctx, child, 'idle')
|
||||
const grandchild = childOf(ctx, 'grandchild', child.id)
|
||||
await liveAgent(ctx, grandchild, 'running')
|
||||
const fork = ctx.sessions.create(SessionId('fork'), { meta: { parentSession: parent.id } })
|
||||
await liveAgent(ctx, fork, 'running')
|
||||
ctx.on('workspace/session-activity', async (_request, next) => [...(await next()), { kind: 'probe' }])
|
||||
|
||||
expect(await ask(ctx, parent.id)).toEqual([{ kind: 'subagent', items: [{ id: 'grandchild' }] }, { kind: 'probe' }])
|
||||
childAgent.setStatus('running')
|
||||
expect(await ask(ctx, parent.id)).toEqual([
|
||||
{ kind: 'subagent', items: [{ id: 'child', label: 'reviewer' }, { id: 'grandchild' }] },
|
||||
{ kind: 'probe' },
|
||||
])
|
||||
// A fork is an independent conversation: its own turn (reported by the
|
||||
// composed Agent registry) does not hold its source.
|
||||
expect(await ask(ctx, fork.id)).toEqual([{ kind: 'turn' }, { kind: 'probe' }])
|
||||
expect(await ask(ctx, SessionId('cold-or-unknown'))).toEqual([{ kind: 'probe' }])
|
||||
})
|
||||
|
||||
it('names a child by id alone without the Session query service or with a damaged descriptor', async () => {
|
||||
const queryless = await harness({ sessionQuery: false })
|
||||
const parent = queryless.sessions.create(SessionId('parent'))
|
||||
await liveAgent(queryless, parent, 'idle')
|
||||
await liveAgent(queryless, childOf(queryless, 'child', parent.id, 'reviewer'), 'running')
|
||||
expect(await ask(queryless, parent.id)).toEqual([{ kind: 'subagent', items: [{ id: 'child' }] }])
|
||||
|
||||
const ctx = await harness()
|
||||
const damagedParent = ctx.sessions.create(SessionId('damaged-parent'))
|
||||
await liveAgent(ctx, damagedParent, 'idle')
|
||||
const damaged = ctx.sessions.create(SessionId('damaged-child'), { meta: { parentSession: damagedParent.id, origin: 'subagent' } })
|
||||
// A current-version payload whose label is not a string fails the descriptor parse.
|
||||
damaged.append('subagent/descriptor', { version: SUBAGENT_DESCRIPTOR_VERSION, mode: 'continuable', provider: 'spawn', label: 7 } as never)
|
||||
await liveAgent(ctx, damaged, 'running')
|
||||
expect(await ask(ctx, damagedParent.id)).toEqual([{ kind: 'subagent', items: [{ id: 'damaged-child' }] }])
|
||||
})
|
||||
|
||||
it('visits a damaged lineage that loops only once', async () => {
|
||||
const ctx = await harness()
|
||||
// Two headers naming each other as subagent parents cannot be created by
|
||||
// delegation; a report over such data terminates instead of hanging.
|
||||
const first = ctx.sessions.create(SessionId('loop-a'), { meta: { parentSession: SessionId('loop-b'), origin: 'subagent' } })
|
||||
const second = ctx.sessions.create(SessionId('loop-b'), { meta: { parentSession: first.id, origin: 'subagent' } })
|
||||
await liveAgent(ctx, first, 'idle')
|
||||
await liveAgent(ctx, second, 'running')
|
||||
expect(await ask(ctx, first.id)).toEqual([{ kind: 'subagent', items: [{ id: 'loop-b' }] }])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Subagent archive admission: stopping', () => {
|
||||
it('cancels running descendants as their parent and leaves idle children and forks alone', async () => {
|
||||
const ctx = await harness()
|
||||
const parent = ctx.sessions.create(SessionId('stop-me'))
|
||||
const parentAgent = await liveAgent(ctx, parent, 'running')
|
||||
const childAgent = await liveAgent(ctx, childOf(ctx, 'stop-child', parent.id), 'running')
|
||||
const idleChildAgent = await liveAgent(ctx, childOf(ctx, 'idle-child', parent.id), 'idle')
|
||||
const fork = ctx.sessions.create(SessionId('stop-fork'), { meta: { parentSession: parent.id } })
|
||||
const forkAgent = await liveAgent(ctx, fork, 'running')
|
||||
|
||||
await stop(ctx, parent.id)
|
||||
await stop(ctx, SessionId('nobody'))
|
||||
expect(childAgent.cancels).toEqual([[{ kind: 'parent' }, undefined]])
|
||||
expect(idleChildAgent.cancels).toEqual([])
|
||||
expect(forkAgent.cancels).toEqual([])
|
||||
// The parent's own turn is the composed Agent registry's family, cancelled with the user cause.
|
||||
expect(parentAgent.cancels).toEqual([[{ kind: 'user' }, undefined]])
|
||||
})
|
||||
|
||||
it('keeps asking the remaining descendants to stop when one cancel throws', async () => {
|
||||
const ctx = await harness()
|
||||
const parent = ctx.sessions.create(SessionId('stubborn'))
|
||||
await liveAgent(ctx, parent, 'running')
|
||||
const stubborn = await liveAgent(ctx, childOf(ctx, 'stubborn-child', parent.id), 'running')
|
||||
stubborn.cancel = () => { throw new Error('child refuses cancel') }
|
||||
const quiet = await liveAgent(ctx, childOf(ctx, 'quiet-child', parent.id), 'running')
|
||||
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {})
|
||||
|
||||
await stop(ctx, parent.id)
|
||||
expect(quiet.cancels).toEqual([[{ kind: 'parent' }, undefined]])
|
||||
expect(warn.mock.calls.map(([message]) => String(message))).toEqual([
|
||||
expect.stringContaining('cancelling "stubborn-child"'),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Subagent archive admission: lifetime', () => {
|
||||
it('stops answering once the runtime fiber is disposed', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SessionProjectionRegistry)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
const fiber = await ctx.plugin(SubagentRuntime)
|
||||
const parent = ctx.sessions.create(SessionId('watched'))
|
||||
await liveAgent(ctx, parent, 'idle')
|
||||
const child = await liveAgent(ctx, childOf(ctx, 'watched-child', parent.id), 'running')
|
||||
expect(await ask(ctx, parent.id)).toEqual([{ kind: 'subagent', items: [{ id: 'watched-child' }] }])
|
||||
await fiber.dispose()
|
||||
expect(await ask(ctx, parent.id)).toEqual([])
|
||||
await stop(ctx, parent.id)
|
||||
expect(child.cancels).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
// The runtime knows only its own family; this suite merges a trailing one to observe ordering.
|
||||
declare module '@deepseek-ai/dsh-workspace/types' {
|
||||
interface SessionActivityKindMap {
|
||||
probe: true
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,9 @@
|
||||
{
|
||||
"path": "../../session-query/session-query"
|
||||
},
|
||||
{
|
||||
"path": "../../workspace/workspace"
|
||||
},
|
||||
{
|
||||
"path": "../../jobs/jobs"
|
||||
},
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/workspace/workspace/README.md
|
||||
README.md: 897aca8eb725bc9c3e0808e394e5c976886dd877
|
||||
README.zh.md: dc7a81340c9b134110975e700d43d5569fa6f03d
|
||||
README.md: 59b314a7302c5cf83314d08f6e8bdfe2dccba37f
|
||||
README.zh.md: 78d563ee59048d5b1b6f976ac26f64907174645d
|
||||
Loaded 100 of 109 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user