docs: telemetry subsystem page and Chinese counterparts for the nine new pages
telemetry.md joins the eight pages after the OTel telemetry seam landed on master (ctx.telemetry + telemetry/record were the one service and event scope still without a page). All nine pages gain zh counterparts translated whole-document per dsh-translate-docs; the eleven touched pairs (nine new + core + the subsystems-catalog note) are recorded; TelemetryRecord moves from TYPE_LINK_EXEMPTIONS to LINK_MAP and the catalogs are regenerated.
This commit is contained in:
6
docs/subsystems/client-modules.i18n.yaml
Normal file
6
docs/subsystems/client-modules.i18n.yaml
Normal file
@@ -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 docs/subsystems/client-modules.md
|
||||
client-modules.md: bd5ecdc85ea26b040e1705e373ceac4e7cb17b3c
|
||||
client-modules.zh.md: bbcdb04cdce8a0d5256d2490deb238812e9a47fc
|
||||
63
docs/subsystems/client-modules.zh.md
Normal file
63
docs/subsystems/client-modules.zh.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Client 模块
|
||||
|
||||
[English](client-modules.md) | 中文
|
||||
|
||||
Web 插件表:[dsh-client-modules](../../packages/client/modules) 中 client 模块系统的 Node 半,以 `ctx.clientModuleHost`(`ClientModuleHostService`)形式提供。它扫描宿主 Loader 的 entry 找出 `dshClient` 包(package),组合出 `window.__DSH_BOOT__` entry 图,在 `/plugins/<id>/client.js` 提供各个 bundle,并经 index 转换(index tap)注入启动 manifest(元数据清单)——这是同一个服务的四个面。它是 Web GUI 栈的一项可选能力,不属于 agent loop(智能体循环)主干,并且是 [dsh-host-webserver](../../packages/host/webserver) 的消费方:[http-server.md](http-server.md) 所述的载体提供本服务注册的前缀路由与 index 转换。同一个包的浏览器半(`ctx.modules`,即拉取并物化这些 bundle 的 lazy CJS 模块表)属于内核机件,记录在[包 README](../../packages/client/modules/README.md)中,不在本页。
|
||||
|
||||
源码:[`packages/client/modules/src/client/manifest.ts`](../../packages/client/modules/src/client/manifest.ts)
|
||||
|
||||
## wire
|
||||
|
||||
图是 Node 半与浏览器半之间的线上单源:宿主从扫描到的包组合出 `WebBootEntry` 行,把图作为 `<head>` 中的第一个脚本注入(`window.__DSH_BOOT__`,其中 `<` 已转义,插件可控的字符串因此无法逃出 script 元素),壳则在启动任何东西之前先解析它。没有有效 manifest 的页面无法启动——浏览器侧的解析器在图缺失或畸形时大声抛错。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One composed client entry pushed by the host (web2 §0 graph row). Wire
|
||||
* single source: the host node half (package root) produces this same shape.
|
||||
* `immediately` marks stage-one prefetch; `inject` is informational graph
|
||||
* metadata (the authoritative edges live in each package's dshClient
|
||||
* declaration and reach fibers through entry creation).
|
||||
*/
|
||||
interface WebBootEntry {
|
||||
/** Entry name == package name. */
|
||||
id: string
|
||||
/** Bundle endpoint, '/plugins/<id>/client.js?rev=<rev>'. */
|
||||
url: string
|
||||
/** Bundle content hash (cache-busting consistency anchor). */
|
||||
rev: string
|
||||
/** Package-name dependency edges, informational (preflight display / HMR diffing). */
|
||||
inject?: string[]
|
||||
/** Stage-one prefetch mark: fetch + execute (factory registration) during module-face boot. */
|
||||
immediately?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** The composed client entry graph the host injects as `window.__DSH_BOOT__`. */
|
||||
interface WebBootGraph {
|
||||
/** Consistency anchor over the whole graph (content + bundle hashes). */
|
||||
rev: string
|
||||
/** Composed entries; order carries no semantics (activation order is fiber inject waiting). */
|
||||
entries: WebBootEntry[]
|
||||
}
|
||||
```
|
||||
|
||||
每一行的 `rev` 是该 bundle 的内容哈希,并作为使缓存失效的查询参数附在 URL 上;图的 `rev` 对组合后的各行做哈希,因此任何一行的变化都会改变它。`immediately` 标记第一阶段预取档位(在模块面启动期间 fetch 并执行,只做工厂登记);惰性行在首次 import 时才拉取。
|
||||
|
||||
## 扫描
|
||||
|
||||
包加入这张表的方式,是在自己的 package.json 中声明 `dshClient`(`platform: 'web'`、可选的 `inject` 边、可选的 `immediately`),并在 `exports["./client"]` 导出构建好的 bundle。包解析锚定在配置树的 `ctx.baseUrl`——即 cordis.yml 所在目录,该目录的包把每个被组合的插件声明为依赖——这一锚点未设置时,构造即抛错。
|
||||
|
||||
扫描是单包增量的;不存在全量重扫代码路径。每次 cordis `internal/plugin` 发射都把该 fiber 的 entry 名标脏,一次微任务 flush 把每个脏名与实时 loader entry 对账。激活趟以全部当前 entry 灌入同一个脏集合并同步 flush,因此初扫与稳态共享一条实现——但失败姿态相反。激活时,已加载 entry 中的畸形声明或缺失 bundle 会聚合为一个大声的 `AggregateError`,列出每个损坏的包:该 fiber 进入 FAILED,由启动的大声失败 sweep 上报。稳态下,损坏的包只记录一条警告,且不得殃及其他包。
|
||||
|
||||
包元数据——包括「非 client 包」这一否定结论——按名缓存且永不过期:插件集合的变更在重启后生效。fiber 重启原样复用其行与 rev;bundle 内容变更只经 `rebuilt()` 到达图。
|
||||
|
||||
## 路由
|
||||
|
||||
`GET`/`HEAD /plugins/<id>/client.js` 以 `no-cache` 从磁盘提供已注册的 bundle(锚定一致性的是 rev 查询参数,而非 HTTP 缓存);其他方法返回 405。未知 id——或已注册、但 bundle 因尚未构建而不可读的行——回应一个大声的 404,而不是让载体的 SPA 回退把 HTML 当作 JavaScript 发出。index 转换在每次 index 渲染时注入当前图,因此刷新页面总是针对实时组合启动。
|
||||
|
||||
## 服务
|
||||
|
||||
`ClientModuleHostService`(`ctx.clientModuleHost`,定义于 [`packages/client/modules/src/index.ts`](../../packages/client/modules/src/index.ts))暴露读取面与重建面;签名见生成的[服务目录](../cordis-catalog/services.md#ctxclientmodulehost--clientmodulehostservice)。`graph()` 返回当前组合出的图(两次变更之间是同一个稳定对象),`clientPath(id)` 返回该 bundle 的绝对路径。`rebuilt(id)` 是 bundle 内容到达图的唯一入口:它对文件重新哈希,只有 rev 真正变化才会重新组合图并发出通知。`onRebuilt` 按发生变化的 bundle 逐个触发并携带新 rev;`onGraphChanged` 在任何一次重新组合了图的 flush 之后触发(行的增删,或 rebuilt 带来的 rev 变化),并采用拉取模型——监听器自行重读 `graph()`。两条通知路径都会兜住监听器异常,因此一个抛错的订阅者既不能让后续订阅者被跳过,也不能杀死触发这次 flush 的一方。
|
||||
|
||||
开发环境下,[dsh-client-hmr](../../packages/client/hmr/README.md) 是注册表的监视驱动:它的 Node 半从同步取得的基线出发,对图中每一行的 bundle 做 stat 轮询,变化时调用 `rebuilt(id)`,经 `onGraphChanged` 重新同步监视集合,并通过 SSE(Server-Sent Events)把 rev 变化广播给浏览器半。生产环境的图完全不含 HMR(热模块替换)行;模块宿主自身从不监视文件。
|
||||
@@ -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/core.md
|
||||
core.md: 8f413a7a064ad6f63e0caec31354869e51139020
|
||||
core.zh.md: d0f02f0cfc2cd30fc67aacf5b17d1daf324295c5
|
||||
core.md: 1d0bf6c61500817860dfdd452bddc25adbbeffcc
|
||||
core.zh.md: 06fd3b5a48d5267c9856c20589772047b6105b2c
|
||||
|
||||
@@ -54,6 +54,7 @@ Everything else is documented on a **sub-page**, not here. The rule that draws t
|
||||
| [tui.md](tui.md) | the terminal-extension seam: `TuiOverlayRequest`/`Host`/`Session`, close reasons and outcomes, the modal queue |
|
||||
| [workspace.md](workspace.md) | the workspace registry: `Workspace`/`WorkspaceId`, registration and resolution, the session `cwd` relationship |
|
||||
| [client-modules.md](client-modules.md) | the web plugin table: `dshClient` declarations, `WebBootGraph` wire composition, the bundle route and index tap |
|
||||
| [telemetry.md](telemetry.md) | the outbound reporting seam: `TelemetryRecord`/`TelemetrySeverity`, the `TelemetryBackend` contract, the `telemetry/record` redact waterfall |
|
||||
|
||||
> Type declarations and their JSDoc on these pages are source-equivalent and drift-checked by `pnpm run verify-type-equiv` (see [development.md](../development.md#documenting-types-verbatim-ts-type-equiv)). Ordinary blocks preserve complete declarations; `public-api` blocks preserve body-stripped public class declarations. Cordis services use the generated [service catalog](../cordis-catalog/services.md).
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ harness 是一个微内核:一个极小的核心加上众多插件。大多数
|
||||
| [tui.md](tui.md) | 终端扩展 seam:`TuiOverlayRequest`/`Host`/`Session`、关闭原因与结果、模态队列 |
|
||||
| [workspace.md](workspace.md) | 工作区注册表:`Workspace`/`WorkspaceId`、注册与解析、与会话 `cwd` 的关系 |
|
||||
| [client-modules.md](client-modules.md) | Web 插件表:`dshClient` 声明、`WebBootGraph` 线上组合、bundle 路由与 index 转换 |
|
||||
| [telemetry.md](telemetry.md) | 对外上报 seam:`TelemetryRecord`/`TelemetrySeverity`、`TelemetryBackend` 契约、`telemetry/record` 脱敏 waterfall |
|
||||
|
||||
> 这些页面上的类型声明及其 JSDoc 与源码等价,并由 `pnpm run verify-type-equiv` 检查漂移(见 [development.md](../development.md#documenting-types-verbatim-ts-type-equiv))。普通块保留完整声明;`public-api` 块保留去除实现体的公开 class 声明。Cordis 服务使用生成的[服务目录](../cordis-catalog/services.md)。
|
||||
|
||||
|
||||
6
docs/subsystems/http-server.i18n.yaml
Normal file
6
docs/subsystems/http-server.i18n.yaml
Normal file
@@ -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 docs/subsystems/http-server.md
|
||||
http-server.md: 565df72f2720a1211f50650d4b1bc8d6df4a007a
|
||||
http-server.zh.md: 93b056648f044f6c79b85a6069deb3123f9a7a3e
|
||||
49
docs/subsystems/http-server.zh.md
Normal file
49
docs/subsystems/http-server.zh.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# HTTP 服务器
|
||||
|
||||
[English](http-server.md) | 中文
|
||||
|
||||
[dsh-host-webserver](../../packages/host/webserver) 是 GUI 宿主 web 形态的 HTTP 载体:单个提供 `ctx.httpServer` 的 `node:http` 插件,由具名路由注册表加 index.html 转换挂点组成,兜底是静态 dist 回退。它不属于 agent loop(智能体循环)主干,也不是能力 seam:它不了解任何 harness 概念,每个功能表面(`/api` 桥接、插件 bundle、HMR(热模块替换)事件流)都是由其他插件注册的一条路由([分层说明](../../.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md))。仅限 web(浏览器)形态:Electron 通过 `file://` 加载 dist,并经 IPC 桥接承载 fetch,不经过本服务器。
|
||||
|
||||
源码:[`packages/host/webserver/src/index.ts`](../../packages/host/webserver/src/index.ts)
|
||||
|
||||
## 路由
|
||||
|
||||
```ts type-equiv
|
||||
/** Route match kind: 'exact' matches the pathname verbatim; 'prefix' p matches p and p/<anything>. */
|
||||
type WebRouteKind = 'exact' | 'prefix'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** One named route registration. */
|
||||
interface WebRoute {
|
||||
kind: WebRouteKind
|
||||
/** Absolute pathname, no trailing slash. */
|
||||
path: string
|
||||
/** Owns the full response lifecycle (may hold the response open, e.g. SSE). */
|
||||
handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
匹配顺序固定:先查 exact 表,再取最长匹配前缀,最后落到静态 dist 回退。注册顺序不携带任何面向请求的语义:具名路由在组合上互不相交,启动窗口期内尚未被认领的请求全部由回退应答。回退保持锁定语义:非 GET/HEAD 返回 405,越出 dist 根目录的路径穿越返回 403,任何未命中都以 HTTP 200 回退到 `index.html`(SPA 路由),未知扩展名按 octet-stream 交付([`static.ts`](../../packages/host/webserver/src/static.ts))。
|
||||
|
||||
## 配置
|
||||
|
||||
```ts type-equiv
|
||||
/** Gateway config: listen address plus the static dist anchor (injected by the composing app, never self-resolved). */
|
||||
interface Config {
|
||||
/** Listen host; the two supported values are loopback and all-interfaces. */
|
||||
host: '127.0.0.1' | '0.0.0.0'
|
||||
/** Listen port; zero requests an OS-assigned port. */
|
||||
port: number
|
||||
/** Absolute path of index.html inside the static root (dist location is workspace knowledge of the app). */
|
||||
distIndex: string
|
||||
}
|
||||
```
|
||||
|
||||
`host` 只接受 `127.0.0.1`(默认姿态)和 `0.0.0.0`(刻意的网络暴露);没有 TLS、认证或 origin 策略,因此绑定到非回环地址会把服务器暴露给该网络。`distIndex` 是组合应用解析后注入的组装事实。
|
||||
|
||||
## 服务
|
||||
|
||||
`HttpServerService`(`ctx.httpServer`)在激活时立即监听;监听失败(EADDRINUSE 等)会从 init 抛出,形成一个 FAILED fiber,由启动阶段的大声失败巡检报告。`register(route)` 添加一条具名路由并返回其 disposer;重复的 `(kind, path)` 抛出异常,因为路由模式是组合层面的契约,冲突即配置错误。`tapIndex(transform)` 添加一个纯的 html 到 html 转换,按注册顺序应用于每个 index 响应(`/` 和每次 SPA 回退);[dsh-client-modules](../../packages/client/modules) 用它注入启动 manifest(元数据清单)。`port` 读取监听端口,`config.port` 为 0 时读到的是操作系统分配的值。
|
||||
|
||||
处理过程中抛出异常的请求(畸形的 % 转义撞上 `decodeURIComponent`、客户端在请求体中途断开)会记录为警告并应答 400(响应头已发出时则销毁 socket),绝不导致进程退出。dispose(资源释放)把 `close()` 与 `closeAllConnections()` 配对使用,因为处理器可能保持响应打开(如 SSE(Server-Sent Events)),而这类连接永远不会自行结束;没有强制关闭,拆卸就会挂起。该包从不打印输出:URL 行归 shell 所有。逐包运维细节(含开发模式的 bundle 监视流水线)留在 [README](../../packages/host/webserver/README.md) 中。
|
||||
6
docs/subsystems/invariants.i18n.yaml
Normal file
6
docs/subsystems/invariants.i18n.yaml
Normal file
@@ -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 docs/subsystems/invariants.md
|
||||
invariants.md: 143a7feabbab18556f595a78c7aef17caa6d3028
|
||||
invariants.zh.md: ab7aa0221101400d552ff47e1cfa3275d99a0b6d
|
||||
59
docs/subsystems/invariants.zh.md
Normal file
59
docs/subsystems/invariants.zh.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# 运行时不变式
|
||||
|
||||
[English](invariants.md) | 中文
|
||||
|
||||
[dsh-invariants](../../packages/support/invariants) 是面向包(package)自有运行时不变式检查的可配置注册表服务(`ctx.invariants`)。它是一个 support 组的包,不是三包能力 seam,也不属于 agent loop(智能体循环)主干:注册表拥有选择逻辑、名称保留、子 fiber 生命周期和归因到包的失败,而每个工作区包发布一个 `./invariant` 伴随插件,以自己确切的 npm 包名注册检查。检查可以断言什么(权威事件流或可变数据,绝不是服务或方法是否存在)是 [AGENTS.md](../../AGENTS.md#conventions) 中的运行时不变式约定;seam 设计由[不变式服务 Agent Note(agent 决策记录)](../../.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.md)规定。
|
||||
|
||||
源码:[`packages/support/invariants/src/index.ts`](../../packages/support/invariants/src/index.ts)
|
||||
|
||||
## 选择
|
||||
|
||||
```ts type-equiv
|
||||
/** Runtime invariant selection configured on the service plugin. */
|
||||
interface Config {
|
||||
/** Global switch; defaults to `true`. */
|
||||
readonly enabled?: boolean
|
||||
/** Case-sensitive JavaScript regex sources that admit package names; empty admits all. */
|
||||
readonly package_allowlist?: string[]
|
||||
/** Case-sensitive JavaScript regex sources that exclude package names after allowlist matching. */
|
||||
readonly package_blocklist?: string[]
|
||||
}
|
||||
```
|
||||
|
||||
一个包被选中的条件是:服务已启用,允许列表为空或至少一个模式匹配其完整 npm 名称,且没有任何阻止列表模式匹配;阻止列表匹配优先于允许列表匹配。条目用 `new RegExp(source)` 编译:除非模式自带 `^` 和 `$`,匹配不锚定;`/pattern/flags` 语法不被解析。校验在服务启动时大声失败:空白、带空白填充、重复或无效的条目会抛出异常,而不是被跳过。有效模式可以不匹配任何当前已加载的包,因此后续加载与 HMR(热模块替换)保持确定性;过滤器在服务生命周期内固定不变([README](../../packages/support/invariants/README.md))。
|
||||
|
||||
## 安装器
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Throw a package-attributed invariant failure.
|
||||
* @param message - violated package contract without the standard prefix.
|
||||
* @returns never because reporting a violation throws.
|
||||
*/
|
||||
type InvariantFailure = (message: string) => never
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Install one package's checks into the registration's child context. */
|
||||
interface InvariantInstaller {
|
||||
/**
|
||||
* Install the package contribution.
|
||||
* @param ctx - child context owned by this invariant registration.
|
||||
* @param fail - reporter bound to the registering package name.
|
||||
* @returns nothing, or a promise settling after asynchronous checks finish.
|
||||
*/
|
||||
(ctx: Context, fail: InvariantFailure): void | Promise<void>
|
||||
/** Services the child installer fiber may access. */
|
||||
readonly inject?: Inject
|
||||
}
|
||||
```
|
||||
|
||||
被启用的安装器在专属的子 Cordis fiber 中运行;`installer.inject` 声明该 fiber 可以访问的服务,注册成功之前会先等待安装器同步或异步地执行完毕。`fail(message)` 抛出 `InvariantError`(`extends Error`,带稳定的 `code: 'INVARIANT'`、所属 `packageName`,以及前缀为 `invariant violated by "<package>": …` 的消息),因此违规可归因,而注册表无需导入任何产品包。
|
||||
|
||||
## 服务
|
||||
|
||||
`ctx.invariants.register(packageName, installer)` 为完整 npm 包名保留唯一一个活跃注册,并返回其绑定到 effect 的 disposer。即使过滤器使安装器保持不活跃,保留依然成立,因此两个插件绝不可能静默认领同一个包名;重复、空白或含空白字符的名称会抛出异常。安装器失败会原子地 dispose(资源释放)子 fiber 并释放保留。服务拥有每个注册 fiber,而返回的 disposer 同时属于伴随插件的 fiber:卸载任一侧都会移除监听器、trace 状态和保留项,因此伴随插件可以重载并再次注册同一名称,不留残余状态。
|
||||
|
||||
## 伴随插件契约
|
||||
|
||||
每个工作区包都拥有一个 `./invariant` 伴随插件([包契约](../../packages/AGENTS.md));发布与注册是穷尽式的,但断言刻意不为凑数而虚构。只有当包对某个可观察事件或可变数据拥有所有权关系时,伴随插件才安装检查;否则它导出一个空安装器,其起始注释以 `No runtime invariant:` 开头,针对该包具体解释为什么没有可检查项。`pnpm run verify-package-invariants` 机械地拒绝「生成文件」标记、无解释的空安装器、遗漏或忽略报告器的非空安装器、错误的注册名称,以及不完整的导出、发布、依赖或打包接线([机械规则 Agent Note](../../.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md))。可执行伴随插件的目录与标准组合方式见[包 README](../../packages/support/invariants/README.md)。
|
||||
6
docs/subsystems/permission.i18n.yaml
Normal file
6
docs/subsystems/permission.i18n.yaml
Normal file
@@ -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 docs/subsystems/permission.md
|
||||
permission.md: 49dd51ff86d0dbce26fca0ed21527f71bb144dec
|
||||
permission.zh.md: 5fb93e9f923582e6c530bac4a22e2ce62cff156d
|
||||
63
docs/subsystems/permission.zh.md
Normal file
63
docs/subsystems/permission.zh.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# 权限预设
|
||||
|
||||
[English](permission.md) | 中文
|
||||
|
||||
[dsh-permission](../../packages/ui/permission) 的权限预设层(`ctx.permission`,`PermissionService`)把两个相互独立的强制执行旋钮,即[沙箱模式](sandbox.md)(`sandbox/mode`)与[审批策略](approval.md)(`approval/policy`),捆绑成具名预设,供客户端作为单个权限(Permissions)选择器提供。它是一项可选能力,不属于 agent loop(智能体循环)主干,也不拥有任何强制执行:执行、提示词叙述与回放仍然读取各自旋钮的折叠结果,预设切换只记录意图,并通过每个旋钮各自的规范 setter 写入。[包(package)README](../../packages/ui/permission/README.md) 负责组合状态与限制;[沙箱切换设计](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)负责决策依据。
|
||||
|
||||
源码:[`packages/ui/permission/src/index.ts`](../../packages/ui/permission/src/index.ts)
|
||||
|
||||
## 预设表
|
||||
|
||||
预设是一个表键,映射到一组沙箱/审批组合,外加可选的客户端展示信息;默认预设表自带 `workspace-write`(`workspace-write` + `ask`)和 `danger-full-access`(`danger-full-access` + `never`)。
|
||||
|
||||
```ts type-equiv
|
||||
/** One preset's sandbox/approval bundle and optional client presentation. */
|
||||
interface PresetSpec {
|
||||
/** The `sandbox/mode` value the preset writes through. */
|
||||
sandbox: SandboxMode
|
||||
/** The `approval/policy` value the preset writes through. */
|
||||
approval: ApprovalPolicy
|
||||
/** The display label a client shows for this preset; the raw table key when omitted. */
|
||||
name?: string
|
||||
/** One user-facing sentence on what the preset means; omitted when not configured. */
|
||||
description?: string
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** The {@link PermissionService} config: the deployment's preset table. */
|
||||
interface Config {
|
||||
/**
|
||||
* The preset table: name → knob bundle. Defaults to `workspace-write`
|
||||
* (workspace-write + ask) and `danger-full-access` (danger-full-access +
|
||||
* never). The name `custom` is reserved for the derived not-a-preset state.
|
||||
*/
|
||||
presets?: Record<string, PresetSpec>
|
||||
}
|
||||
```
|
||||
|
||||
该服务要求一个施加沙箱隔离的 `ctx.bash` 执行器和 `ctx.approval`,配置错误在插件加载时即失败:名为 `custom` 的表项会抛出异常(该名称保留给派生的「非预设」状态);在不施加隔离的 bash 执行器(没有 `sandboxMode` 能力事实)之上组合同样抛出异常,因为预设捆绑了一个沙箱模式。
|
||||
|
||||
## 当前预设与派生的 `custom`
|
||||
|
||||
`current(events)` 从旋钮派生实际生效的预设,而不是只看自身事件:它折叠会话的生效沙箱模式(回退到执行器配置的模式)与生效审批策略(先回退到审批服务配置,再回退到 `ask`),优先取仍然匹配的已记录选择,其次取声明顺序中第一个匹配的表项,否则返回 `CUSTOM_PRESET`(`'custom'`)。`custom` 只是派生值:客户端可以把它显示为当前值,但它绝不是切换目标,也绝不出现在事件 payload 中。
|
||||
|
||||
`names` 按预设表声明顺序列出可切换的预设;`optionOf(name)` 为某个表键(label 回退为该键)或 `custom` 构建客户端渲染的选项,传入其他任何名称都会抛出异常。
|
||||
|
||||
```ts type-equiv
|
||||
/** The select-option shape a presentation layer advertises for one preset (or for the derived `custom` state). */
|
||||
interface PresetOption {
|
||||
/** Stable option value: the table key, or `custom`. */
|
||||
value: string
|
||||
/** The display label. */
|
||||
name: string
|
||||
/** One user-facing sentence on what the value means. */
|
||||
description?: string
|
||||
}
|
||||
```
|
||||
|
||||
## 切换与 `permission/preset` 事件
|
||||
|
||||
`set(session, name)` 解析预设(未知名称抛出异常),在 `name` 尚不是生效预设时追加一条仅记日志的 `permission/preset` 事件,然后通过各旋钮自己的 setter([dsh-sandbox-policy](../../packages/sandbox/sandbox-policy) 的 `setSandboxMode` 与 [dsh-user-approval](../../packages/ui/user-approval) 的 `setApprovalPolicy`)写入,且仅当该旋钮的生效值发生变化时才写。同一轮次内,选择事件先于旋钮事件出现;重新选择当前生效的预设则什么都不追加。
|
||||
|
||||
`permission/preset` 是持久、仅记日志的用户意图:它不进入模型 transcript(文本记录),模型可见的后果由旋钮事件经各自消费方承担;它存在是为了在两个预设共享同一个旋钮组合时,让 `current()` 仍能保住用户选择的究竟是哪一个预设;`effectivePermissionPreset(events)` 折叠最后一条,回放不需要任何追赶状态。完整事件声明见[持久化日志事件目录](../persistence-catalog.md);方法签名见生成的[服务目录](../cordis-catalog/services.md#ctxpermission--permissionservice)。
|
||||
6
docs/subsystems/plan.i18n.yaml
Normal file
6
docs/subsystems/plan.i18n.yaml
Normal file
@@ -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 docs/subsystems/plan.md
|
||||
plan.md: f67e7734ca3fd55c284d3d41227f8241de7b9f5f
|
||||
plan.zh.md: 565cf2016636acb6c26d20c731b9e6e374cee1da
|
||||
39
docs/subsystems/plan.zh.md
Normal file
39
docs/subsystems/plan.zh.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# 计划模式
|
||||
|
||||
[English](plan.md) | 中文
|
||||
|
||||
计划模式是 [dsh-plan-mode](../../packages/plan/plan-mode) 拥有的、记录到日志的逐 agent(智能体)协作状态(`ctx.planMode`,`PlanModeService`):激活期间,一段部署持有的指引段落会影响每个模型请求。它是**软性指引**,有意独立于[沙箱模式](sandbox.md)与[审批策略](approval.md)这两条强制执行轴:那些旋钮(knob)从不读写计划状态,需要硬边界的部署另行组合两者。该包(package)是一项可选能力,不属于 agent loop(智能体循环)主干;它的对外表面是 `plan:policy` 提示词段落、始终保持注册的 `exit_plan_mode` 工具和 `/plan` 命令。[设计说明](../../.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.md)负责决策依据;[包 README](../../packages/plan/plan-mode/README.md) 负责模型体验与限制细节。
|
||||
|
||||
源码:[`packages/plan/plan-mode/src/index.ts`](../../packages/plan/plan-mode/src/index.ts)
|
||||
|
||||
## 已记录状态与恢复
|
||||
|
||||
`plan/mode`(`{ active: boolean }`)是仅记日志、整值替换的[会话事件](session.md):持久且可回放,绝不进入模型 transcript(文本记录)。`foldPlanMode(events, end?)` 返回前缀中最后一条已记录值,没有时返回 `false`:生效状态始终是会话日志的纯折叠,因此恢复、fork 与压缩(compaction)无需实时镜像即可将其复原,UI 通过 `session/event` 观察已提交的切换。完整事件声明见[持久化日志事件目录](../persistence-catalog.md)。
|
||||
|
||||
## 待定意图与轮次边界冲刷
|
||||
|
||||
由于每个会话事件都位于轮次之内,用户的选择会作为待定意图保留到轮次边界。`set(agent, active)` 记录待定选择(目标值与已记录或已在待定中的状态相同时不做任何事),`get(agent)` 返回 `{ active: boolean; pending?: boolean }`,即影响当前步骤的已记录状态,加上正在等待边界的乐观选择。
|
||||
|
||||
服务会在三类边界处、于受影响的请求组装之前冲刷一条待定选择:提示词提交、普通轮次延续和请求恢复重试。冲刷在下游监听器链之后运行,因此在某个异步监听器等待期间到达的选择,仍会影响该边界紧接其后的那个请求。冲刷失败会被隔离(计划策略绝不能阻塞提示词或轮次),追加失败的选择保持待定,等待后续边界。已冲刷的用户选择还会以一条插件来源的 `user/message` 通知叙述这次切换,但仅当最后记录的请求头描述的是另一种状态时才叙述,因此模型恰好在上下文变化时被告知,且绝不重复。空闲时做出的待定选择只存在于进程内,进程在下一个边界之前退出即丢失([README 限制](../../packages/plan/plan-mode/README.md#known-limitations-and-deferred-work))。
|
||||
|
||||
## 配置
|
||||
|
||||
```ts type-equiv
|
||||
/** Deployment-owned plan guidance. */
|
||||
interface PlanModeConfig {
|
||||
/** Guidance rendered as the `plan:policy` prompt section while plan mode is active. */
|
||||
section: string
|
||||
}
|
||||
```
|
||||
|
||||
`section` 缺失、为空白或不是字符串,以及任何未知键,都会在插件加载时失败,而不是静默地什么都不影响。计划模式激活期间,确切的 `section` 文本以 order 50 渲染为 `plan:policy` [系统提示词段落](system-prompt.md);未激活的计划模式不贡献任何文本。
|
||||
|
||||
## 退出工具与 `/plan` 命令
|
||||
|
||||
[`exit_plan_mode`](../tool-catalog.md#deepseek-aidsh-plan-mode) 在计划模式未激活时仍保持注册,因此跨越边界只改变提示词段落,绝不改变请求的工具目录;在计划模式之外执行会失败。在计划模式中,它要求一份以 `#` 标题开头的完整 markdown 计划,并通过[用户交互 seam](user-interaction.md) 呈交评审。批准返回 `{ approved: true }`,并记录一个静默(不叙述)的待定退出,在该步骤之后冲刷:计划指引在 assistant 本批工具调用的剩余部分继续生效,而工具结果本身叙述这次转换。「继续规划」则是一次携带用户反馈的失败调用,模型据此修订并再次呈交;评审期间交互通道缺失或服务重载同样使调用失败,而不是静默离开计划模式。
|
||||
|
||||
当 [`ctx.commands`](commands.md) 被组合时,插件注册 `/plan [off|message]`:单独的 `/plan` 选择计划模式;任何其他非空消息先选择计划模式,再通过 `agent.steer()` 提交该文本,使其在计划指引下成为下一步骤的普通已记录用户消息;确切参数 `off` 选择未激活,这还会在计划模式尚未进入任何请求之前,取消尚未冲刷的待定条目。
|
||||
|
||||
## 服务
|
||||
|
||||
`ctx.planMode` 拥有已记录的计划状态、边界处的应用与叙述、`plan:policy` 段落、`/plan` 命令和稳定注册的退出工具;`get`/`set` 签名见生成的[服务目录](../cordis-catalog/services.md#ctxplanmode--planmodeservice)。
|
||||
6
docs/subsystems/storage.i18n.yaml
Normal file
6
docs/subsystems/storage.i18n.yaml
Normal file
@@ -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 docs/subsystems/storage.md
|
||||
storage.md: 5d54d7337b5ef52e24179758130b9fa8cd196376
|
||||
storage.zh.md: c575f989fe4b3536ce3c8d3b3af08ebda418166d
|
||||
125
docs/subsystems/storage.zh.md
Normal file
125
docs/subsystems/storage.zh.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# 存储
|
||||
|
||||
[English](storage.md) | 中文
|
||||
|
||||
存储子系统持久保存一切不属于会话事件日志的数据(会话日志有自己的 seam——见 [persistence.md](persistence.md))。它是一项可选能力,不属于 agent loop(智能体循环)主干,并按[能力 seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)拆分:枢纽(hub)与后端接口([dsh-storage](../../packages/storage/storage),`ctx.storage`)、后端实现(注册为 `json` 的 [dsh-storage-json](../../packages/storage/storage-json) 与注册为 `sqlite` 的 [dsh-storage-sqlite](../../packages/storage/storage-sqlite)),以及领域数据形式([dsh-storage-domain](../../packages/storage/storage-domain),`ctx.storageDomain`,也可经 `ctx.storage.domain` 访问)——它是后端 seam 的唯一消费方,也是其他一切所使用的类型化 API。枢纽自身不做任何 IO:后端拥有介质,数据形式拥有语义,产品包(package)绝不直接触碰后端。设计记录:[领域 KV 存储 Agent Note(agent 决策记录)](../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md)。
|
||||
|
||||
源码:[`packages/storage/storage/src/backend.ts`](../../packages/storage/storage/src/backend.ts) · [`packages/storage/storage-domain/src/spec.ts`](../../packages/storage/storage-domain/src/spec.ts) · [`packages/storage/storage-domain/src/events.ts`](../../packages/storage/storage-domain/src/events.ts)
|
||||
|
||||
## 枢纽:`ctx.storage`
|
||||
|
||||
`Storage`([签名](../cordis-catalog/services.md#ctxstorage--storage))是汇合点,不是存储本体。`ctx.storage.backend` 是一张名称 → 后端的表:多个后端并排保持挂载,哪个后端服务哪个消费方由该消费方自己的配置决定(即领域层的路由表),绝不是枢纽全局的选择。`register(name, backend)` 返回 disposer;重复名称与查找未知名称都抛出 `StorageError`。dispose(资源释放)只注销名称——由拥有插件在注销之后自行关闭后端。每个后端插件还会发布一个仅用于生命周期的服务键(`storageBackendServiceKey(name)`),数据形式提供方注入它,使自身激活不会与后端注册发生竞态。
|
||||
|
||||
数据形式以一张可合并扩展的键 map 挂载到枢纽上:
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Data forms mountable on the hub, keyed by form name. Form owners extend
|
||||
* this map via declaration merging (the domain layer merges
|
||||
* `domain: DomainFacility`) and mount the facility in their `apply`.
|
||||
*/
|
||||
interface StorageForms {}
|
||||
```
|
||||
|
||||
`mount(form, facility)` 是一个 effect,其 disposer 负责卸载;对同一键的第二次挂载抛出 `duplicate-mount`。`form(form)` 解析已挂载的 facility,在拥有插件加载之前抛出 `form-not-mounted`——组合方应据此安排插件顺序,而不是静默推迟。领域层合并 `domain: DomainFacility`,因此 `ctx.storage.domain` 与 `ctx.storageDomain` 是同一个对象。
|
||||
|
||||
## 后端 seam
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One registered backend. A backend owns exactly one medium and shares its
|
||||
* lifecycle across all facets; facets are optional members — a backend that
|
||||
* cannot serve a shape simply omits it, and resolution fails loud instead.
|
||||
*/
|
||||
interface StorageBackend {
|
||||
/** Key-value data shape; absent when this backend cannot serve it. */
|
||||
readonly kv?: KvFacet
|
||||
|
||||
/**
|
||||
* Drain in-flight writes across all open units and release the medium.
|
||||
* Idempotent; concurrent and repeated calls resolve once teardown finishes.
|
||||
* @returns resolution after the medium is released.
|
||||
*/
|
||||
close(): Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
一个后端拥有一个介质(一棵文件树的根目录、一个数据库文件),并暴露可选的数据形状 facet;目前 `kv` 是唯一的 facet。`KvFacet.open(descriptor)` 打开一个具名 unit——`KvUnitDescriptor` 携带名称、格式版本、表名清单,以及是否存在全局单例槽位——并返回提供 `loadAll`、`putRecord`、`deleteRecord`、`setGlobal` 和 `close` 的 `KvUnit`。unit 名与表名必须匹配 `UNIT_NAME_RE`(既可安全用作文件名,也可安全用作 SQL 标识符片段);记录键是任意字符串,绝不进入文件路径。unit 不对并发写入做串行化——顺序由调用方负责——但每次单独调用在介质上都是原子的,且 resolve 后即已持久。介质上记录的版本与之不同时拒绝 `version-mismatch`;无法按该 unit 解析的介质拒绝 `malformed-medium`(不做迁移:预发布立场)。[`backend.ts`](../../packages/storage/storage/src/backend.ts) 是逐条款的规范性契约,[`tests/contract.ts`](../../packages/storage/storage/tests/contract.ts) 中的共享一致性套件对每个后端断言其中每一条款。[json 后端](../../packages/storage/storage-json/README.md)以原子方式为每个 unit 整文件重新发布一份人类可读文件;[sqlite 后端](../../packages/storage/storage-sqlite/README.md)在单个数据库中按一行一文档存储,是高频更新领域的路由选择。
|
||||
|
||||
## 声明领域
|
||||
|
||||
领域由其拥有包声明一次,形式是一个 spec 对象——它是该领域的身份、布局和记录 schema 的单一来源(schema 用 zod 编写,因此 `z.infer` 让消费方类型无需重复声明):
|
||||
|
||||
```ts type-equiv
|
||||
/** Static declaration of one domain: identity, version, and record layout. */
|
||||
interface DomainSpec {
|
||||
/** Domain name; must match `UNIT_NAME_RE` (doubles as the backend unit name). */
|
||||
readonly name: string
|
||||
/** Domain format version; a medium stamped with a different version rejects at open. */
|
||||
readonly version: number
|
||||
/** Optional global singleton slot. */
|
||||
readonly global?: DomainGlobalSpec<unknown>
|
||||
/** Table declarations keyed by table name; each name must match `UNIT_NAME_RE`. */
|
||||
readonly tables: Record<string, DomainTableSpec>
|
||||
}
|
||||
```
|
||||
|
||||
`defineDomain(spec)` 固定 spec 的字面量类型,并在拥有方的模块加载时、任何介质被触碰之前就大声失败:领域名或表名不匹配 `UNIT_NAME_RE`、版本不是非负整数、global schema 接受 `null`,这些都会抛出(`null` 是介质的「从未写入」哨兵值,可空的 global 一旦存储就无法往返还原)。`domainTable<K, V>(schema)` 声明一张表,其键类型是编译期的幻影类型(通常是[品牌化 id](core.md#branded-ids));`descriptorOf(spec)` 投影出面向后端的 unit 描述符。
|
||||
|
||||
## 打开的领域
|
||||
|
||||
```ts type-equiv
|
||||
/** One open domain, typed by its spec. */
|
||||
interface Domain<S extends DomainSpec> {
|
||||
/** Domain name from the spec. */
|
||||
readonly name: string
|
||||
/** Global singleton handle; a spec without `global` has no usable handle (`never`). */
|
||||
readonly global: DomainGlobalHandleOf<S>
|
||||
/**
|
||||
* Resolve one declared table handle. Handles are stable — repeated calls
|
||||
* return the same instance.
|
||||
* @param name - Declared table name.
|
||||
* @returns the typed table handle.
|
||||
*/
|
||||
table<N extends keyof S['tables'] & string>(name: N): KvTable<TableKeyOf<S, N>, TableValueOf<S, N>>
|
||||
|
||||
/**
|
||||
* Close this domain: reject new writes immediately, drain already-queued
|
||||
* writes (their events still emit), release the backend unit, then free
|
||||
* the domain name for a later open. Idempotent — repeated calls share one
|
||||
* teardown. The consumer owns this call (typically as its own `ctx.effect`
|
||||
* disposer); the facility closes any domain left open when it unmounts.
|
||||
* @returns resolution after the unit is released.
|
||||
*/
|
||||
close(): Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
读取是同步的,来自权威的内存态:`KvTable` 暴露 `get`/`entries`/`keys`/`size`(快照迭代器,在排队写入落地期间保持稳定),global 句柄的 `get()` 在第一次 `set` 将槽位物化到介质之前一直返回 spec 的 `initial`。每次写入——`put`、`delete`、`update`、`global.set`——都在同一条逐领域写链上排队,先到达后端持久性,再更新内存,最后发出 `domain/changed`;后端写入被拒时内存原样不动,因此读取绝不会偏离介质。`update(key, fn)` 在其写链槽位上是一次原子的读-改-写(键缺失时拒绝 `missing-key`);`delete` 一个不存在的键 resolve 为 `false`,不产生写入也不产生事件。返回的记录就是存储的对象本身,不是副本——请经 `put`/`update` 整体替换,绝不要就地修改。
|
||||
|
||||
## 领域 facility:`ctx.storageDomain`
|
||||
|
||||
`DomainFacility`([签名](../cordis-catalog/services.md#ctxstoragedomain--domainfacility))在经过路由的后端之上打开已声明的领域。路由是领域插件的配置,绝不属于枢纽:`backend` 指定必填的默认路由,`routes` 按领域名逐个覆盖。`open(spec)` 逐步执行,任一步失败都使整个调用失败:拒绝已打开或仍在关闭中的名称(`already-open`),解析路由(`backend-not-found`),要求后端具备 `kv` facet(`facet-unsupported`),打开 unit(后端的 `version-mismatch`/`malformed-medium` 原样透传),并按 spec 的 zod schema 校验每条已存储记录和 global(`invalid-record`,附带出错的表与键)。调用方拥有返回的句柄,并用 `Domain.close()` 释放它;插件卸载时仍处于打开状态的领域由 facility 负责关闭,已关闭领域的名称只有在拆除完全结束后才释放出来供重新打开。`get(name)` 是无类型的诊断查找;`closeAll()` 是卸载路径。
|
||||
|
||||
## 变更事件:`domain/changed`
|
||||
|
||||
每次持久写入都发出一个事件,严格发生在后端确认持久性之后,顺序遵循该领域的写链([事件条目](../cordis-catalog/events.md#domainchanged--emit)):
|
||||
|
||||
```ts type-equiv
|
||||
/** Shared location fields of one durable domain change. */
|
||||
interface DomainChangedBase {
|
||||
/** Owning domain name. */
|
||||
readonly domain: string
|
||||
/** Table name; `''` for a global-singleton write. */
|
||||
readonly table: string
|
||||
/** Record key; `''` for a global-singleton write. */
|
||||
readonly key: string
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** One durable domain change; a closed union — switch on `operation`. */
|
||||
type DomainChanged = DomainChangedPut | DomainChangedDeleted
|
||||
```
|
||||
|
||||
`put`(插入、覆写和 global 写入)在 `value` 中携带新快照——绝不携带旧值;需要做差异比较的消费方自行保留上一份快照。`deleted` 是不携带值的墓碑。该事件是通知,不是事务参与者:发出时提交点已经过去,因此同步抛出的监听器会被兜住并记录一条警告,而不会让已经持久的写入被拒绝;发出的值等于发出时刻的内存态。该事件仅限进程内;跨进程的变更推送是延后工作,记录在[包 README](../../packages/storage/storage-domain/README.md)中。
|
||||
6
docs/subsystems/telemetry.i18n.yaml
Normal file
6
docs/subsystems/telemetry.i18n.yaml
Normal file
@@ -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 docs/subsystems/telemetry.md
|
||||
telemetry.md: 4942a402dacb40ed64070c2b7824bfea7a14c872
|
||||
telemetry.zh.md: c99b287fd4cb4e9bc585ffe365ca08df37f6ccd1
|
||||
109
docs/subsystems/telemetry.md
Normal file
109
docs/subsystems/telemetry.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Telemetry
|
||||
|
||||
English | [中文](telemetry.zh.md)
|
||||
|
||||
Outbound session reporting, split as a [capability seam](../capability-seams.md): the seam ([dsh-session-telemetry](../../packages/telemetry/session-telemetry), `ctx.telemetry`) owns the capture points, the fixed chunk projection, the `telemetry/record` redaction waterfall, the handoff cursor, and the minimal backend contract; the backend a deployment loads ([dsh-session-telemetry-otel](../../packages/telemetry/session-telemetry-otel)) is the OpenTelemetry JS SDK's log pipeline configured verbatim. It is one optional capability, not part of the agent-loop spine, and nothing here reaches a model request. The boundary axiom — the harness's aspect ends at `emit()`; batching, retry, queueing, and loss policy belong to the reporting SDK — and the rejected alternatives are pinned in the [revival Agent Note](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md); the capture points, cursor, and projection contracts live in the [seam README](../../packages/telemetry/session-telemetry/README.md).
|
||||
|
||||
Source: [`packages/telemetry/session-telemetry/src/index.ts`](../../packages/telemetry/session-telemetry/src/index.ts)
|
||||
|
||||
## The logical record
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Severity of a telemetry record, pre-mapped at capture so a receiver can
|
||||
* alert with zero configuration: `error` for events whose own outcome flag
|
||||
* says so (`tool/result.isError`, `turn/end` error reasons) and for
|
||||
* `agent-error` operational records. Captured events otherwise default to
|
||||
* `info`; `warn` remains available to `telemetry/record` policies and
|
||||
* backends.
|
||||
*/
|
||||
type TelemetrySeverity = 'info' | 'warn' | 'error'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One logical record handed to a backend — the seam's whole outbound
|
||||
* vocabulary. Ledger records mirror session-log events one-to-one;
|
||||
* operational records (`channel: 'ops'`) carry the two signals with no log
|
||||
* home (`agent-error`, `shutdown`) and deliberately omit `event.seq`-style
|
||||
* identity so they can never be mistaken for ledger rows.
|
||||
*/
|
||||
interface TelemetryRecord {
|
||||
/** Ledger (session-log mirror) or ops (operational signal) channel; backends keep the two under separate instrumentation scopes. */
|
||||
channel: 'ledger' | 'ops'
|
||||
/** Unix epoch milliseconds — the source event's append time for ledger records, the emission time for ops records. */
|
||||
time: number
|
||||
/** Pre-mapped alerting severity; see {@link TelemetrySeverity}. */
|
||||
severity: TelemetrySeverity
|
||||
/**
|
||||
* Identity attributes, deliberately minimal: ledger records carry
|
||||
* `session.id`, `event.type`, `event.seq`, plus `session.cwd` /
|
||||
* `session.parent_id` when the header has them; ops records carry
|
||||
* `telemetry.op`, `session.id`, and (for `agent-error`) `agent.id`,
|
||||
* `turn`, `step`, `error.name`. Anything recoverable from the body is
|
||||
* intentionally NOT duplicated here.
|
||||
*/
|
||||
attributes: Record<string, string | number>
|
||||
/**
|
||||
* The complete payload: a deep copy of the session event's `data` for
|
||||
* ledger records (JSON-serializable by `Session.append`'s own
|
||||
* validation), or the op payload for ops records. Never mutated after
|
||||
* handoff.
|
||||
*/
|
||||
body: unknown
|
||||
}
|
||||
```
|
||||
|
||||
Only the first `assistant/chunk` of each `(turn, step)` ships — the stream-started signal; the rest drop at capture, so `seq` gaps are routine on the wire and never a loss signal. Every other [session event](session.md) type, including plugin-merged ones the seam never heard of, passes through whole. Delivery is at-most-once downstream of the handoff; receivers dedupe on `(session.id, event.seq)`.
|
||||
|
||||
## The backend contract
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* The backend contract the coordinator hands records to — the minimum any
|
||||
* reporting SDK satisfies with zero bending. {@link Telemetry} is its
|
||||
* service-registered form; tests compose the coordinator with a bare
|
||||
* implementation of this interface.
|
||||
*/
|
||||
interface TelemetryBackend {
|
||||
/**
|
||||
* Hand one record to the backend's pipeline. MUST be a non-blocking
|
||||
* enqueue — the coordinator calls this synchronously from the
|
||||
* `session/event` hot path, so anything slower than a queue push would tax
|
||||
* the agent loop. Errors thrown here are contained by the coordinator and
|
||||
* logged; they never reach the loop.
|
||||
* @param record - the logical record to report; owned by the backend after the call.
|
||||
*/
|
||||
emit(record: TelemetryRecord): void
|
||||
/**
|
||||
* Optional hint that a natural boundary (turn end) passed — a backend may
|
||||
* forward it to its SDK's flush so records land at turn boundaries. Called
|
||||
* fire-and-forget; implementations must not block and must not throw
|
||||
* meaningfully (the coordinator contains exceptions). Most backends should
|
||||
* leave this unimplemented and let their SDK's own batching cadence govern
|
||||
* export timing: a backend that does implement it owns the interaction
|
||||
* between its concurrent flushes and {@link shutdown}'s drain (the OTel
|
||||
* backend removed its implementation for exactly that hazard — see the
|
||||
* revival Agent Note).
|
||||
*/
|
||||
flush?(): void
|
||||
/**
|
||||
* Forward the fiber's disposal to the SDK: flush whatever is queued and
|
||||
* reach quiescence, per the SDK's own shutdown contract. Everything
|
||||
* emitted before this call must still be delivered — including records
|
||||
* enqueued while a {@link flush} hint is in flight, so a backend whose SDK
|
||||
* guards against concurrent flushes orders behind the outstanding one (the
|
||||
* coordinator emits its dispose-time `shutdown` markers immediately before
|
||||
* calling this). Awaited by the coordinator's dispose; a rejection is
|
||||
* logged as a warning and never fails application teardown.
|
||||
* @returns resolves when the backend's pipeline has quiesced.
|
||||
*/
|
||||
shutdown(): Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
`Telemetry` (`ctx.telemetry`, [signatures](../cordis-catalog/services.md#ctxtelemetry--telemetry-abstract-seam)) is the contract's loadable form — one implementation per context, duplicate load throws — and a backend composes the seam's `TelemetryCoordinator` in its constructor to install the capture side.
|
||||
|
||||
## The redact waterfall: `telemetry/record`
|
||||
|
||||
Every record passes the `telemetry/record` [waterfall](../cordis-primer.md#cordis-waterfall-semantics) between projection and `emit()` ([event entry](../cordis-catalog/events.md#telemetryrecord--waterfall)). The seam ships NO rules of its own: with no listener mounted, records reach the backend exactly as captured, so exported data is precisely as clean as the rules a deployment mounts. Listeners stack by transforming `next()`'s return value; returning without `next()` replaces everything beneath; a throwing listener withholds that one record fail-closed inside the coordinator's containment. Redaction applies to the exported copy only — the canonical session log is never rewritten.
|
||||
109
docs/subsystems/telemetry.zh.md
Normal file
109
docs/subsystems/telemetry.zh.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# 遥测(telemetry)
|
||||
|
||||
[English](telemetry.md) | 中文
|
||||
|
||||
对外的会话上报,拆分为一项[能力 seam](../capability-seams.md):seam 一侧([dsh-session-telemetry](../../packages/telemetry/session-telemetry),`ctx.telemetry`)拥有捕获点、固定分片投影、`telemetry/record` 脱敏 waterfall(瀑布式事件)、handoff 游标与最小后端契约;部署方加载的后端([dsh-session-telemetry-otel](../../packages/telemetry/session-telemetry-otel))则是原样配置的 OpenTelemetry JS SDK 日志流水线。它是一项可选能力,不属于 agent loop(智能体循环)主干,这里也没有任何内容会进入模型请求。边界公理(harness 的职责止于 `emit()`;批处理、重试、排队与丢失策略都属于上报 SDK)连同被否决的替代方案,均已在[复活 Agent Note(agent 决策记录)](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md)中定案;捕获点、游标与投影的契约见 [seam README](../../packages/telemetry/session-telemetry/README.md)。
|
||||
|
||||
源码:[`packages/telemetry/session-telemetry/src/index.ts`](../../packages/telemetry/session-telemetry/src/index.ts)
|
||||
|
||||
## 逻辑记录
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Severity of a telemetry record, pre-mapped at capture so a receiver can
|
||||
* alert with zero configuration: `error` for events whose own outcome flag
|
||||
* says so (`tool/result.isError`, `turn/end` error reasons) and for
|
||||
* `agent-error` operational records. Captured events otherwise default to
|
||||
* `info`; `warn` remains available to `telemetry/record` policies and
|
||||
* backends.
|
||||
*/
|
||||
type TelemetrySeverity = 'info' | 'warn' | 'error'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One logical record handed to a backend — the seam's whole outbound
|
||||
* vocabulary. Ledger records mirror session-log events one-to-one;
|
||||
* operational records (`channel: 'ops'`) carry the two signals with no log
|
||||
* home (`agent-error`, `shutdown`) and deliberately omit `event.seq`-style
|
||||
* identity so they can never be mistaken for ledger rows.
|
||||
*/
|
||||
interface TelemetryRecord {
|
||||
/** Ledger (session-log mirror) or ops (operational signal) channel; backends keep the two under separate instrumentation scopes. */
|
||||
channel: 'ledger' | 'ops'
|
||||
/** Unix epoch milliseconds — the source event's append time for ledger records, the emission time for ops records. */
|
||||
time: number
|
||||
/** Pre-mapped alerting severity; see {@link TelemetrySeverity}. */
|
||||
severity: TelemetrySeverity
|
||||
/**
|
||||
* Identity attributes, deliberately minimal: ledger records carry
|
||||
* `session.id`, `event.type`, `event.seq`, plus `session.cwd` /
|
||||
* `session.parent_id` when the header has them; ops records carry
|
||||
* `telemetry.op`, `session.id`, and (for `agent-error`) `agent.id`,
|
||||
* `turn`, `step`, `error.name`. Anything recoverable from the body is
|
||||
* intentionally NOT duplicated here.
|
||||
*/
|
||||
attributes: Record<string, string | number>
|
||||
/**
|
||||
* The complete payload: a deep copy of the session event's `data` for
|
||||
* ledger records (JSON-serializable by `Session.append`'s own
|
||||
* validation), or the op payload for ops records. Never mutated after
|
||||
* handoff.
|
||||
*/
|
||||
body: unknown
|
||||
}
|
||||
```
|
||||
|
||||
每个 `(turn, step)` 只发出第一条 `assistant/chunk`,即「流已开始」的信号;其余分片在捕获时丢弃,因此导出流中的 `seq` 缺口是常态,绝不是丢失信号。其他所有[会话事件](session.md)类型都会完整透传,包括该 seam 从未听说过、由插件合并进来的事件类型。交接下游的投递为至多一次(at-most-once);接收端基于 `(session.id, event.seq)` 去重。
|
||||
|
||||
## 后端契约
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* The backend contract the coordinator hands records to — the minimum any
|
||||
* reporting SDK satisfies with zero bending. {@link Telemetry} is its
|
||||
* service-registered form; tests compose the coordinator with a bare
|
||||
* implementation of this interface.
|
||||
*/
|
||||
interface TelemetryBackend {
|
||||
/**
|
||||
* Hand one record to the backend's pipeline. MUST be a non-blocking
|
||||
* enqueue — the coordinator calls this synchronously from the
|
||||
* `session/event` hot path, so anything slower than a queue push would tax
|
||||
* the agent loop. Errors thrown here are contained by the coordinator and
|
||||
* logged; they never reach the loop.
|
||||
* @param record - the logical record to report; owned by the backend after the call.
|
||||
*/
|
||||
emit(record: TelemetryRecord): void
|
||||
/**
|
||||
* Optional hint that a natural boundary (turn end) passed — a backend may
|
||||
* forward it to its SDK's flush so records land at turn boundaries. Called
|
||||
* fire-and-forget; implementations must not block and must not throw
|
||||
* meaningfully (the coordinator contains exceptions). Most backends should
|
||||
* leave this unimplemented and let their SDK's own batching cadence govern
|
||||
* export timing: a backend that does implement it owns the interaction
|
||||
* between its concurrent flushes and {@link shutdown}'s drain (the OTel
|
||||
* backend removed its implementation for exactly that hazard — see the
|
||||
* revival Agent Note).
|
||||
*/
|
||||
flush?(): void
|
||||
/**
|
||||
* Forward the fiber's disposal to the SDK: flush whatever is queued and
|
||||
* reach quiescence, per the SDK's own shutdown contract. Everything
|
||||
* emitted before this call must still be delivered — including records
|
||||
* enqueued while a {@link flush} hint is in flight, so a backend whose SDK
|
||||
* guards against concurrent flushes orders behind the outstanding one (the
|
||||
* coordinator emits its dispose-time `shutdown` markers immediately before
|
||||
* calling this). Awaited by the coordinator's dispose; a rejection is
|
||||
* logged as a warning and never fails application teardown.
|
||||
* @returns resolves when the backend's pipeline has quiesced.
|
||||
*/
|
||||
shutdown(): Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
`Telemetry`(`ctx.telemetry`,[签名](../cordis-catalog/services.md#ctxtelemetry--telemetry-abstract-seam))是该契约的可加载形态:每个上下文只允许一个实现,重复加载会抛出异常;后端在其构造函数中组合 seam 的 `TelemetryCoordinator`,以此装配捕获侧。
|
||||
|
||||
## 脱敏 waterfall:`telemetry/record`
|
||||
|
||||
每条记录在投影与 `emit()` 之间都要经过 `telemetry/record` [waterfall](../cordis-primer.md#cordis-waterfall-semantics)([事件条目](../cordis-catalog/events.md#telemetryrecord--waterfall))。seam 自身不带任何规则:未挂载监听器时,记录以捕获时的原样到达后端;导出数据能干净到什么程度,恰恰取决于部署方挂载了什么规则。监听器通过变换 `next()` 的返回值来堆叠;不调用 `next()` 就返回,即替换其下方的全部逻辑;抛出异常的监听器会在协调器的隔离范围内以 fail-closed 方式扣下这一条记录。脱敏只作用于导出副本;权威会话日志永不改写。
|
||||
6
docs/subsystems/workspace.i18n.yaml
Normal file
6
docs/subsystems/workspace.i18n.yaml
Normal file
@@ -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 docs/subsystems/workspace.md
|
||||
workspace.md: 2726115457905687e3c2cfc35a7a3f7251e6347c
|
||||
workspace.zh.md: 190c44b2d73f9e123242c34b5429839abe3b9297
|
||||
121
docs/subsystems/workspace.zh.md
Normal file
121
docs/subsystems/workspace.zh.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# 工作区
|
||||
|
||||
[English](workspace.md) | 中文
|
||||
|
||||
工作区(workspace)是用户工作目录的持久记录:一个建立在规范路径之上的稳定 id、一个显示标题,以及归属于它的会话的有序账本。该子系统是单个包(package)([dsh-workspace](../../packages/workspace/workspace),`ctx.workspace`)——一项宿主侧可选能力,不属于 agent loop(智能体循环)主干,并且对模型不可见(没有工具、没有提示词文本、没有会话事件)。它通过[存储领域数据形式](storage.md)存储自己的记录,并对照 [`SessionHeader.cwd`](persistence.md#sessionheader--metadata-beside-the-log) 校验会话成员资格,因此 `storageDomain` 与 `sessionPersistence` 是必需的启动依赖:持久化这一侧不可用时插件保持 pending,而不会被误认为历史为空。设计记录:[领域 KV 存储 Agent Note(agent 决策记录)](../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md);引导与 GUI 顺序:[Workspace UI 产品流程 Agent Note](../../.agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.md)。
|
||||
|
||||
源码:[`packages/workspace/workspace/src/types.ts`](../../packages/workspace/workspace/src/types.ts)
|
||||
|
||||
## 标识
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Identifies one workspace record. A generated uuid, never the path: path
|
||||
* normalization rewrites paths, and a reference anchor must stay stable.
|
||||
*/
|
||||
type WorkspaceId = Branded<'WorkspaceId'>
|
||||
```
|
||||
|
||||
`WorkspaceId` 是[品牌化 id](core.md#branded-ids)。路径标识与之分离:`realpathNormalize`(`fs.realpath`;尾部斜杠、`..` 与符号链接全部解析)是唯一的一套唯一性规范——工作区路径以规范化形式存储,唯一性即规范路径的字符串相等(指向已被拥有目录的符号链接会与之冲突),attach 时的会话 cwd 检查也走同一套规范。
|
||||
|
||||
## 工作区实体
|
||||
|
||||
消费方只看到 `Workspace` 接口;实现保持包内私有。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One workspace: a stable id over an existing directory, a display title, and
|
||||
* an ordered candidate account of sessions. Membership requires both an id in
|
||||
* that account and a session header whose canonical cwd equals the workspace
|
||||
* path. Consumers only see this interface; the implementation stays private.
|
||||
*/
|
||||
interface Workspace {
|
||||
/** Stable record id (generated uuid). */
|
||||
readonly id: WorkspaceId
|
||||
|
||||
/**
|
||||
* Canonical directory path: the `fs.realpath` of the path given at create
|
||||
* time (trailing slashes, `..`, and symlinks all resolved). Never rewritten
|
||||
* afterwards, even when the directory disappears (see {@link status}).
|
||||
*/
|
||||
readonly path: string
|
||||
|
||||
/** Display title. Defaults to `basename(path)` at create; duplicates are allowed. */
|
||||
readonly title: string
|
||||
|
||||
/** ISO-8601 creation instant, stamped at create and never rewritten. */
|
||||
readonly createdAt: string
|
||||
|
||||
/** ISO-8601 instant of the last durable mutation (create counts as one). */
|
||||
readonly updatedAt: string
|
||||
|
||||
/**
|
||||
* Header-validated sessions in manually owned order: a new session is
|
||||
* prepended at attach, explicit reordering goes through
|
||||
* `insertSessionBefore`, and activity never reorders. The durable candidate
|
||||
* account is filtered synchronously: missing headers, invalid cwd values,
|
||||
* and canonical cwd mismatches are never returned. A subsequent workspace
|
||||
* mutation prunes those filtered candidates durably.
|
||||
*/
|
||||
readonly sessionIds: readonly SessionId[]
|
||||
|
||||
/**
|
||||
* Replace the display title durably.
|
||||
* @param title - New title; any string, duplicates across workspaces allowed.
|
||||
* @returns resolution after durability.
|
||||
*/
|
||||
setTitle(title: string): Promise<void>
|
||||
|
||||
/**
|
||||
* Prepend a session to this workspace's candidate account. An already
|
||||
* accounted id resolves without writing. A new id's live or persisted
|
||||
* header cwd must resolve to an existing directory equal to {@link path};
|
||||
* unknown ids, missing or invalid cwd values, and mismatches reject without
|
||||
* writing.
|
||||
* @param sessionId - The session to record.
|
||||
* @returns resolution after durability.
|
||||
*/
|
||||
attachSession(sessionId: SessionId): Promise<void>
|
||||
|
||||
/**
|
||||
* Move an accounted session within the manual order, DOM-insertBefore-like:
|
||||
* with an anchor the session lands before it, without one it appends to the
|
||||
* end. Only the moved id changes position. A session or anchor absent from
|
||||
* the account rejects without writing; a move to the current position
|
||||
* resolves without writing (decided on the domain write chain).
|
||||
* @param sessionId - The accounted session to move.
|
||||
* @param beforeSessionId - Accounted anchor to insert before; omitted appends.
|
||||
* @returns resolution after durability.
|
||||
*/
|
||||
insertSessionBefore(sessionId: SessionId, beforeSessionId?: SessionId): Promise<void>
|
||||
|
||||
/**
|
||||
* Remove a session from this workspace's account. Idempotent: an id not on
|
||||
* the account resolves without writing (decided on the domain write chain,
|
||||
* like attach). Never touches the session's own stored log.
|
||||
* @param sessionId - The session to remove.
|
||||
* @returns resolution after durability.
|
||||
*/
|
||||
detachSession(sessionId: SessionId): Promise<void>
|
||||
|
||||
/**
|
||||
* Live directory check, uncached: whether {@link path} currently exists and
|
||||
* is a directory. A missing directory never mutates the record — the
|
||||
* directory may only be temporarily moved.
|
||||
* @returns `'ok'` when the directory exists, `'missing-dir'` otherwise.
|
||||
*/
|
||||
status(): Promise<'ok' | 'missing-dir'>
|
||||
}
|
||||
```
|
||||
|
||||
所有权的真源是记录中有序的 `sessionIds`,绝不从会话 cwd 派生——但成员资格要求两者同时成立:账本上有其 id,且 header 的规范 cwd 等于工作区路径,因此一个会话在结构上至多属于一个工作区。失败的写入会拒绝(`insertSessionBefore` 的账本错误以 `WorkspaceMoveInvalidError` 拒绝,存储失败以普通错误拒绝);每次被接受的变更都盖上 `updatedAt` 时间戳,并持久修剪不再通过成员资格检查的候选项。
|
||||
|
||||
## 注册表:`ctx.workspace`
|
||||
|
||||
`WorkspaceRegistry`([签名](../cordis-catalog/services.md#ctxworkspace--workspaceregistry))拥有注册与解析。`create(path, title?)` 规范化路径,拒绝不存在的路径(原样抛出 `ENOENT`)或非目录;当规范路径已被拥有时原样返回既有实体;否则创建一条标题为 `title ?? basename(path)` 的记录并前插到持久的注册表顺序中——新记录不得与既有显示标题重复(`WorkspaceNameConflictError`)。`get(id)` 与有序的 `list()` 是同步缓存读取;`resolveByPath(path)` 应用同一套 realpath 规范但不创建。`delete(id)` 只移除注册记录、顺序条目和会话账本——目录、用户文件、实时会话和已持久化日志一概不动,因此这些会话变为 Ungrouped([决策](../../.agents/notes/implemented/feature/2026-07-27-workspace-registration-deletion.md));未知 id 返回 `false`。create 与 delete 会在其两次写入(记录 + 顺序)可能分叉之前先持久写入一个待定变更标记;启动时恰好补完被标记的那次变更,而没有标记的顺序/表不一致则作为损坏大声失败。
|
||||
|
||||
会话的 cwd 在创建时由创建者赋予,而不是由本注册表赋予——API 网关从所选工作区的 `path` 解析新会话的 cwd(回退到显式或默认 cwd),先创建会话使 cwd 落入其不可变的 [`SessionHeader`](persistence.md#sessionheader--metadata-beside-the-log),再调用 `attachSession`,后者会把已存储的 header cwd 与工作区路径重新校验一遍。首次启动时,注册表仅凭已持久化的 header(`id`、`cwd`、`createdAt`——绝不读事件正文)引导历史:把规范 cwd 有效的会话按目录分组为工作区,最新的排在最前;「已初始化」标记最后写入,因此被中断的引导可以安全续跑,而没有 cwd 的历史遗留会话保持 Ungrouped。
|
||||
|
||||
## 消费方
|
||||
|
||||
[dsh-host-apiproxy](../../packages/host/apiproxy) 是产品消费方:它经 `ctx.workspace` 向 GUI 客户端提供工作区的 CRUD,并执行上文「先建会话再 attach」的流程。[dsh-workspace-context](../../packages/context/workspace-context) 尽管名字如此,却**不是**消费方:它在 agent 自己的 cwd 下发现 AGENTS.md 风格的指令文件,从不触碰 `ctx.workspace`——两者共用的这个词指的是用户的工作目录,而非本注册表的实体。
|
||||
Reference in New Issue
Block a user