Merge branch 'master' into feat/web-plugin-settings-tabs

This commit is contained in:
Ziya
2026-08-12 18:40:38 +08:00
committed by GitHub
124 changed files with 3272 additions and 222 deletions

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 .agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md
2026-07-23-session-telemetry-otel-revival.md: 67ab0d28a168422a6b787a6088648746cd6079b6
2026-07-23-session-telemetry-otel-revival.zh.md: d59dcd07e9879e1c7cafc7a104d4f2515596cab8
2026-07-23-session-telemetry-otel-revival.md: c981a067efcc7107a7b7a9b9344a59f1a3bfe3d9
2026-07-23-session-telemetry-otel-revival.zh.md: c79d29687feeb1645b04031cc4c300a6465b1c38

View File

@@ -14,7 +14,7 @@ Every deployment that wants harness sessions in an observability stack must hand
- **`@deepseek-ai/dsh-session-telemetry`** — the seam. `TelemetryBackend` (`emit`/`flush?`/`shutdown`), the service-registered `Telemetry` form, and `TelemetryCoordinator` owning capture: live adoption with cursor read-back and the per-append firehose (project → `structuredClone` → redact → `emit`, zero I/O), buffer-free on-demand replay from the canonical log, the fixed first-chunk-per-(turn, step) projection, the live `agent/error` relay, and live dispose-time `shutdown` records.
- **The `telemetry/record` waterfall** — the delta over the branch version and the seam's redaction extension point. Every record passes it before reaching any backend; the seam ships NO rules of its own — the innermost `next()` is a pass-through, deployments mount their rules as listeners (stacking by transforming `next()`'s return value), and a throwing rule withholds the record fail-closed. Redaction applies to the exported copy only; the canonical log is never rewritten.
- **`@deepseek-ai/dsh-session-telemetry-otel`** — the reference backend: OTel JS SDK log pipeline (`LoggerProvider``BatchLogRecordProcessor` → OTLP/HTTP exporter), configured verbatim through `exporter`/`processor` passthroughs. Its default `FULL` mode requires `exporter.url`; the later [feedback-gated telemetry decision](2026-08-05-feedback-gated-session-telemetry.md) adds `FEEDBACK_ONLY` and `DISABLED` delivery modes without moving the redaction or backend boundary, while [buffer-free feedback replay](../simplification/2026-08-06-buffer-free-feedback-telemetry.md) avoids a second in-memory copy of the session prefix.
- **`@deepseek-ai/dsh-session-telemetry-otel`** — the reference backend: OTel JS SDK log pipeline (`LoggerProvider``BatchLogRecordProcessor` → OTLP/HTTP exporter), configured verbatim through `exporter`/`processor` passthroughs. `DISABLED` is the default and constructs no transport; the [feedback-gated telemetry decision](2026-08-05-feedback-gated-session-telemetry.md) defines the explicit `FULL` and `FEEDBACK_ONLY` delivery modes, which require `exporter.url`, without moving the redaction or backend boundary. [Buffer-free feedback replay](../simplification/2026-08-06-buffer-free-feedback-telemetry.md) avoids a second in-memory copy of the session prefix.
The boundary axiom holds: the harness's aspect ends at `emit()`. Batching, retry, queueing, and loss policy are the reporting SDK's, configured through passthroughs — delivery is best-effort (at-most-once across a crash), which the READMEs state plainly.
@@ -34,4 +34,4 @@ The boundary axiom holds: the harness's aspect ends at `emit()`. Batching, retry
## Consequences
A deployment adds one `cordis.yml` entry with an OTLP endpoint and gets its session stream in any OTel-compatible stack. `FULL` preserves that behavior by default, `FEEDBACK_ONLY` replays a canonical-log prefix when feedback is recorded, and `DISABLED` constructs no reporting pipeline; removing the entry remains a silent opt-out, while the disabled mode keeps the local feedback warning. A rule-free deployment exports records exactly as captured — including any credentials embedded in file contents or command output — so a deployment crossing a trust boundary must mount `telemetry/record` listeners, and both READMEs state this plainly. Where rules are mounted, exported bodies can differ from canonical log bytes, so receivers must not treat telemetry as a byte-exact replica; the log remains the source of truth. Crash durability is explicitly out of scope until the outbox decision above is revisited.
A deployment adds one `cordis.yml` entry with an OTLP endpoint and explicitly selects `FULL` to stream sessions into an OTel-compatible stack or `FEEDBACK_ONLY` to replay a canonical-log prefix when feedback is recorded. `DISABLED` is the [default](2026-08-10-telemetry-default-off.md) and constructs no reporting pipeline; removing the entry remains a silent opt-out, while the disabled mode keeps the local feedback warning. A rule-free deployment exports records exactly as captured — including any credentials embedded in file contents or command output — so a deployment crossing a trust boundary must mount `telemetry/record` listeners, and both READMEs state this plainly. Where rules are mounted, exported bodies can differ from canonical log bytes, so receivers must not treat telemetry as a byte-exact replica; the log remains the source of truth. Crash durability is explicitly out of scope until the outbox decision above is revisited.

View File

@@ -14,7 +14,7 @@ Status: implemented
- **`@deepseek-ai/dsh-session-telemetry`** —— seam 本体。`TelemetryBackend``emit`/`flush?`/`shutdown`)、服务注册形态的 `Telemetry`、以及拥有捕获侧的 `TelemetryCoordinator`:带游标回读的实时纳管与逐 append 的 firehose投影 → `structuredClone` → 脱敏 → `emit`,零 I/O、从权威日志进行的无缓冲按需回放、固定的每个轮次、步骤组合首分片投影、实时 `agent/error` 转发,以及实时 dispose资源释放时的 `shutdown` 记录。
- **`telemetry/record` waterfall瀑布式事件** —— 相对分支版本的增量,也是该 seam 的脱敏扩展点。每条记录抵达任何后端前必经此处seam 自身不带任何规则——最内层 `next()` 原样透传,部署方以监听器挂载自己的规则(通过变换 `next()` 的返回值堆叠),抛异常的规则将该记录 fail-closed 扣下。脱敏只作用于导出副本canonical log 永不改写。
- **`@deepseek-ai/dsh-session-telemetry-otel`** —— 参考后端OTel JS SDK 日志流水线(`LoggerProvider``BatchLogRecordProcessor` → OTLP/HTTP exporter`exporter`/`processor` passthrough 原样配置。其默认 `FULL` 模式要求 `exporter.url`;后续的[反馈门控遥测决策](2026-08-05-feedback-gated-session-telemetry.md)增加了 `FEEDBACK_ONLY``DISABLED` 投递模式,但未移动脱敏或后端边界,而[无缓冲反馈回放](../simplification/2026-08-06-buffer-free-feedback-telemetry.md)避免在内存中创建会话前缀的第二份副本。
- **`@deepseek-ai/dsh-session-telemetry-otel`** —— 参考后端OTel JS SDK 日志流水线(`LoggerProvider``BatchLogRecordProcessor` → OTLP/HTTP exporter`exporter`/`processor` passthrough 原样配置。`DISABLED` 是默认值,且不构造任何传输;[反馈门控遥测决策](2026-08-05-feedback-gated-session-telemetry.md)定义了需显式启用的 `FULL``FEEDBACK_ONLY` 投递模式,这两种模式要求 `exporter.url`,且不移动脱敏或后端边界[无缓冲反馈回放](../simplification/2026-08-06-buffer-free-feedback-telemetry.md)避免在内存中创建会话前缀的第二份副本。
边界公理保持不变harness 的职责止于 `emit()`。批处理、重试、排队与丢失策略属于 reporting SDK经 passthrough 配置——投递是尽力而为(崩溃时至多一次),两份 README 对此如实陈述。
@@ -34,4 +34,4 @@ Status: implemented
## 后果
部署方在 `cordis.yml` 加一个带 OTLP endpoint 的 Cordis 配置项即可把会话流接入任何 OTel 兼容体系`FULL` 默认保留该行为,`FEEDBACK_ONLY` 在记录反馈时回放权威日志前缀`DISABLED` 不构造上报流水线;删除该配置项仍是静默退出方式,而禁用模式会保留本地反馈警告。未挂载规则的部署导出的记录与捕获时完全一致,包括文件内容与命令输出中内嵌的任何凭据。因此,跨信任边界的部署必须挂载 `telemetry/record` 监听器,两个 README 对此如实陈述。挂载规则后,导出的 body 可能与 canonical log 字节不同,接收端不得把遥测当作字节精确副本;日志仍是真源。崩溃持久性在上述 outbox 决定重新审议前明确不在范围内。
部署方在 `cordis.yml` 加一个带 OTLP endpoint 的 Cordis 配置项,并显式选择 `FULL`即可把会话流接入任何 OTel 兼容体系;选择 `FEEDBACK_ONLY` 则会在记录反馈时回放权威日志前缀`DISABLED` 是[默认值](2026-08-10-telemetry-default-off.md),且不构造上报流水线;删除该配置项仍是静默退出方式,而禁用模式会保留本地反馈警告。未挂载规则的部署导出的记录与捕获时完全一致,包括文件内容与命令输出中内嵌的任何凭据。因此,跨信任边界的部署必须挂载 `telemetry/record` 监听器,两个 README 对此如实陈述。挂载规则后,导出的 body 可能与 canonical log 字节不同,接收端不得把遥测当作字节精确副本;日志仍是真源。崩溃持久性在上述 outbox 决定重新审议前明确不在范围内。

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 .agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md
2026-07-30-versioned-gui-welcome-onboarding.md: ff4bfc860492d7eca345d55449e4e4726b988854
2026-07-30-versioned-gui-welcome-onboarding.zh.md: dd293fe9e1e77414c4b352aa50e6b0456f866cce
2026-07-30-versioned-gui-welcome-onboarding.md: f8cb3b9f72335e34e5b6f5aef6966c8822717d5c
2026-07-30-versioned-gui-welcome-onboarding.zh.md: 5c9d87d4f834065bec8c67b55c3d3dba4eaa1d52

View File

@@ -12,7 +12,7 @@ The GUI's credential onboarding begins with a DeepSeek-specific readiness check,
**The Settings shell coordinates ordered steps.** `settings.onboarding` remains a root-scoped list, but `ui-settings` projects its entry ids and order into one coordinator and mounts only the first incomplete step. The active registrant receives `complete()` and `openSection(id)`; no later step mounts until ownership transfers. The product welcome registers at order `-100`, while `ui-models` retains only the conditional DeepSeek readiness and credential-routing step at order `0`.
**Ownerless product onboarding belongs to `ui-settings-general`.** `src/onboarding-copy.ts` is the single editable source for the complete notice, the Continue label, and `WELCOME_NOTICE_VERSION`; both supported GUI locales intentionally render the same Chinese owner copy. Runtime locale dictionaries derive their welcome values from that file, and tests import the same owner instead of repeating paragraph text. The notice is browser UI only: it creates no Session event and contributes no model-visible content. The notice identifies `DSH_TELEMETRY_DISABLED=1` as the telemetry opt-out.
**Ownerless product onboarding belongs to `ui-settings-general`.** `src/onboarding-copy.ts` is the single editable source for the complete notice, the Continue label, and `WELCOME_NOTICE_VERSION`; both supported GUI locales intentionally render the same Chinese owner copy. Runtime locale dictionaries derive their welcome values from that file, and tests import the same owner instead of repeating paragraph text. The notice is browser UI only: it creates no Session event and contributes no model-visible content. The notice states that session telemetry is [disabled by default](2026-08-10-telemetry-default-off.md) and names the `FEEDBACK_ONLY` and `FULL` opt-in modes.
**Loopback acknowledgement is durable per Harness profile.** The Host half registers a `ui-onboarding` section in the user-settings seam, stored under the active `$DSH_HOME/settings.yaml`. The connection plugin publishes whether the current page uses a loopback authority as `ctx.connection.isLoopback`; hostname classification remains internal to the connection package, and other client plugins consume the service state instead of importing its implementation. A loopback browser shows the notice unless `welcomeNoticeVersion` equals the owner constant exactly. Continue applies one path mutation with the current version and calls `complete()` only after the Host commits it; a failed write leaves the notice open, and closing the page or process writes nothing. Bumping the constant intentionally makes every profile acknowledge the revised copy once. A non-loopback browser must not call the loopback-only settings API. It presents the same notice, but explicit Continue completes the step only in the current browser process; reload or a new process presents it again.

View File

@@ -12,7 +12,7 @@ GUI 的凭据引导从 DeepSeek 专用的就绪状态检查开始,但内部测
**设置外壳协调有序步骤。** `settings.onboarding` 仍是根作用域 list`ui-settings` 会把其中各条目的 id 和顺序投影到一个协调器中,并且只挂载第一个未完成的步骤。当前注册方会收到 `complete()``openSection(id)`;所有权转移前,不会挂载后续步骤。产品欢迎步骤的顺序为 `-100``ui-models` 则只保留顺序为 `0` 的 DeepSeek 条件式就绪状态与凭据跳转步骤。
**不属于单一功能的产品引导由 `ui-settings-general` 持有。** `src/onboarding-copy.ts` 是完整通知、「继续」按钮文案和 `WELCOME_NOTICE_VERSION` 的唯一可编辑来源GUI 支持的两种 locale 都有意渲染同一份中文所有者文案。运行时 locale 字典从该文件派生欢迎文案,测试也导入同一个所有者,而不重复段落文本。该通知只存在于浏览器 UI它不会创建会话事件也不会贡献任何模型可见内容。通知明确以 `DSH_TELEMETRY_DISABLED=1` 作为遥测关闭方式。
**不属于单一功能的产品引导由 `ui-settings-general` 持有。** `src/onboarding-copy.ts` 是完整通知、「继续」按钮文案和 `WELCOME_NOTICE_VERSION` 的唯一可编辑来源GUI 支持的两种 locale 都有意渲染同一份中文所有者文案。运行时 locale 字典从该文件派生欢迎文案,测试也导入同一个所有者,而不重复段落文本。该通知只存在于浏览器 UI它不会创建会话事件也不会贡献任何模型可见内容。通知说明会话遥测[默认禁用](2026-08-10-telemetry-default-off.md),并列出 `FEEDBACK_ONLY``FULL` 两种显式启用模式。
**loopback 确认状态按 Harness profile 持久化。** 宿主端在 user-settings seam 中注册 `ui-onboarding` 分节,并存入当前 `$DSH_HOME/settings.yaml`。connection 插件通过 `ctx.connection.isLoopback` 统一发布当前页面是否使用 loopback authorityhostname 判定留在 connection 包内,其他客户端插件只消费服务状态,而不导入其实现。除非 `welcomeNoticeVersion` 与文案所有者文件中的常量精确相等,否则 loopback 浏览器会显示通知。「继续」会以当前版本执行一次路径变更,并且仅在宿主端提交成功后调用 `complete()`;写入失败时通知保持打开,关闭页面或进程则不会写入任何内容。更新该常量会有意要求每个 profile 对修订后的文案重新确认一次。非 loopback 浏览器不能调用仅限 loopback 的 settings API它仍显示同一通知但显式点击「继续」只会在当前浏览器进程中完成该步骤重新加载或新进程会再次显示通知。

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 .agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md
2026-07-31-web-telemetry-default-mount.md: 3e20fa44666139778e85a8bd5b948ab2df14529b
2026-07-31-web-telemetry-default-mount.zh.md: 97b153255a1e64aba0a686faf1817b69467fb663
2026-07-31-web-telemetry-default-mount.md: 93bb788bd48ab8c82bd013c8f844e01913f537e4
2026-07-31-web-telemetry-default-mount.zh.md: d75d3e36f5e4b6c9fab146b28e301cfc3b11f8b6

View File

@@ -10,23 +10,24 @@ The telemetry seam and OTel backend ([revival Note](2026-07-23-session-telemetry
## Decision
The shared `dsh` base (`apps/cli/config/base.cordis.yml`) mounts the `telemetry-otel` row by default with a baked-in production endpoint, so Web and headless report; the raw-config command also mounts it before applying its required deployment overlay. This is the **internal-testing deployment stance** — reporting is on when an endpoint exists, and users opt out through the environment. Web and headless use the [bounded, escalating process-shutdown controller](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.md) on SIGINT/SIGTERM, giving the backend's three-second shutdown deadline time to drain before the five-second launcher bound.
The shared dsh base bundle (`packages/bundle/base/cordis.patch.yml`) mounts the `telemetry-otel` row with a baked-in production endpoint, so every profile has one consistent telemetry capability. The [default-off decision](2026-08-10-telemetry-default-off.md) keeps that row in `DISABLED` mode unless a deployment explicitly selects `FULL` or `FEEDBACK_ONLY`; the endpoint alone does not authorize reporting. Web and headless use the [bounded, escalating process-shutdown controller](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.md) on SIGINT/SIGTERM, giving an enabled backend's three-second shutdown deadline time to drain before the five-second launcher bound.
| Ruling | Value | Rationale |
|---|---|---|
| Mount surface | base.cordis.yml (raw config + Web + headless) | One deployment stance for every tree that loads the shared base; the raw overlay decides whether that deployment creates sessions |
| Mount surface | `packages/bundle/base/cordis.patch.yml` | One capability row for every profile that loads the shared base |
| Sharing mode | `DSH_TELEMETRY_MODE`, default `DISABLED`; explicit `FULL` or `FEEDBACK_ONLY` opts in | A fresh profile makes no telemetry network request, while internal deployments retain both upload policies |
| Endpoint | `DSH_TELEMETRY_OTLP_URL`, default `https://harness-telemetry.deepseeksvc.com/v1/logs` | Internal collector; the env override serves local/dev runs |
| Opt-out switch | any non-empty `DSH_TELEMETRY_DISABLED` (including `0`/`false`) disables | A privacy switch prefers off-by-mistake over on-by-mistake; a row can only be disabled at AppCLIEntry's patch layer (config has no disable semantic, and the switch must precede the load-time `exporter.url` validation) |
| Cadence | `processor.scheduledDelayMillis: 10000` (10s/batch) | Streaming while the session runs, never exit-time-only; a crash loses at most the last unexported interval |
| Hard opt-out | any non-empty `DSH_TELEMETRY_DISABLED` (including `0`/`false`) disables the row | The launcher patch takes effect before load-time transport validation and overrides every configured mode |
| Cadence | `processor.scheduledDelayMillis: 10000` (10s/batch) in uploading modes | Streaming while the session runs, never exit-time-only; a crash loses at most the last unexported interval |
| Exit-drain bound | `exporter.timeoutMillis: 1000` + `maxExportBatchSize: 2048` (== maxQueueSize) + `exportTimeoutMillis: 1500` + `shutdownTimeoutMillis: 3000` | Ordinary unreachable-collector failure releases in ~1s: timeoutMillis is the per-attempt socket timeout and retry deadline, while one queue-sized batch avoids sequential drain multiplication. The DSH-owned 3s outer bound covers the SDK's preceding unbounded `forceFlush()` wait when the transport Promise never obtains a socket. |
| Compression | `compression: gzip` | Event bodies carry full content; cross-datacenter bandwidth |
| CI isolation | top-level `env: DSH_TELEMETRY_DISABLED: '1'` in all 8 GitHub workflows | Every CI channel that boots the web composition (e2e/snapshot/built smokes) must not stream test sessions to the production endpoint |
| CI isolation | top-level `env: DSH_TELEMETRY_DISABLED: '1'` in GitHub workflows | Defense in depth keeps test sessions local even when a job explicitly selects an uploading mode |
The keyless integration test `apps/cli/tests/telemetry-web.e2e.ts` pins the deployment-level behavior: an in-test OTLP collector plus a mock LLM server, a real `dsh web` boot, asserting ledger coverage, seq monotonicity, the first-of-step chunk projection, and the ops `shutdown` marker arriving through the SIGINT drain.
The base bundle test pins the shipped `DISABLED` mode expression, the backend suite pins that omitted mode constructs no transport, and the real Loader composition suite explicitly selects each uploading mode when it verifies OTLP delivery.
## Alternatives considered
**No default mount; deployments add the row themselves (continuing the SDK stance).** Rejected for this stage: this repo's web/headless composition IS the internal deployment, and default-on reporting is that deployment's product requirement; the SDK stance survives in the Service Definition packages (unmounted = nothing leaves).
**No default mount; deployments add the row themselves.** Rejected because the mounted `DISABLED` mode retains a local feedback warning and gives all profiles one patch target without authorizing any upload.
**A config field instead of an env patch for the switch.** Infeasible: cordis rows have no config-level disable semantic, and `exporter.url` validation fails loud at plugin construction, so the switch must take effect before the Loader — AppCLIEntry's patch layer is the only seat.
@@ -34,6 +35,6 @@ The keyless integration test `apps/cli/tests/telemetry-web.e2e.ts` pins the depl
## Consequences
- A developer running `dsh web` without a local collector POSTs to the production endpoint every 10s (silent failure when unreachable; no OTel diag logger is registered); local development sets `DSH_TELEMETRY_DISABLED=1` or points `DSH_TELEMETRY_OTLP_URL` locally.
- **No redaction rule is mounted yet**: exports are the raw captured copy (full user/assistant message text, tool arguments and results, the system prompt, the local `session.cwd` path). Crossing a trust boundary requires `telemetry/record` rules first — the redaction rule, the remaining identity Resource attributes (hostname / surface; the anonymous user id shipped via the [anonymous-user-id Note](2026-07-31-telemetry-anonymous-user-id.md)), and the usage-metrics track are the explicit follow-ups of this decision.
- Test rigs reusing this tree (e.g. `apps/web/tests/scaffold.ts`) must explicitly disable the row, or fixture sessions stream to whatever collector the environment happens to name.
- A developer running `dsh web` without telemetry configuration makes no telemetry network request. An internal deployment sets `DSH_TELEMETRY_MODE` and may point `DSH_TELEMETRY_OTLP_URL` at another collector.
- **No redaction rule is mounted**: explicitly enabled exports are the raw captured copy (full user/assistant message text, tool arguments and results, the system prompt, the local `session.cwd` path). Crossing a trust boundary requires `telemetry/record` rules first — the redaction rule, remaining identity Resource attributes, and usage metrics remain separate deployment work. The anonymous user id ships through the [anonymous-user-id Note](2026-07-31-telemetry-anonymous-user-id.md).
- Test rigs remain local by default; explicit uploading-mode tests provide their own collector and mode.

View File

@@ -10,23 +10,24 @@ Status: implemented
## 决策
`dsh` 共享 base`apps/cli/config/base.cordis.yml`)默认挂载 `telemetry-otel` 行,内置生产 endpoint因此 Web 与 headless 都会上报;原始配置命令也会先挂载该行,再应用其必需的部署 overlay。这是**内部测试期的部署立场**——有 endpoint 就上报,用户可通过环境变量退出。Web 与 headless 在 SIGINT/SIGTERM 时使用[有界、可升级的进程关闭控制器](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.md),在启动器 5 秒上限到期前,先给后端 3 秒关闭截止时间完成排空。
共享 dsh 基础组合包(`packages/bundle/base/cordis.patch.yml`)挂载带有内置生产 endpoint 的 `telemetry-otel` 配置行,使每个 profile 都具有一致的遥测能力。[默认关闭决策](2026-08-10-telemetry-default-off.md)让该配置行保持 `DISABLED` 模式,除非部署方显式选择 `FULL``FEEDBACK_ONLY`;仅配置 endpoint 不构成上报授权。Web 与 headless 在 SIGINT/SIGTERM 时使用[有界、可升级的进程关闭控制器](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.md),在启动器 5 秒上限到期前,先给已启用的后端 3 秒关闭截止时间完成排空。
| 决策项 | 取值 | 理由 |
|---|---|---|
| 挂载面 | base.cordis.yml原始配置 + Web + headless | 所有加载共享 base 的配置树采用同一个部署立场;原始配置 overlay 决定该部署是否创建会话 |
| 挂载面 | `packages/bundle/base/cordis.patch.yml` | 每个加载共享基础组合包的 profile 都使用同一个能力配置行 |
| 共享模式 | `DSH_TELEMETRY_MODE`,默认 `DISABLED`;显式设置 `FULL``FEEDBACK_ONLY` 即启用 | 新 profile 不发出遥测网络请求,内部部署仍可使用两种上传策略 |
| endpoint | `DSH_TELEMETRY_OTLP_URL`,缺省 `https://harness-telemetry.deepseeksvc.com/v1/logs` | 内部 collectorenv 覆盖供本地/联调 |
| 退出开关 | `DSH_TELEMETRY_DISABLED` 非空(含 `0`/`false`)即关 | 隐私向开关取「宁关勿误开」;行级 disable 只能在 AppCLIEntry 的 patch 层做config 无 disable 语义,且必须先于 `exporter.url` 的加载期校验生效) |
| 上报节奏 | `processor.scheduledDelayMillis: 10000`10s/批) | 流式回流,非退出才报;崩溃至多丢失最后一个尚未导出间隔内的数据 |
| 硬性退出 | `DSH_TELEMETRY_DISABLED` 非空(含 `0`/`false`)即禁用该配置行 | 启动器 patch 在加载期传输校验之前生效,并覆盖所有已配置模式 |
| 上报节奏 | 上传模式中为 `processor.scheduledDelayMillis: 10000`10s/批) | 在会话运行期间流式上报,而非仅在退出时上报;崩溃至多丢失最后一个尚未导出间隔内的数据 |
| 退出 drain 上界 | `exporter.timeoutMillis: 1000` + `maxExportBatchSize: 2048`(与 maxQueueSize 相等) + `exportTimeoutMillis: 1500` + `shutdownTimeoutMillis: 3000` | collector 不可达的常规故障会在约 1s 内放行timeoutMillis 是单次 socket 超时与重试 deadline使用与队列等大的单批可避免依次排空导致耗时倍增。由 DSH 管理的 3s 外层上限覆盖 SDK 先执行的无界 `forceFlush()` 等待,即传输 Promise 始终无法取得 socket 的情况。 |
| 压缩 | `compression: gzip` | 事件 body 含全文,跨机房带宽 |
| CI 隔离 | 全部 8 个 GitHub 工作流顶层 `env: DSH_TELEMETRY_DISABLED: '1'` | CI 启动 web 组合的所有通道e2e/快照/已构建版本的冒烟测试)不得向生产 endpoint 泄测试会话 |
| CI 隔离 | GitHub 工作流顶层 `env: DSH_TELEMETRY_DISABLED: '1'` | 即使 CI 任务显式选择上传模式,纵深防御也会让测试会话留在本地 |
集成测试 `apps/cli/tests/telemetry-web.e2e.ts`keyless钉住部署级行为测试内 OTLP collector + mock LLM大语言模型服务器真启动 `dsh web`,断言 ledger 覆盖、seq 单调、步骤首个分片投影、以及 SIGINT drain 后 ops `shutdown` 标记到达
基础组合包测试固定交付的 `DISABLED` 模式表达式,后端测试套件固定省略模式时不构造传输,真实 Loader 组合测试则在验证 OTLP 投递时显式选择每种上传模式
## 考虑过的替代方案
**默认不挂载,部署方自行加行SDK 立场的延续)。** 当前阶段不采用:本仓的 web/headless 组合就是内部部署本身「上报默认开」是这个部署的产品要求SDK 立场仍由 Service Definition 包保持(不挂 = 零外发)
**默认不挂载,部署方自行添加配置行。** 不采用:挂载的 `DISABLED` 模式会保留本地反馈警告,并为所有 profile 提供同一个 patch 目标,同时不授权任何上传
**开关做成 config 字段而非 env patch。** 不可行cordis 行没有 config 层的 disable 语义,且 `exporter.url` 校验在插件构造期 fail-loud开关必须在 Loader 之前生效——AppCLIEntry patch 层是唯一落点。
@@ -34,6 +35,6 @@ Status: implemented
## 后果
- 无本地 collector 的开发者跑 `dsh web` 会对生产 endpoint 每 10s 发一次 POST联不通则静默失败OTel diag logger 未注册);本地开发设 `DSH_TELEMETRY_DISABLED=1` `DSH_TELEMETRY_OTLP_URL`本地
- **当前尚未挂载任何脱敏规则**:导出即原始捕获副本(用户/助手消息全文、工具参数与工具结果、系统提示词、`session.cwd` 本地路径)。跨信任边界前必须 `telemetry/record` 规则——脱敏规则、其余身份 Resource 属性hostname/surface匿名 user id 由[匿名用户 id Note](2026-07-31-telemetry-anonymous-user-id.md)落地)、使用数据 metrics 轨是本决策明确的后续工作
- 复用这棵树的测试载具(如 `apps/web/tests/scaffold.ts`)须显式关停该行,否则 fixture测试前置数据会话会流向 env 里碰巧存在的 collector。
- 开发者运行没有遥测配置的 `dsh web` 时,不会发出遥测网络请求。内部部署需设置 `DSH_TELEMETRY_MODE`,并可让 `DSH_TELEMETRY_OTLP_URL`向其他 collector
- **没有挂载任何脱敏规则**显式启用的导出即原始捕获副本(用户/助手消息全文、工具参数与工具结果、系统提示词、`session.cwd` 本地路径)。跨信任边界前必须先挂载 `telemetry/record` 规则脱敏规则、其余身份 Resource 属性和使用情况指标仍是独立的部署工作。匿名 user id 由[匿名 user id Note](2026-07-31-telemetry-anonymous-user-id.md)交付
- 测试载具默认将数据留在本地;显式启用上传模式的测试提供自己的 collector 和模式

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 .agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.md
2026-08-05-feedback-gated-session-telemetry.md: 7d923a7e4cf61e8d1119187564b87e4cbb2065b7
2026-08-05-feedback-gated-session-telemetry.zh.md: e2fa6f9561de6e7295cbf860527c7d7915fec0bf
2026-08-05-feedback-gated-session-telemetry.md: 524eb9defa9981c6d535ff68012d206b9c0cff94
2026-08-05-feedback-gated-session-telemetry.zh.md: 557e3681f627931560d19172ddd8fa69a2984af1

View File

@@ -6,15 +6,15 @@ English | [中文](2026-08-05-feedback-gated-session-telemetry.zh.md)
## Problem
Session telemetry originally has one mounted behavior: every accepted record enters the reporting backend immediately. Deployments need two stricter policies without replacing the plugin: hold a session's telemetry unless its user records feedback, or disable reporting while still explaining what happens to feedback. The policy must preserve the existing full-export default and the telemetry seam's redaction-before-backend boundary.
Session telemetry originally has one mounted behavior: every accepted record enters the reporting backend immediately. Deployments need two stricter policies without replacing the plugin: hold a session's telemetry unless its user records feedback, or disable reporting while still explaining what happens to feedback. The policy must preserve the telemetry seam's redaction-before-backend boundary.
## Decision
`@deepseek-ai/dsh-session-telemetry-otel` exposes the string-valued `TelemetryMode` enum to TypeScript callers and accepts the same three uppercase `mode` values in serialized configuration:
- `FULL` is the default and preserves immediate delivery to the configured OTel pipeline.
- `FULL` explicitly selects immediate delivery to the configured OTel pipeline.
- `FEEDBACK_ONLY` reads the canonical session log when `feedback/record` is appended and hands over the unreleased prefix through that exact event. Records appended after that boundary remain local until another feedback event.
- `DISABLED` constructs no exporter, processor, or logger provider. A `feedback/record` listener prints that nothing is shared and the feedback remains local.
- `DISABLED` is the [default](2026-08-10-telemetry-default-off.md), constructs no exporter, processor, or logger provider, and prints that nothing is shared and the feedback remains local when it observes `feedback/record`.
The generic telemetry coordinator owns `live` and `on-demand` capture. Live capture projects, clones, redacts, and hands each event to the backend on the session firehose. On-demand capture registers no continuous capture listeners; `captureSession(session, throughSeq)` reads the canonical log from the handoff cursor through an inclusive boundary, then projects, clones, redacts, and hands over that prefix. The cursor advances only for handed-over records. The [buffer-free replay decision](../simplification/2026-08-06-buffer-free-feedback-telemetry.md) owns why the on-demand path uses the canonical log instead of copied records.
@@ -32,4 +32,4 @@ Mode resolution is a closed, fail-before-setup check: an unknown direct-construc
## Consequences
`FULL` remains source- and wire-compatible with the original default. `FEEDBACK_ONLY` adds no telemetry-owned per-event buffer before feedback; direct service calls and non-canonical feedback events upload nothing, and a crash before feedback uploads nothing from that prefix. Replay applies the redaction policy mounted when feedback is recorded and excludes operational records that do not exist in the canonical log. Feedback-only streams therefore carry neither `agent-error` nor `shutdown` records, and shutdown absence is not a crash signal. Each later feedback captures the suffix accumulated since the previous boundary. `DISABLED` can omit `exporter.url`, does no reporting work, and keeps feedback only in the canonical session log.
`FULL` retains the original source and wire behavior as an explicit opt-in. `FEEDBACK_ONLY` adds no telemetry-owned per-event buffer before feedback; direct service calls and non-canonical feedback events upload nothing, and a crash before feedback uploads nothing from that prefix. Replay applies the redaction policy mounted when feedback is recorded and excludes operational records that do not exist in the canonical log. Feedback-only streams therefore carry neither `agent-error` nor `shutdown` records, and shutdown absence is not a crash signal. Each later feedback captures the suffix accumulated since the previous boundary. `DISABLED` can omit `exporter.url`, does no reporting work, and keeps feedback only in the canonical session log.

View File

@@ -6,15 +6,15 @@ Status: implemented
## 问题
会话遥测原本只有一种已挂载行为:每条已接受记录都立即进入上报后端。部署方需要两种更严格的策略,且不替换插件:只有用户记录反馈时才释放该会话的遥测,或禁用上报并仍向用户说明反馈的去向。该策略必须保留现有的全量导出默认值,以及遥测 seam 在记录抵达后端之前脱敏的边界。
会话遥测原本只有一种已挂载行为:每条已接受记录都立即进入上报后端。部署方需要两种更严格的策略,且不替换插件:只有用户记录反馈时才释放该会话的遥测,或禁用上报并仍向用户说明反馈的去向。该策略必须保留遥测 seam 在记录抵达后端之前脱敏的边界。
## 决策
`@deepseek-ai/dsh-session-telemetry-otel` 向 TypeScript 调用方公开以字符串为值的 `TelemetryMode` 枚举,并在序列化配置中接受相同的三个大写 `mode` 值:
- `FULL` 是默认值,保留向已配置 OTel 流水线即时投递。
- `FULL` 显式选择向已配置 OTel 流水线即时投递。
- `FEEDBACK_ONLY` 在追加 `feedback/record` 时读取权威会话日志,并交接截至该事件的未释放前缀。该边界后追加的记录会留在本地,直到另一个反馈事件。
- `DISABLED` 不构造导出器、处理器或日志提供方`feedback/record` 监听器会输出警告,说明什么都不会共享,且反馈仍留在本地。
- `DISABLED` 是[默认值](2026-08-10-telemetry-default-off.md)不构造导出器、处理器或日志提供方,并在观察到 `feedback/record` 输出警告,说明什么都不会共享,且反馈仍留在本地。
通用遥测协调器拥有 `live``on-demand` 捕获。实时捕获在会话 firehose 上投影、深拷贝、脱敏每个事件,并将其交给后端。按需捕获不注册持续捕获监听器;`captureSession(session, throughSeq)` 从 handoff 游标起读取权威日志,直至含边界的指定序列号,然后投影、深拷贝、脱敏并交接该前缀。游标只为已交接记录推进。[无缓冲回放决策](../simplification/2026-08-06-buffer-free-feedback-telemetry.md)说明了按需路径为何使用权威日志而非记录副本。
@@ -32,4 +32,4 @@ Status: implemented
## 后果
`FULL` 与原有默认值保持源码协议兼容`FEEDBACK_ONLY` 在反馈前不增加遥测自有的逐事件缓冲;直接服务调用与非权威反馈事件均不上传任何内容,且反馈前发生崩溃时,该前缀也不上传任何内容。回放使用记录反馈时挂载的脱敏策略,并排除权威日志中不存在的运维记录。因此,仅反馈的流既不携带 `agent-error` 记录,也不携带 `shutdown` 记录,而缺少 shutdown 不是崩溃信号。每个后续反馈都会捕获从上一个边界起累积的后缀。`DISABLED` 可省略 `exporter.url`,不执行任何上报工作,并仅在权威会话日志中保留反馈。
`FULL` 作为显式启用模式保留原有的源码协议行为`FEEDBACK_ONLY` 在反馈前不增加遥测自有的逐事件缓冲;直接服务调用与非权威反馈事件均不上传任何内容,且反馈前发生崩溃时,该前缀也不上传任何内容。回放使用记录反馈时挂载的脱敏策略,并排除权威日志中不存在的运维记录。因此,仅反馈的流既不携带 `agent-error` 记录,也不携带 `shutdown` 记录,而缺少 shutdown 不是崩溃信号。每个后续反馈都会捕获从上一个边界起累积的后缀。`DISABLED` 可省略 `exporter.url`,不执行任何上报工作,并仅在权威会话日志中保留反馈。

View 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 .agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md
2026-08-10-telemetry-default-off.md: 4bda346c2b05a94106eb5658c3ee558a4b32407f
2026-08-10-telemetry-default-off.zh.md: 706f2c18fbbf226e0357fa99bf3fd61c39fce08a

View File

@@ -0,0 +1,31 @@
# Agent Note: Telemetry requires explicit opt-in
Status: implemented
English | [中文](2026-08-10-telemetry-default-off.zh.md)
## Problem
DeepSeek Harness has two outbound telemetry feeds. During internal testing, the shared base mounted telemetry with a baked-in production endpoint, and both feeds reported by default to help diagnose reported problems: the session OTel backend could export complete session content, tool data, prompts, and workspace paths when its mode was omitted, while the dsh-sdk launcher feed did so unconditionally. A fresh installation therefore permitted outbound reporting without a positive deployment choice.
## Decision
Both feeds use `DSH_TELEMETRY_MODE` as their positive consent setting. Unset and empty values resolve to `DISABLED`. `@deepseek-ai/dsh-session-telemetry-otel` also resolves an omitted `mode` to `DISABLED`, which constructs no OTel provider, processor, or exporter and leaves feedback in the local session log. The shared dsh base keeps the backend row mounted so disabled feedback can still explain that nothing was shared. A deployment opts into Session Log sharing through `FULL` or `FEEDBACK_ONLY`; only `FULL` also permits dsh-sdk launcher reporting. Any non-empty `DSH_TELEMETRY_DISABLED` remains an authoritative pre-load hard opt-out. The [default-mount decision](2026-07-31-web-telemetry-default-mount.md) continues to own the endpoint, batching cadence, and exit-drain settings.
The dsh-sdk launcher reads the same variable without parsing `cordis.yml` or booting Cordis. `FULL` permits reporting; `FEEDBACK_ONLY`, `DISABLED`, unset, and empty values deny it. Consent is frozen from the launching environment before the command runs, because `dsh-sdk start` loads a project `.env` and project code can mutate `process.env`: resolving afterwards would let a project grant reporting of its own configuration, which the [configuration source ownership decision](../architecture/2026-08-04-configuration-source-ownership.md) denies for the whole `DSH_*` namespace. An unsupported mode denies rather than throwing at that boundary, since telemetry may never change a command's result. This rule supersedes only the default-on launcher consent in the [SDK follow-up proposal](../../proposed/feature/2026-07-17-sdk-follow-up-capabilities.md); its other capabilities remain proposed.
The versioned Web welcome notice states that Session Log upload is off by default, names `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` and `DSH_TELEMETRY_MODE=FULL` as the two opt-in choices, and discloses that `FULL` also enables dsh-sdk command telemetry. Its version changes with that material privacy statement so every profile acknowledges the current copy.
## Alternatives considered
**Keep opt-out defaults and improve disclosure.** Rejected because disclosure does not make a missing configuration a positive authorization to send data, especially when session telemetry can contain complete local content.
**Default session telemetry to `FEEDBACK_ONLY`.** Rejected because recording feedback would still trigger an upload without a deployment explicitly enabling outbound reporting. The default must keep both the session and its feedback local.
**Add project-level consent markers.** Rejected because `DSH_TELEMETRY_MODE` already expresses consent for both feeds; another configuration entry would create conflicting settings and require launcher-specific parsing.
**Remove both telemetry implementations.** Rejected because internal deployments still need explicit `FULL` and feedback-gated reporting, and the launcher feed remains useful under `FULL`.
## Consequences
Fresh profiles and projects make no telemetry network request. Internal deployments select one mode for both feeds: `FEEDBACK_ONLY` permits only feedback-triggered Session Log sharing, while `FULL` also enables launcher reporting. The existing hard opt-out remains effective, and uploading modes retain their endpoint validation, redaction responsibility, batching, and shutdown behavior.

View File

@@ -0,0 +1,31 @@
# Agent Note: 遥测必须显式启用
Status: implemented
[English](2026-08-10-telemetry-default-off.md) | 中文
## 问题
DeepSeek Harness 有两路出站遥测数据流。在内测阶段,共享基础配置挂载了带内建生产 endpoint 的遥测,两路数据流默认上报以帮助诊断上报的问题:会话 OTel 后端在省略 `mode` 时可能导出完整会话内容、工具数据、提示词和工作区路径,而 dsh-sdk 启动器数据流则无条件外发。因此,全新安装无需部署方明确选择便允许向外上报。
## 决策
两路数据流都使用 `DSH_TELEMETRY_MODE` 作为正向授权配置。未设置和空值都解析为 `DISABLED``@deepseek-ai/dsh-session-telemetry-otel` 也将省略的 `mode` 解析为 `DISABLED`;该模式不构造 OTel 提供方、处理器或导出器并将反馈留在本地会话日志中。dsh 共享基础配置继续挂载后端配置行,使禁用模式仍可在记录反馈时说明没有共享任何内容。部署方通过 `FULL``FEEDBACK_ONLY` 显式启用 Session Log 共享;只有 `FULL` 还允许 dsh-sdk 启动器上报。任何非空 `DSH_TELEMETRY_DISABLED` 仍是具有最高优先级的加载前硬性退出开关。[默认挂载决策](2026-07-31-web-telemetry-default-mount.md)继续负责 endpoint、批处理节奏和退出排空设置。
dsh-sdk 启动器读取同一变量,不解析 `cordis.yml`,也不启动 Cordis。`FULL` 允许上报;`FEEDBACK_ONLY``DISABLED`、未设置和空值都会拒绝。授权在命令执行前从启动环境冻结:`dsh-sdk start` 会加载项目 `.env`,项目代码也能修改 `process.env`,若在执行后解析,项目便能自行授权上报其自身配置,而[配置来源所有权决策](../architecture/2026-08-04-configuration-source-ownership.md)对整个 `DSH_*` 命名空间禁止这种行为。在该边界上,不受支持的模式按拒绝处理而非抛出,因为遥测不得改变命令结果。此规则仅取代 [SDK 后续功能提案](../../proposed/feature/2026-07-17-sdk-follow-up-capabilities.md)中启动器默认允许上报的规则;其余能力仍处于提案状态。
带版本的 Web 欢迎通知说明会话日志上传默认关闭,将 `DSH_TELEMETRY_MODE=FEEDBACK_ONLY``DSH_TELEMETRY_MODE=FULL` 列为两种显式启用选项,并披露 `FULL` 同时会启用 dsh-sdk 命令遥测。其版本随这项重要的隐私声明一同变更,使每个 profile 都确认当前文案。
## 考虑过的替代方案
**保留默认退出机制并改进披露。** 不采用:披露不能让缺少配置构成发送数据的明确授权,尤其是会话遥测可能包含完整的本地内容。
**将会话遥测默认设为 `FEEDBACK_ONLY`。** 不采用:即使部署方没有显式启用向外上报,记录反馈仍会触发上传。默认值必须让会话及其反馈都留在本地。
**添加项目级授权标记。** 不采用:`DSH_TELEMETRY_MODE` 已能表达两路数据流的授权;另一个配置项会产生冲突设置,并需要启动器专用的解析逻辑。
**删除两种遥测实现。** 不采用:内部部署仍需要显式启用 `FULL` 与反馈触发的上报;在 `FULL` 下,启动器数据流也仍有用。
## 后果
全新 profile 和项目不发出任何遥测网络请求。内部部署为两路数据流选择一个模式:`FEEDBACK_ONLY` 只允许由反馈触发的 Session Log 共享,`FULL` 还会启用启动器上报。现有硬性退出继续生效,上传模式也保留 endpoint 校验、脱敏责任、批处理和关闭行为。

View 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 .agents/notes/implemented/feature/2026-08-11-message-feedback-web-surface.md
2026-08-11-message-feedback-web-surface.md: 316e5ce7fbab051f3bc5bd586be11f9516505058
2026-08-11-message-feedback-web-surface.zh.md: eb6740f8264b41cb25a1cd60e8db7a295ce8e031

View File

@@ -0,0 +1,57 @@
# Agent Note: Web surface for message feedback
Status: implemented
English | [中文](2026-08-11-message-feedback-web-surface.zh.md)
## Problem
[PR #2217](https://github.com/deepseek-harness/deepseek-harness/pull/2217) landed the durable message-feedback sidecar and its three Host Remote methods, but it was explicitly backend-only: no client package consumed `messageFeedback.list`, `put`, or `delete`, so the Web GUI had no way to record a rating. Its Agent Note deferred "client Remote aggregate mounting and UI" to a separate owner. Issue #1326 asks for the Web surface and was closed by that backend merge without the user-visible half existing.
An earlier full-stack attempt, [PR #1010](https://github.com/deepseek-harness/deepseek-harness/pull/1010), carried a UI layer but was built against its own backend with a different shape: one Session-wide `revision` for compare-and-swap and RPC named `feedback.upsert`. #2217 shipped per-item `ifVersion` and `messageFeedback.put` instead, so #1010's controller logic no longer matched the contract, and its branch had also drifted structurally (it edited `packages/cordis/`, renamed to `packages/self-modification/`, and added a top-level `packages/session-feedback/` that conflicts with the consolidated `packages/feedback/`). It was closed as superseded rather than rebased.
The blocking gap for any UI was that the browser could not name a feedback target. The Host accepts only an append-origin `assistant/message` addressed by `MessageId`, but `AssistantMessageNode` — the client's finalized-assistant node — carried `seq`, `turn`, and `step` and no message identity. Only `SteeringMessageNode` had a `messageId`.
## Decision
Three seams, each owned where its authority already lives.
**Message identity in the client node.** `AssistantMessageNode` gains an optional `messageId`, copied from `event.data.message.id` where the node is materialized from a finalized `assistant/message`. It stays absent on interruption-frozen partials, which were never finalized and address no durable message, and on the synthetic sentinel the trajectory layout builds for an unfinalized partial. The field is optional precisely so those two cases remain unrepresentable as feedback targets rather than being papered over with a placeholder. `ui-conversation` and `ui-trajectory` each materialize their own copy of this node, so both finalized branches were updated; the interrupted branches were deliberately left alone. This mirrors the Host's own target rule, which filters on `isAppendSurfaceEvent`, so client and Host agree on what is addressable without sharing code.
**A declared slot rather than a direct dependency.** `ui-conversation` declares `conversation.chat.assistant-actions` (list kind, session scope, owner `{messageId}`) and authorizes it as a second child of the `turn-tail` node renderer, next to the existing `conversation.chat.turnTail` chain. `TurnTailNodeView` renders it and threads the result into `MessageIconActions` through a new `extraActions` prop, placed between copy and branch. The render site skips the slot entirely when `messageId` is absent, so an interrupted turn shows no controls. The feedback package therefore contributes an entry and never imports the conversation implementation; the strip renders nothing at zero cost when the plugin is composed out of `cordis.yml`.
`extraActions` is a `ReactNode` prop rather than a second render-slot hole because `MessageIconActions` is shared chrome for user and assistant messages: the assistant caller resolves the slot and passes the result down, so the user path stays unaware of a slot it must never render.
**Per-item CAS in a per-session controller.** `@deepseek-ai/dsh-client-ui-feedback` holds one `FeedbackController` per Session, keyed by `MessageId` in a map. A single `list` seeds every control in that Session's transcript. Each mutation sends the version that controller last observed as `ifVersion``null` when it knows of no item, which is exactly the Host's "must not exist" precondition.
The conflict path is where this diverges most from #1010. `MessageFeedbackVersionConflict` carries the authoritative `current` item (or `null`), so a lost race reconciles from the reply itself; #1010 answered every conflict with a blind full refresh. A conflict reporting `current: null` deletes the local entry, which is how a rating removed in another tab disappears here. Mutations serialize on a per-Session tail so a queued operation always compares against the committed version rather than the version read when the click landed.
The list read is deferred to the first hover or focus, not fired on mount, because the controls mount once per settled message in the visible history; a transcript-wide read on mount would fan out one request per message strip. `connection/reset` refreshes only Sessions whose status is no longer `cold`, so a reconnect does not warm Sessions nobody has looked at.
Toggle semantics keep the two verbs honest: re-clicking the recorded rating calls `delete`, switching sides calls `put` and carries any existing note forward, and clearing a message with no known item returns success without a call because it is already in the requested state.
**Remote mounting.** `@deepseek-ai/dsh-api-remotes` now mounts `messageFeedbackRemote` alongside `goalsRemote` and composes both disposers in reverse order. The generated `./remote` artifact already existed in #2217's package exports, so no codegen change was needed; the client calls `ctx.remote.messageFeedback` and never touches the transport. Business results cross this boundary as the ordinary tagged union — the gateway throws only on transport failure — so the controller pattern-matches `ok` and translates a throw into the same settled result shape the controls already render.
## Alternatives considered
**Reuse `conversation.chat.turnTail` instead of a new slot.** Rejected: `turnTail` is a chain keyed on the Turn and carries `TurnTailOwnerProps {turn, seq, openFile}`, which addresses a Turn boundary rather than a message identity. Feedback needs `MessageId`, and a chain is selector-routed one-at-a-time where the action strip is genuinely a list of independent contributors.
**Put `messageId` on the chat node's `id` field.** Rejected: that id is `"${turn}:${step}"` and is load-bearing for keyed dispatch and stable React keys. Overloading it would couple node identity to model output identity, and a message id is not unique per node anyway once replacement-origin events exist.
**Keep #1010's session-wide revision.** Not available: the merged Host contract is per-item `ifVersion`. Even as a client-side simplification it would be worse — one Session revision makes unrelated per-message edits conflict, which is the precise problem #2217's Agent Note records as the reason for per-item versions.
**Rebase #1010.** Rejected after inspection: 102 files, `mergeable: false`, a duplicate backend and RPC layer that #2217 supersedes under different names, and two directory renames since. Only its ~1,400-line UI layer had residual value, and that layer called `feedback.upsert` with a revision it no longer has. Rewriting the UI against the merged contract was less work than reconciling the branch, and the closing comment on #1010 records that reasoning.
## Consequences
The Web GUI records per-message ratings and notes. #1326's user-visible half now exists; the issue was reopened because the backend merge had closed it while no entry point existed.
`AssistantMessageNode.messageId` is optional, so every existing reader compiles unchanged, but any future consumer must handle absence rather than assume a finalized message. The two parallel materializers remain a duplication hazard: a third view that builds this node must remember to copy the id, and nothing enforces it. Only the chat view renders controls today, even though trajectory and waterfall nodes now carry the same id.
Feedback stays invisible to the model — the sidecar reaches neither the Session log, model context, nor telemetry — so the package's Model Experience is an audited `none` entry rather than a structured block.
The sidecar publishes no live frames, so a second tab's rating surfaces on reconnect or on the next conflict reply, not immediately. The note editor does not pre-check `maxNoteBytes` (8192 in the Web bundle), so an oversized note fails on save with `note-too-large` rather than while typing.
Twenty-four existing Web UI snapshots gained the two rating buttons across 27 assistant messages, confirming the strip reaches every settled assistant message in the shipped composition rather than only the fixture under test.
A dedicated Web E2E covers rate, note, reload restore, and retract against the shipped bundle. It has to hover the unrated control after a reload before asserting the restored state, because the deferred list read is what makes the sidecar value appear — the test documents that ordering rather than working around it. Sabotaging the controller's list restore makes the spec fail, so the durability assertion has teeth.

View File

@@ -0,0 +1,57 @@
# Agent Note消息反馈的 Web 界面
Status: implemented
[English](2026-08-11-message-feedback-web-surface.md) | 中文
## 问题
[PR #2217](https://github.com/deepseek-harness/deepseek-harness/pull/2217) 交付了持久化的消息反馈 sidecar 及其三个 Host Remote 方法,但它明确只做后端:没有任何客户端包消费 `messageFeedback.list``put``delete`,因此 Web GUI 无法记录评价。它的 Agent Note 把「客户端 Remote aggregate 挂载与 UI」留给了另一个负责人。Issue #1326 要求的正是 Web 界面,却在该后端合并时被关闭,而用户可见的那一半并不存在。
更早的全栈尝试 [PR #1010](https://github.com/deepseek-harness/deepseek-harness/pull/1010) 带有 UI 层,但它基于自己的后端、形状不同:整个 Session 一个 `revision` 做 compare-and-swapRPC 名为 `feedback.upsert`#2217 最终交付的是逐条 `ifVersion``messageFeedback.put`,因此 #1010 的 controller 逻辑不再匹配契约;它的分支在结构上也已漂移(改动了 `packages/cordis/`,该目录已重命名为 `packages/self-modification/`;新增的顶层 `packages/session-feedback/` 与整合后的 `packages/feedback/` 冲突)。它作为 superseded 关闭,而不是 rebase。
任何 UI 的阻塞缺口在于浏览器无法指名一个反馈目标。Host 只接受以 `MessageId` 寻址的 append 来源 `assistant/message`,但 `AssistantMessageNode`——客户端表示已完成 assistant 输出的节点——只携带 `seq``turn``step`,没有消息身份。只有 `SteeringMessageNode``messageId`
## 决策
三个接缝,各自归属于其权威已经所在的位置。
**客户端节点中的消息身份。** `AssistantMessageNode` 增加可选的 `messageId`,在该节点由已完成的 `assistant/message` 物化时从 `event.data.message.id` 复制。它在被中断冻结的部分输出上保持缺失——那些从未完成、不指向任何持久消息——在 trajectory 布局为未完成部分输出构造的合成哨兵上同样缺失。该字段之所以可选,正是为了让这两种情况无法被表示为反馈目标,而不是用占位值掩盖过去。`ui-conversation``ui-trajectory` 各自物化自己的该节点副本,因此两条「已完成」分支都做了更新;「被中断」分支被有意保留原样。这与 Host 自身的目标规则一致——它按 `isAppendSurfaceEvent` 过滤——因此客户端与 Host 在「什么是可寻址的」上取得一致,而不需要共享代码。
**声明式槽位而非直接依赖。** `ui-conversation` 声明 `conversation.chat.assistant-actions`list 类型、session 作用域、owner 为 `{messageId}`),并把它授权为 `turn-tail` 节点渲染器的第二个子项,与既有的 `conversation.chat.turnTail` 链并列。`TurnTailNodeView` 渲染它,并通过新的 `extraActions` prop 把结果传入 `MessageIconActions`,位置在复制与分支之间。当 `messageId` 缺失时渲染点整体跳过该槽位,因此被中断的 Turn 不显示任何控件。反馈包因此只贡献一个 entry从不引入 conversation 的实现;当该插件从 `cordis.yml` 组装中移除时,这条操作栏以零成本渲染为空。
`extraActions` 是一个 `ReactNode` prop 而不是第二个 render-slot 洞,因为 `MessageIconActions` 是用户消息与 assistant 消息共享的外壳:由 assistant 一侧解析槽位并把结果向下传递,用户路径则对这个它永远不该渲染的槽位保持无感。
**per-session controller 中的逐条 CAS。** `@deepseek-ai/dsh-client-ui-feedback` 为每个 Session 持有一个 `FeedbackController`,以 `MessageId` 为键存入 map。一次 `list` 为该 Session 转录中的所有控件播种。每次 mutation 发送该 controller 最后观察到的版本作为 `ifVersion`——当它不知道任何条目时为 `null`,这正是 Host 的「必须不存在」前置条件。
冲突路径是与 #1010 分歧最大的地方。`MessageFeedbackVersionConflict` 携带权威的 `current` 条目(或 `null`),因此竞争失败方直接从回复本身收敛;#1010 对每次冲突都以一次盲目的全量刷新作答。报告 `current: null` 的冲突会删除本地条目这就是在另一个标签页中被移除的评价在此处消失的方式。mutation 在 per-Session 的尾部串行化,因此排队中的操作总是与已提交的版本比较,而不是与点击落下那一刻读到的版本比较。
list 读取被推迟到首次 hover 或 focus而不是在 mount 时触发,因为控件会为可见历史中每条已结算消息各 mount 一次;在 mount 时做全转录读取会导致每条消息栏各发一个请求。`connection/reset` 只刷新状态不再是 `cold` 的 Session因此重连不会预热没人看过的 Session。
切换语义让两个动词保持诚实:再次点击已记录的评价调用 `delete`,切换到另一侧调用 `put` 并携带已有备注,而对没有已知条目的消息执行清除会直接返回成功且不发起调用,因为它已处于被请求的状态。
**Remote 挂载。** `@deepseek-ai/dsh-api-remotes` 现在把 `messageFeedbackRemote``goalsRemote` 并列挂载,并以相反顺序组合两个 disposer。生成的 `./remote` 产物在 #2217 的包导出中已存在,因此不需要 codegen 改动;客户端调用 `ctx.remote.messageFeedback`,从不接触传输层。业务结果以普通的 tagged union 穿过该边界——gateway 只在传输失败时抛出——因此 controller 对 `ok` 做模式匹配,并把抛出翻译为控件已经在渲染的同一种结算结果形状。
## 考虑过的替代方案
**复用 `conversation.chat.turnTail` 而不新增槽位。** 否决:`turnTail` 是以 Turn 为键的链,携带 `TurnTailOwnerProps {turn, seq, openFile}`,寻址的是 Turn 边界而非消息身份。反馈需要 `MessageId`,而链是选择器路由的一次一个,操作栏则确实是一组互相独立的贡献者的列表。
**把 `messageId` 放到 chat 节点的 `id` 字段上。** 否决:该 id 是 `"${turn}:${step}"`,且承载着 keyed dispatch 与稳定 React key 的作用。重载它会把节点身份与模型输出身份耦合起来,而且一旦存在 replacement 来源的事件,消息 id 本身在每个节点上也并非唯一。
**保留 #1010 的 session 级 revision。** 不可行:已合并的 Host 契约是逐条 `ifVersion`。即便作为客户端侧的简化也更糟——单一 Session revision 会让互不相关的逐条编辑相互冲突,而这正是 #2217 的 Agent Note 记录的采用逐条版本的原因。
**Rebase #1010。** 经检查后否决102 个文件、`mergeable: false`、一个被 #2217 以不同名称取代的重复后端与 RPC 层,以及此后的两次目录重命名。只有其约 1400 行的 UI 层有残余价值,而该层调用的 `feedback.upsert` 及其 revision 已不复存在。基于已合并的契约重写 UI 比调和该分支更省力,#1010 的关闭评论记录了这一理由。
## 结果
Web GUI 可以记录逐条消息的评价与备注。#1326 中用户可见的那一半现在存在了;该 Issue 之所以被重开,是因为后端合并在没有任何入口存在的情况下关闭了它。
`AssistantMessageNode.messageId` 是可选的,因此所有既有读取方无需改动即可编译,但任何将来的消费方都必须处理缺失,而不能假定消息已完成。两个并行的物化点仍是重复隐患:第三个构造该节点的视图必须记得复制该 id而没有任何机制强制这一点。今天只有 chat 视图渲染控件,尽管 trajectory 与 waterfall 节点现在携带同一个 id。
反馈对模型保持不可见——该 sidecar 既不进入 Session 日志、也不进入模型上下文与 telemetry——因此该包的 Model Experience 是一条经审计的 `none` 条目,而不是结构化区块。
该 sidecar 不发布实时帧,因此第二个标签页的评价会在重连时或下一次冲突回复时才浮现,而不是立即。备注编辑器不预先校验 `maxNoteBytes`Web bundle 中为 8192因此过大的备注会在保存时以 `note-too-large` 失败,而不是在输入过程中。
24 个既有 Web UI 快照在 27 条 assistant 消息上获得了这两个评价按钮,确认这条操作栏在已发布的组装中触达每条已结算的 assistant 消息,而不仅是被测试的那个 fixture。
一个专门的 Web E2E 针对已发布 bundle 覆盖评分、备注、重载恢复与撤回。它必须在重载后先 hover 未评分的控件,再断言恢复后的状态,因为正是那次延迟的 list 读取让 sidecar 的值出现——该测试记录了这一顺序,而不是绕开它。把 controller 的 list 恢复逻辑破坏掉会让该规格失败,因此这条持久性断言是有效的。