dsh-decision-layer 0.7.4 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  `dsh-decision-layer` adds structured, explicitly scoped decisions to the DeepSeek Harness (DSH) agent loop, backed by a pluggable adjudication backend. Dangerous-call gating, output self-check, tool narrowing, and loop prevention hook into the agent loop as automatic interventions.
6
6
 
7
- > **0.7.4 is a preview release.** The plugin works, but how much real value the adjudication interventions add is still being explored — every threshold is an unvalidated heuristic default. Treat it as something to observe, not a proven guarantee of impact. See [ROADMAP.md](<ROADMAP.md>) for scope and progress.
7
+ > **0.7.5 is a preview release.** The plugin works, but how much real value the adjudication interventions add is still being explored — every threshold is an unvalidated heuristic default. Treat it as something to observe, not a proven guarantee of impact. See [ROADMAP.md](<ROADMAP.md>) for scope and progress.
8
8
 
9
9
  ## Installation
10
10
 
@@ -41,6 +41,7 @@ Restart `dsh web` and refresh after installing or updating: the host and client
41
41
  - **Output self-check and quality trend** — scores the final reply at turn end (0–2), observe-only by default; an opt-in low-score re-prompt fires at most once per turn. Several consecutive high-confidence low scores tint the dock trigger grey → amber → red as a reminder. Self-check rows carry the turn number and the same task excerpt, and low rows carry a "low score" badge; the decision-analysis tab can show low scores only, so the scored turn is one click away in a long session (low-confidence low scores stay marked as noise to ignore).
42
42
  - **Tool narrowing** — at turn start and again when a new user request arrives inside the same turn, drops optional tools by task relevance, intersecting the host-allowed set and never expanding it; each re-judgement releases the previous restriction first, the restriction is lifted when the turn stops, and notices such as time or background-job updates never trigger one. Core tools and keep-prefix matches are always kept, over-cap falls back to observe-only, too-few-kept is treated as untrustworthy and skipped, and small-context backends are batched automatically. Enforced by default; observe-only is available. Narrowing logs include the turn and step numbers plus an expandable task excerpt, so you can assess the dropped tools against the task; older logs are not backfilled. The host fixes the tool set before the decision point for a step fires, so a drop reaches the model from the next step on; narrowing covers the tool channel only — UI fences in the reply body and skill instructions are unaffected.
43
43
  - **Task-completion review** — when the user spells out multiple delivery conditions, each is judged satisfied / unsatisfied / insufficient evidence. Conditions come only from the turn's latest request, so a retired list cannot be revived, and this turn's tool evidence is primary (an earlier turn's success does not count; self-reported completion is not evidence). Observe-only by default, with an opt-in re-prompt.
44
+ - **Read-only process checks (off by default)** — off by default. After explicit enablement in advanced settings, progress heartbeats, repeated unresolved failures and edits after successful verification can schedule a background check within the same turn without another user prompt. The backend receives the current request and bounded tool evidence; findings are observation signals, with uncertain results kept separate. No prompts are added and no tools are restricted. Single flight, a rolling budget, short deadlines and stale-result cancellation keep the main agent independent. See [ADR-0017](<docs/adr/0017-read-only-execution-checkpoints.md>); task-level benefits have not been established.
44
45
  - **Loop guard** — denies the same call repeated consecutively past a threshold; deterministic, local, and backend-free. The per-feature switches do not affect it, but the session master switch pauses it too.
45
46
  - **Session panel and decision-analysis view** — the panel shows automatic-decision metrics, the session master switch, and the recent log; the "Decision analysis" tab shows a per-session process profile, trend backtest, and decision accuracy, with per-row ratings and a session intervention switch.
46
47
  - **Configuration and persistence** — each decision point has its own switch and configurable thresholds in the plugin details, written to `~/.config/dsh-decision-layer/config.json` and applied live. Defaults and field names are in [ADR-0006](<docs/adr/0006-configurable-thresholds.md>). You can also edit that file by hand, but only with valid values: an out-of-range value, a misspelled field, or a wrong type makes the whole config fail validation, the config page cannot load it, and every decision point reverts to its own safe default (see [ADR-0005](<docs/adr/0005-config-wholesale-replace-and-live-reload.md>)). Decision logs and session metrics persist for 30 days for review and calibration.
@@ -53,15 +54,16 @@ Once a key is configured, the plugin sends adjudication requests to the remote e
53
54
  - Gate: the tool name plus redacted command/path summaries (up to 2048 characters each); no conversation content.
54
55
  - Self-check: this turn's request, the recent user asks and AI replies it needs for context (background only, each with its own budget, credentials masked), and the turn's final reply (opening and closing within a combined ~24K-character budget; the middle of an over-long reply is marked as omitted so the conclusion is not lost).
55
56
  - Narrowing: the current request, the necessary recent background, and the latest execution progress (tool name, ok/error, bounded command/path and shell-result excerpts) plus candidate tool names/descriptions.
57
+ - Process checks (off by default): when enabled, the current request and bounded tool names, outcomes, redacted command/path and shell excerpts for the active window. No file-read, search or webpage result bodies, reasoning or PTC scripts. Budgets and invalidation are specified in ADR-0017.
56
58
  - Task-completion review: delivery conditions extracted from this turn's latest user request (up to 20; a turn's earlier requests are not merged) and that request with its necessary background (credentials masked), this turn's tool evidence (tool name, ok/error, bounded command/path, plus a bounded shell-result excerpt with long unlabeled blobs masked again), and the first ~8K characters of the reply. Raw results from file reads, searches and network tools are never sent.
57
59
  - Manual verdicts: `decision_evaluate` and the `decision-layer` skill send whatever the agent submits, unchanged.
58
60
 
59
61
  Only real user messages are used as requests: host-injected content such as time notices, runtime details, memory snippets, system reminders and background-job updates is excluded or stripped, but anything you paste into the conversation still leaves your machine. The remote provider may log, retain, or otherwise use it depending on its terms; redaction and truncation reduce risk but are not anonymization.
60
62
 
61
- Two kinds of data are also stored locally for 30 days: decision logs in `~/.config/dsh-decision-layer/logs/` and session snapshots in `~/.config/dsh-decision-layer/state/`. They include redacted command/path summaries; every decision row's task excerpt is at most 512 characters (injected memory/system-reminder blocks removed, common credential forms masked) and a row may carry its turn and step numbers. No extra copies of the full background, execution progress, tool results or reply text are persisted — only the excerpt and decision counters. The excerpts are shown in the local session panel and decision-analysis view only; outbound adjudication content is the list above, and you can delete the local data yourself.
63
+ Two kinds of data are also stored locally for 30 days: decision logs in `~/.config/dsh-decision-layer/logs/` and session snapshots in `~/.config/dsh-decision-layer/state/`. They include redacted command/path summaries; every decision row's task excerpt is at most 512 characters (injected memory/system-reminder blocks removed, common credential forms masked) and a row may carry its turn and step numbers. No extra copies of the full background, execution progress, tool results or reply text are persisted — only the excerpt and decision counters. New source process checks additionally retain trigger codes, the evidence sequence boundary and candidate counts. Automatic decisions can retain request correlation ids, latency, physical request counts and reported usage. Missing usage is not zero; usage returned after a retry describes the successful response, not necessarily all billed attempts. An unfinished request at plugin disposal may lack its final cost record. The excerpts are shown in the local session panel and decision-analysis view only; outbound adjudication content is the list above, and you can delete the local data yourself.
62
64
 
63
65
  **If your session content is sensitive, prefer a locally deployed model** so adjudication data never leaves the machine. `tev1` is served natively by ollama at `/v1/systemone`; see [Running with a local tev1](<docs/en/deploy-tev1-with-plugin.md>). Any other local model works as long as it exposes a compatible `/v1/systemone` endpoint, pointed to from the config page.
64
66
 
65
67
  ## Safety boundary and development
66
68
 
67
- Automatic intervention only subtracts: the gate only denies, narrowing only removes tools, and neither grants new permissions. The gate only blocks on a high-confidence deny; on backend failure, low confidence, or an invalid response it does not intervene and runs the host's original path. Self-check, narrowing, and loop-guard errors skip intervention. The redacted gate summary sent to the backend carries only tool name and command/path with secret redaction; the plugin never reads file content, but the command itself (for example a heredoc or text written with echo) is included as-is. Both halves are TypeScript under strict mode: `npm run build` type-checks with `tsc` and emits `lib/host/*.js` plus `lib/client.js` ([ADR-0015](<docs/adr/0015-typed-host-build.md>)); `npm test` builds first and then runs the tests, and `npm run check` matches CI (build, tests, and the packed-tarball check). Node.js `>=20.11` at runtime. **Build output is not committed** ([ADR-0012](<docs/adr/0012-distribution-via-github-actions.md>)): `lib/` is gitignored and the source is the only source of truth. Releases run through `.github/workflows/release.yml`, which builds, gates, `npm pack`s and then asserts with `scripts/assert-pack.mjs` that the tarball really carries the host entry and `lib/client.js`, and leaks no sources — only then does it publish to npm or open a Release. It runs when a version bump lands on main and publishes through npm Trusted Publishing (OIDC), so no long-lived token is involved. Architecture decisions live in [docs/adr/](<docs/adr/README.md>): why each rule exists and what was rejected. 0.7.4 is a preview: the interventions work, but their real value and thresholds are still being explored — feedback on misjudgments and feel observed in the decision log is welcome.
69
+ Automatic intervention only subtracts: the gate only denies, narrowing only removes tools, and neither grants new permissions. The gate only blocks on a high-confidence deny; on backend failure, low confidence, or an invalid response it does not intervene and runs the host's original path. Self-check, narrowing, and loop-guard errors skip intervention. The redacted gate summary sent to the backend carries only tool name and command/path with secret redaction; the plugin never reads file content, but the command itself (for example a heredoc or text written with echo) is included as-is. Both halves are TypeScript under strict mode: `npm run build` type-checks with `tsc` and emits `lib/host/*.js` plus `lib/client.js` ([ADR-0015](<docs/adr/0015-typed-host-build.md>)); `npm test` builds first and then runs the tests, and `npm run check` matches CI (build, tests, and the packed-tarball check). Node.js `>=20.11` at runtime. **Build output is not committed** ([ADR-0012](<docs/adr/0012-distribution-via-github-actions.md>)): `lib/` is gitignored and the source is the only source of truth. Releases run through `.github/workflows/release.yml`, which builds, gates, `npm pack`s and then asserts with `scripts/assert-pack.mjs` that the tarball really carries the host entry and `lib/client.js`, and leaks no sources — only then does it publish to npm or open a Release. It runs when a version bump lands on main and publishes through npm Trusted Publishing (OIDC), so no long-lived token is involved. Architecture decisions live in [docs/adr/](<docs/adr/README.md>): why each rule exists and what was rejected. 0.7.5 is a preview: the interventions work, but their real value and thresholds are still being explored — feedback on misjudgments and feel observed in the decision log is welcome.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  DeepSeek Harness(DSH)的结构化裁决插件。它把有明确判断标准的问题交给同协议的裁决后端,供 agent 在需要时使用,并在 agent loop 上接入危险门控、产出自检、工具收窄和防循环等自动介入。
6
6
 
7
- > **0.7.4 是体验版。** 插件功能已可用,但裁决介入能带来多少实际价值仍在探索中——各决策点的阈值都是未经验证的启发式默认值,建议先以观察心态体验,别当成经过验证的效果保证。功能与后续进度以 [ROADMAP.md](<ROADMAP.md>) 为准。
7
+ > **0.7.5 是体验版。** 插件功能已可用,但裁决介入能带来多少实际价值仍在探索中——各决策点的阈值都是未经验证的启发式默认值,建议先以观察心态体验,别当成经过验证的效果保证。功能与后续进度以 [ROADMAP.md](<ROADMAP.md>) 为准。
8
8
 
9
9
  ## 安装
10
10
 
@@ -41,6 +41,7 @@ dsh plugin --profile web add file:/绝对路径/dsh-decision-layer
41
41
  - **产出自检与质量趋势**:回合结束给最终回复打分(0–2),默认只记录;可开启低分补完,同一回合最多补一次。连续多次高置信低分时,dock 触发器图标会从灰变橙、变红作为提醒。自检日志带回合号与同一套任务节选,低分行另有「低评分」标记;决策分析页可一键「只看低评分」,长会话里也能直接定位到被打分的回合(低置信低分仍标为噪声,可忽略)。
42
42
  - **工具收窄**:回合开始时、以及同一回合内你提出新要求时,按任务相关性摘掉可选工具,只在宿主已允许的集合内取交集、不扩权;每次重新判断前先释放上一轮的旧限制,回合结束即解除,时间提示、后台通知等非用户要求不会触发重判。核心工具与保留前缀命中的工具始终保留,可选工具过多时自动降为仅观察、保留过少时视为判定不可信并跳过,对小上下文后端自动分批。默认真正生效,也可切到仅观察。收窄日志附带回合号、步骤号和可展开的任务节选,便于对照被摘除的工具评估判断是否合理;旧日志不会补录场景。宿主在本步决策点触发前就已确定工具集,所以摘除从下一步起才进入模型请求;收窄只作用于工具通道——回复正文里的 UI 围栏、技能说明等非工具能力不受影响。
43
43
  - **任务完成核对**:用户明确列出多个交付条件时,逐项判定「已满足 / 未满足 / 证据不足」,条件只取本回合最近一次要求(避免旧清单被恢复),以本回合工具证据为准(上一回合的成功不算数,回复自述不算证据);默认只观察,可配补完。
44
+ - **只读过程检查(默认关闭)**:默认关闭,在插件详情页高级设置启用后,可在同一回合没有新用户输入时按进展心跳、持续未解决失败或验证后又修改产物安排后台检查。使用当前要求与有界工具证据,结果为「未见需复核问题 / 值得复核 / 证据不足」,低置信判断保持不确定;不追加提示、不限制工具、不改变已有自检/完成核对。每会话单在途、合并最新候选、滚动预算和短截止时间;错误、关闭、取消或任务变化不阻塞主任务。参数和边界见 [ADR-0017](<docs/adr/0017-read-only-execution-checkpoints.md>)。尚未取得效果对照数据。
44
45
  - **防循环**:同一调用连续重复超过阈值即拒绝,确定性本地逻辑,不依赖后端;不受各决策点开关影响,但会话总开关关闭时同样暂停。
45
46
  - **会话面板与决策分析页**:面板展示自动裁决指标、会话总开关和最近日志;「决策分析」tab 按会话展示过程画像、趋势回测与判断准确度,并支持逐条标注和开关本会话的自动介入。
46
47
  - **配置与持久化**:各决策点可独立开关,阈值在插件详情页调整,写入 `~/.config/dsh-decision-layer/config.json` 并热生效。默认值与字段说明见 [ADR-0006](<docs/adr/0006-configurable-thresholds.md>)。也可直接手改该文件,但只能写合法值:越界、拼错字段名或类型不符会让整份配置校验失败,配置页读不出来,各决策点退回各自的安全默认(见 [ADR-0005](<docs/adr/0005-config-wholesale-replace-and-live-reload.md>))。决策日志与会话指标落盘 30 天,供回看和校准。
@@ -53,12 +54,13 @@ dsh plugin --profile web add file:/绝对路径/dsh-decision-layer
53
54
  - 门控:工具名与脱敏后的命令 / 路径摘要(各最多 2048 字符),不含对话内容。
54
55
  - 产出自检:本轮请求、最近必要的用户要求与 AI 回复(作背景,各有限额度,已遮蔽凭据),以及本轮最终回复(开头与结尾合计约 24K 字符;超长回复的中间部分会标注省略,避免只看开头漏掉结论)。
55
56
  - 工具收窄:当前有效请求、必要前文与最近执行进展(工具名、成败、有限的命令 / 路径与 shell 结果摘录)与候选工具的名称 / 描述。
57
+ - 过程检查(默认关闭):启用后发送当前要求和当前窗口的有界工具名、成败、脱敏命令/路径及 shell 摘录;不发送文件读取、搜索或网页结果原文、reasoning 或 PTC 脚本。输入限额和过期规则见 ADR-0017。
56
58
  - 完成核对:从本回合最近一次用户要求中提取的交付条件(最多 20 条,不合并同一回合的多次要求)、本回合最近一次要求与必要前文(作背景,已遮蔽凭据)、本回合工具证据(工具名、成败、有限的命令 / 路径,shell 类工具另有有限结果摘录,并对无标签的长随机串再遮蔽一次)、回复前约 8K 字符。文件读取、搜索与网络工具的结果原文不外发。
57
59
  - 手动裁决:`decision_evaluate` 与 `decision-layer` skill 会把 agent 提交的内容原样发送。
58
60
 
59
61
  进入裁决的用户消息只取真实的那些,时间提示、运行环境、记忆片段、系统提醒与后台通知等宿主注入内容会被排除或剥离;但用户自己粘贴进对话的内容仍会离开本机。远程服务提供方可能记录、留存或用于其他用途,具体取决于其服务条款;脱敏和截断只降低风险,不等于匿名化。
60
62
 
61
- 本地还会落盘两类数据,各保留 30 天:决策日志在 `~/.config/dsh-decision-layer/logs/`,会话快照在 `~/.config/dsh-decision-layer/state/`,其中包含脱敏后的命令 / 路径;各决策行的任务节选最多 512 字符(过滤记忆 / 系统提醒注入块并遮蔽常见凭据写法),行上可带回合号与步骤号。落盘不额外保存完整前文、执行进展、工具结果或回复原文,只保留节选与决策计数。节选展示在本地会话面板和决策分析页,外发内容以上一节逐项列出为准,可按需自行清理。
63
+ 本地还会落盘两类数据,各保留 30 天:决策日志在 `~/.config/dsh-decision-layer/logs/`,会话快照在 `~/.config/dsh-decision-layer/state/`,其中包含脱敏后的命令 / 路径;各决策行的任务节选最多 512 字符(过滤记忆 / 系统提醒注入块并遮蔽常见凭据写法),行上可带回合号与步骤号。落盘不额外保存完整前文、执行进展、工具结果或回复原文,只保留节选与决策计数;过程检查另保存触发类型、证据截至序号和候选数量,自动裁决可保存请求关联 id、耗时、后端请求次数和已报告用量。未知用量不补零,重试时成功响应的 usage 不代表所有尝试的总计费;插件卸载后仍未结算的请求可能缺失最终成本记录。节选展示在本地会话面板和决策分析页,外发内容以上一节逐项列出为准,可按需自行清理。
62
64
 
63
65
  **会话内容敏感时,推荐改用本地部署的模型**,让裁决数据不出本机。`tev1` 可经 ollama 原生的 `/v1/systemone` 端点直接接入,部署步骤见 [用本地 tev1 配合插件(部署指南)](<docs/zh/deploy-tev1-with-plugin.md>);其他本地模型只要提供兼容 `/v1/systemone` 的端点,同样可以在配置页把服务地址指向本机。
64
66
 
@@ -72,4 +74,4 @@ dsh plugin --profile web add file:/绝对路径/dsh-decision-layer
72
74
 
73
75
  **构建产物不入库**([ADR-0012](<docs/adr/0012-distribution-via-github-actions.md>)):`lib/` 已被 `.gitignore` 覆盖,源码是唯一事实来源。发布由 GitHub Actions 的 `release.yml` 完成——构建、跑门禁、`npm pack`,并用 `scripts/assert-pack.mjs` 断言产物里确实有 host 入口与 `lib/client.js`、且没有源码泄漏,之后才发 npm / 建 Release。该 workflow 在版本号变更推入 main 时自动执行;npm 侧使用 Trusted Publishing(OIDC),不依赖长期 token。
74
76
 
75
- 架构决策记录见 [docs/adr/](<docs/adr/README.md>):每条规则为什么存在、当时放弃了哪些备选。0.7.4 为体验版:介入能力已可用,但实际价值与阈值仍在探索,欢迎反馈决策日志观察到的误判与体感。
77
+ 架构决策记录见 [docs/adr/](<docs/adr/README.md>):每条规则为什么存在、当时放弃了哪些备选。0.7.5 为体验版:介入能力已可用,但实际价值与阈值仍在探索,欢迎反馈决策日志观察到的误判与体感。
package/docs/en/DETAIL.md CHANGED
@@ -8,6 +8,7 @@ A structured set of explicitly scoped decisions for the DeepSeek Harness agent l
8
8
  - **Output self-check and quality trend** — scores the final reply at turn end and can append one re-prompt when the score is low and the model is confident; several consecutive confident low scores raise an alert on the trigger button.
9
9
  - **Task-completion review** — when the user lists multiple delivery conditions, each is judged satisfied / unsatisfied / insufficient evidence. Conditions come only from the turn's latest request, with this turn's tool evidence primary (an earlier turn's success does not count) and self-reported completion not counting.
10
10
  - **Tool narrowing** — at turn start, and again when a new user request arrives inside the same turn, judges each optional tool's relevance to the task and drops the ones not needed; each re-judgement releases the previous restriction first, and notices such as time or background-job updates trigger no re-judgement. It intersects the host-allowed set and never expands it. The host resolves the tool set before the decision point for a step fires, so a drop reaches the model from the next step on; narrowing covers the tool channel only — UI fences in the reply body and skill instructions are unaffected.
11
+ - **Read-only process checks** — off by default; explicitly enable and save in advanced settings. Progress heartbeats, repeated unresolved failures and edits after verification can schedule background read-only judgements within a turn, without adding prompts or restricting tools; the session switch still applies. Low-confidence and insufficient evidence stay uncertain. Additional outbound data is limited to the current request and bounded redacted tool summaries; no file-read/search/webpage result bodies, reasoning or PTC scripts. Findings are separate from final-reply scores, and execution benefits have not been established.
11
12
  - **Loop guard** — denies the same call repeated consecutively past a threshold; deterministic and backend-free.
12
13
  - **Decision panel and analysis view** — shows automatic-decision metrics and the decision log, and lets you rate each decision correct / wrong / unsure; the analysis view's header can enable or disable automatic interventions for the current session.
13
14
 
package/docs/zh/DETAIL.md CHANGED
@@ -8,6 +8,7 @@
8
8
  - **产出自检与质量趋势**:回合结束给最终回复打分,低分且模型有把握时可自动补一轮;连续多次高置信低分会在触发按钮上给出告警。
9
9
  - **任务完成核对**:用户明确列出多个交付条件时,逐项判定「已满足 / 未满足 / 证据不足」,条件只取本回合最近一次要求,以本回合工具证据为准(上一回合的成功不算数),回复自述不算证据。
10
10
  - **工具收窄**:回合开始时、以及同一回合内出现新的用户要求时,判断各可选工具与任务的相关性并摘掉用不上的;每次重新判断前先释放旧限制,时间提示与后台通知不触发重判。只在宿主已允许的工具集内取交集,绝不扩权。宿主在本步决策点触发前已解析工具集,所以摘除从下一步起才进入模型请求;收窄只覆盖工具通道,回复正文里的 UI 围栏、技能说明等非工具能力不受影响。
11
+ - **只读过程检查**:默认关闭,需在高级配置显式启用并保存。在同回合中按进展心跳、持续未解决失败和验证后修改安排后台只读判断,不追加提示或改工具限制;低置信与证据不足保留为不确定,仍受会话总开关控制。额外外发当前要求和有界脱敏的工具摘要,不发读文件、搜索、网页结果原文、reasoning 或 PTC 脚本。结果与最终回复自检分列,尚未证明提升执行效果。
11
12
  - **防循环**:同一调用连续重复超过阈值即拒绝,确定性本地逻辑,不依赖后端。
12
13
  - **决策面板与分析页**:展示自动裁决指标与决策日志,并可逐条标注「判对 / 判错 / 说不准」;分析页顶部可直接开关本会话的自动介入。
13
14
 
package/lib/client.js CHANGED
@@ -38,6 +38,7 @@ var zh = {
38
38
  metrics: "\u81EA\u52A8\u88C1\u51B3\u7EDF\u8BA1",
39
39
  triggerDecisions: "\u51B3\u7B56",
40
40
  empty: "\u5C1A\u65E0\u81EA\u52A8\u88C1\u51B3\u3002\u624B\u52A8\u88C1\u51B3\u4E0D\u8BA1\u5165\u8FD9\u91CC\u3002",
41
+ checkpointDockHint: "\u53E6\u6709 {count} \u6B21\u53EA\u8BFB\u8FC7\u7A0B\u68C0\u67E5\uFF0C\u89C1\u300C\u51B3\u7B56\u5206\u6790\u300D\u9875\u3002",
41
42
  attempts: "\u8BC4\u4F30\u6B21\u6570",
42
43
  failures: "\u5931\u8D25\u56DE\u9000",
43
44
  suggestions: "\u6A21\u578B\u5EFA\u8BAE\uFF08\u653E\u884C / \u8BE2\u95EE / \u62D2\u7EDD\uFF09",
@@ -185,6 +186,49 @@ var zh = {
185
186
  completeUnsatisfied: "\u672A\u6EE1\u8DB3",
186
187
  completeInsufficient: "\u8BC1\u636E\u4E0D\u8DB3",
187
188
  completeSteered: "\u5DF2\u8865\u4E00\u8F6E",
189
+ checkpointSettings: "\u8FC7\u7A0B\u68C0\u67E5\uFF08\u53EA\u8BFB\uFF09",
190
+ checkpointEnabled: "\u542F\u7528\u53EA\u8BFB\u8FC7\u7A0B\u68C0\u67E5",
191
+ checkpointHint: "\u9ED8\u8BA4\u5173\u95ED\uFF0C\u5E76\u4ECD\u53D7\u4F1A\u8BDD\u603B\u5F00\u5173\u63A7\u5236\u3002\u663E\u5F0F\u5F00\u542F\u5E76\u4FDD\u5B58\u540E\uFF0C\u4F1A\u628A\u5F53\u524D\u8981\u6C42\u4E0E\u6709\u754C\u7684\u8131\u654F\u8FC7\u7A0B\u8BC1\u636E\uFF08\u5DE5\u5177\u540D\u3001\u6210\u8D25\u3001\u547D\u4EE4\u3001\u8DEF\u5F84\u548C Shell \u6458\u5F55\uFF09\u53D1\u9001\u5230\u5DF2\u914D\u7F6E\u7684\u88C1\u51B3\u7AEF\u70B9\uFF0C\u5728\u540E\u53F0\u505A\u53EA\u8BFB\u5224\u65AD\uFF1B\u4E0D\u81EA\u52A8\u8FFD\u52A0\u63D0\u793A\uFF0C\u4E5F\u4E0D\u6539\u53D8\u5DE5\u5177\u9650\u5236\u3002\u4E0D\u53D1\u9001\u8BFB\u6587\u4EF6\u3001\u641C\u7D22\u6216\u7F51\u9875\u7ED3\u679C\u539F\u6587\u3002",
192
+ checkpointHeartbeatLabel: "\u5FC3\u8DF3\u6B65\u6570",
193
+ checkpointHeartbeatHint: "\u7D2F\u8BA1\u591A\u5C11\u6B65\u540E\u5B89\u6392\u4E00\u6B21\u8FC7\u7A0B\u68C0\u67E5\uFF0C\u9ED8\u8BA4 6\uFF1B\u6574\u6570\uFF0C2\u2013100\u3002",
194
+ checkpointFailureLabel: "\u8FDE\u7EED\u5931\u8D25\u9608\u503C",
195
+ checkpointFailureHint: "\u672A\u89E3\u51B3\u5931\u8D25\u8FBE\u5230\u6B64\u6570\u91CF\u65F6\u5B89\u6392\u68C0\u67E5\uFF0C\u9ED8\u8BA4 3\uFF1B\u6574\u6570\uFF0C2\u201310\u3002",
196
+ checkpointTimeoutLabel: "\u8FC7\u7A0B\u68C0\u67E5\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09",
197
+ checkpointTimeoutHint: "\u5355\u6B21\u540E\u53F0\u68C0\u67E5\u7684\u622A\u6B62\u65F6\u95F4\uFF0C\u9ED8\u8BA4 1500\uFF1B\u6574\u6570\uFF0C100\u201310000\u3002\u8D85\u65F6\u4E0D\u5F71\u54CD\u6267\u884C\u3002",
198
+ logCheckpoint: "\u8FC7\u7A0B\u68C0\u67E5",
199
+ checkpointReadOnly: "\u53EA\u8BFB",
200
+ checkpointHeartbeat: "\u5FC3\u8DF3\u68C0\u67E5",
201
+ checkpointFailure: "\u672A\u89E3\u51B3\u5931\u8D25",
202
+ checkpointVerification: "\u4FEE\u6539\u540E\u9A8C\u8BC1",
203
+ checkpointTriggerUnknown: "\u68C0\u67E5\u89E6\u53D1",
204
+ checkpointOk: "\u89C2\u5BDF\u672A\u89C1\u7591\u70B9",
205
+ checkpointConcern: "\u89C2\u5BDF\u5230\u5F85\u590D\u6838\u4FE1\u53F7",
206
+ checkpointUncertain: "\u5224\u65AD\u4E0D\u786E\u5B9A",
207
+ checkpointSkipped: "\u5DF2\u8DF3\u8FC7",
208
+ checkpointThroughSeq: "\u8BC1\u636E\u622A\u81F3 #{seq}",
209
+ checkpointEvidenceCount: "\u5019\u9009\u8BC1\u636E {count} \u6761",
210
+ checkpointReasonUnknown: "\u672A\u4EA7\u751F\u53EF\u7528\u5224\u65AD",
211
+ checkpointReasonCanceled: "\u5DF2\u53D6\u6D88 / \u7ED3\u679C\u5DF2\u5931\u6548",
212
+ checkpointReasonDisabled: "\u8FC7\u7A0B\u68C0\u67E5\u5DF2\u5173\u95ED",
213
+ checkpointReasonNoTask: "\u65E0\u7ED1\u5B9A\u4EFB\u52A1",
214
+ checkpointReasonStale: "\u4E0A\u4E0B\u6587\u5DF2\u53D8\u5316",
215
+ checkpointReasonTimeout: "\u68C0\u67E5\u8D85\u65F6",
216
+ checkpointReasonBusy: "\u68C0\u67E5\u9884\u7B97\u6216\u961F\u5217\u5DF2\u6EE1",
217
+ checkpointReasonUnchanged: "\u6CA1\u6709\u65B0\u8BC1\u636E",
218
+ checkpointProfileNote: "\u53EA\u8BFB\u89C2\u5BDF\u4FE1\u53F7\uFF1B\u8DF3\u8FC7\u4E0D\u8BA1\u5165\u8BC4\u4F30\u6B21\u6570\uFF0C\u4E5F\u4E0D\u5F71\u54CD\u6700\u7EC8\u56DE\u590D\u8D28\u91CF\u8D8B\u52BF\u3002\u89C2\u5BDF\u672A\u89C1\u7591\u70B9\u4E0D\u4EE3\u8868\u4EFB\u52A1\u901A\u8FC7\u9A8C\u6536\u3002",
219
+ cardCheckpoint: "\u8FC7\u7A0B\u68C0\u67E5",
220
+ telemetryTitle: "\u51B3\u7B56\u6D4B\u91CF",
221
+ telemetryCoverage: "\u8017\u65F6\u6D4B\u91CF\u8986\u76D6",
222
+ telemetryUsageCoverage: "\u7528\u91CF\u6D4B\u91CF\u8986\u76D6",
223
+ telemetryMeanLatency: "\u5E73\u5747\u8017\u65F6",
224
+ telemetryP95Latency: "P95 \u8017\u65F6",
225
+ telemetryInputTokens: "\u8F93\u5165 token",
226
+ telemetryOutputTokens: "\u8F93\u51FA token",
227
+ telemetryBackendAttempts: "\u540E\u7AEF\u8BF7\u6C42\u6B21\u6570",
228
+ telemetryMeasuredCount: "\u5DF2\u6D4B {count} \u6B21",
229
+ telemetryUnknown: "\u672A\u77E5",
230
+ telemetryMs: "{value} ms",
231
+ telemetryNote: "\u8986\u76D6\u6309\u5DF2\u8BC4\u4F30\u7684\u51B3\u7B56\u884C\u8BA1\u7B97\uFF0C\u4E0D\u542B\u8FC7\u7A0B\u68C0\u67E5\u7684\u8DF3\u8FC7\uFF1B\u7528\u91CF\u4EC5\u6C47\u603B\u5DF2\u8BB0\u5F55\u6837\u672C\u3002\u7F3A\u5931\u4E0D\u7B49\u4E8E\u96F6\uFF0C\u8FD9\u4E9B\u6D4B\u91CF\u4E0D\u8BC1\u660E\u51C6\u786E\u7387\u6216\u6210\u672C\u6536\u76CA\u3002",
188
232
  backend: "\u88C1\u51B3\u540E\u7AEF",
189
233
  url: "\u670D\u52A1\u5730\u5740",
190
234
  model: "\u6A21\u578B",
@@ -240,6 +284,7 @@ var en = {
240
284
  metrics: "Automatic decisions",
241
285
  triggerDecisions: "decisions",
242
286
  empty: "No automatic decisions yet. Manual verdicts are not counted here.",
287
+ checkpointDockHint: "Also {count} read-only process checks; see the Decision analysis tab.",
243
288
  attempts: "Evaluations",
244
289
  failures: "Fallback failures",
245
290
  suggestions: "Model suggestions (allow / ask / deny)",
@@ -387,6 +432,49 @@ var en = {
387
432
  completeUnsatisfied: "unmet",
388
433
  completeInsufficient: "insufficient",
389
434
  completeSteered: "re-prompted",
435
+ checkpointSettings: "Process checks (read-only)",
436
+ checkpointEnabled: "Enable read-only process checks",
437
+ checkpointHint: "Off by default and still subject to the session switch. After you explicitly enable and save, the current request and bounded, redacted process evidence (tool names, outcomes, commands, paths and shell excerpts) are sent to the configured decision endpoint for a background, read-only judgement. No prompts are added and no tool restrictions change. File-read, search and webpage result bodies are not sent.",
438
+ checkpointHeartbeatLabel: "Heartbeat steps",
439
+ checkpointHeartbeatHint: "Schedule a process check after this many steps; default 6. Integer, 2\u2013100.",
440
+ checkpointFailureLabel: "Consecutive failure threshold",
441
+ checkpointFailureHint: "Schedule a check when unresolved failures reach this count; default 3. Integer, 2\u201310.",
442
+ checkpointTimeoutLabel: "Process check timeout (ms)",
443
+ checkpointTimeoutHint: "Deadline for one background check; default 1500. Integer, 100\u201310000. A timeout does not affect execution.",
444
+ logCheckpoint: "Process check",
445
+ checkpointReadOnly: "read-only",
446
+ checkpointHeartbeat: "heartbeat",
447
+ checkpointFailure: "unresolved failures",
448
+ checkpointVerification: "verification after change",
449
+ checkpointTriggerUnknown: "check trigger",
450
+ checkpointOk: "no concern observed",
451
+ checkpointConcern: "signal for review observed",
452
+ checkpointUncertain: "uncertain judgement",
453
+ checkpointSkipped: "skipped",
454
+ checkpointThroughSeq: "evidence through #{seq}",
455
+ checkpointEvidenceCount: "{count} candidate evidence items",
456
+ checkpointReasonUnknown: "no usable judgement",
457
+ checkpointReasonCanceled: "canceled / result discarded",
458
+ checkpointReasonDisabled: "process checks disabled",
459
+ checkpointReasonNoTask: "no bound task",
460
+ checkpointReasonStale: "context changed",
461
+ checkpointReasonTimeout: "check timed out",
462
+ checkpointReasonBusy: "check budget or queue full",
463
+ checkpointReasonUnchanged: "no new evidence",
464
+ checkpointProfileNote: "Read-only observation signals. Skips do not count as evaluations or affect final-reply quality trends. No concern observed does not mean the task passed acceptance.",
465
+ cardCheckpoint: "Process checks",
466
+ telemetryTitle: "Decision measurements",
467
+ telemetryCoverage: "Latency coverage",
468
+ telemetryUsageCoverage: "Usage coverage",
469
+ telemetryMeanLatency: "Mean latency",
470
+ telemetryP95Latency: "P95 latency",
471
+ telemetryInputTokens: "Input tokens",
472
+ telemetryOutputTokens: "Output tokens",
473
+ telemetryBackendAttempts: "Backend requests",
474
+ telemetryMeasuredCount: "{count} measured",
475
+ telemetryUnknown: "unknown",
476
+ telemetryMs: "{value} ms",
477
+ telemetryNote: "Coverage uses evaluated decision rows and excludes skipped process checks. Usage totals cover measured samples only. Missing is not zero; these measurements do not establish accuracy or cost benefits.",
390
478
  backend: "Decision backend",
391
479
  url: "Server URL",
392
480
  model: "Model",
@@ -721,7 +809,86 @@ function renderComplete({ t }, entry) {
721
809
  steer
722
810
  ] });
723
811
  }
812
+ function checkpointReason(t, reason) {
813
+ switch (reason) {
814
+ case "disabled":
815
+ return t("checkpointReasonDisabled");
816
+ case "no-task":
817
+ return t("checkpointReasonNoTask");
818
+ case "stale":
819
+ case "superseded":
820
+ return t("checkpointReasonStale");
821
+ case "canceled":
822
+ case "aborted":
823
+ return t("checkpointReasonCanceled");
824
+ case "timeout":
825
+ return t("checkpointReasonTimeout");
826
+ case "queue-full":
827
+ case "budget":
828
+ return t("checkpointReasonBusy");
829
+ case "unchanged":
830
+ case "no-evidence":
831
+ return t("checkpointReasonUnchanged");
832
+ case "low-confidence":
833
+ return t("reasonLowConfidence");
834
+ case "unavailable":
835
+ case "unreachable":
836
+ return t("reasonUnavailable");
837
+ case "http-error":
838
+ return t("reasonHttpError");
839
+ case "invalid-response":
840
+ return t("reasonInvalidResponse");
841
+ case "config":
842
+ return t("reasonConfig");
843
+ default:
844
+ return t("checkpointReasonUnknown");
845
+ }
846
+ }
847
+ var checkpointOutcome = (entry) => entry.outcome === "concern" && entry.lowConfidence === true ? "uncertain" : entry.outcome;
848
+ function renderCheckpoint({ t }, entry) {
849
+ const outcome = checkpointOutcome(entry);
850
+ const discarded = outcome === "error" && (entry.reason === "canceled" || entry.reason === "aborted");
851
+ const result = discarded ? t("checkpointReasonCanceled") : outcome === "ok" ? t("checkpointOk") : outcome === "concern" ? t("checkpointConcern") : outcome === "error" ? t("logEvalFailed") : outcome === "skipped" ? t("checkpointSkipped") : t("checkpointUncertain");
852
+ const trigger = entry.trigger === "heartbeat" ? t("checkpointHeartbeat") : entry.trigger === "unresolved_failure" ? t("checkpointFailure") : entry.trigger === "verification_after_change" ? t("checkpointVerification") : t("checkpointTriggerUnknown");
853
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
854
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("logCheckpoint") }),
855
+ turnTag(t, entry),
856
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-log-detail", children: [
857
+ " \xB7 ",
858
+ t("checkpointReadOnly"),
859
+ " \xB7 ",
860
+ trigger
861
+ ] }),
862
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: outcome === "uncertain" || outcome === "skipped" || discarded ? "decision-log-muted-verdict" : "decision-log-verdict", children: [
863
+ " \xB7 ",
864
+ result
865
+ ] }),
866
+ Number.isSafeInteger(entry.throughSeq) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-log-detail", children: [
867
+ " \xB7 ",
868
+ t("checkpointThroughSeq", { seq: entry.throughSeq })
869
+ ] }),
870
+ Number.isSafeInteger(entry.evidenceCount) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-log-detail", children: [
871
+ " \xB7 ",
872
+ t("checkpointEvidenceCount", { count: entry.evidenceCount })
873
+ ] }),
874
+ typeof entry.confidence === "number" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-log-detail", children: [
875
+ " \xB7 ",
876
+ t("logConfidence"),
877
+ " ",
878
+ entry.confidence
879
+ ] }),
880
+ entry.reason && !discarded && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-log-detail", children: [
881
+ " \xB7 ",
882
+ checkpointReason(t, entry.reason)
883
+ ] })
884
+ ] });
885
+ }
724
886
  function outcomeTagOf(entry) {
887
+ if (entry.kind === "checkpoint") {
888
+ const outcome = checkpointOutcome(entry);
889
+ const discarded = outcome === "error" && (entry.reason === "canceled" || entry.reason === "aborted");
890
+ return discarded ? "muted" : outcome === "concern" ? "warn" : outcome === "error" ? "error" : outcome === "ok" ? "ok" : "muted";
891
+ }
725
892
  if (entry.kind === "narrow") return entry.outcome === "error" ? "error" : "ok";
726
893
  if (entry.kind === "complete") return entry.outcome === "error" ? "error" : entry.outcome === "unsatisfied" ? "warn" : "ok";
727
894
  if (entry.outcome === "deny") return "deny";
@@ -730,7 +897,7 @@ function outcomeTagOf(entry) {
730
897
  return "ok";
731
898
  }
732
899
  function renderBody(props, entry) {
733
- return entry.kind === "gate" ? renderGate(props, entry) : entry.kind === "narrow" ? renderNarrow(props, entry) : entry.kind === "complete" ? renderComplete(props, entry) : renderCheck(props, entry);
900
+ return entry.kind === "gate" ? renderGate(props, entry) : entry.kind === "narrow" ? renderNarrow(props, entry) : entry.kind === "complete" ? renderComplete(props, entry) : entry.kind === "checkpoint" ? renderCheckpoint(props, entry) : renderCheck(props, entry);
734
901
  }
735
902
  function RatingButtons({ id, t, ratings, onRate }) {
736
903
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-rate", role: "group", "aria-label": t("rateGroup"), children: [
@@ -1014,7 +1181,10 @@ function SessionPanel({ sessionId, t, useProjection }) {
1014
1181
  ] }),
1015
1182
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { children: [
1016
1183
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { children: t("metrics") }),
1017
- !metrics?.gate && !metrics?.check && !metrics?.narrow && !metrics?.complete ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "decision-empty", role: "status", children: t("empty") }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "decision-cards", children: [
1184
+ !metrics?.gate && !metrics?.check && !metrics?.narrow && !metrics?.complete ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1185
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "decision-empty", role: "status", children: t("empty") }),
1186
+ metrics?.checkpoint && metrics.checkpoint.attempts + metrics.checkpoint.skipped > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "decision-muted", children: t("checkpointDockHint", { count: metrics.checkpoint.attempts + metrics.checkpoint.skipped }) })
1187
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "decision-cards", children: [
1018
1188
  metrics.gate && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("article", { className: "decision-card decision-card-wide", children: [
1019
1189
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("header", { className: "decision-card-head", children: [
1020
1190
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "decision-card-title", children: t("cardGate") }),
@@ -1212,6 +1382,11 @@ function AnalysisView({ sessionId, t }) {
1212
1382
  const profile = analysis?.profile;
1213
1383
  const backtest = analysis?.trendBacktest;
1214
1384
  const annotations = analysis?.annotations;
1385
+ const checkpoint = profile?.checkpoint;
1386
+ const telemetry = profile?.telemetry;
1387
+ const measuredTotal = profile ? profile.gate.attempts + profile.check.attempts + profile.narrow.attempts + profile.complete.attempts + (checkpoint?.attempts ?? 0) : 0;
1388
+ const latency = (value) => typeof value === "number" && Number.isFinite(value) ? t("telemetryMs", { value: Math.round(value * 10) / 10 }) : t("telemetryUnknown");
1389
+ const transport = telemetry && typeof telemetry.transportMeasured === "number" && telemetry.transportMeasured > 0 ? `${telemetry.backendAttempts} \xB7 ${t("telemetryMeasuredCount", { count: telemetry.transportMeasured })}` : t("telemetryUnknown");
1215
1390
  const lowCount = profile?.check.low ?? 0;
1216
1391
  const meanScore = profile && profile.check.scoreCount > 0 ? Math.round(profile.check.scoreSum / profile.check.scoreCount * 10) / 10 : null;
1217
1392
  const completeTotal = profile ? profile.complete.satisfied + profile.complete.unsatisfied + profile.complete.insufficient : 0;
@@ -1276,6 +1451,30 @@ function AnalysisView({ sessionId, t }) {
1276
1451
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("completeUnsatisfied"), value: profile.complete.unsatisfied, pct: share(profile.complete.unsatisfied, completeTotal), tone: "warn" }),
1277
1452
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("completeInsufficient"), value: profile.complete.insufficient, pct: share(profile.complete.insufficient, completeTotal), tone: "muted" })
1278
1453
  ] })
1454
+ ] }),
1455
+ checkpoint && checkpoint.attempts + checkpoint.skipped > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("article", { className: "decision-card", children: [
1456
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CardHead, { title: t("cardCheckpoint"), total: checkpoint.attempts, unit: t("cardTimes") }),
1457
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "decision-card-stats", children: [
1458
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("checkpointOk"), value: Math.max(0, checkpoint.attempts - checkpoint.concern - checkpoint.uncertain - checkpoint.error) }),
1459
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("checkpointConcern"), value: checkpoint.concern, tone: "warn" }),
1460
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("checkpointUncertain"), value: checkpoint.uncertain, tone: "muted" }),
1461
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("logEvalFailed"), value: checkpoint.error, tone: "muted" }),
1462
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("checkpointSkipped"), value: checkpoint.skipped, tone: "muted" })
1463
+ ] }),
1464
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "decision-muted decision-analysis-note", children: t("checkpointProfileNote") })
1465
+ ] }),
1466
+ telemetry && telemetry.measured + telemetry.usageMeasured + (telemetry.transportMeasured ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("article", { className: "decision-card", children: [
1467
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CardHead, { title: t("telemetryTitle"), total: telemetry.measured, unit: t("cardTimes") }),
1468
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "decision-card-stats", children: [
1469
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("telemetryCoverage"), value: measuredTotal > 0 ? `${telemetry.measured}/${measuredTotal}` : t("telemetryUnknown") }),
1470
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("telemetryUsageCoverage"), value: measuredTotal > 0 ? `${telemetry.usageMeasured}/${measuredTotal}` : t("telemetryUnknown") }),
1471
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("telemetryMeanLatency"), value: latency(telemetry.measured > 0 ? telemetry.latencyMsSum / telemetry.measured : null) }),
1472
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("telemetryP95Latency"), value: latency(telemetry.measured > 0 ? telemetry.latencyMsP95 : null) }),
1473
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("telemetryInputTokens"), value: telemetry.usageMeasured > 0 ? telemetry.inputTokens : t("telemetryUnknown") }),
1474
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("telemetryOutputTokens"), value: telemetry.usageMeasured > 0 ? telemetry.outputTokens : t("telemetryUnknown") }),
1475
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("telemetryBackendAttempts"), value: transport })
1476
+ ] }),
1477
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "decision-muted decision-analysis-note", children: t("telemetryNote") })
1279
1478
  ] })
1280
1479
  ] })
1281
1480
  ] }),
@@ -1343,6 +1542,13 @@ var DEFAULT_KEEP_PREFIXES = ["mcp__openviking"];
1343
1542
  var DEFAULT_MAX_CANDIDATES = 20;
1344
1543
  var DEFAULT_THRESHOLD = 0.7;
1345
1544
  var DEFAULT_BATCH_MODE = "auto";
1545
+ var CHECKPOINT_DEFAULTS = { enabled: false, heartbeatSteps: 6, failureThreshold: 3, timeoutMs: 1500 };
1546
+ var checkpointFieldsOf = (settings) => ({
1547
+ enabled: settings?.enabled === true,
1548
+ heartbeatSteps: String(settings?.heartbeatSteps ?? CHECKPOINT_DEFAULTS.heartbeatSteps),
1549
+ failureThreshold: String(settings?.failureThreshold ?? CHECKPOINT_DEFAULTS.failureThreshold),
1550
+ timeoutMs: String(settings?.timeoutMs ?? CHECKPOINT_DEFAULTS.timeoutMs)
1551
+ });
1346
1552
  var DEFAULT_RUBRIC = [
1347
1553
  "\u660E\u663E\u7B54\u975E\u6240\u95EE\uFF0C\u6216\u5B58\u5728\u4E25\u91CD\u9057\u6F0F / \u81EA\u76F8\u77DB\u76FE",
1348
1554
  "\u57FA\u672C\u7B54\u5230\u95EE\u9898\uFF0C\u4F46\u6709\u53EF\u5BDF\u89C9\u7684\u9057\u6F0F\u6216\u4E0D\u4E25\u8C28",
@@ -1446,6 +1652,7 @@ function ConfigForm({ t, fetchFn = fetch }) {
1446
1652
  const [narrowOn, setNarrowOn] = (0, import_react4.useState)(true);
1447
1653
  const [completeOn, setCompleteOn] = (0, import_react4.useState)(true);
1448
1654
  const [completeMode, setCompleteMode] = (0, import_react4.useState)("observe");
1655
+ const [checkpoint, setCheckpoint] = (0, import_react4.useState)(() => checkpointFieldsOf());
1449
1656
  const [key, setKey] = (0, import_react4.useState)("");
1450
1657
  const [busy, setBusy] = (0, import_react4.useState)(false);
1451
1658
  const [advancedDirty, setAdvancedDirty] = (0, import_react4.useState)(false);
@@ -1466,6 +1673,7 @@ function ConfigForm({ t, fetchFn = fetch }) {
1466
1673
  setCheckMode(value.checkSettings?.mode ?? "observe");
1467
1674
  setNarrowMode(value.narrowSettings?.mode ?? "enforce");
1468
1675
  setCompleteMode(value.completeSettings?.mode ?? "observe");
1676
+ setCheckpoint(checkpointFieldsOf(value.checkpointSettings));
1469
1677
  setKeepPrefixes(prefixesText(value.narrowSettings));
1470
1678
  setMaxCandidates(String(maxCandidatesOf(value.narrowSettings)));
1471
1679
  setThreshold(String(thresholdOf(value.narrowSettings)));
@@ -1571,6 +1779,13 @@ function ConfigForm({ t, fetchFn = fetch }) {
1571
1779
  mode: completeMode,
1572
1780
  minConditions: clampNumber(thresholds.minConditions, THRESHOLD_DEFAULTS.minConditions, 2, 20, true),
1573
1781
  minConfidence: clampNumber(thresholds.completeMin, THRESHOLD_DEFAULTS.completeMin, 0, 1)
1782
+ },
1783
+ checkpointSettings: {
1784
+ ...config.checkpointSettings,
1785
+ enabled: checkpoint.enabled,
1786
+ heartbeatSteps: clampNumber(checkpoint.heartbeatSteps, CHECKPOINT_DEFAULTS.heartbeatSteps, 2, 100, true),
1787
+ failureThreshold: clampNumber(checkpoint.failureThreshold, CHECKPOINT_DEFAULTS.failureThreshold, 2, 10, true),
1788
+ timeoutMs: clampNumber(checkpoint.timeoutMs, CHECKPOINT_DEFAULTS.timeoutMs, 100, 1e4, true)
1574
1789
  }
1575
1790
  })
1576
1791
  });
@@ -1579,7 +1794,8 @@ function ConfigForm({ t, fetchFn = fetch }) {
1579
1794
  gateSettings: value.gateSettings ?? previous.gateSettings,
1580
1795
  checkSettings: value.checkSettings ?? previous.checkSettings,
1581
1796
  narrowSettings: value.narrowSettings ?? previous.narrowSettings,
1582
- completeSettings: value.completeSettings ?? previous.completeSettings
1797
+ completeSettings: value.completeSettings ?? previous.completeSettings,
1798
+ checkpointSettings: value.checkpointSettings ?? previous.checkpointSettings
1583
1799
  }));
1584
1800
  applyAdvanced(value);
1585
1801
  setAdvancedDirty(false);
@@ -1990,6 +2206,84 @@ ${url}`)) return;
1990
2206
  ] })
1991
2207
  ] })
1992
2208
  ] }),
2209
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "decision-field-block decision-field-block-wide", children: [
2210
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "decision-subhead", children: t("checkpointSettings") }),
2211
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2212
+ Toggle,
2213
+ {
2214
+ label: t("checkpointEnabled"),
2215
+ checked: checkpoint.enabled,
2216
+ disabled: !canEdit,
2217
+ onChange: (enabled) => {
2218
+ setCheckpoint((previous) => ({ ...previous, enabled }));
2219
+ setAdvancedDirty(true);
2220
+ }
2221
+ }
2222
+ ),
2223
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "decision-muted", children: t("checkpointHint") }),
2224
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "decision-threshold-grid", children: [
2225
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "decision-field decision-field-inline", children: [
2226
+ t("checkpointHeartbeatLabel"),
2227
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2228
+ "input",
2229
+ {
2230
+ className: "decision-narrow-cap",
2231
+ type: "number",
2232
+ min: 2,
2233
+ max: 100,
2234
+ step: 1,
2235
+ value: checkpoint.heartbeatSteps,
2236
+ disabled: !canEdit || !checkpoint.enabled,
2237
+ onChange: (event) => {
2238
+ setCheckpoint((previous) => ({ ...previous, heartbeatSteps: event.target.value }));
2239
+ setAdvancedDirty(true);
2240
+ }
2241
+ }
2242
+ ),
2243
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("small", { className: "decision-muted", children: t("checkpointHeartbeatHint") })
2244
+ ] }),
2245
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "decision-field decision-field-inline", children: [
2246
+ t("checkpointFailureLabel"),
2247
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2248
+ "input",
2249
+ {
2250
+ className: "decision-narrow-cap",
2251
+ type: "number",
2252
+ min: 2,
2253
+ max: 10,
2254
+ step: 1,
2255
+ value: checkpoint.failureThreshold,
2256
+ disabled: !canEdit || !checkpoint.enabled,
2257
+ onChange: (event) => {
2258
+ setCheckpoint((previous) => ({ ...previous, failureThreshold: event.target.value }));
2259
+ setAdvancedDirty(true);
2260
+ }
2261
+ }
2262
+ ),
2263
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("small", { className: "decision-muted", children: t("checkpointFailureHint") })
2264
+ ] }),
2265
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "decision-field decision-field-inline", children: [
2266
+ t("checkpointTimeoutLabel"),
2267
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2268
+ "input",
2269
+ {
2270
+ className: "decision-narrow-cap",
2271
+ type: "number",
2272
+ min: 100,
2273
+ max: 1e4,
2274
+ step: 1,
2275
+ value: checkpoint.timeoutMs,
2276
+ disabled: !canEdit || !checkpoint.enabled,
2277
+ onChange: (event) => {
2278
+ setCheckpoint((previous) => ({ ...previous, timeoutMs: event.target.value }));
2279
+ setAdvancedDirty(true);
2280
+ }
2281
+ }
2282
+ ),
2283
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("small", { className: "decision-muted", children: t("checkpointTimeoutHint") })
2284
+ ] })
2285
+ ] })
2286
+ ] }),
1993
2287
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "decision-field-block decision-field-block-wide", children: [
1994
2288
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "decision-subhead", children: t("advancedRubric") }),
1995
2289
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "decision-field", children: [
@@ -1,6 +1,7 @@
1
- import type { BackendConfig, EvaluateInput, EvaluateResult } from './types.js';
1
+ import type { BackendConfig, EvaluateInput, EvaluateResult, EvaluationTelemetry } from './types.js';
2
2
  export type BackendError = Error & {
3
3
  reason: string;
4
+ telemetry?: EvaluationTelemetry;
4
5
  };
5
6
  export declare function validateRequest(input: unknown): EvaluateInput;
6
7
  export declare function createBackend({ config, fetcher, timeoutMs, retryDelayMs }: {
@@ -9,7 +10,7 @@ export declare function createBackend({ config, fetcher, timeoutMs, retryDelayMs
9
10
  timeoutMs?: number;
10
11
  retryDelayMs?: number;
11
12
  }): {
12
- evaluate(input: EvaluateInput, { signal }?: {
13
+ evaluate(input: EvaluateInput, options?: {
13
14
  signal?: AbortSignal;
14
15
  }): Promise<EvaluateResult>;
15
16
  };