From 94abd8631ae83d2ff1e65a422e8a59abfb7d369c Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 7 Aug 2026 16:01:41 +0800 Subject: [PATCH 01/10] fix(feedback): include session id in acknowledgement --- .../feedback/command-feedback/src/index.ts | 48 +++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/packages/feedback/command-feedback/src/index.ts b/packages/feedback/command-feedback/src/index.ts index 37205b76e2..8922df008e 100644 --- a/packages/feedback/command-feedback/src/index.ts +++ b/packages/feedback/command-feedback/src/index.ts @@ -8,6 +8,7 @@ import type { Context } from '@deepseek-ai/cordis' import type { CommandInvocation, CommandResult } from '@deepseek-ai/dsh-commands' +import type { Telemetry, TelemetrySharingStatus } from '@deepseek-ai/dsh-session-telemetry' import type { Session } from '@deepseek-ai/dsh-session' import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-user-id' @@ -16,6 +17,42 @@ export const inject = ['commands'] const USAGE = 'Usage: /feedback ' +/** Fail closed when a future sharing status reaches the sentence switch. */ +/* v8 ignore next 3 -- only the ignored default arm calls this; the closed union cannot reach it via the public API. */ +function assertNever(value: never): never { + throw new Error(`command-feedback: unsupported sharing status ${JSON.stringify(value)}`) +} + +/** The acknowledgement's sharing sentence for a disclosed policy. */ +function sharingSentence(sharing: TelemetrySharingStatus): string { + switch (sharing) { + case 'full': + return 'Session sharing is enabled.' + case 'feedback-only': + return 'Session sharing is feedback-gated; recording feedback releases the session prefix for sharing.' + case 'disabled': + return 'Session sharing is disabled.' + /* v8 ignore next 2 -- the seam's closed union cannot reach the default; a future status must be given a sentence here. */ + default: + return assertNever(sharing) + } +} + +/** + * The sharing disclosure appended to the acknowledgement: the mounted + * backend's disclosed policy, or a "not configured" notice when no backend + * is mounted. Read through the plugin context so the command still works + * when the telemetry service is absent. + * @param telemetry - the mounted telemetry service, or undefined. + * @returns one sentence describing this session's sharing policy. + */ +function sharingDisclosure(telemetry: Telemetry | undefined): string { + if (telemetry === undefined) { + return 'Session sharing is not configured.' + } + return sharingSentence(telemetry.sharing) +} + declare module '@deepseek-ai/dsh-session/types' { interface SessionEventMap { /** @@ -42,17 +79,20 @@ export function recordFeedback(session: Session, text: string): void { * Validate, record, and acknowledge one feedback entry. Returning an error * leaves no `feedback/record` event. * @param invocation - receiving agent, raw command input, and UI cancellation. + * @param ctx - plugin context used to read the optional telemetry service. * @returns an acknowledgement containing the receiving session and anonymous - * user ids, or a usage error when no feedback text was supplied. + * user ids plus the session-sharing disclosure, or a usage error when no + * feedback text was supplied. */ -function executeFeedbackCommand(invocation: CommandInvocation): CommandResult { +function executeFeedbackCommand(invocation: CommandInvocation, ctx: Context): CommandResult { if (invocation.rawInput.trim().length === 0) { return { kind: 'error', text: `Feedback text is required. ${USAGE}` } } recordFeedback(invocation.agent.session, invocation.rawInput) + const telemetry = ctx.get('telemetry') return { kind: 'success', - text: `Feedback recorded for session ${invocation.agent.session.id}\nUser: ${getOrCreateAnonymousUserId()}`, + text: `Feedback recorded for session ${invocation.agent.session.id}\nUser: ${getOrCreateAnonymousUserId()}. ${sharingDisclosure(telemetry)}`, } } @@ -63,6 +103,6 @@ export function apply(ctx: Context): void { description: 'record feedback about this session', input: { hint: '' }, recordInput: false, - handler: executeFeedbackCommand, + handler: invocation => executeFeedbackCommand(invocation, ctx), }) } From 3f9d0436eb4ec1b070ae49a651091044a80ad9d6 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Sat, 8 Aug 2026 02:27:37 +0800 Subject: [PATCH 02/10] feat(feedback): disclose session sharing in the /feedback acknowledgement The /feedback acknowledgement now echoes the receiving session id and reports the mounted telemetry backend's sharing policy: the telemetry seam exposes a backend-independent TelemetrySharingStatus through a required abstract sharing member on the Telemetry service, the OTel backend maps its mode onto it, and the command appends one policy-only sharing sentence (full / feedback-only / disabled / not configured) to the acknowledgement. The web client renders the text through the existing command row without a client change; a new assembled-browser e2e mounts the shipped telemetry row in FULL mode against a local dead endpoint and pins the shipped default sentence as a keyless golden. --- .../2026-07-28-feedback-command.i18n.yaml | 4 +- .../feature/2026-07-28-feedback-command.md | 2 +- .../feature/2026-07-28-feedback-command.zh.md | 2 +- ...knowledgement-sharing-disclosure.i18n.yaml | 6 ++ ...back-acknowledgement-sharing-disclosure.md | 27 ++++++ ...k-acknowledgement-sharing-disclosure.zh.md | 27 ++++++ apps/web/tests/feedback-command.e2e.ts | 89 +++++++++++++++++++ apps/web/tests/scaffold.ts | 15 +++- .../feedback-command/ack.expected.md | 35 ++++++++ .../snapshots/feedback-command/session.jsonl | 17 ++++ apps/web/tsconfig.json | 1 + packages/feedback/command-feedback/README.md | 16 +++- .../feedback/command-feedback/README.zh.md | 16 +++- .../feedback/command-feedback/package.json | 2 + .../feedback/command-feedback/src/index.ts | 5 ++ .../tests/command-feedback.spec.ts | 57 ++++++++++-- .../tests/loader-composition.spec.ts | 2 +- .../session-telemetry-otel/README.i18n.yaml | 4 +- .../session/session-telemetry-otel/README.md | 2 + .../session-telemetry-otel/README.zh.md | 2 + .../session-telemetry-otel/src/index.ts | 14 +++ .../session-telemetry-otel/tests/otel.spec.ts | 25 ++++++ .../session-telemetry/README.i18n.yaml | 4 +- packages/session/session-telemetry/README.md | 6 ++ .../session/session-telemetry/README.zh.md | 8 ++ .../session/session-telemetry/src/index.ts | 18 ++++ scripts/type-equiv.manifest.json | 5 ++ tsconfig.host.json | 1 + 28 files changed, 394 insertions(+), 18 deletions(-) create mode 100644 .agents/notes/implemented/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.i18n.yaml create mode 100644 .agents/notes/implemented/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.md create mode 100644 .agents/notes/implemented/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.zh.md create mode 100644 apps/web/tests/feedback-command.e2e.ts create mode 100644 apps/web/tests/snapshots/feedback-command/ack.expected.md create mode 100644 apps/web/tests/snapshots/feedback-command/session.jsonl diff --git a/.agents/notes/implemented/feature/2026-07-28-feedback-command.i18n.yaml b/.agents/notes/implemented/feature/2026-07-28-feedback-command.i18n.yaml index 809e37044f..e0ba016659 100644 --- a/.agents/notes/implemented/feature/2026-07-28-feedback-command.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-28-feedback-command.i18n.yaml @@ -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-28-feedback-command.md -2026-07-28-feedback-command.md: 3edb29283c289d6d006891a4c19087b01fa8166f -2026-07-28-feedback-command.zh.md: c2513d2570474cbbaf8d94f87603d8ce10d40c14 +2026-07-28-feedback-command.md: d3b2774e41a82f6edb4303280f813ddbed75ebd1 +2026-07-28-feedback-command.zh.md: 3eeef92f2ed39c9546f013f217dd7f851d30c78c diff --git a/.agents/notes/implemented/feature/2026-07-28-feedback-command.md b/.agents/notes/implemented/feature/2026-07-28-feedback-command.md index 3edb29283c..d3b2774e41 100644 --- a/.agents/notes/implemented/feature/2026-07-28-feedback-command.md +++ b/.agents/notes/implemented/feature/2026-07-28-feedback-command.md @@ -18,7 +18,7 @@ The package declares the log-only `feedback/record { text }` session event and e `dsh-commands` still writes its `command/run` / `command/done` lifecycle pair around `/feedback`, but this command sets `recordInput: false`. Its `command/run` therefore carries the command identity and source without `args`; the feedback text exists only in `feedback/record`, while `command/done` carries the acknowledgement outcome. All three records are log-only and non-surface. Their appends enter persistence's ordinary bounded write path; nothing forces a flush, so acknowledgement reports that the feedback is in the log rather than already on disk. -Capture remains inert for the running agent and model. The optional OTel telemetry package later adds one infrastructure consumer: it observes `feedback/record` as a release trigger in `FEEDBACK_ONLY` mode and as the local-only warning trigger in `DISABLED` mode, without changing the feedback event or command path. See [Feedback-gated session telemetry](2026-08-05-feedback-gated-session-telemetry.md). +Capture remains inert for the running agent and model. The optional OTel telemetry package later adds one infrastructure consumer: it observes `feedback/record` as a release trigger in `FEEDBACK_ONLY` mode and as the local-only warning trigger in `DISABLED` mode, without changing the feedback event or command path. See [Feedback-gated session telemetry](2026-08-05-feedback-gated-session-telemetry.md) and the [acknowledgement sharing disclosure](2026-08-07-feedback-acknowledgement-sharing-disclosure.md). ### Why feedback owns an event diff --git a/.agents/notes/implemented/feature/2026-07-28-feedback-command.zh.md b/.agents/notes/implemented/feature/2026-07-28-feedback-command.zh.md index c2513d2570..3eeef92f2e 100644 --- a/.agents/notes/implemented/feature/2026-07-28-feedback-command.zh.md +++ b/.agents/notes/implemented/feature/2026-07-28-feedback-command.zh.md @@ -18,7 +18,7 @@ Status: implemented `dsh-commands` 仍会围绕 `/feedback` 写入 `command/run` / `command/done` 生命周期配对,但该命令设置了 `recordInput: false`。因此,它的 `command/run` 携带命令标识与来源,但不携带 `args`;反馈文本只存在于 `feedback/record` 中,而 `command/done` 携带确认结果。三个记录都仅写入日志且非 surface。它们的追加会进入持久化的常规有界写入路径;没有任何环节强制 flush,因此确认文本报告的是反馈已进入日志,而非已经落盘。 -采集对正在运行的 agent(智能体)与模型仍不产生后续动作。可选的 OTel 遥测包后续增加了一个基础设施消费方:它在 `FEEDBACK_ONLY` 模式下将 `feedback/record` 作为释放触发器,在 `DISABLED` 模式下将其作为仅限本地的警告触发器,且不改变反馈事件或命令路径。见[反馈门控的会话遥测](2026-08-05-feedback-gated-session-telemetry.md)。 +采集对正在运行的 agent(智能体)与模型仍不产生后续动作。可选的 OTel 遥测包后续增加了一个基础设施消费方:它在 `FEEDBACK_ONLY` 模式下将 `feedback/record` 作为释放触发器,在 `DISABLED` 模式下将其作为仅限本地的警告触发器,且不改变反馈事件或命令路径。见[反馈门控的会话遥测](2026-08-05-feedback-gated-session-telemetry.md)与[确认文本中的共享披露](2026-08-07-feedback-acknowledgement-sharing-disclosure.md)。 ### 为何反馈拥有自己的事件 diff --git a/.agents/notes/implemented/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.i18n.yaml b/.agents/notes/implemented/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.i18n.yaml new file mode 100644 index 0000000000..b5c7f142f1 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.i18n.yaml @@ -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-07-feedback-acknowledgement-sharing-disclosure.md +2026-08-07-feedback-acknowledgement-sharing-disclosure.md: 1e9cd0fb95d78aff9f6434e0583154e2c3f847da +2026-08-07-feedback-acknowledgement-sharing-disclosure.zh.md: ac26b18ad523feeabc297b212210dd73eff93a0a diff --git a/.agents/notes/implemented/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.md b/.agents/notes/implemented/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.md new file mode 100644 index 0000000000..1e9cd0fb95 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.md @@ -0,0 +1,27 @@ +# Agent Note: Feedback acknowledgement sharing disclosure + +Status: implemented + +English | [中文](2026-08-07-feedback-acknowledgement-sharing-disclosure.zh.md) + +## Problem + +The `/feedback` command records a log-only `feedback/record` event and acknowledges the user, but the acknowledgement carried no durable context about what happened to the session: deployments that mount session telemetry (`FULL`, `FEEDBACK_ONLY`, or `DISABLED`) had no way to tell the user whether their feedback and session left the process, and the receiving session id was not echoed. The command plugin could not read the sharing policy because the telemetry seam exposed capture only, and the OTel mode enum lived in the optional backend package. + +## Decision + +The telemetry seam (`@deepseek-ai/dsh-session-telemetry`) now owns a backend-independent sharing vocabulary: `TelemetrySharingStatus` (`full` | `feedback-only` | `disabled`) plus a required abstract `sharing` member on the `Telemetry` service class — every backend must disclose its policy, so a consumer renders "not configured" only when no telemetry service is mounted. `@deepseek-ai/dsh-session-telemetry-otel` maps its serialized `TelemetryMode` (the [feedback-gated delivery decision](2026-08-05-feedback-gated-session-telemetry.md) owns the mode semantics) onto that status in the constructor and discloses it, including in `DISABLED`. The `/feedback` handler reads the mounted service through the plugin context (`ctx.get('telemetry')`, never a declared injection, so the command loads and runs without telemetry) and appends one sharing sentence to the acknowledgement: `Feedback recorded for session {id}. `. No service → `Session sharing is not configured.`; `disabled` → `Session sharing is disabled.`; `feedback-only` → `Session sharing is feedback-gated; recording feedback releases the session prefix for sharing.`; `full` → `Session sharing is enabled.` + +The disclosure states the current sharing policy only; it never promises delivery or retention. Handoff is the backend's non-blocking enqueue and batching, retry, and loss policy stay the backend SDK's, and a later reconfiguration can change what was shared, so the sentences claim nothing about what reached a collector or about future retention. The disclosure adds no session event and never reaches the model surface; the web client renders it through the existing command row (`CommandNode` outcome text) with no client change. + +## Alternatives considered + +**A client-side status RPC and badge.** Rejected because the acknowledgement is host-produced and the web client already renders the command result text verbatim in the command row; a separate RPC would duplicate the status in a second surface and add a wire contract for a sentence. + +**Declared `telemetry` injection in `command-feedback`.** Rejected because telemetry is optional: a declared injection fails plugin load when the service is absent, while the command must work without it. The plugin reads the service with `ctx.get('telemetry')` at handler time instead. + +**OTel package owns the vocabulary.** Rejected because `command-feedback` must not depend on the optional OTel backend package. The seam owns `TelemetrySharingStatus` so any backend can disclose a policy. + +## Consequences + +The acknowledgement is user-visible: it names the receiving session and reports the current sharing policy, honest about the fire-and-forget handoff. Package tests pin the sentence for each status and for the absent-service case; the assembled-browser e2e mounts the shipped telemetry row in FULL mode against a local dead endpoint and pins the shipped default sentence (`Session sharing is enabled.`) as a golden. The seam member is required, so a mounted backend always discloses a policy and the "not configured" sentence truthfully means no telemetry service; the `/feedback` command keeps working with no telemetry mounted. A still-blank web session renders no command row, so feedback recorded before the first message gets no visible acknowledgement (documented under the package README's limitations). diff --git a/.agents/notes/implemented/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.zh.md b/.agents/notes/implemented/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.zh.md new file mode 100644 index 0000000000..ac26b18ad5 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.zh.md @@ -0,0 +1,27 @@ +# Agent Note: 反馈确认中的会话共享披露 + +Status: implemented + +[English](2026-08-07-feedback-acknowledgement-sharing-disclosure.md) | 中文 + +## 问题 + +`/feedback` 命令会记录一个仅写入日志的 `feedback/record` 事件并确认用户,但确认文本没有携带关于会话去向的持久信息:挂载了会话遥测(`FULL`、`FEEDBACK_ONLY` 或 `DISABLED`)的部署无法告知用户其反馈和会话是否离开了进程,确认文本也没有回显接收会话的 id。命令插件无法读取共享策略,因为遥测 seam 只暴露采集能力,而 OTel 模式枚举位于可选的后端包中。 + +## 决策 + +遥测 seam(`@deepseek-ai/dsh-session-telemetry`)现在拥有与后端无关的共享词汇:`TelemetrySharingStatus`(`full` | `feedback-only` | `disabled`),并在 `Telemetry` 服务类上增加一个必需的抽象 `sharing` 成员——每个后端都必须披露其策略,因此消费方只有在未挂载任何遥测服务时才渲染「未配置」。`@deepseek-ai/dsh-session-telemetry-otel` 在构造函数中把序列化的 `TelemetryMode`(模式语义由[反馈门控投递决策](2026-08-05-feedback-gated-session-telemetry.md)负责)映射到该状态并披露,包括 `DISABLED` 模式。`/feedback` 处理器通过插件上下文读取已挂载的服务(`ctx.get('telemetry')`,绝不是声明的注入,因此命令在无遥测时也能加载和运行),并在确认文本后追加一句共享披露:`Feedback recorded for session {id}. <句子>`。无服务 → `Session sharing is not configured.`;`disabled` → `Session sharing is disabled.`;`feedback-only` → `Session sharing is feedback-gated; recording feedback releases the session prefix for sharing.`;`full` → `Session sharing is enabled.` + +披露只陈述当前的共享策略,绝不承诺投递或留存:交接是后端的非阻塞入队,批处理、重试与丢失策略仍归后端 SDK,且后续重新配置可能改变已共享的内容,因此句子不声称任何内容已到达采集端,也不声称未来的留存。披露不新增任何会话事件,也绝不会进入模型 surface;Web 客户端通过现有的命令行(`CommandNode` 的结果文本)原样渲染,无需客户端改动。 + +## 备选方案 + +**客户端新增状态 RPC 与徽标。** 拒绝,因为确认文本由宿主生成,Web 客户端已经在命令行中原样渲染命令结果文本;单独的 RPC 会在第二个 surface 重复该状态,并为一句文案新增线上契约。 + +**在 `command-feedback` 中声明 `telemetry` 注入。** 拒绝,因为遥测是可选的:服务缺失时声明注入会导致插件加载失败,而命令必须在无遥测时可用。插件改为在处理器执行时用 `ctx.get('telemetry')` 读取服务。 + +**由 OTel 包拥有词汇。** 拒绝,因为 `command-feedback` 不能依赖可选的 OTel 后端包。seam 拥有 `TelemetrySharingStatus`,任何后端都能披露策略。 + +## 后果 + +确认文本对用户可见:它点名接收会话并报告当前的共享策略,如实说明 fire-and-forget 交接。包级测试为每种状态以及无服务场景固定句子;组装浏览器 e2e 以 FULL 模式挂载随附的遥测行(指向本地 dead 端点),并以 golden 固定随附默认句子(`Session sharing is enabled.`)。seam 成员是必需的,因此已挂载的后端总会披露策略,「未配置」句子如实地表示没有遥测服务;`/feedback` 命令在未挂载遥测时仍能正常工作。仍为空白的新 Web 会话不渲染命令行,因此首条消息之前记录的反馈没有可见确认(已在包 README 的限制中记录)。 diff --git a/apps/web/tests/feedback-command.e2e.ts b/apps/web/tests/feedback-command.e2e.ts new file mode 100644 index 0000000000..6dd8ac19a0 --- /dev/null +++ b/apps/web/tests/feedback-command.e2e.ts @@ -0,0 +1,89 @@ +// Keyless assembled-browser coverage for the /feedback command over the +// shipped Web bundles and the real host wire. The command plane settles +// without a model turn: the host appends the log-only command/run + +// feedback/record + command/done lifecycle, and the transcript renders the +// acknowledgement — the recorded session id plus the session-sharing +// disclosure — as a persistent command row. The scaffold mounts the shipped +// telemetry row in FULL mode against a local dead endpoint (no record leaves +// the process), so the golden pins the shipped default sentence +// `Session sharing is enabled.`; the per-status sentences are pinned by the +// package and OTel unit tests. +import { readFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import { join } from 'node:path' +import type { Browser, Page } from 'playwright' +import { chromium } from 'playwright' +import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' +import { + assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts, + launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold, +} from './scaffold.ts' +import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts' + +const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/feedback-command', import.meta.url)) +const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl') +const ACK_EXPECTED = join(SNAPSHOT_DIR, 'ack.expected.md') +const MODE = webSnapshotMode() +// Discard port: loopback listener never binds, so FULL telemetry discloses +// the shipped default policy without any record reaching a collector. +const TELEMETRY_URL = 'http://127.0.0.1:9/v1/logs' + +const PROMPT = 'Reply with the single word LIGHTHOUSE and stop.' + +describe('web e2e: /feedback command acknowledgement', () => { + let scaffold: WebScaffold + let browser: Browser + let page: Page + let tripwire: ReturnType + + beforeAll(async () => { + scaffold = await launchWebScaffold({ + telemetryUrl: TELEMETRY_URL, + ...(MODE === 'record' ? {} : { replayFixture: FIXTURE }), + }) + browser = await chromium.launch() + page = await newEnglishPage(browser) + tripwire = watchConsole(page) + await page.goto(scaffold.baseUrl, { waitUntil: 'load' }) + await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + // Fresh world: connecting a workspace births the blank session whose + // live composer accepts the slash line. + await connectFreshWorkspace(page, scaffold.workspaceCwd) + }, 120_000) + + afterAll(async () => { + await browser?.close() + await scaffold?.close() + }) + + it('records feedback and renders the acknowledgement with session id and sharing status', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-feedback-command')) + if (MODE !== 'record') { + expect(fixtureUserPrompts(await readFile(FIXTURE, 'utf8'))).toEqual([PROMPT]) + } + + // First send the recorded prompt so the transcript is active — a command + // row does not render while a fresh session is still blank. + const input = page.locator('textarea').first() + await input.fill(PROMPT) + await input.press('Enter') + await scaffold.whenTurnSettled() + await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 }) + + await input.fill('/feedback the diff view is unreadable') + await input.press('Enter') + // The command plane settles without a model turn: the ack row names the + // recorded session and the mounted FULL backend's disclosure. + await page.getByText(/Feedback recorded for session/).waitFor({ timeout: 10_000 }) + expect(await page.getByText(/Session sharing is enabled/).count()).toBe(1) + const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd) + await compareOrRefreshGolden(ACK_EXPECTED, snapshot, MODE) + + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + }, 60_000) + + it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => { + await assertFixtureInventory(SNAPSHOT_DIR, ['session.jsonl', 'ack.expected.md']) + }) +}) diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts index a93828282e..772bd4ae91 100644 --- a/apps/web/tests/scaffold.ts +++ b/apps/web/tests/scaffold.ts @@ -245,6 +245,13 @@ export interface LaunchOptions { } /** Leave the current welcome notice unacknowledged; ordinary scenarios publish it as complete before browser boot. */ welcomeNoticePending?: boolean + /** + * Mount the shipped telemetry row in FULL mode against this exporter URL + * instead of disabling it. Used to pin a real backend disclosure in + * assembled coverage; point the URL at a local dead endpoint so no record + * leaves the process. + */ + telemetryUrl?: string /** * Browse through a trusted non-loopback hostname that the browser resolves * to loopback (for example `*.localhost`). The test server stays bound to @@ -334,6 +341,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise failures.push(cleanupError)) + restoreSkillRootEnvironment() if (failures.length > 1) throw new AggregateError(failures, 'web scaffold temp-root setup failed') throw error } @@ -395,8 +403,11 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise` | Append `feedback/record` and acknowledge with `Feedback recorded for session {sessionId}` followed by `User: {userId}`. | +| `/feedback ` | Append `feedback/record` and acknowledge with `Feedback recorded for session {sessionId}`, `User: {userId}`, plus the session-sharing disclosure. | | `/feedback` | Return a direct usage error. Whitespace-only input is treated as empty. | Surrounding whitespace is discarded, but feedback is otherwise unparsed: no truncation, case folding, or control words. Text that looks like another command, such as `/feedback /plan felt slow`, is feedback content. Repeated commands each produce their own event; nothing is replaced or merged. +## Session-sharing disclosure + +The acknowledgement names the receiving session id and reports how that session is shared, read from the mounted [`telemetry`](../../session/session-telemetry/README.md) service through the plugin context (`ctx.get('telemetry')`, never a declared injection). The disclosure is one sentence chosen from the backend's [`TelemetrySharingStatus`](../../session/session-telemetry/README.md): + +| Disclosed status | Acknowledgement sentence | +|---|---| +| `full` | `Session sharing is enabled.` | +| `feedback-only` | `Session sharing is feedback-gated; recording feedback releases the session prefix for sharing.` | +| `disabled` | `Session sharing is disabled.` | +| no service | `Session sharing is not configured.` | + +The disclosure states the deployment's current sharing policy only; it never promises delivery or retention. With `full` or `feedback-only`, records are handed to the backend's non-blocking enqueue and the SDK owns batching, retry, and loss policy, so the sentence claims nothing about what reached a collector; `disabled` claims nothing about future reconfiguration. The disclosure adds no event and never enters the model surface. + ## What this plugin does and does not do `recordFeedback(session, text)` is the command-independent write path. It rejects empty normalized text and appends `feedback/record { text }`; a different UI, hook, or host integration can call it without constructing a slash command. The `/feedback` handler uses that producer and starts no model work. The optional [`dsh-session-telemetry-otel`](../../session/session-telemetry-otel) consumer observes the event without changing its capture contract. @@ -56,4 +69,5 @@ Independent of the model request path. Recording appends to the session log only - **No structured fields** — an entry is one free-text string with no category, severity, or referenced-event link, so feedback cannot be filtered by subject without re-reading its text. - **No amend or withdraw** — the session log is append-only and this package adds no tombstone, so a mistaken entry stays recorded and can only be superseded by a later one. - **No explicit durability barrier** — the acknowledgement follows the append, not a flush, so an entry recorded immediately before a crash can be lost with any other unflushed tail. Feedback is not worth forcing a synchronous disk write for; a consumer that needs one awaits `ctx.sessions.flush(session)`. +- **No visible acknowledgement on a fresh session** — the web transcript renders command rows only once a session is active, so `/feedback` on a still-blank session records the event but shows no acknowledgement row. Recording feedback after the first message renders normally. - **Web only among the shipped entry points** — headless mode, ACP automation, and JSON-RPC do not provide a command adapter, so `/feedback` is unavailable there. diff --git a/packages/feedback/command-feedback/README.zh.md b/packages/feedback/command-feedback/README.zh.md index ca74d53f25..12a4dcace0 100644 --- a/packages/feedback/command-feedback/README.zh.md +++ b/packages/feedback/command-feedback/README.zh.md @@ -8,11 +8,24 @@ | 输入 | 结果 | |---|---| -| `/feedback ` | 追加 `feedback/record`,并以 `Feedback recorded for session {sessionId}` 确认,随后显示 `User: {userId}`。 | +| `/feedback ` | 追加 `feedback/record`,并以 `Feedback recorded for session {sessionId}`、`User: {userId}` 加会话共享披露确认。 | | `/feedback` | 返回一个直接用法错误。仅含空白的输入视为空输入。 | 前后空白会被丢弃,但除此之外,反馈内容不会被解析:没有截断、大小写折叠或控制词。看起来像另一个命令的文本(例如 `/feedback /plan felt slow`)就是反馈内容。重复执行命令时,每次都会产生一个事件;不会发生替换或合并。 +## 会话共享披露 + +确认文本会点名接收会话的 id,并报告该会话如何被共享;该信息通过插件上下文(`ctx.get('telemetry')`,绝不是声明的注入)从已挂载的 [`telemetry`](../../session/session-telemetry/README.md) 服务读取。披露是依据后端 [`TelemetrySharingStatus`](../../session/session-telemetry/README.md) 选择的一句话: + +| 披露的状态 | 确认文本中的句子 | +|---|---| +| `full` | `Session sharing is enabled.` | +| `feedback-only` | `Session sharing is feedback-gated; recording feedback releases the session prefix for sharing.` | +| `disabled` | `Session sharing is disabled.` | +| 无服务 | `Session sharing is not configured.` | + +披露只陈述部署当前的共享策略,绝不承诺投递或留存:在 `full` 或 `feedback-only` 下,记录被交给后端的非阻塞入队,批处理、重试与丢失策略归 SDK 负责,因此句子不声称任何内容已到达采集端;`disabled` 也不声称未来不会重新配置。披露不新增任何事件,也绝不会进入模型 surface。 + ## 本插件做什么、不做什么 `recordFeedback(session, text)` 是不依赖命令的写入路径。它拒绝规范化后为空的文本,并追加 `feedback/record { text }`;其他 UI、钩子或 host 集成无需构造斜杠命令即可调用它。`/feedback` 处理器通过该生产方写入,且不启动任何模型工作。可选的 [`dsh-session-telemetry-otel`](../../session/session-telemetry-otel) 消费方会观察该事件,但不改变它的采集约定。 @@ -56,4 +69,5 @@ - **没有结构化字段**:一条条目就是一个自由文本字符串,没有类别、严重程度或关联事件链接,因此无法在不重读文本的情况下按主题过滤反馈。 - **不支持修改或撤回**:会话日志是仅追加的,本包也不新增 tombstone,因此错误的条目会一直保留在记录中,只能由后续条目取代。 - **没有显式持久化屏障**:确认文本紧随追加而非 flush,因此紧临崩溃前记录的条目可能与其他未 flush 的尾部一同丢失。为反馈强制同步写盘并不值得;需要该保证的消费方可自行等待 `ctx.sessions.flush(session)`。 +- **新会话上没有可见的确认**:Web 转录只在会话激活后渲染命令行,因此在仍为空白的新会话上执行 `/feedback` 会记录事件但不会显示确认行。发送首条消息后再记录反馈即可正常渲染。 - **随附的产品入口中只有 Web 使用此命令**:无头模式、ACP 自动化和 JSON-RPC 不提供命令适配器,因此 `/feedback` 在那里不可用。 diff --git a/packages/feedback/command-feedback/package.json b/packages/feedback/command-feedback/package.json index b557eb788b..f45d504814 100644 --- a/packages/feedback/command-feedback/package.json +++ b/packages/feedback/command-feedback/package.json @@ -35,6 +35,7 @@ "@deepseek-ai/dsh-commands": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", + "@deepseek-ai/dsh-session-telemetry": "workspace:^", "@deepseek-ai/dsh-user-id": "workspace:^", "@deepseek-ai/cordis": "workspace:^" }, @@ -46,6 +47,7 @@ "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", + "@deepseek-ai/dsh-session-telemetry": "workspace:^", "@deepseek-ai/dsh-user-id": "workspace:^", "@deepseek-ai/cordis": "workspace:^" } diff --git a/packages/feedback/command-feedback/src/index.ts b/packages/feedback/command-feedback/src/index.ts index 8922df008e..daeee26f79 100644 --- a/packages/feedback/command-feedback/src/index.ts +++ b/packages/feedback/command-feedback/src/index.ts @@ -83,6 +83,9 @@ export function recordFeedback(session: Session, text: string): void { * @returns an acknowledgement containing the receiving session and anonymous * user ids plus the session-sharing disclosure, or a usage error when no * feedback text was supplied. + * @returns an acknowledgement containing the receiving session id and the + * session-sharing disclosure, or a usage error when no feedback text was supplied. +>>>>>>> 632abf2957 (feat(feedback): disclose session sharing in the /feedback acknowledgement) */ function executeFeedbackCommand(invocation: CommandInvocation, ctx: Context): CommandResult { if (invocation.rawInput.trim().length === 0) { @@ -93,6 +96,8 @@ function executeFeedbackCommand(invocation: CommandInvocation, ctx: Context): Co return { kind: 'success', text: `Feedback recorded for session ${invocation.agent.session.id}\nUser: ${getOrCreateAnonymousUserId()}. ${sharingDisclosure(telemetry)}`, + text: `Feedback recorded for session ${invocation.agent.session.id}. ${sharingDisclosure(telemetry)}`, +>>>>>>> 632abf2957 (feat(feedback): disclose session sharing in the /feedback acknowledgement) } } diff --git a/packages/feedback/command-feedback/tests/command-feedback.spec.ts b/packages/feedback/command-feedback/tests/command-feedback.spec.ts index 453d9c17fc..ca965bff0d 100644 --- a/packages/feedback/command-feedback/tests/command-feedback.spec.ts +++ b/packages/feedback/command-feedback/tests/command-feedback.spec.ts @@ -5,6 +5,7 @@ import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent' import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent' import CommandService from '@deepseek-ai/dsh-commands' import SessionStore, { foldSurface, Session, SessionId } from '@deepseek-ai/dsh-session' +import { Telemetry, type TelemetrySharingStatus } from '@deepseek-ai/dsh-session-telemetry' import * as commandFeedback from '@deepseek-ai/dsh-command-feedback' const { USER_ID, getOrCreateAnonymousUserId } = vi.hoisted(() => { @@ -25,6 +26,20 @@ interface Harness { readonly plugin: Awaited> } +/** Minimal mounted backend disclosing one sharing policy. */ +class FakeTelemetry extends Telemetry { + override readonly sharing: TelemetrySharingStatus + + constructor(ctx: Context, config: { sharing: TelemetrySharingStatus }) { + super(ctx) + this.sharing = config.sharing + } + + emit(): void {} + + async shutdown(): Promise {} +} + /** Build a live idle agent over a store-owned session, as an app's spine does. */ function stubAgent(ctx: Context, id: string): { agent: Agent; session: Session } { const session = ctx.sessions.create(SessionId(id)) @@ -48,12 +63,17 @@ function stubAgent(ctx: Context, id: string): { agent: Agent; session: Session } return { agent, session } } -/** Mount the real command registry and this producer. */ -async function harness(): Promise { +/** + * Mount the real command registry, this producer, and optionally a telemetry + * backend disclosing one sharing policy. Without `sharing`, no telemetry + * service exists and the acknowledgement reports "not configured". + */ +async function harness(sharing?: TelemetrySharingStatus): Promise { const ctx = new Context() await ctx.plugin(CommandService) await ctx.plugin(AgentRegistry) await ctx.plugin(SessionStore) + if (sharing !== undefined) await ctx.plugin(FakeTelemetry, { sharing }) const plugin = await ctx.plugin(commandFeedback) const { agent, session } = stubAgent(ctx, `command-feedback-${Math.random()}`) ctx.agents.register(agent) @@ -104,7 +124,7 @@ describe('/feedback human command', () => { const test = await harness() await expect(run(test, ' the diff view is unreadable')).resolves.toEqual({ kind: 'success', - text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}`, + text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}. Session sharing is not configured.`, }) expect(feedbackTexts(test.session)).toEqual(['the diff view is unreadable']) const commandRun = test.session.events.find(event => event.type === 'command/run') @@ -152,12 +172,39 @@ describe('/feedback human command', () => { test.ctx.commands.execute(test.agent, '/feedback second', signal), ]) expect(settled.map(item => item?.result)).toEqual([ - { kind: 'success', text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}` }, - { kind: 'success', text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}` }, + { kind: 'success', text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}. Session sharing is not configured.` }, + { kind: 'success', text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}. Session sharing is not configured.` }, ]) expect(feedbackTexts(test.session)).toEqual(['first', 'second']) }) + it('discloses full session sharing in the acknowledgement', async () => { + const test = await harness('full') + await expect(run(test, ' everything shared')).resolves.toEqual({ + kind: 'success', + text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}. Session sharing is enabled.`, + }) + expect(feedbackTexts(test.session)).toEqual(['everything shared']) + }) + + it('discloses feedback-gated session sharing in the acknowledgement', async () => { + const test = await harness('feedback-only') + await expect(run(test, ' gated sharing')).resolves.toEqual({ + kind: 'success', + text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}. Session sharing is feedback-gated; recording feedback releases the session prefix for sharing.`, + }) + expect(feedbackTexts(test.session)).toEqual(['gated sharing']) + }) + + it('discloses disabled session sharing in the acknowledgement', async () => { + const test = await harness('disabled') + await expect(run(test, ' local only')).resolves.toEqual({ + kind: 'success', + text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}. Session sharing is disabled.`, + }) + expect(feedbackTexts(test.session)).toEqual(['local only']) + }) + it('keeps every recorded event off the model surface and out of derived history', async () => { const test = await harness() await run(test, ' invisible to the model') diff --git a/packages/feedback/command-feedback/tests/loader-composition.spec.ts b/packages/feedback/command-feedback/tests/loader-composition.spec.ts index 2060fe2207..e777f13124 100644 --- a/packages/feedback/command-feedback/tests/loader-composition.spec.ts +++ b/packages/feedback/command-feedback/tests/loader-composition.spec.ts @@ -93,7 +93,7 @@ describe('/feedback real Loader composition through cordis.yml', () => { const userId = getOrCreateAnonymousUserId({ env: { DSH_HOME: root } }) expect(accepted?.result).toEqual({ kind: 'success', - text: `Feedback recorded for session feedback-loader-agent\nUser: ${userId}`, + text: `Feedback recorded for session feedback-loader-agent\nUser: ${userId}. Session sharing is not configured.`, }) const rejected = await context.commands.execute(owner, '/feedback', signal) expect(rejected?.result).toEqual({ diff --git a/packages/session/session-telemetry-otel/README.i18n.yaml b/packages/session/session-telemetry-otel/README.i18n.yaml index 2897eb7dac..161f201cfb 100644 --- a/packages/session/session-telemetry-otel/README.i18n.yaml +++ b/packages/session/session-telemetry-otel/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/session-telemetry-otel/README.md -README.md: 585995ce409255df9608bc33b76625374bc67669 -README.zh.md: 7f0b93363fbb4aebb80f0d3cc8108e58ce3f647f +README.md: e3eae475a180419c7822d51858ae156052a663d6 +README.zh.md: cfdf36ac5783850cc5e63bbb2b622584f1064b0c diff --git a/packages/session/session-telemetry-otel/README.md b/packages/session/session-telemetry-otel/README.md index 585995ce40..e3eae475a1 100644 --- a/packages/session/session-telemetry-otel/README.md +++ b/packages/session/session-telemetry-otel/README.md @@ -29,6 +29,8 @@ Programmatic TypeScript configuration uses the exported `TelemetryMode` enum (`T Upload authorization is positive and fail-closed. An unknown direct-construction mode fails before transport configuration is read. Only `FULL` accepts direct `ctx.telemetry.emit()` calls. `FEEDBACK_ONLY` gives its on-demand coordinator a private backend capability and treats only the exact `feedback/record` object already stored at `session.events[event.seq]` as consent; an independently emitted bus value is ignored. `DISABLED` never constructs the SDK pipeline, even when exporter options are present. +The mounted service discloses the resolved mode through the seam's [`TelemetrySharingStatus`](../session-telemetry/README.md#the-sharing-disclosure) `sharing` property (`full` / `feedback-only` / `disabled`), so the `/feedback` acknowledgement can report whether and how the session is shared. The disclosure is set in the constructor and is independent of capture: even `DISABLED` discloses `disabled`. + `exporter.url` is required in `FULL` and `FEEDBACK_ONLY`, has no default, and must parse as `http(s)`; it is optional and unused in `DISABLED`. In uploading modes, `shutdownTimeoutMillis` is a positive finite DSH-owned outer deadline that defaults to 3000 ms, and a non-positive-integer `processor.maxExportBatchSize` also fails at plugin load because the SDK accepts it but then hangs on shutdown. Both SDK blocks pass through whole: every `OTLPExporterNodeConfigBase` field (`headers`, `timeoutMillis`, `compression`, `keepAlive`, …) reaches the exporter, and batching, export cadence (`scheduledDelayMillis`), retry, queue bounds, and loss policy under sustained failure are SDK behavior tuned through `processor`. The backend implements no `flush()`: the batch processor owns ordinary flushing. During shutdown, OTel awaits `exporter.forceFlush()` before the processor's `exportTimeoutMillis`-bounded completion promise; if that transport promise never settles, this package abandons the wait at `shutdownTimeoutMillis`, logs the contained shutdown failure through the coordinator, and lets application teardown continue. The deadline cannot cancel the SDK transport, so records still pending then may be lost at process exit. ## What leaves the machine diff --git a/packages/session/session-telemetry-otel/README.zh.md b/packages/session/session-telemetry-otel/README.zh.md index 7f0b93363f..cfdf36ac57 100644 --- a/packages/session/session-telemetry-otel/README.zh.md +++ b/packages/session/session-telemetry-otel/README.zh.md @@ -29,6 +29,8 @@ 上传授权采用显式许可,且为 fail-closed。通过直接构造传入未知模式时,会在读取传输配置前失败。只有 `FULL` 接受对 `ctx.telemetry.emit()` 的直接调用。`FEEDBACK_ONLY` 向其按需协调器提供私有后端能力,并且仅在 `feedback/record` 对象已经存储于 `session.events[event.seq]` 且对象身份完全相同时,才将其视为同意;独立发出的总线值会被忽略。即使存在导出器选项,`DISABLED` 也绝不会构造 SDK 流水线。 +已挂载的服务通过 seam 的 [`TelemetrySharingStatus`](../session-telemetry/README.md#the-sharing-disclosure) `sharing` 属性披露解析后的模式(`full` / `feedback-only` / `disabled`),因此 `/feedback` 的确认文本可以报告会话是否以及如何被共享。该披露在构造函数中设置,与采集相互独立:即使 `DISABLED` 也会披露 `disabled`。 + `exporter.url` 在 `FULL` 与 `FEEDBACK_ONLY` 中必填,无默认值,且必须能解析为 `http(s)`;在 `DISABLED` 中可省略且不使用。在上传模式中,`shutdownTimeoutMillis` 是由 DSH 管理的有限正数外层截止时间,默认值为 3000 ms;`processor.maxExportBatchSize` 不是正整数时也会在插件加载时失败,因为 SDK 会接受该值,随后却在关闭时挂起。两个 SDK 配置块都整体透传(passthrough):`OTLPExporterNodeConfigBase` 的每个字段(`headers`、`timeoutMillis`、`compression`、`keepAlive` 等)都会到达导出器;批处理、导出节奏(`scheduledDelayMillis`)、重试、队列上限,以及持续失败下的丢失策略,都是通过 `processor` 调节的 SDK 行为。该后端不实现 `flush()`:常规 flush 由批处理器负责。关闭期间,OTel 会先等待 `exporter.forceFlush()`,再等待受处理器 `exportTimeoutMillis` 限制的完成 promise;如果该传输 promise 始终不结算,本包会在 `shutdownTimeoutMillis` 到期时放弃等待,通过协调器记录已隔离的关闭失败,并让应用继续拆卸。该截止时间无法取消 SDK 传输,因此届时仍待处理的记录可能在进程退出时丢失。 ## 哪些数据会离开本机 diff --git a/packages/session/session-telemetry-otel/src/index.ts b/packages/session/session-telemetry-otel/src/index.ts index 5a5102ca51..1f208394ff 100644 --- a/packages/session/session-telemetry-otel/src/index.ts +++ b/packages/session/session-telemetry-otel/src/index.ts @@ -22,6 +22,7 @@ import { type TelemetryBackend, type TelemetryRecord, type TelemetrySeverity, + type TelemetrySharingStatus, } from '@deepseek-ai/dsh-session-telemetry' import { APP_IDENTITY } from '@deepseek-ai/dsh-llm' import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-user-id' @@ -71,6 +72,17 @@ function assertNever(value: never): never { throw new Error(`session-telemetry-otel: unsupported mode ${JSON.stringify(value)}`) } +/** Map the serialized mode onto the seam's backend-independent sharing vocabulary. */ +function sharingStatusFor(mode: TelemetryMode): TelemetrySharingStatus { + switch (mode) { + case TelemetryMode.FULL: return 'full' + case TelemetryMode.FEEDBACK_ONLY: return 'feedback-only' + case TelemetryMode.DISABLED: return 'disabled' + /* v8 ignore next 2 -- resolveMode already rejected unknown values before this switch; the closed enum cannot reach the default. */ + default: return assertNever(mode) + } +} + /** * Plugin configuration: one sharing policy, two verbatim SDK option objects, * and one DSH-owned shutdown bound. Uploading modes validate their endpoint @@ -139,10 +151,12 @@ export class TelemetryOtel extends Telemetry { private readonly directEmit: TelemetryBackend['emit'] private readonly provider: LoggerProvider | undefined private readonly shutdownTimeoutMillis: number + override readonly sharing: TelemetrySharingStatus constructor(ctx: Context, config: Config) { const mode = resolveMode(config.mode) super(ctx) + this.sharing = sharingStatusFor(mode) if (mode === TelemetryMode.DISABLED) { this.directEmit = DROP_RECORD this.provider = undefined diff --git a/packages/session/session-telemetry-otel/tests/otel.spec.ts b/packages/session/session-telemetry-otel/tests/otel.spec.ts index 3e14bf3c9d..a5bb9d06ae 100644 --- a/packages/session/session-telemetry-otel/tests/otel.spec.ts +++ b/packages/session/session-telemetry-otel/tests/otel.spec.ts @@ -364,6 +364,31 @@ describe('TelemetryOtel wire', () => { expect(captures).toEqual([]) }) + it('discloses the sharing policy for every mode', async () => { + const { url, captures } = await mockCollector() + + const fullCtx = new Context() + await fullCtx.plugin(SessionStore) + const full = await fullCtx.plugin(TelemetryOtel, { exporter: { url } }) + expect(fullCtx.telemetry.sharing).toBe('full') + await full.dispose() + + const gatedCtx = new Context() + await gatedCtx.plugin(SessionStore) + const gated = await gatedCtx.plugin(TelemetryOtel, { mode: TelemetryMode.FEEDBACK_ONLY, exporter: { url } }) + expect(gatedCtx.telemetry.sharing).toBe('feedback-only') + await gated.dispose() + + const disabledCtx = new Context() + await disabledCtx.plugin(SessionStore) + const disabled = await disabledCtx.plugin(TelemetryOtel, { mode: TelemetryMode.DISABLED }) + expect(disabledCtx.telemetry.sharing).toBe('disabled') + await disabled.dispose() + + // No record was emitted by any mode, so nothing reached the collector. + expect(captures).toEqual([]) + }) + it('defaults direct construction to full delivery', async () => { const { url, captures } = await mockCollector() const ctx = new Context() diff --git a/packages/session/session-telemetry/README.i18n.yaml b/packages/session/session-telemetry/README.i18n.yaml index 3d4650361f..4b3169d5fe 100644 --- a/packages/session/session-telemetry/README.i18n.yaml +++ b/packages/session/session-telemetry/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/session-telemetry/README.md -README.md: 827554dd53a81eab5a5fd7f145df3f835db9c173 -README.zh.md: a350ea5935a2143cb0f876eeb1eb0520ffee5c53 +README.md: 707dcfcdb0c8dfbd622630351928ac43562535ec +README.zh.md: bd080adceebf83cd9e53d72a7093db376cf6cbd1 diff --git a/packages/session/session-telemetry/README.md b/packages/session/session-telemetry/README.md index 827554dd53..707dcfcdb0 100644 --- a/packages/session/session-telemetry/README.md +++ b/packages/session/session-telemetry/README.md @@ -8,6 +8,12 @@ The telemetry Service Definition declares the `TelemetryBackend` contract, and i `TelemetryBackend` has three members: `emit(record)` MUST enqueue without blocking because it runs synchronously during `session/event` or explicit canonical-log replay; optional `flush()` is a fire-and-forget hint after a turn ends, and most backends omit it and use their SDK's normal batching schedule; `shutdown()` drains queued records and resolves when the SDK stops, and disposal awaits it. An implementation that provides `flush()` must order concurrent flushes with the final `shutdown()` drain. `Telemetry` registers this API under the `telemetry` context key; each context accepts one implementation, and a duplicate load throws. A backend constructs `TelemetryCoordinator` with `live` or `on-demand` capture and calls `captureSession(session, throughSeq?)` at its chosen trigger. +The service also carries the required [`TelemetrySharingStatus`](#the-sharing-disclosure) `sharing` member: the deployment-selected sharing policy every backend must disclose to human-facing acknowledgement surfaces (the `/feedback` command's confirmation). A consumer renders "not configured" only when no telemetry service is mounted. The seam owns the vocabulary (`full` | `feedback-only` | `disabled`) so any backend can disclose a policy without depending on the OTel package. + +## The sharing disclosure + +The acknowledgement of a recorded feedback entry reports whether and how the session is shared, read from the mounted backend's `sharing`. A backend sets the property from its deployment configuration: `full` (every event is handed over as it happens), `feedback-only` (nothing is handed over until a `feedback/record` event releases the unreleased prefix through it), or `disabled` (nothing is handed over at all). Consumers map the status onto user-facing copy; the disclosure never claims delivery — handoff is the non-blocking enqueue, and batching, retry, and loss policy stay the backend SDK's. + ## Capture points In `live` mode the coordinator registers, all through the composing fiber's effects: `session/created` (adopt: record the header, read the log back through the projection from the construction boundary — constructor seeds from fork/resume never re-emit on the firehose and never re-export), `session/event` (project, deep-copy, redact, then hand off; zero I/O), `session/flush` (forward the optional `flush()` hint and return void — the loop's awaited parallel must never wait on telemetry), `session/disposed` (capture the session's `shutdown` operational record at its termination edge, then retire it), `agent/error` (the one live-bus relay; the session event vocabulary intentionally has no operational-error record), a dispose effect (capture shutdown for each still-live session, then await the backend's `shutdown()`; failures warn instead of throwing), and an adoption sweep of `ctx.sessions.list()` (a hot reload does not replay `session/created`). In `on-demand` mode it registers only the dispose effect: `captureSession()` reads the canonical log through an optional inclusive sequence boundary, while flush hints and operational events remain local. diff --git a/packages/session/session-telemetry/README.zh.md b/packages/session/session-telemetry/README.zh.md index a350ea5935..bd080adcee 100644 --- a/packages/session/session-telemetry/README.zh.md +++ b/packages/session/session-telemetry/README.zh.md @@ -8,6 +8,14 @@ `TelemetryBackend` 有三个成员:`emit(record)` 必须入队且不能阻塞,因为它会在 `session/event` 或显式权威日志回放期间同步执行;可选的 `flush()` 是轮次结束后的提示,调用方不等待结果,多数后端省略它并使用 SDK 的常规批处理计划;`shutdown()` 排空已入队记录,并在 SDK 停止后结束,dispose(资源释放)会等待它。提供 `flush()` 的实现必须安排并发 flush 与 `shutdown()` 最终排空的先后顺序。`Telemetry` 将此 API 注册在 `telemetry` 上下文键下:每个上下文只允许一个实现,重复加载会抛出异常。后端以 `live` 或 `on-demand` 捕获构造 `TelemetryCoordinator`,并在自己选择的触发器中调用 `captureSession(session, throughSeq?)`。 +该服务还携带必需的 [`TelemetrySharingStatus`](#the-sharing-disclosure) `sharing` 成员:每个后端都必须向面向用户的确认 surface(`/feedback` 命令的确认文本)披露的部署级共享策略。消费方只有在未挂载任何遥测服务时才渲染「未配置」。seam 拥有该词汇(`full` | `feedback-only` | `disabled`),因此任何后端都可以披露策略,而无需依赖 OTel 包。 + + + +## 共享披露 + +一条已记录的反馈条目的确认文本会报告该会话是否以及如何被共享,读取自已挂载后端的 `sharing`。后端根据其部署配置设置该属性:`full`(每个事件在发生时立即交接)、`feedback-only`(在 `feedback/record` 事件释放其之前的未释放前缀之前,不交接任何内容)或 `disabled`(完全不交接任何内容)。消费方把状态映射为面向用户的文案;披露从不声称投递——交接是非阻塞入队,批处理、重试与丢失策略仍归后端 SDK。 + ## 捕获点 在 `live` 模式中,协调器的全部注册都经由组合方 fiber 的 effect 完成:`session/created`(收养:记录 header,并经投影从构造边界起回读日志;来自 fork 或恢复的构造函数种子绝不会在 firehose 上再次发出,也绝不会再次导出)、`session/event`(投影、深拷贝、脱敏,再交接;零 I/O)、`session/flush`(转发可选的 `flush()` 提示并返回 void;循环所等待的并行任务绝不能等待遥测)、`session/disposed`(在会话自身的终止边缘捕获该会话的 `shutdown` 运维记录,然后将其退役)、`agent/error`(唯一的实时总线转发;会话事件词汇有意不包含运维错误记录)、一个 dispose effect(捕获每个仍存活会话的 shutdown,再等待后端的 `shutdown()`;失败只发出警告而不抛出),以及对 `ctx.sessions.list()` 的收养扫描(热重载不会重放 `session/created`)。在 `on-demand` 模式中,协调器只注册 dispose effect:`captureSession()` 读取权威日志,直至可选的序列号边界(含边界);flush 提示与运维事件留在本地。 diff --git a/packages/session/session-telemetry/src/index.ts b/packages/session/session-telemetry/src/index.ts index 19b58d1ee2..0900d9cdff 100644 --- a/packages/session/session-telemetry/src/index.ts +++ b/packages/session/session-telemetry/src/index.ts @@ -130,6 +130,15 @@ export interface TelemetryBackend { shutdown(): Promise } +/** + * Deployment-selected session-sharing policy disclosed by a mounted + * {@link Telemetry} backend to human-facing acknowledgement surfaces (the + * `/feedback` command's confirmation text). The seam owns the vocabulary so + * any backend can disclose a policy without depending on the OTel package; + * the values mirror the OTel backend's serialized `TelemetryMode` choices. + */ +export type TelemetrySharingStatus = 'full' | 'feedback-only' | 'disabled' + /** * Loadable form of the backend contract: one implementation per context — * the cordis `Service` registration under the `telemetry` key throws on a @@ -141,6 +150,15 @@ export abstract class Telemetry extends Service implements TelemetryBackend { super(ctx, 'telemetry') } + /** + * Deployment-selected session-sharing policy, disclosed for acknowledgement + * surfaces that report whether recorded feedback leaves the process. Every + * backend must disclose its policy; a consumer renders "not configured" only + * when no telemetry service is mounted. The seam owns this vocabulary so the + * disclosure is backend-independent. + */ + abstract readonly sharing: TelemetrySharingStatus + /** * See {@link TelemetryBackend.emit} — that declaration is the contract's one home. * @param record - the logical record to report; owned by the backend after the call. diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index 1a9d998029..c1b1b2753a 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -1620,6 +1620,11 @@ "symbol": "WebBootGraph", "source": "packages/client/modules/src/client/manifest.ts" }, + { + "doc": "docs/subsystems/telemetry.md", + "symbol": "TelemetrySharingStatus", + "source": "packages/session/session-telemetry/src/index.ts" + }, { "doc": "docs/subsystems/telemetry.md", "symbol": "TelemetrySeverity", diff --git a/tsconfig.host.json b/tsconfig.host.json index 32ae7df42d..12c5d365d6 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -52,6 +52,7 @@ "apps/web/tests/agent-preset-authoring.e2e.ts", "apps/web/tests/shipped-composition.e2e.ts", "apps/web/tests/goal-bar.e2e.ts", + "apps/web/tests/feedback-command.e2e.ts", "apps/web/tests/startup-auto-selection.e2e.ts", "apps/web/tests/produced-files.e2e.ts", "apps/web/tests/produced-file-mentions.e2e.ts", From 6a6148a08c103ad321dc72012d22754465e3e830 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Mon, 10 Aug 2026 14:53:05 +0800 Subject: [PATCH 03/10] test(feedback): fix the assembled e2e record path and teardown restore The feedback-command e2e now drives the recorded prompt through a separate all-modes test that arms whenTurnSettled before sending and writes the fixture back via recordFixture in record mode; the acknowledgement golden test runs only in replay/refresh. The scaffold restores the pinned DSH_HOME on the persistence-root setup failure path, and the telemetry subsystems page links the README's sharing-disclosure anchor. --- apps/web/tests/feedback-command.e2e.ts | 28 +++++++++++----- .../feedback-command/ack.expected.md | 2 ++ docs/module-graph.i18n.yaml | 4 +-- docs/subsystems/telemetry.i18n.yaml | 4 +-- docs/subsystems/telemetry.md | 32 ++++++++++++++----- docs/subsystems/telemetry.zh.md | 32 ++++++++++++++----- .../command-feedback/README.i18n.yaml | 4 +-- 7 files changed, 76 insertions(+), 30 deletions(-) diff --git a/apps/web/tests/feedback-command.e2e.ts b/apps/web/tests/feedback-command.e2e.ts index 6dd8ac19a0..577e77a97a 100644 --- a/apps/web/tests/feedback-command.e2e.ts +++ b/apps/web/tests/feedback-command.e2e.ts @@ -16,7 +16,7 @@ import { chromium } from 'playwright' import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' import { assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts, - launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold, + launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts' @@ -56,20 +56,32 @@ describe('web e2e: /feedback command acknowledgement', () => { await scaffold?.close() }) - it('records feedback and renders the acknowledgement with session id and sharing status', async () => { - onTestFailed(() => saveFailureShot(page, 'web-e2e-feedback-command')) + it('drives the recorded prompt to a settled turn (all modes)', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-feedback-drive')) if (MODE !== 'record') { + // Drift guard: the committed fixture must carry exactly the drive prompt. expect(fixtureUserPrompts(await readFile(FIXTURE, 'utf8'))).toEqual([PROMPT]) } - - // First send the recorded prompt so the transcript is active — a command - // row does not render while a fresh session is still blank. const input = page.locator('textarea').first() + await input.waitFor({ timeout: 10_000 }) + // Arm the turn-boundary waiter BEFORE sending, so a burst replay cannot + // miss the turn/end that settles the recorded turn. + const settled = scaffold.whenTurnSettled() await input.fill(PROMPT) await input.press('Enter') - await scaffold.whenTurnSettled() - await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 }) + const sessionId = await settled + if (MODE === 'record') { + await recordFixture(scaffold, sessionId, FIXTURE) + } + }, 60_000) + it.skipIf(MODE === 'record')('records feedback and renders the acknowledgement with session id and sharing status', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-feedback-command')) + // The drive test settled the recorded turn: the transcript is active (a + // command row does not render while a fresh session is still blank) and + // the replayed reply is on screen. + await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 }) + const input = page.locator('textarea').first() await input.fill('/feedback the diff view is unreadable') await input.press('Enter') // The command plane settles without a model turn: the ack row names the diff --git a/apps/web/tests/snapshots/feedback-command/ack.expected.md b/apps/web/tests/snapshots/feedback-command/ack.expected.md index 5e6a769e74..fdc43ad90d 100644 --- a/apps/web/tests/snapshots/feedback-command/ack.expected.md +++ b/apps/web/tests/snapshots/feedback-command/ack.expected.md @@ -1,6 +1,8 @@ - banner: - navigation "Session hierarchy": - button "Reply with the single word" [disabled] + - img + - text: 标准模式 - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index 1f864cf46d..a5f2d4e167 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/module-graph.md -module-graph.md: 2218d79e28e835ab96abce96eaf92bbae25e2182 -module-graph.zh.md: 276b70d69c2898d74ac6897e398b02a8944fd503 +module-graph.md: 8dea030a68f5dde3ce072a8f9ae7156ad162967b +module-graph.zh.md: a611ca8300f17b19c5d4f01032dc767dd04743c1 diff --git a/docs/subsystems/telemetry.i18n.yaml b/docs/subsystems/telemetry.i18n.yaml index f5cda71a1d..5c8d376079 100644 --- a/docs/subsystems/telemetry.i18n.yaml +++ b/docs/subsystems/telemetry.i18n.yaml @@ -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: 5ea5c67210ce1387cbd886935e914baf7f904fbb -telemetry.zh.md: bd8fc8acc4c8522d8b1e4bc543431c0abf224411 +telemetry.md: 1b34f25361049611483ac9a10b2f90d7dac64439 +telemetry.zh.md: 9d20831d74792944f5b17e43e6ed14f02dc00275 diff --git a/docs/subsystems/telemetry.md b/docs/subsystems/telemetry.md index 5ea5c67210..1b34f25361 100644 --- a/docs/subsystems/telemetry.md +++ b/docs/subsystems/telemetry.md @@ -2,7 +2,7 @@ English | [中文](telemetry.zh.md) -Outbound session reporting is one [capability seam](../capability-seams.md): its Service Definition ([dsh-session-telemetry](../../packages/session/session-telemetry), `ctx.telemetry`) declares the minimal backend contract, and its capture coordinator owns the capture points, fixed chunk projection, `telemetry/record` redaction waterfall, and handoff cursor; the Service provider a deployment loads ([dsh-session-telemetry-otel](../../packages/session/session-telemetry-otel)) uses the OpenTelemetry JS SDK's log pipeline with its configuration unchanged. This optional capability is not part of the agent loop, and nothing here reaches a model request. The harness stops after it calls `emit()`; the reporting SDK owns batching, retry, queueing, and loss policy. The [revival Agent Note](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md) records that rule and the rejected alternatives. The [Service Definition README](../../packages/session/session-telemetry/README.md) defines the capture-point, cursor, and projection contracts. +Outbound session reporting is split as a [capability seam](../capability-seams.md): the Service Definition and capture coordinator ([dsh-session-telemetry](../../packages/session/session-telemetry), `ctx.telemetry`) own the capture points, fixed chunk projection, `telemetry/record` redaction waterfall, handoff cursor, and minimal backend contract; the Service provider 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 [Service Definition README](../../packages/session/session-telemetry/README.md). Source: [`packages/session/session-telemetry/src/index.ts`](../../packages/session/session-telemetry/src/index.ts) @@ -56,12 +56,28 @@ interface TelemetryRecord { Only the first `assistant/chunk` of each `(turn, step)` ships — the stream-started signal; the rest drop at capture, so `seq` gaps are routine on the wire and never a loss signal. Every other [session event](session.md) type, including plugin-merged ones the seam never heard of, passes through whole. Delivery is best-effort: the cursor marks handed-off, not delivered, records can be lost (crash, reload window) and duplicated (cursor-less re-adoption, SDK retries), so receivers dedupe ledger records on `(session.id, event.seq)`; ops records deliberately omit that identity — they are signals to alert on, not entries to sum, and tolerate duplicates instead. +## The sharing disclosure + +The seam's acknowledgement contract (owned by the [Service Definition README's sharing-disclosure section](../../packages/session/session-telemetry/README.md#the-sharing-disclosure)): every backend discloses its deployment-selected sharing policy through the required abstract `sharing` member on `ctx.telemetry`, and consumers render "not configured" only when no telemetry service is mounted. The disclosure states the current policy, never delivery or retention — handoff is the non-blocking enqueue, and batching, retry, and loss policy stay the reporting SDK's. + +```ts type-equiv +/** + * Deployment-selected session-sharing policy disclosed by a mounted + * {@link Telemetry} backend to human-facing acknowledgement surfaces (the + * `/feedback` command's confirmation text). The seam owns the vocabulary so + * any backend can disclose a policy without depending on the OTel package; + * the values mirror the OTel backend's serialized `TelemetryMode` choices. + */ +type TelemetrySharingStatus = 'full' | 'feedback-only' | 'disabled' +``` + ## The backend contract ```ts type-equiv /** - * The minimum backend contract the coordinator requires. {@link Telemetry} is - * its service-registered form; tests compose the coordinator with a bare + * The backend contract the coordinator hands records to — the minimum any + * reporting SDK satisfies with zero bending. {@link Telemetry} is its + * service-registered form; tests compose the coordinator with a bare * implementation of this interface. */ interface TelemetryBackend { @@ -76,8 +92,8 @@ interface TelemetryBackend { */ emit(record: TelemetryRecord): void /** - * Optional hint that a turn ended. A backend may forward it to its SDK's - * flush so records are exported after each turn. Called + * Optional hint that a natural boundary (turn end) passed — a backend may + * forward it to its SDK's flush so records land at turn boundaries. Called * fire-and-forget; implementations must not block and must not throw * meaningfully (the coordinator contains exceptions). Most backends should * leave this unimplemented and let their SDK's own batching cadence govern @@ -104,7 +120,7 @@ interface TelemetryBackend { } ``` -`Telemetry` (`ctx.telemetry`, [signatures](#ctxtelemetry--telemetry-abstract-seam)) is the loadable form of this contract: each context accepts one implementation and throws on a duplicate. A backend constructs `TelemetryCoordinator` in its constructor to install capture. +`Telemetry` (`ctx.telemetry`, [signatures](#ctxtelemetry--telemetry-abstract-seam)) is the contract's loadable form — one implementation per context, duplicate load throws — and a backend composes the seam's `TelemetryCoordinator` in its constructor to install the capture side. ## The redact waterfall: `telemetry/record` @@ -122,7 +138,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp ### `ctx.telemetry` — `Telemetry` (abstract seam) -Loadable form of the backend contract: one implementation per context — the cordis `Service` registration under the `telemetry` key throws on a duplicate, cordis' standard behavior. A backend composes a TelemetryCoordinator in its constructor to install the capture side. +The backend contract in its loadable form: one implementation per context — the cordis `Service` registration under the `telemetry` key throws on a duplicate, cordis' standard behavior. A backend composes a TelemetryCoordinator in its constructor to install the capture side. ```ts cordis-catalog /** @@ -141,7 +157,7 @@ flush?(): void abstract shutdown(): Promise ``` -Source: [`packages/session/session-telemetry/src/index.ts:139`](../../packages/session/session-telemetry/src/index.ts) +Source: [`packages/session/session-telemetry/src/index.ts:149`](../../packages/session/session-telemetry/src/index.ts) diff --git a/docs/subsystems/telemetry.zh.md b/docs/subsystems/telemetry.zh.md index bd8fc8acc4..9d20831d74 100644 --- a/docs/subsystems/telemetry.zh.md +++ b/docs/subsystems/telemetry.zh.md @@ -2,7 +2,7 @@ [English](telemetry.md) | 中文 -对外会话上报是一项[能力 seam](../capability-seams.md):其 Service Definition([dsh-session-telemetry](../../packages/session/session-telemetry),`ctx.telemetry`)声明最小后端约定,其捕获协调器负责捕获点、固定分片投影、`telemetry/record` 脱敏 waterfall(瀑布式事件)和 handoff 游标;部署方加载的 Service provider([dsh-session-telemetry-otel](../../packages/session/session-telemetry-otel))按原配置使用 OpenTelemetry JS SDK 日志流水线。这项能力可选,不属于 agent loop(智能体循环),这里也没有任何内容会进入模型请求。Harness 调用 `emit()` 后停止处理;上报 SDK 负责批处理、重试、排队和丢失策略。[复活 Agent Note](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md)记录了这条规则和被否决的替代方案。[Service Definition README](../../packages/session/session-telemetry/README.md) 定义捕获点、游标和投影约定。 +对外的会话上报拆分为一项[能力 seam](../capability-seams.md):Service Definition 与捕获协调器([dsh-session-telemetry](../../packages/session/session-telemetry),`ctx.telemetry`)拥有捕获点、固定分片投影、`telemetry/record` 脱敏 waterfall(瀑布式事件)、handoff 游标与最小后端约定;部署方加载的 Service provider([dsh-session-telemetry-otel](../../packages/session/session-telemetry-otel))则是原样配置的 OpenTelemetry JS SDK 日志流水线。它是一项可选能力,不属于 agent loop(智能体循环)主干,这里也没有任何内容会进入模型请求。边界公理(harness 的职责止于 `emit()`;批处理、重试、排队与丢失策略都属于上报 SDK)连同被否决的替代方案,均已在[复活 Agent Note(agent 决策记录)](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md)中定案;捕获点、游标与投影的约定见 [Service Definition README](../../packages/session/session-telemetry/README.md)。 源码:[`packages/session/session-telemetry/src/index.ts`](../../packages/session/session-telemetry/src/index.ts) @@ -56,12 +56,28 @@ interface TelemetryRecord { 每个 `(turn, step)` 只发出第一条 `assistant/chunk`,即「流已开始」的信号;其余分片在捕获时丢弃,因此导出流中的 `seq` 缺口是常态,绝不是丢失信号。其他所有[会话事件](session.md)类型都会完整透传,包括该 seam 从未听说过、由插件合并进来的事件类型。投递是尽力而为的:游标标记的是「已交接」而非「已送达」,记录可能丢失(崩溃、重载窗口)也可能重复(无游标的重新接管、SDK 重试),因此接收端对 ledger 记录基于 `(session.id, event.seq)` 去重;ops 记录刻意省略这类标识——它们是用于告警的信号,而非用于累加的条目,重复被容忍而非被去重。 +## 共享披露 + +该 seam 的确认契约(归属 [Service Definition README 的共享披露段](../../packages/session/session-telemetry/README.md#the-sharing-disclosure)):每个后端都通过 `ctx.telemetry` 上必需的抽象 `sharing` 成员披露其部署级共享策略,消费方只有在未挂载任何遥测服务时才渲染「未配置」。披露只陈述当前策略,绝不承诺投递或留存——交接是非阻塞入队,批处理、重试与丢失策略仍归上报 SDK。 + +```ts type-equiv +/** + * Deployment-selected session-sharing policy disclosed by a mounted + * {@link Telemetry} backend to human-facing acknowledgement surfaces (the + * `/feedback` command's confirmation text). The seam owns the vocabulary so + * any backend can disclose a policy without depending on the OTel package; + * the values mirror the OTel backend's serialized `TelemetryMode` choices. + */ +type TelemetrySharingStatus = 'full' | 'feedback-only' | 'disabled' +``` + ## 后端约定 ```ts type-equiv /** - * The minimum backend contract the coordinator requires. {@link Telemetry} is - * its service-registered form; tests compose the coordinator with a bare + * The backend contract the coordinator hands records to — the minimum any + * reporting SDK satisfies with zero bending. {@link Telemetry} is its + * service-registered form; tests compose the coordinator with a bare * implementation of this interface. */ interface TelemetryBackend { @@ -76,8 +92,8 @@ interface TelemetryBackend { */ emit(record: TelemetryRecord): void /** - * Optional hint that a turn ended. A backend may forward it to its SDK's - * flush so records are exported after each turn. Called + * Optional hint that a natural boundary (turn end) passed — a backend may + * forward it to its SDK's flush so records land at turn boundaries. Called * fire-and-forget; implementations must not block and must not throw * meaningfully (the coordinator contains exceptions). Most backends should * leave this unimplemented and let their SDK's own batching cadence govern @@ -104,7 +120,7 @@ interface TelemetryBackend { } ``` -`Telemetry`(`ctx.telemetry`,[签名](#ctxtelemetry--telemetry-abstract-seam))是该约定的可加载类型:每个上下文只允许一个实现,重复加载会抛出异常。后端在构造函数中创建 `TelemetryCoordinator`,以安装捕获处理。 +`Telemetry`(`ctx.telemetry`,[签名](#ctxtelemetry--telemetry-abstract-seam))是该约定的可加载形态:每个上下文只允许一个实现,重复加载会抛出异常;后端在其构造函数中组合 seam 的 `TelemetryCoordinator`,以此装配捕获侧。 ## 脱敏 waterfall:`telemetry/record` @@ -122,7 +138,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp ### `ctx.telemetry` — `Telemetry` (abstract seam) -Loadable form of the backend contract: one implementation per context — the cordis `Service` registration under the `telemetry` key throws on a duplicate, cordis' standard behavior. A backend composes a TelemetryCoordinator in its constructor to install the capture side. +The backend contract in its loadable form: one implementation per context — the cordis `Service` registration under the `telemetry` key throws on a duplicate, cordis' standard behavior. A backend composes a TelemetryCoordinator in its constructor to install the capture side. ```ts cordis-catalog /** @@ -141,7 +157,7 @@ flush?(): void abstract shutdown(): Promise ``` -Source: [`packages/session/session-telemetry/src/index.ts:139`](../../packages/session/session-telemetry/src/index.ts) +Source: [`packages/session/session-telemetry/src/index.ts:149`](../../packages/session/session-telemetry/src/index.ts) diff --git a/packages/feedback/command-feedback/README.i18n.yaml b/packages/feedback/command-feedback/README.i18n.yaml index ea0c591ae2..f199e9f4eb 100644 --- a/packages/feedback/command-feedback/README.i18n.yaml +++ b/packages/feedback/command-feedback/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/feedback/command-feedback/README.md -README.md: 52b8fb6a423fca69f76397deec36ecd22a6a6023 -README.zh.md: ca74d53f2531a46c2c16aa1423cee52e89c8256f +README.md: 6db5bc4b18815778628d5caa74b75c8b46d3f6a8 +README.zh.md: d9ad1aba9bdbc04110430aa3e3f6604313b8ee20 From ac7c44a5dfebb7cc3f8514d780a13442c2233c55 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Mon, 10 Aug 2026 18:33:44 +0800 Subject: [PATCH 04/10] fix(feedback): drop rebase residue from the sharing acknowledgement The post-rebase cleanup removes leftover conflict-marker lines and the superseded acknowledgement text from the command source, re-adds the session-telemetry project reference, and restores the lockfile importer link for the sharing dependency. --- packages/feedback/command-feedback/src/index.ts | 5 ----- packages/feedback/command-feedback/tsconfig.json | 3 +++ pnpm-lock.yaml | 3 +++ 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/feedback/command-feedback/src/index.ts b/packages/feedback/command-feedback/src/index.ts index daeee26f79..8922df008e 100644 --- a/packages/feedback/command-feedback/src/index.ts +++ b/packages/feedback/command-feedback/src/index.ts @@ -83,9 +83,6 @@ export function recordFeedback(session: Session, text: string): void { * @returns an acknowledgement containing the receiving session and anonymous * user ids plus the session-sharing disclosure, or a usage error when no * feedback text was supplied. - * @returns an acknowledgement containing the receiving session id and the - * session-sharing disclosure, or a usage error when no feedback text was supplied. ->>>>>>> 632abf2957 (feat(feedback): disclose session sharing in the /feedback acknowledgement) */ function executeFeedbackCommand(invocation: CommandInvocation, ctx: Context): CommandResult { if (invocation.rawInput.trim().length === 0) { @@ -96,8 +93,6 @@ function executeFeedbackCommand(invocation: CommandInvocation, ctx: Context): Co return { kind: 'success', text: `Feedback recorded for session ${invocation.agent.session.id}\nUser: ${getOrCreateAnonymousUserId()}. ${sharingDisclosure(telemetry)}`, - text: `Feedback recorded for session ${invocation.agent.session.id}. ${sharingDisclosure(telemetry)}`, ->>>>>>> 632abf2957 (feat(feedback): disclose session sharing in the /feedback acknowledgement) } } diff --git a/packages/feedback/command-feedback/tsconfig.json b/packages/feedback/command-feedback/tsconfig.json index c39f55f60f..fe189a9c3e 100644 --- a/packages/feedback/command-feedback/tsconfig.json +++ b/packages/feedback/command-feedback/tsconfig.json @@ -23,6 +23,9 @@ { "path": "../../session/user-id" }, + { + "path": "../../session/session-telemetry" + }, { "path": "../../support/invariants" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 771706fb80..a268f8e951 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3782,6 +3782,9 @@ importers: '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session + '@deepseek-ai/dsh-session-telemetry': + specifier: workspace:^ + version: link:../../session/session-telemetry '@deepseek-ai/dsh-user-id': specifier: workspace:^ version: link:../../session/user-id From d9f8270cc3e3ed796572851e02a133a222d292e1 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Mon, 10 Aug 2026 18:33:49 +0800 Subject: [PATCH 05/10] docs: sync sharing-disclosure catalogs and module graph after rebase Regenerates the ack golden for the merged acknowledgement format, records the zh counterparts and pairing hashes for the telemetry and catalog pages, and restores the command-feedback to session-telemetry edge and dependency in the module graph. --- .../snapshots/feedback-command/ack.expected.md | 6 ++++-- docs/config-catalog.i18n.yaml | 2 +- docs/config-catalog.md | 2 +- docs/module-graph.i18n.yaml | 4 ++-- docs/module-graph.md | 3 ++- docs/module-graph.zh.md | 3 ++- docs/persistence-catalog.i18n.yaml | 2 +- docs/persistence-catalog.md | 2 +- docs/subsystems/telemetry.i18n.yaml | 4 ++-- docs/subsystems/telemetry.md | 13 ++++++------- docs/subsystems/telemetry.zh.md | 13 ++++++------- packages/feedback/command-feedback/README.i18n.yaml | 4 ++-- 12 files changed, 30 insertions(+), 28 deletions(-) diff --git a/apps/web/tests/snapshots/feedback-command/ack.expected.md b/apps/web/tests/snapshots/feedback-command/ack.expected.md index fdc43ad90d..9a854ec81a 100644 --- a/apps/web/tests/snapshots/feedback-command/ack.expected.md +++ b/apps/web/tests/snapshots/feedback-command/ack.expected.md @@ -23,8 +23,10 @@ - button "Branch into a new conversation": - img - text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s -- img -- text: feedback Feedback recorded for session session-{{uuid}}. Session sharing is enabled. +- 'button "feedback Feedback recorded for session session-{{uuid}} User: {{uuid}}. Session sharing is enabled."': + - img + - img + - text: "feedback Feedback recorded for session session-{{uuid}} User: {{uuid}}. Session sharing is enabled." - textbox "Message the agent" - button "Commands": - img diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 3957d29f57..2f05b260a2 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/config-catalog.md -config-catalog.md: 0813c9e1f1d761b69180bc919d0629e10c7661bc +config-catalog.md: 646198cea4d30ddc799ef4886af309f376cfa9f2 config-catalog.zh.md: cda44f7904196fe2bf401fed2dc5b5e8b28ccf1c diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 0813c9e1f1..646198cea4 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1562,7 +1562,7 @@ export enum TelemetryMode { Depends on: `BatchLogRecordProcessorOptions` (`@opentelemetry/sdk-logs`) · `OTLPExporterNodeConfigBase` (`@opentelemetry/otlp-exporter-base`) -Source: [`packages/session/session-telemetry-otel/src/index.ts:79`](../packages/session/session-telemetry-otel/src/index.ts) +Source: [`packages/session/session-telemetry-otel/src/index.ts:91`](../packages/session/session-telemetry-otel/src/index.ts) ## `@deepseek-ai/dsh-session-title` diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index a5f2d4e167..71b5e667c7 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/module-graph.md -module-graph.md: 8dea030a68f5dde3ce072a8f9ae7156ad162967b -module-graph.zh.md: a611ca8300f17b19c5d4f01032dc767dd04743c1 +module-graph.md: aaa75f1578679495555f4169a5b031159a6e3cbb +module-graph.zh.md: 56f76bbac65fd485fcdbb2f9bdeedb52cecc8616 diff --git a/docs/module-graph.md b/docs/module-graph.md index 2218d79e28..cbe1a2d75e 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -715,6 +715,7 @@ flowchart TD pkg_command_feedback --> pkg_commands pkg_command_feedback --> pkg_invariants pkg_command_feedback --> pkg_session + pkg_command_feedback --> pkg_session_telemetry pkg_command_feedback --> pkg_user_id pkg_permission --> pkg_bash pkg_permission --> pkg_commands @@ -1373,7 +1374,7 @@ flowchart TD | [`client-test-runtime`](../packages/client/test-runtime) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/support/invariants) | | [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | | [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) | -| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`user-id`](../packages/session/user-id) | +| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry), [`user-id`](../packages/session/user-id) | | [`permission`](../packages/interaction/permission) | `interaction` | [`bash`](../packages/bash/bash), [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`user-approval`](../packages/interaction/user-approval) | | [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`pty-local`](../packages/pty/pty-local) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`pty`](../packages/pty/pty), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index 276b70d69c..3c7511cfd3 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -717,6 +717,7 @@ flowchart TD pkg_command_feedback --> pkg_commands pkg_command_feedback --> pkg_invariants pkg_command_feedback --> pkg_session + pkg_command_feedback --> pkg_session_telemetry pkg_command_feedback --> pkg_user_id pkg_permission --> pkg_bash pkg_permission --> pkg_commands @@ -1375,7 +1376,7 @@ flowchart TD | [`client-test-runtime`](../packages/client/test-runtime) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/support/invariants) | | [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | | [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) | -| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`user-id`](../packages/session/user-id) | +| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry), [`user-id`](../packages/session/user-id) | | [`permission`](../packages/interaction/permission) | `interaction` | [`bash`](../packages/bash/bash), [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`user-approval`](../packages/interaction/user-approval) | | [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`pty-local`](../packages/pty/pty-local) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`pty`](../packages/pty/pty), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess) | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index ee0b5cbdd6..5778b13667 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/persistence-catalog.md -persistence-catalog.md: f44569d3bacec0a832f4b4bca6acf4abb0846a0d +persistence-catalog.md: 1b94ecc541f2b9da216a5d10e02a8a5aa46f7cfb persistence-catalog.zh.md: 21ed29a3da2587a604ec90d201030fd644fc5bd4 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index f44569d3ba..1b94ecc541 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -364,7 +364,7 @@ Source: [`packages/compact/compact/src/types.ts:33`](../packages/compact/compact 'feedback/record': { text: string } ``` -Source: [`packages/feedback/command-feedback/src/index.ts:25`](../packages/feedback/command-feedback/src/index.ts) +Source: [`packages/feedback/command-feedback/src/index.ts:62`](../packages/feedback/command-feedback/src/index.ts) ### `goal/*` diff --git a/docs/subsystems/telemetry.i18n.yaml b/docs/subsystems/telemetry.i18n.yaml index 5c8d376079..09caaa6039 100644 --- a/docs/subsystems/telemetry.i18n.yaml +++ b/docs/subsystems/telemetry.i18n.yaml @@ -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: 1b34f25361049611483ac9a10b2f90d7dac64439 -telemetry.zh.md: 9d20831d74792944f5b17e43e6ed14f02dc00275 +telemetry.md: 97694a9a5a209224087d0d8454d83e29ce568ea4 +telemetry.zh.md: 9e8b17f4bddb3debdf4dff9d3c3fed1296ebf3d7 diff --git a/docs/subsystems/telemetry.md b/docs/subsystems/telemetry.md index 1b34f25361..97694a9a5a 100644 --- a/docs/subsystems/telemetry.md +++ b/docs/subsystems/telemetry.md @@ -75,9 +75,8 @@ type TelemetrySharingStatus = 'full' | 'feedback-only' | 'disabled' ```ts type-equiv /** - * The backend contract the coordinator hands records to — the minimum any - * reporting SDK satisfies with zero bending. {@link Telemetry} is its - * service-registered form; tests compose the coordinator with a bare + * The minimum backend contract the coordinator requires. {@link Telemetry} is + * its service-registered form; tests compose the coordinator with a bare * implementation of this interface. */ interface TelemetryBackend { @@ -92,8 +91,8 @@ interface TelemetryBackend { */ emit(record: TelemetryRecord): void /** - * Optional hint that a natural boundary (turn end) passed — a backend may - * forward it to its SDK's flush so records land at turn boundaries. Called + * Optional hint that a turn ended. A backend may forward it to its SDK's + * flush so records are exported after each turn. Called * fire-and-forget; implementations must not block and must not throw * meaningfully (the coordinator contains exceptions). Most backends should * leave this unimplemented and let their SDK's own batching cadence govern @@ -138,7 +137,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp ### `ctx.telemetry` — `Telemetry` (abstract seam) -The backend contract in its loadable form: one implementation per context — the cordis `Service` registration under the `telemetry` key throws on a duplicate, cordis' standard behavior. A backend composes a TelemetryCoordinator in its constructor to install the capture side. +Loadable form of the backend contract: one implementation per context — the cordis `Service` registration under the `telemetry` key throws on a duplicate, cordis' standard behavior. A backend composes a TelemetryCoordinator in its constructor to install the capture side. ```ts cordis-catalog /** @@ -157,7 +156,7 @@ flush?(): void abstract shutdown(): Promise ``` -Source: [`packages/session/session-telemetry/src/index.ts:149`](../../packages/session/session-telemetry/src/index.ts) +Source: [`packages/session/session-telemetry/src/index.ts:148`](../../packages/session/session-telemetry/src/index.ts) diff --git a/docs/subsystems/telemetry.zh.md b/docs/subsystems/telemetry.zh.md index 9d20831d74..9e8b17f4bd 100644 --- a/docs/subsystems/telemetry.zh.md +++ b/docs/subsystems/telemetry.zh.md @@ -75,9 +75,8 @@ type TelemetrySharingStatus = 'full' | 'feedback-only' | 'disabled' ```ts type-equiv /** - * The backend contract the coordinator hands records to — the minimum any - * reporting SDK satisfies with zero bending. {@link Telemetry} is its - * service-registered form; tests compose the coordinator with a bare + * The minimum backend contract the coordinator requires. {@link Telemetry} is + * its service-registered form; tests compose the coordinator with a bare * implementation of this interface. */ interface TelemetryBackend { @@ -92,8 +91,8 @@ interface TelemetryBackend { */ emit(record: TelemetryRecord): void /** - * Optional hint that a natural boundary (turn end) passed — a backend may - * forward it to its SDK's flush so records land at turn boundaries. Called + * Optional hint that a turn ended. A backend may forward it to its SDK's + * flush so records are exported after each turn. Called * fire-and-forget; implementations must not block and must not throw * meaningfully (the coordinator contains exceptions). Most backends should * leave this unimplemented and let their SDK's own batching cadence govern @@ -138,7 +137,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp ### `ctx.telemetry` — `Telemetry` (abstract seam) -The backend contract in its loadable form: one implementation per context — the cordis `Service` registration under the `telemetry` key throws on a duplicate, cordis' standard behavior. A backend composes a TelemetryCoordinator in its constructor to install the capture side. +Loadable form of the backend contract: one implementation per context — the cordis `Service` registration under the `telemetry` key throws on a duplicate, cordis' standard behavior. A backend composes a TelemetryCoordinator in its constructor to install the capture side. ```ts cordis-catalog /** @@ -157,7 +156,7 @@ flush?(): void abstract shutdown(): Promise ``` -Source: [`packages/session/session-telemetry/src/index.ts:149`](../../packages/session/session-telemetry/src/index.ts) +Source: [`packages/session/session-telemetry/src/index.ts:148`](../../packages/session/session-telemetry/src/index.ts) diff --git a/packages/feedback/command-feedback/README.i18n.yaml b/packages/feedback/command-feedback/README.i18n.yaml index f199e9f4eb..fe49d8e490 100644 --- a/packages/feedback/command-feedback/README.i18n.yaml +++ b/packages/feedback/command-feedback/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/feedback/command-feedback/README.md -README.md: 6db5bc4b18815778628d5caa74b75c8b46d3f6a8 -README.zh.md: d9ad1aba9bdbc04110430aa3e3f6604313b8ee20 +README.md: 24a975476b6783b439d4ec94c449f2acbe0b432f +README.zh.md: 12a4dcace001442351916b17fca0d7e2f2c76245 From 806f6d625f97662d82331f7014d3049a0eb67041 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Mon, 10 Aug 2026 23:11:29 +0800 Subject: [PATCH 06/10] fix(web): accept sharing disclosure suffix in seeded-history feedback test The /feedback acknowledgement now appends a sharing-policy sentence after the anonymous user id. The seeded-history e2e regex anchored on the end of the User line, and the golden snapshot did not include the disclosure. Update both to match the new format, and re-record the module-graph translation-pairing hash after rebasing onto master (which picked up the windows-native ACL coverage fix in #2182). --- apps/web/tests/seeded-history.e2e.ts | 4 ++-- .../tests/snapshots/seeded-history/feedback-row.expected.md | 6 +++--- docs/module-graph.i18n.yaml | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/web/tests/seeded-history.e2e.ts b/apps/web/tests/seeded-history.e2e.ts index a112933f9c..9a521a9d48 100644 --- a/apps/web/tests/seeded-history.e2e.ts +++ b/apps/web/tests/seeded-history.e2e.ts @@ -468,9 +468,9 @@ describe('web e2e: seeded history renders through cold resume', () => { if (done?.type !== 'command/done') throw new Error('feedback command did not settle') const [sessionLine, userLine, extraLine] = done.data.text?.split('\n') ?? [] expect(sessionLine).toBe(`Feedback recorded for session ${SEED_ID}`) - expect(userLine).toMatch(/^User: [0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i) + expect(userLine).toMatch(/^User: [0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\./i) expect(extraLine).toBeUndefined() - const userId = userLine?.slice('User: '.length) + const userId = userLine?.match(/^User: ([0-9a-f-]+)/i)?.[1] if (userId === undefined) throw new Error('feedback command omitted the user id') const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)) diff --git a/apps/web/tests/snapshots/seeded-history/feedback-row.expected.md b/apps/web/tests/snapshots/seeded-history/feedback-row.expected.md index 87b763d37c..6928b95777 100644 --- a/apps/web/tests/snapshots/seeded-history/feedback-row.expected.md +++ b/apps/web/tests/snapshots/seeded-history/feedback-row.expected.md @@ -38,10 +38,10 @@ - text: Context injection AGENTS.md - img - text: permission preset read-only -- 'button "feedback Feedback recorded for session {{seededId}} User: {{uuid}}" [expanded]': +- 'button "feedback Feedback recorded for session {{seededId}} User: {{uuid}}. Session sharing is not configured." [expanded]': - img - - text: "feedback Feedback recorded for session {{seededId}} User: {{uuid}}" -- text: "Feedback recorded for session {{seededId}} User: {{uuid}}" + - text: "feedback Feedback recorded for session {{seededId}} User: {{uuid}}. Session sharing is not configured." +- text: "Feedback recorded for session {{seededId}} User: {{uuid}}. Session sharing is not configured." - textbox "Message the agent" - button "Commands": - img diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index 71b5e667c7..91e49bbc86 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/module-graph.md -module-graph.md: aaa75f1578679495555f4169a5b031159a6e3cbb -module-graph.zh.md: 56f76bbac65fd485fcdbb2f9bdeedb52cecc8616 +module-graph.md: 1cc8764c34e01386c4d8ce9a66198bcfa6ece1e7 +module-graph.zh.md: 508d2f4789aa2d50b22efc35254aa20fb031d5e5 From 725f0639ef089c3360cca420159418a7968f5036 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Tue, 11 Aug 2026 00:50:56 +0800 Subject: [PATCH 07/10] ci: retrigger after rebase onto master From 4786b3be89cde0448fd777db2593274e7d06e85c Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Tue, 11 Aug 2026 01:05:53 +0800 Subject: [PATCH 08/10] ci: trigger From 893228b19063e16f84b47d0e1a2040fc9dc1126b Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Tue, 11 Aug 2026 01:32:32 +0800 Subject: [PATCH 09/10] test(feedback): refresh ack golden for master banner locale --- apps/web/tests/snapshots/feedback-command/ack.expected.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/tests/snapshots/feedback-command/ack.expected.md b/apps/web/tests/snapshots/feedback-command/ack.expected.md index 9a854ec81a..89d40acb3b 100644 --- a/apps/web/tests/snapshots/feedback-command/ack.expected.md +++ b/apps/web/tests/snapshots/feedback-command/ack.expected.md @@ -2,7 +2,7 @@ - navigation "Session hierarchy": - button "Reply with the single word" [disabled] - img - - text: 标准模式 + - text: Standard mode - tablist: - tab "Chat" [selected] - tab "Trajectory" From bb40c2b07936ee6be4884f20f8c2093e884e1ecb Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Tue, 11 Aug 2026 11:02:05 +0800 Subject: [PATCH 10/10] docs: re-record module-graph translation pairing after rebase --- docs/module-graph.i18n.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index 91e49bbc86..e0a5822779 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/module-graph.md -module-graph.md: 1cc8764c34e01386c4d8ce9a66198bcfa6ece1e7 -module-graph.zh.md: 508d2f4789aa2d50b22efc35254aa20fb031d5e5 +module-graph.md: cbe1a2d75ef33c44b31ac3b84bb9a54def97d0e5 +module-graph.zh.md: 3c7511cfd391ec69548588df3ab597457a420334