docs: qualify plan-mode timing and workspace no-write claims

ds-review-bot round 3:
- set() after a turn's final step lands in a later turn (selection
  never forces continuation) — the same-turn phrasing overstated the
  guarantee; set() JSDoc + subsystem page (both languages).
- Workspace attach/insert/detach no-op claims now carry the
  filtered-candidate-prune caveat: mutate() durably prunes stale
  candidates even when the requested change is a no-op, a state the
  sessionIds contract explicitly permits; Workspace fence re-synced on
  both page sides; catalogs regenerated.
This commit is contained in:
Tianyi Cui
2026-07-28 14:11:18 +08:00
parent 3e1c7351be
commit 44b035c711
30 changed files with 187 additions and 531 deletions

View File

@@ -20,7 +20,7 @@ Each fact has one home: the tier whose job it is. Elsewhere, link to that home.
|---|---|---|
| Root `AGENTS.md` | Standing orders: rules an agent needs in context in every session, one to three lines each, linking its home | Stories, worked examples, situational procedures, anything restated from a linked home |
| Subtree `AGENTS.md` (`packages/`, `examples/`, `docs/`, `.agents/notes/`) | Orders specific to that subtree | Repo-wide rules the root file already carries |
| [architecture.md](architecture.md) | The system map: services, the loop, extension seams — read before changing `packages/` | Type shapes (→ core-data-structures), per-package detail (→ package READMEs), decision rationale (→ Agent Notes), implementation-status annotations |
| [architecture.md](architecture.md) | The system map: services, the loop, extension seams — read before changing `packages/` | Type shapes (→ subsystems), per-package detail (→ package READMEs), decision rationale (→ Agent Notes), implementation-status annotations |
| [subsystems/](subsystems/core.md) | The type catalog: literal shapes and semantics of the spine and seam vocabulary | Behavior narration (→ architecture.md) |
| [Agent Notes](../.agents/notes/README.md) | Decision records under their own lifecycle contract | Migration plans, checklists, and spec-speak once implemented; archived notes are frozen history |
| [postmortem/](postmortem/README.md) | Incident stories — the only tier where war-story narrative belongs | — |
@@ -37,7 +37,7 @@ Each fact has one home: the tier whose job it is. Elsewhere, link to that home.
- **Every non-trivial change includes at least one Agent Note in the same PR.** Update the owning note or add one; only mechanical/local edits are exempt ([scope](../.agents/notes/README.md#when-to-write-one)).
- **One physical line per paragraph** (`verify-md-wrap`): use editor soft-wrap. Code blocks, tables, and list structure keep their formatting; code comments stay under the linter's column limit.
- **Fenced `ts` blocks must compile** (`doc-typecheck`); a pasted type declaration and its original JSDoc use ` ```ts type-equiv `, while a body-stripped public class declaration uses ` ```ts public-api `; register either in the manifest so neither can drift ([mechanics](development.md#documenting-types-verbatim-ts-type-equiv)).
- **The [core-data-structures catalog](subsystems/core.md) updates in the same change** that reshapes a documented type. `verify-type-equiv` catches drifted pastes, not never-documented new types ([what counts as core](subsystems/core.md#what-counts-as-core)).
- **The [subsystems catalog](subsystems/core.md) updates in the same change** that reshapes a documented type. `verify-type-equiv` catches drifted pastes, not never-documented new types ([what counts as core](subsystems/core.md#what-counts-as-core)).
- **Bilingual pairs update together**: editing either side obligates the counterpart and a re-record in the same change ([i18n contract](i18n/README.md)).
- **Comments and JSDoc state complete contracts, not reasoning transcripts.** Preserve behavior, timing, modality, exceptions, consequences, and non-obvious orientation; delete narration, test walkthroughs, review analysis, and code restatement. Keep the local contract and link its rationale. Use [dsh-prose-standard](../.agents/skills/dsh-prose-standard/SKILL.md) for details.
- Your audience is professional programmers. Prefer concise and straight-forward English over metaphor. Do not overuse words like "gate", "vocabulary", "surface", "seams".

View File

@@ -824,6 +824,8 @@ Transform one outbound record before it reaches the backend. This waterfall is t
'telemetry/record'(record: TelemetryRecord, next: () => TelemetryRecord): TelemetryRecord
```
Types: [TelemetryRecord](../subsystems/telemetry.md)
Source: [`packages/session/session-telemetry/src/index.ts:43`](../../packages/session/session-telemetry/src/index.ts)
## `tools/*`
@@ -1090,8 +1092,7 @@ The framework events every plugin also sees, beyond the harness vocabulary above
- `internal/listener` — A listener was registered. ([`vendor/cordis/src/events.ts:340`](../../vendor/cordis/src/events.ts))
- `internal/dispatch` — An event is being dispatched to listeners. ([`vendor/cordis/src/events.ts:342`](../../vendor/cordis/src/events.ts))
- `hmr/change` — A watched source file changed on disk. ([`vendor/hmr/src/index.ts:20`](../../vendor/hmr/src/index.ts))
- `hmr/reload` — Plugins are being reloaded after a change. ([`vendor/hmr/src/index.ts:22`](../../vendor/hmr/src/index.ts))
- `hmr/config-update-failed` — A watched config-file refresh failed. ([`vendor/hmr/src/index.ts:29`](../../vendor/hmr/src/index.ts))
- `hmr/reload` — Plugins are being reloaded after a change. ([`vendor/hmr/src/index.ts:21`](../../vendor/hmr/src/index.ts))
- `exit` — The process is exiting on a signal. ([`vendor/loader/src/index.ts:23`](../../vendor/loader/src/index.ts))
- `loader/config-update` — The loader config tree changed. ([`vendor/loader/src/index.ts:24`](../../vendor/loader/src/index.ts))
- `loader/entry-init` — A config entry is being initialized. ([`vendor/loader/src/index.ts:25`](../../vendor/loader/src/index.ts))

View File

@@ -377,6 +377,8 @@ onRebuilt(listener: (id: string, rev: string) => void): () => void
onGraphChanged(listener: () => void): () => void
```
Types: [WebBootGraph](../subsystems/client-modules.md)
Source: [`packages/client/modules/src/index.ts:184`](../../packages/client/modules/src/index.ts)
## `ctx.codeRuntime` — `CodeRuntime` (abstract seam)
@@ -866,6 +868,8 @@ tapIndex(transform: (html: string) => string): () => void
applyIndexTaps(html: string): string
```
Types: [WebRoute](../subsystems/http-server.md)
Source: [`packages/host/webserver/src/index.ts:60`](../../packages/host/webserver/src/index.ts)
## `ctx.invariants` — `InvariantService`
@@ -884,6 +888,8 @@ Package-owned invariant registry with global and regex-based selection.
register(packageName: string, installer: InvariantInstaller): () => void
```
Types: [InvariantInstaller](../subsystems/invariants.md)
Source: [`packages/support/invariants/src/index.ts:94`](../../packages/support/invariants/src/index.ts)
## `ctx.llm` — `LlmService`
@@ -1060,7 +1066,7 @@ optionOf(name: string): PresetOption
set(session: Session, name: string): void
```
Types: [Session](../subsystems/session.md) · [SessionEvent](../subsystems/core.md)
Types: [PresetOption](../subsystems/permission.md) · [PresetSpec](../subsystems/permission.md) · [Session](../subsystems/session.md) · [SessionEvent](../subsystems/core.md)
Source: [`packages/interaction/permission/src/index.ts:159`](../../packages/interaction/permission/src/index.ts)
@@ -2036,6 +2042,8 @@ mount<K extends keyof StorageForms>(form: K, facility: StorageForms[K]): () => v
form<K extends keyof StorageForms>(form: K): StorageForms[K]
```
Types: [StorageForms](../subsystems/storage.md)
Source: [`packages/storage/storage/src/index.ts:47`](../../packages/storage/storage/src/index.ts)
## `ctx.storageDomain` — `DomainFacility`
@@ -2079,6 +2087,8 @@ get(name: string): DomainImpl | undefined
async closeAll(): Promise<void>
```
Types: [Domain](../subsystems/storage.md) · [DomainSpec](../subsystems/storage.md)
Source: [`packages/storage/storage-domain/src/index.ts:69`](../../packages/storage/storage-domain/src/index.ts)
## `ctx.subagents` — `SubagentService`
@@ -2473,6 +2483,8 @@ flush?(): void
abstract shutdown(): Promise<void>
```
Types: [TelemetryRecord](../subsystems/telemetry.md)
Source: [`packages/session/session-telemetry/src/index.ts:140`](../../packages/session/session-telemetry/src/index.ts)
## `ctx.tokenMeter` — `TokenMeterService`
@@ -2885,7 +2897,7 @@ archiveSession(sessionId: SessionId): Promise<void>
async resolveByPath(path: string): Promise<Workspace | undefined>
```
Types: [SessionId](../subsystems/core.md)
Types: [SessionId](../subsystems/core.md) · [Workspace](../subsystems/workspace.md) · [WorkspaceId](../subsystems/workspace.md)
Source: [`packages/workspace/workspace/src/index.ts:81`](../../packages/workspace/workspace/src/index.ts)

View File

@@ -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/client-modules.md
client-modules.md: bd5ecdc85ea26b040e1705e373ceac4e7cb17b3c
client-modules.zh.md: 5da7740e4c9272a3516f1a28712be7c95f29a6b3
client-modules.md: 5dc89a87db7e79e4a297002fa059fdeffcceda20
client-modules.zh.md: b3aa9c2aecf8e051a41b7b05f05d364b9ff8d2a6

View File

@@ -27,7 +27,7 @@ interface WebBootEntry {
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. */
/** Stage-one prefetch mark: load the script for factory registration during module-face boot. */
immediately?: boolean
}
```

View File

@@ -27,7 +27,7 @@ interface WebBootEntry {
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. */
/** Stage-one prefetch mark: load the script for factory registration during module-face boot. */
immediately?: boolean
}
```

View File

@@ -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: 1d0bf6c61500817860dfdd452bddc25adbbeffcc
core.zh.md: 2b4766dcfcf1901de2bb8a8f9168888a67af7fba
core.md: 3a8ccbef4163eabba1cd287284eda68135f2db13
core.zh.md: 0cd6a57971ee98c75b52dd45b6f648632dab8a90

View File

@@ -51,7 +51,6 @@ Everything else is documented on a **sub-page**, not here. The rule that draws t
| [invariants.md](invariants.md) | the runtime-invariant registry: selection `Config`, `InvariantInstaller`/`InvariantFailure`, the empty-companion contract |
| [http-server.md](http-server.md) | the HTTP carrier: `WebRouteKind`/`WebRoute`, match order, the static dist fallback, index taps |
| [storage.md](storage.md) | the storage subsystem: the backend seam (`StorageBackend`), `StorageForms`, `DomainSpec`/`Domain`, `domain/changed` |
| [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 |

View File

@@ -51,7 +51,6 @@ harness 是一个微内核:一个极小的核心加上众多插件。大多数
| [invariants.md](invariants.md) | 运行时不变式注册表:选择配置 `Config``InvariantInstaller`/`InvariantFailure`、空配套插件契约 |
| [http-server.md](http-server.md) | HTTP 载体:`WebRouteKind`/`WebRoute`、匹配顺序、静态 dist 回退、index 转换 |
| [storage.md](storage.md) | 存储子系统:后端 seam`StorageBackend`)、`StorageForms``DomainSpec`/`Domain``domain/changed` |
| [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 |

View File

@@ -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/http-server.md
http-server.md: 565df72f2720a1211f50650d4b1bc8d6df4a007a
http-server.zh.md: 69e4a0bd7a1576737583625b23fac2ed05d8ca61
http-server.md: 200d3eed683f203b65824ea3f62cf56ce3df912f
http-server.zh.md: 9a12d3769a93270810927075be5f236875688b91

View File

@@ -2,7 +2,7 @@
English | [中文](http-server.zh.md)
[dsh-host-webserver](../../packages/host/webserver) is the web-shape HTTP carrier for the GUI host: a single `node:http` plugin providing `ctx.httpServer`, a named-route registry plus index.html transform taps over a static dist fallback. It is not part of the agent-loop spine and not a capability seam — it knows no harness concepts, and every feature surface (the `/api` bridge, plugin bundles, the HMR event stream) is a route some other plugin registers ([layering note](../../.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md)). Web (browser) shape only: Electron loads dist over `file://` and carries fetch over an IPC bridge, not this server.
[dsh-host-webserver](../../packages/host/webserver) is the web-shape HTTP carrier for the GUI host: a single `node:http` plugin providing `ctx.httpServer`, a named-route registry, index.html transform taps, and a single claimable fallback seat. It is not part of the agent-loop spine and not a capability seam — it knows no harness concepts, and every feature surface (the `/api` bridge, plugin bundles, the HMR event stream) is a route some other plugin registers ([layering note](../../.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md)). Web (browser) shape only: Electron loads dist over `file://` and carries fetch over an IPC bridge, not this server.
Source: [`packages/host/webserver/src/index.ts`](../../packages/host/webserver/src/index.ts)
@@ -24,23 +24,21 @@ interface WebRoute {
}
```
Match order is fixed: exact table first, then longest matching prefix, then the static dist fallback. Registration order carries no request-facing semantics — named routes are composed to be disjoint, and the fallback answers anything not yet claimed during the boot window. The fallback keeps locked semantics: non-GET/HEAD is 405, traversal outside the dist root is 403, any miss falls back to `index.html` with HTTP 200 (SPA routing), and unknown extensions ship as octet-stream ([`static.ts`](../../packages/host/webserver/src/static.ts)).
Match order is fixed: exact table first, then longest matching prefix, then the registered fallback. Registration order carries no request-facing semantics — named routes are composed to be disjoint, and the fallback seat answers anything no named route claims; one owner only, a second registration throws. The shipped Web composition claims the seat with [`dsh-frontend-static`](../../packages/host/frontend-static/src/index.ts), the SPA dist server with locked semantics: non-GET/HEAD is 405, traversal outside the dist root is 403, any miss falls back to `index.html` with HTTP 200 (SPA routing), and unknown extensions ship as octet-stream.
## Config
```ts type-equiv
/** Gateway config: listen address plus the static dist anchor (injected by the composing app, never self-resolved). */
/** Gateway config: the listen address. */
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` accepts only `127.0.0.1` (default posture) and `0.0.0.0` (deliberate network exposure); there is no TLS, auth, or origin policy, so a non-loopback bind exposes the server to that network. `distIndex` is an assembly fact the composing app resolves and injects.
`host` accepts only `127.0.0.1` (default posture) and `0.0.0.0` (deliberate network exposure); there is no TLS, auth, or origin policy, so a non-loopback bind exposes the server to that network. The dist location is an assembly fact of the frontend plugin that claims the seat.
## The service

View File

@@ -24,23 +24,21 @@ interface WebRoute {
}
```
匹配顺序固定:先查 exact 表,再取最长匹配前缀,最后落到静态 dist 回退。注册顺序不携带任何面向请求的语义:具名路由在组合上互不相交,启动窗口期内尚未被认领的请求全部由回退应答。回退遵循固定语义:非 GET/HEAD 返回 405越出 dist 根目录的遍历返回 403任何未命中都以 HTTP 200 回退到 `index.html`SPA 路由),未知扩展名按 octet-stream 提供([`static.ts`](../../packages/host/webserver/src/static.ts)
匹配顺序固定:先查 exact 表,再取最长匹配前缀,最后落到已注册的回退。注册顺序不携带任何面向请求的语义:具名路由在组合上互不相交,任何未被具名路由认领的请求由回退席位应答;席位只有一个所有者,第二次注册会抛出异常。发布的 Web 组合用 [`dsh-frontend-static`](../../packages/host/frontend-static/src/index.ts) 认领席位,即遵循固定语义的 SPA dist 服务器:非 GET/HEAD 返回 405越出 dist 根目录的遍历返回 403任何未命中都以 HTTP 200 回退到 `index.html`SPA 路由),未知扩展名按 octet-stream 发送
## 配置
```ts type-equiv
/** Gateway config: listen address plus the static dist anchor (injected by the composing app, never self-resolved). */
/** Gateway config: the listen address. */
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` 是组合应用解析后注入的组装事实。
`host` 只接受 `127.0.0.1`(默认姿态)和 `0.0.0.0`(刻意的网络暴露);没有 TLS、认证或 origin 策略因此绑定到非回环地址会把服务器暴露给该网络。dist 位置是认领席位的前端插件的组装事实。
## 服务

View File

@@ -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/permission.md
permission.md: 49dd51ff86d0dbce26fca0ed21527f71bb144dec
permission.zh.md: c2ab6ecdfb4b80511107bee41e080b06fae45f2c
permission.md: ec8cd4a923b617934f1612c31284103266aea4ef
permission.zh.md: 4534c33e2715e4a500344f547c7895128877f4fb

View File

@@ -2,9 +2,9 @@
English | [中文](permission.zh.md)
The permission-preset layer of [dsh-permission](../../packages/ui/permission) (`ctx.permission`, `PermissionService`) bundles the two independent enforcement knobs — [sandbox mode](sandbox.md) (`sandbox/mode`) and [approval policy](approval.md) (`approval/policy`) — into named presets a client offers as one Permissions selector. It is one optional capability, not part of the agent-loop spine, and it owns no enforcement: execution, prompt narration, and replay keep reading their knob folds, and a preset switch only records intent and writes through each knob's canonical setter. The [package README](../../packages/ui/permission/README.md) owns composition status and limitations; the [sandbox switching design](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) owns the rationale.
The permission-preset layer of [dsh-permission](../../packages/interaction/permission) (`ctx.permission`, `PermissionService`) bundles the two independent enforcement knobs — [sandbox mode](sandbox.md) (`sandbox/mode`) and [approval policy](approval.md) (`approval/policy`) — into named presets a client offers as one Permissions selector. It is one optional capability, not part of the agent-loop spine, and it owns no enforcement: execution, prompt narration, and replay keep reading their knob folds, and a preset switch only records intent and writes through each knob's canonical setter. The [package README](../../packages/interaction/permission/README.md) owns composition status and limitations; the [sandbox switching design](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) owns the rationale.
Source: [`packages/ui/permission/src/index.ts`](../../packages/ui/permission/src/index.ts)
Source: [`packages/interaction/permission/src/index.ts`](../../packages/interaction/permission/src/index.ts)
## The preset table
@@ -25,7 +25,7 @@ interface PresetSpec {
```
```ts type-equiv
/** The {@link PermissionService} config: the deployment's preset table. */
/** The {@link PermissionService} config: preset table and composition default. */
interface Config {
/**
* The preset table: name → knob bundle. Defaults to `workspace-write`
@@ -33,6 +33,11 @@ interface Config {
* never). The name `custom` is reserved for the derived not-a-preset state.
*/
presets?: Record<string, PresetSpec>
/**
* Default for new sessions. When omitted, the preset matching the composed
* sandbox and approval defaults is used.
*/
defaultPreset?: string
}
```
@@ -51,13 +56,13 @@ interface PresetOption {
value: string
/** The display label. */
name: string
/** One user-facing sentence on what the value means. */
/** One user-facing sentence on what the value means; omitted when not configured. */
description?: string
}
```
## Switching and the `permission/preset` event
`set(session, name)` resolves the preset (unknown names throw), appends a log-only `permission/preset` event unless `name` is already the effective preset, then writes each knob through its own setter — `setSandboxMode` from [dsh-sandbox-policy](../../packages/sandbox/sandbox-policy) and `setApprovalPolicy` from [dsh-user-approval](../../packages/ui/user-approval) — only when that knob's effective value changes. The selection event precedes the knob events in the same turn, and re-selecting the effective preset appends nothing at all.
`set(session, name)` resolves the preset (unknown names throw), appends a log-only `permission/preset` event unless `name` is already the effective preset, then writes each knob through its own setter — `setSandboxMode` from [dsh-sandbox-policy](../../packages/sandbox/sandbox-policy) and `setApprovalPolicy` from [dsh-user-approval](../../packages/interaction/user-approval) — only when that knob's effective value changes. The selection event precedes the knob events in the same turn, and re-selecting the effective preset appends nothing at all.
`permission/preset` is durable, log-only user intent: it stays out of the model transcript (the knob events own the model-visible consequences through their consumers), and it exists so `current()` can preserve WHICH preset the user chose when two presets share a bundle; `effectivePermissionPreset(events)` folds the last one, and replay needs no catch-up state. The complete event declaration is in the [persistence log event catalog](../persistence-catalog.md); the method signatures are in the generated [service catalog](../cordis-catalog/services.md#ctxpermission--permissionservice).

View File

@@ -2,9 +2,9 @@
[English](permission.md) | 中文
[dsh-permission](../../packages/ui/permission) 的权限预设层(`ctx.permission``PermissionService`把两个相互独立的强制执行旋钮knob即[沙箱模式](sandbox.md)`sandbox/mode`)与[审批策略](approval.md)`approval/policy`捆绑成具名预设供客户端作为单个权限Permissions选择器提供。它是一项可选能力不属于 agent loop智能体循环主干也不拥有任何强制执行执行、提示词叙述与回放仍然读取各自旋钮的折叠结果预设切换只记录意图并通过每个旋钮各自的规范 setter 写入。[packageREADME](../../packages/ui/permission/README.md) 负责组合状态与限制;[沙箱切换设计](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)负责决策依据。
[dsh-permission](../../packages/interaction/permission) 的权限预设层(`ctx.permission``PermissionService`把两个相互独立的强制执行旋钮knob即[沙箱模式](sandbox.md)`sandbox/mode`)与[审批策略](approval.md)`approval/policy`捆绑成具名预设供客户端作为单个权限Permissions选择器提供。它是一项可选能力不属于 agent loop智能体循环主干也不拥有任何强制执行执行、提示词叙述与回放仍然读取各自旋钮的折叠结果预设切换只记录意图并通过每个旋钮各自的规范 setter 写入。[packageREADME](../../packages/interaction/permission/README.md) 负责组合状态与限制;[沙箱切换设计](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)负责决策依据。
源码:[`packages/ui/permission/src/index.ts`](../../packages/ui/permission/src/index.ts)
源码:[`packages/interaction/permission/src/index.ts`](../../packages/interaction/permission/src/index.ts)
## 预设表
@@ -25,7 +25,7 @@ interface PresetSpec {
```
```ts type-equiv
/** The {@link PermissionService} config: the deployment's preset table. */
/** The {@link PermissionService} config: preset table and composition default. */
interface Config {
/**
* The preset table: name → knob bundle. Defaults to `workspace-write`
@@ -33,6 +33,11 @@ interface Config {
* never). The name `custom` is reserved for the derived not-a-preset state.
*/
presets?: Record<string, PresetSpec>
/**
* Default for new sessions. When omitted, the preset matching the composed
* sandbox and approval defaults is used.
*/
defaultPreset?: string
}
```
@@ -51,13 +56,13 @@ interface PresetOption {
value: string
/** The display label. */
name: string
/** One user-facing sentence on what the value means. */
/** One user-facing sentence on what the value means; omitted when not configured. */
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`)写入,且仅当该旋钮的生效值发生变化时才写。同一轮次内,选择事件先于旋钮事件出现;重新选择当前生效的预设则什么都不追加。
`set(session, name)` 解析预设(未知名称抛出异常),在 `name` 尚不是生效预设时追加一条仅记日志的 `permission/preset` 事件,然后通过各旋钮自己的 setter[dsh-sandbox-policy](../../packages/sandbox/sandbox-policy) 的 `setSandboxMode` 与 [dsh-user-approval](../../packages/interaction/user-approval) 的 `setApprovalPolicy`)写入,且仅当该旋钮的生效值发生变化时才写。同一轮次内,选择事件先于旋钮事件出现;重新选择当前生效的预设则什么都不追加。
`permission/preset` 是持久、仅记日志的用户意图:它不进入模型 transcript文本记录模型可见的后果由旋钮事件经各自消费方承担它存在是为了在两个预设共享同一个旋钮组合时让 `current()` 仍能保住用户选择的究竟是哪一个预设;`effectivePermissionPreset(events)` 折叠最后一条,回放不需要任何追赶状态。完整事件声明见[持久化日志事件目录](../persistence-catalog.md);方法签名见生成的[服务目录](../cordis-catalog/services.md#ctxpermission--permissionservice)。

View File

@@ -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/plan.md
plan.md: c8bb6c9b6a0bcfd20ed6eadece4552d49f3c757c
plan.zh.md: c8acedaef0724581227b57afbf81418a7e538aed
plan.md: 661a393b18f9154771c0b9358ad7f016a4fdec17
plan.zh.md: 068b63fd9f3bf46b7962031bba154cc7fc7daafd

View File

@@ -12,7 +12,7 @@ Source: [`packages/plan/plan-mode/src/index.ts`](../../packages/plan/plan-mode/s
## Pending intent and the step-boundary flush
Because every session event is turn-enclosed, a user selection is held as pending intent until the next step boundary — within the same turn when one is open. `set(agent, active)` records the pending selection (a no-op when the target equals the logged-or-already-pending state), and `get(agent)` returns `{ active: boolean; pending?: boolean }` — the logged state shaping the current step, plus the optimistic selection awaiting a boundary.
Because every session event is turn-enclosed, a user selection is held as pending intent until the next step boundary — the next request derivation, in whichever turn it occurs (selection never forces continuation, so an intent recorded after a turn's final step lands in a later turn). `set(agent, active)` records the pending selection (a no-op when the target equals the logged-or-already-pending state), and `get(agent)` returns `{ active: boolean; pending?: boolean }` — the logged state shaping the current step, plus the optimistic selection awaiting a boundary.
The sole flush point is a prepended `agent/step` listener — the loop's in-turn interception seam that runs before every request derivation, including turn 1 step 1 and request-recovery retries. Prompt admission itself never flushes: it happens pre-turn, where a `plan/mode` append would land outside any open turn, so a selection made at the prompt is landed by the first step boundary inside the turn it starts. The prepend means the flush runs before the downstream `agent/step` listener chain. A flush failure is contained — plan policy can never block a turn — and the failed append stays pending for a later boundary. A flushed user selection also narrates the switch as one plugin-sourced `user/message` notice, but only when the last logged request header described the other state, so the model is told exactly when its context changed and never redundantly. A pending selection made while idle is process-local and lost on exit before the next boundary ([README limitation](../../packages/plan/plan-mode/README.md#known-limitations-and-deferred-work)).

View File

@@ -12,7 +12,7 @@
## 待定意图与步骤边界冲刷
由于每个会话事件都位于轮次之内,用户的选择会作为待定意图保留到下一个步骤边界——已有轮次开启时即在同一轮次之内`set(agent, active)` 记录待定选择(目标值与已记录或已在待定中的状态相同时不做任何事),`get(agent)` 返回 `{ active: boolean; pending?: boolean }`,即影响当前步骤的已记录状态,加上正在等待边界的乐观选择。
由于每个会话事件都位于轮次之内,用户的选择会作为待定意图保留到下一个步骤边界——即下一次请求派生,落在哪个轮次就在哪个轮次生效(选择绝不强制续行,因此在某轮最后一步之后记录的意图会在之后的轮次落地)`set(agent, active)` 记录待定选择(目标值与已记录或已在待定中的状态相同时不做任何事),`get(agent)` 返回 `{ active: boolean; pending?: boolean }`,即影响当前步骤的已记录状态,加上正在等待边界的乐观选择。
唯一的冲刷点是一个前置prepend注册的 `agent/step` 监听器——agent loop 的轮内拦截 seam在每次请求派生之前运行包括第 1 轮第 1 步和请求恢复重试。提示词提交本身绝不冲刷:它发生在轮次开启之前,此时追加 `plan/mode` 会落在任何开启的轮次之外,因此在提示词处做出的选择由它开启的轮次内的第一个步骤边界落地。前置注册意味着冲刷先于下游的 `agent/step` 监听器链运行。冲刷失败会被收容(计划策略绝不能阻塞轮次),追加失败的选择保持待定,等待后续边界。已冲刷的用户选择还会以一条插件来源的 `user/message` 通知叙述这次切换,但仅当最后记录的请求头描述的是另一种状态时才叙述,因此模型恰好在上下文变化时被告知,且绝不重复。空闲时做出的待定选择只存在于进程内,进程在下一个边界之前退出即丢失([README 限制](../../packages/plan/plan-mode/README.md#known-limitations-and-deferred-work))。

View File

@@ -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/telemetry.md
telemetry.md: 92235a26fe278dfd6ac2b2ea5703b5c0d837b336
telemetry.zh.md: e076abd90bd5054ddbfd6fbe7b725ee45d50a038
telemetry.md: cf570723650c32f048ee797923a58f09c777455d
telemetry.zh.md: 1bcaa86264e3afd741d66d6ee718946e6151496f

View File

@@ -2,9 +2,9 @@
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).
Outbound session reporting, split as a [capability seam](../capability-seams.md): the seam ([dsh-session-telemetry](../../packages/session/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/session/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/session/session-telemetry/README.md).
Source: [`packages/telemetry/session-telemetry/src/index.ts`](../../packages/telemetry/session-telemetry/src/index.ts)
Source: [`packages/session/session-telemetry/src/index.ts`](../../packages/session/session-telemetry/src/index.ts)
## The logical record
@@ -12,7 +12,7 @@ Source: [`packages/telemetry/session-telemetry/src/index.ts`](../../packages/tel
/**
* 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
* says so (the tool-result block's `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.
@@ -69,9 +69,10 @@ 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.
* `session/event` hot path or an explicit canonical-log capture, so anything
* slower than a queue push would tax the agent loop or feedback handling.
* 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
@@ -96,6 +97,8 @@ interface TelemetryBackend {
* 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.
* The coordinator captures dispose-time shutdown markers immediately before
* this call for live capture; on-demand capture creates no ops records.
* @returns resolves when the backend's pipeline has quiesced.
*/
shutdown(): Promise<void>

View File

@@ -2,9 +2,9 @@
[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 Noteagent 决策记录)](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md)中定案;捕获点、游标与投影的契约见 [seam README](../../packages/telemetry/session-telemetry/README.md)。
对外的会话上报,拆分为一项[能力 seam](../capability-seams.md)seam 一侧([dsh-session-telemetry](../../packages/session/session-telemetry)`ctx.telemetry`)拥有捕获点、固定分片投影、`telemetry/record` 脱敏 waterfall瀑布式事件、handoff 游标与最小后端契约;部署方加载的后端([dsh-session-telemetry-otel](../../packages/session/session-telemetry-otel))则是原样配置的 OpenTelemetry JS SDK 日志流水线。它是一项可选能力,不属于 agent loop智能体循环主干这里也没有任何内容会进入模型请求。边界公理harness 的职责止于 `emit()`;批处理、重试、排队与丢失策略都属于上报 SDK连同被否决的替代方案均已在[复活 Agent Noteagent 决策记录)](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md)中定案;捕获点、游标与投影的契约见 [seam README](../../packages/session/session-telemetry/README.md)。
源码:[`packages/telemetry/session-telemetry/src/index.ts`](../../packages/telemetry/session-telemetry/src/index.ts)
源码:[`packages/session/session-telemetry/src/index.ts`](../../packages/session/session-telemetry/src/index.ts)
## 逻辑记录
@@ -12,7 +12,7 @@
/**
* 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
* says so (the tool-result block's `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.
@@ -69,9 +69,10 @@ 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.
* `session/event` hot path or an explicit canonical-log capture, so anything
* slower than a queue push would tax the agent loop or feedback handling.
* 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
@@ -96,6 +97,8 @@ interface TelemetryBackend {
* 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.
* The coordinator captures dispose-time shutdown markers immediately before
* this call for live capture; on-demand capture creates no ops records.
* @returns resolves when the backend's pipeline has quiesced.
*/
shutdown(): Promise<void>

View File

@@ -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: bade3526964055d5bf4448265c5898f1e66a1fce
workspace.zh.md: b0437e42b644c1ce11d9a667972f7b236cf4caa1
workspace.md: 5614631e828e51f3b515ae4af17451bd9eda1dbb
workspace.zh.md: 11e8f0207a8db8b7612d6672cfe744523490aef7

View File

@@ -68,7 +68,9 @@ interface Workspace {
/**
* Prepend a session to this workspace's candidate account. An already
* accounted id resolves without writing. A new id's live or persisted
* accounted id resolves without writing, aside from the durable
* filtered-candidate prune every accepted mutation performs. 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.
@@ -82,7 +84,9 @@ interface Workspace {
* 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).
* resolves without writing, aside from the durable filtered-candidate
* prune every accepted mutation performs; 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.
@@ -91,8 +95,9 @@ interface Workspace {
/**
* 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.
* the account resolves without writing, aside from the durable
* filtered-candidate prune every accepted mutation performs; 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.
*/

View File

@@ -68,7 +68,9 @@ interface Workspace {
/**
* Prepend a session to this workspace's candidate account. An already
* accounted id resolves without writing. A new id's live or persisted
* accounted id resolves without writing, aside from the durable
* filtered-candidate prune every accepted mutation performs. 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.
@@ -82,7 +84,9 @@ interface Workspace {
* 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).
* resolves without writing, aside from the durable filtered-candidate
* prune every accepted mutation performs; 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.
@@ -91,8 +95,9 @@ interface Workspace {
/**
* 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.
* the account resolves without writing, aside from the durable
* filtered-candidate prune every accepted mutation performs; 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.
*/