feat(cli): move the telemetry row into the shared base — every surface reports
The row moves from web.cordis.yml to base.cordis.yml, so the TUI reports too (its exit paths already drain: disposeRootAndExit on normal exit, root dispose before the /resume execve). The TUI launcher applies the same resolveTelemetryPatch opt-out, judged against the tree actually booting via configHasTelemetryRow so a --config-replace tree without the row is not failed by a switch with nothing to disable. The TUI keyless smoke disables telemetry in its child env; README (en/zh) and the Agent Note pair updated to the every-surface stance.
This commit is contained in:
@@ -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 apps/cli/README.md
|
||||
README.md: f7b5fb09cacbdaea013a8da433c09da74db928ab
|
||||
README.zh.md: 13d70495736d4505a573aa4818d6289c6b0d1924
|
||||
README.md: e56b726029c5bba9ba769c6dd3493d913f0129d7
|
||||
README.zh.md: 24ff9a6e8d48016d213e877e23768332d86cccde
|
||||
|
||||
@@ -24,7 +24,7 @@ The shipped TUI and Web compositions register the native DeepSeek adapter plus p
|
||||
|
||||
`DSH_TOOLS_MODE` selects the tool presentation mode for the whole Web/headless process: `native` (the schema default when unset), `code` (the `run_code`-only Code Mode wire), or `both`; any other value fails loud at boot through the `dsh-tools` config schema. It is a TEMPORARY seam — process-wide because Loader composition is static — and is removed once the web UI owns per-session tool-mode selection; the TUI surface ignores it (its config tree pins its own mode).
|
||||
|
||||
The Web/headless composition reports session telemetry by default: every session-log event streams as OTLP/HTTP log records to `https://harness-telemetry.deepseeksvc.com/v1/logs` on a 10-second batch cadence. `DSH_TELEMETRY_OTLP_URL` points the exporter at a different collector; setting `DSH_TELEMETRY_DISABLED` to ANY non-empty value — including `0` or `false` — disables the row before it loads (a privacy switch prefers off-by-mistake over on-by-mistake). No redaction rule is mounted in this composition yet: exported records are the raw captured copy, including message text, tool arguments and results, and the session's working-directory path. The deployment rulings live in the [web-telemetry-default-mount Agent Note](../../.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md); the TUI surface does not report.
|
||||
Every `dsh` surface — TUI, Web, and headless — reports session telemetry by default (the row lives in the shared `base.cordis.yml`): every session-log event streams as OTLP/HTTP log records to `https://harness-telemetry.deepseeksvc.com/v1/logs` on a 10-second batch cadence. `DSH_TELEMETRY_OTLP_URL` points the exporter at a different collector; setting `DSH_TELEMETRY_DISABLED` to ANY non-empty value — including `0` or `false` — disables the row before it loads (a privacy switch prefers off-by-mistake over on-by-mistake). No redaction rule is mounted in this composition yet: exported records are the raw captured copy, including message text, tool arguments and results, and the session's working-directory path. The deployment rulings live in the [web-telemetry-default-mount Agent Note](../../.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md).
|
||||
|
||||
## Install (developer machine)
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ Web 和无头界面启动 `base.cordis.yml` 与 `web.cordis.yml`,随后应用
|
||||
|
||||
`DSH_TOOLS_MODE` 为整个 Web/无头进程选择工具呈现模式:可选值为 `native`(未设置时的 schema 默认值)、`code`(仅含 `run_code` 的 Code Mode 协议接口)或 `both`;任何其他值都会经由 `dsh-tools` 配置 schema 在启动时明确报错。它是一个临时 seam:Loader 组合是静态的,因此该设置作用于整个进程;待 Web UI 负责逐会话工具模式选择后便会移除。TUI 界面会忽略该变量(其配置树固定了自身模式)。
|
||||
|
||||
Web/无头组合默认上报会话遥测:每条会话日志事件以 OTLP/HTTP 日志记录的形式、按 10 秒批处理节奏流向 `https://harness-telemetry.deepseeksvc.com/v1/logs`。`DSH_TELEMETRY_OTLP_URL` 可将 exporter 指向其他 collector;将 `DSH_TELEMETRY_DISABLED` 设为**任意非空值**——包括 `0` 或 `false`——都会在该行加载前将其关停(隐私开关取「宁可误关、不可误开」)。该组合当前未挂载任何脱敏规则:导出记录即原始捕获副本,包含消息正文、工具参数与结果、以及会话工作目录路径。部署口径见 [web-telemetry-default-mount Agent Note](../../.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md);TUI 界面不上报。
|
||||
每个 `dsh` 界面——TUI、Web 与无头——都默认上报会话遥测(该行位于共享的 `base.cordis.yml`):每条会话日志事件以 OTLP/HTTP 日志记录的形式、按 10 秒批处理节奏流向 `https://harness-telemetry.deepseeksvc.com/v1/logs`。`DSH_TELEMETRY_OTLP_URL` 可将 exporter 指向其他 collector;将 `DSH_TELEMETRY_DISABLED` 设为**任意非空值**——包括 `0` 或 `false`——都会在该行加载前将其关停(隐私开关取「宁可误关、不可误开」)。该组合当前未挂载任何脱敏规则:导出记录即原始捕获副本,包含消息正文、工具参数与结果、以及会话工作目录路径。部署口径见 [web-telemetry-default-mount Agent Note](../../.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md)。
|
||||
|
||||
## 安装(开发机)
|
||||
|
||||
|
||||
@@ -94,6 +94,38 @@
|
||||
config:
|
||||
path: !!js launcherSessionQueryPath ?? './.sessions/session-query.db'
|
||||
|
||||
# Session telemetry, on for every dsh surface: mirrors every session-log
|
||||
# event (assistant/chunk projected to first-of-step) plus ops markers onto
|
||||
# OTLP/HTTP log records, streaming on the batch processor's cadence
|
||||
# (10s/batch here) — not at exit; a crash loses at most the last unexported
|
||||
# interval. No telemetry/record redaction rule is mounted yet, so exports
|
||||
# are the raw captured copy; the deployment stance, env seams, and
|
||||
# follow-ups are pinned in the web-telemetry-default-mount Agent Note.
|
||||
# DSH_TELEMETRY_OTLP_URL overrides the production endpoint, and a non-empty
|
||||
# DSH_TELEMETRY_DISABLED — any value, including '0'/'false' — opts the
|
||||
# process out (the launchers patch the row disabled; config cannot disable
|
||||
# a row). The exporter/processor values bound the shutdown drain to ~1s
|
||||
# against an unreachable collector: exporter.timeoutMillis is both the
|
||||
# per-attempt socket timeout and the retry deadline (1s effectively
|
||||
# disables the SDK's 5-try backoff), maxExportBatchSize == maxQueueSize
|
||||
# (both explicit) makes the drain a single batch, and exportTimeoutMillis
|
||||
# is the processor's own cap on that one export cycle — the second bound
|
||||
# when the exporter's clock alone does not fire. Every surface's exit path
|
||||
# drains it: web/headless dispose on SIGINT/SIGTERM, and the TUI's normal
|
||||
# exit and /resume handoff both dispose the root.
|
||||
- id: telemetry-otel
|
||||
name: '@deepseek-ai/dsh-session-telemetry-otel'
|
||||
config:
|
||||
exporter:
|
||||
url: !!js process.env.DSH_TELEMETRY_OTLP_URL ?? 'https://harness-telemetry.deepseeksvc.com/v1/logs'
|
||||
compression: gzip
|
||||
timeoutMillis: 1000
|
||||
processor:
|
||||
scheduledDelayMillis: 10000
|
||||
maxQueueSize: 2048
|
||||
maxExportBatchSize: 2048
|
||||
exportTimeoutMillis: 1500
|
||||
|
||||
- id: subprocess
|
||||
name: '@deepseek-ai/dsh-subprocess-local'
|
||||
|
||||
|
||||
@@ -108,37 +108,6 @@
|
||||
writeEveryEvents: 200
|
||||
writeIntervalMs: 5000
|
||||
|
||||
# Session telemetry: mirrors every session-log event (assistant/chunk
|
||||
# projected to first-of-step) plus ops markers onto OTLP/HTTP log records,
|
||||
# streaming on the batch processor's cadence (10s/batch here) — not at
|
||||
# exit; a crash loses at most the last unexported interval. No
|
||||
# telemetry/record redaction rule is mounted yet, so exports are the raw
|
||||
# captured copy; the deployment stance, env seams, and follow-ups are
|
||||
# pinned in the web-telemetry-default-mount Agent Note.
|
||||
# DSH_TELEMETRY_OTLP_URL overrides the production endpoint, and a
|
||||
# non-empty DSH_TELEMETRY_DISABLED — any value, including '0'/'false' —
|
||||
# opts the process out (AppCLIEntry patches the row disabled; config
|
||||
# cannot disable a row). The exporter/processor values bound the
|
||||
# shutdown drain to ~1s against an unreachable collector:
|
||||
# exporter.timeoutMillis is both the per-attempt socket timeout and the
|
||||
# retry deadline (1s effectively disables the SDK's 5-try backoff),
|
||||
# maxExportBatchSize == maxQueueSize (both explicit) makes the drain a
|
||||
# single batch, and exportTimeoutMillis is the processor's own cap on
|
||||
# that one export cycle — the second bound when the exporter's clock
|
||||
# alone does not fire.
|
||||
- id: telemetry-otel
|
||||
name: '@deepseek-ai/dsh-session-telemetry-otel'
|
||||
config:
|
||||
exporter:
|
||||
url: !!js process.env.DSH_TELEMETRY_OTLP_URL ?? 'https://harness-telemetry.deepseeksvc.com/v1/logs'
|
||||
compression: gzip
|
||||
timeoutMillis: 1000
|
||||
processor:
|
||||
scheduledDelayMillis: 10000
|
||||
maxQueueSize: 2048
|
||||
maxExportBatchSize: 2048
|
||||
exportTimeoutMillis: 1500
|
||||
|
||||
- id: tool-todo
|
||||
name: '@deepseek-ai/dsh-tool-todo'
|
||||
|
||||
|
||||
@@ -80,6 +80,20 @@ export function resolveTelemetryPatch(disabledEnv: string | undefined, hasRow: b
|
||||
return { id: TELEMETRY_ROW_ID, disabled: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a config file carries the telemetry row, parsed under the same
|
||||
* `!!js`-tolerant dialect the boot uses — the `hasRow` input for launchers
|
||||
* that compose their patch lists outside {@link AppCLIEntry} (the TUI).
|
||||
* @param file - absolute path of the config or overlay file.
|
||||
* @returns true when a top-level (or inserted) row has the telemetry id.
|
||||
*/
|
||||
export function configHasTelemetryRow(file: string): boolean {
|
||||
const doc = yaml.load(readFileSync(file, 'utf8'), { schema: includeYamlSchema })
|
||||
if (!Array.isArray(doc)) throw new Error(`dsh: ${file} is not a top-level entry list`)
|
||||
return (doc as { id?: string; insert?: { id?: string }[] }[]).some(row =>
|
||||
row.id === TELEMETRY_ROW_ID || (row.insert ?? []).some(inserted => inserted.id === TELEMETRY_ROW_ID))
|
||||
}
|
||||
|
||||
/** One profile-json key mapped onto a yml row's config field. */
|
||||
interface ProfileMapping {
|
||||
jsonPath: string
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
resolveConfigPath,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { configHasTelemetryRow, resolveTelemetryPatch } from './app-cli-entry.ts'
|
||||
import { SESSION_QUERY_SQLITE_PATH_KEY } from '@deepseek-ai/dsh-session-query-sqlite'
|
||||
import { CONFIGURED_AGENT_IDENTITIES_KEY } from '@deepseek-ai/dsh-agent-loop'
|
||||
import type { Context } from 'cordis'
|
||||
@@ -196,16 +197,26 @@ export async function runTui(
|
||||
// demo or test config would silently run on the user's provider and model.
|
||||
// `--config-replace` additionally discards the base and the surface overlay.
|
||||
const replaceTree = configReplace !== undefined
|
||||
const patches = replaceTree ? [] : [
|
||||
...loadOverlayPatches(NAME, TUI_OVERLAY),
|
||||
...resolvedConfig === undefined
|
||||
? loadPersonalPatches(NAME) ?? []
|
||||
: loadOverlayPatches(NAME, resolveConfigPath(resolvedConfig, undefined)),
|
||||
const bootConfig = resolvedConfigReplace === undefined ? BASE_CONFIG : resolveConfigPath(resolvedConfigReplace, undefined)
|
||||
// Same opt-out semantics as the web surface (resolveTelemetryPatch: any
|
||||
// non-empty value disables; setting the switch against a tree without the
|
||||
// row fails loud rather than silently no-opping a privacy switch). The row
|
||||
// presence is checked against the tree actually booting, so a
|
||||
// --config-replace tree is judged on its own rows, not the shipped base's.
|
||||
const telemetryPatch = resolveTelemetryPatch(process.env.DSH_TELEMETRY_DISABLED, configHasTelemetryRow(bootConfig))
|
||||
const patches = [
|
||||
...replaceTree ? [] : [
|
||||
...loadOverlayPatches(NAME, TUI_OVERLAY),
|
||||
...resolvedConfig === undefined
|
||||
? loadPersonalPatches(NAME) ?? []
|
||||
: loadOverlayPatches(NAME, resolveConfigPath(resolvedConfig, undefined)),
|
||||
],
|
||||
...telemetryPatch === undefined ? [] : [telemetryPatch],
|
||||
]
|
||||
const queryIndexPath = join(tmpdir(), SESSION_QUERY_DB)
|
||||
const ctx = await boot(
|
||||
NAME,
|
||||
resolvedConfigReplace === undefined ? BASE_CONFIG : resolveConfigPath(resolvedConfigReplace, undefined),
|
||||
bootConfig,
|
||||
patches,
|
||||
(hostCtx) => {
|
||||
// The launcher owns session identity and the exit line: a config-mounted
|
||||
|
||||
@@ -130,7 +130,9 @@ function smoke(overrides: Partial<TuiPtySmokeOptions> & { label: string }): Prom
|
||||
tempDirPrefix: 'dsh-tui-smoke-',
|
||||
binScript: dshBinScript,
|
||||
tsconfigPath,
|
||||
env: { DEEPSEEK_API_KEY: 'keyless-tui-no-call' },
|
||||
// Telemetry now mounts in the shared base: keep fixture sessions from
|
||||
// POSTing to the production endpoint when run outside CI's workflow env.
|
||||
env: { DEEPSEEK_API_KEY: 'keyless-tui-no-call', DSH_TELEMETRY_DISABLED: '1' },
|
||||
// Artifact CI builds and smokes concurrently on a contended runner.
|
||||
...(process.env.DSH_EXAMPLE_MODE === 'lib' ? { timeoutMs: 60_000 } : {}),
|
||||
...overrides,
|
||||
|
||||
Reference in New Issue
Block a user