diff --git a/docs/architecture/rfcs/desktop-execution-frontends-v0.md b/docs/architecture/rfcs/desktop-execution-frontends-v0.md index 61ee8bd1d8..7393192ccf 100644 --- a/docs/architecture/rfcs/desktop-execution-frontends-v0.md +++ b/docs/architecture/rfcs/desktop-execution-frontends-v0.md @@ -452,6 +452,11 @@ disconnected and does not silently launch a managed runtime. ## Mode B: Managed Agent Runtime +The [DSH/Pi assessment](./harness-selection-dsh-pi-v0.md) separates the first +passive event source from managed-runtime selection and defines the remaining +lifecycle, readback and measurement gates. Combined diagnostic CLI readback is +available; the managed panel and supervisor are not delivered by that increment. + ### Product flow The managed desktop path is end to end: diff --git a/docs/architecture/rfcs/desktop-execution-frontends-v0.zh-CN.md b/docs/architecture/rfcs/desktop-execution-frontends-v0.zh-CN.md index 33e8066676..48fae6b85f 100644 --- a/docs/architecture/rfcs/desktop-execution-frontends-v0.zh-CN.md +++ b/docs/architecture/rfcs/desktop-execution-frontends-v0.zh-CN.md @@ -380,6 +380,10 @@ Agent 进程或第二个上游会话。 ## 模式 B:托管 Agent 运行时 +[DSH/Pi 评估](./harness-selection-dsh-pi-v0.zh-CN.md) 区分首个被动事件源与 managed runtime +选型,列出生命周期、读取及测量门槛。诊断 CLI 已支持组合读取,但这个增量不交付 +managed 面板或 supervisor。 + ### 产品流程 托管桌面路径是端到端的: diff --git a/docs/architecture/rfcs/harness-selection-dsh-pi-v0.md b/docs/architecture/rfcs/harness-selection-dsh-pi-v0.md new file mode 100644 index 0000000000..ecdf7e31ab --- /dev/null +++ b/docs/architecture/rfcs/harness-selection-dsh-pi-v0.md @@ -0,0 +1,146 @@ +# DSH / Pi: L1 Observation and Managed Runtime Selection + +Status: evidence-backed implementation assessment, not a runtime promotion. +Scope: the shared goals of [Reliability Diagnostics](./long-running-agent-reliability-diagnostics-governed-delivery-v0.md) +and [Desktop Execution Frontends](./desktop-execution-frontends-v0.md). +[中文](./harness-selection-dsh-pi-v0.zh-CN.md) + +## Decision + +Keep **DSH as the first L1 event source**. Do not infer that DSH is already the +preferred production Mode B runtime. Retain Pi as a managed-runtime candidate. +The first choice minimizes the cost of qualifying an existing passive observer; +the second requires lifecycle, provider, crash-recovery and outcome evidence +that a plugin event fixture cannot supply. No quantitative winner is claimed. + +## Evidence Baseline + +LoopX was inspected at `bf217e1e01bec79f357c9ecbd580cf2dfa73db8b`. +The implementation paths below are repository-relative: + +- `packages/dsh-loopx-plugin/src/observer.ts`: pinned activation, session event + compaction, first-append safety, bounded buffering and flush isolation. +- `loopx/capabilities/reliability_diagnostics/{receipt,projection}.py`: independent + validation, integrity classification and authority-free diagnostic readback. +- `loopx/dsh_goal_mode/turn_host_adapter.py`: a bounded Turn connector, opaque + session lineage, SDK calls and failure translation, not a desktop outer loop. +- `loopx/pi_goal_mode/{loopx-goal.ts,pi-goal-loop-runtime.mjs}`: a visible-host + integration with bindings and continuation behavior; not a passive observer. +- `apps/desktop/loopx-control-plane/src-tauri/src/services.rs`: service process + management must not be mistaken for the complete managed Agent lifecycle. + +Upstream references were inspected on 2026-09-06, pinned independently of the +versions validated by LoopX: + +- [DSH README at d347e703](https://github.com/deepseek-ai/deepseek-harness/blob/d347e703908d0406b7a7ef80e3a0e594d86b2215/README.md): + Cordis/plugin architecture and explicit developer-preview compatibility risk. +- [Pi SDK at 9767ba27](https://github.com/earendil-works/pi/blob/9767ba275f3e9a5ee0f5c5342249b629ab1b2282/packages/coding-agent/docs/sdk.md): + event subscription, session operations and runtime replacement APIs. +- [Pi extensions at 9767ba27](https://github.com/earendil-works/pi/blob/9767ba275f3e9a5ee0f5c5342249b629ab1b2282/packages/coding-agent/docs/extensions.md): + event hooks with context-injection, tool-blocking and result-modification power. + +The historical Pi repository URL now redirects to `earendil-works/pi`; the +inspected SDK uses `@earendil-works/pi-coding-agent`. This is an upgrade-check +input, not permission to replace LoopX's installed package or assume API parity. + +## Comparison by Product Requirement + +| Requirement | DSH evidence | Pi evidence | Selection consequence | +| --- | --- | --- | --- | +| Passive observation | LoopX ships a separate observer entry, three session publication hooks and pre-append rejection | SDK offers `session.subscribe`; extensions also offer interception hooks | DSH has a qualified contract slice; a Pi adapter must choose subscription over intervention and prove isolation | +| Session identity / resume | Existing Turn connector derives session lineage; observer separately requires exact goal/session/run identity | SDK separates AgentSession from AgentSessionRuntime replacement/resume operations | Test identity after restart/fork for each adapter; method availability is not durable recovery proof | +| One bounded attempt | LoopX already has a DSH Turn host with timeout and failure mapping | Existing Pi goal mode includes continuation and pause behavior | Neither native loop may silently become the Desktop scheduler; avoid two outer loops | +| Packaging | Dedicated observer export/bundle and packed smokes exist | Extension discovery is part of SDK resource loading | Verify the actually loaded package/profile, not just source imports; neither boundary is OS isolation | +| Provider profiles | SDK connector/version constraints are explicit | SDK exposes runtime/model construction | Qualify the same route, model, tools and budget; harness choice does not establish provider compatibility | +| Public safety | Producer and Python consumer independently validate; shared counterfactuals exist | Tool/context hooks can expose or change raw content | A Pi observer needs first-append redaction and negative fixtures, not transcript copying | +| Performance | Buffer/count/flush accounting exists; no matched real overhead result established here | Subscription is available; no LoopX observer measurement established here | Reject numeric rankings until identical workloads and revisions are measured | +| Maintenance | DSH upstream explicitly warns of breaking changes; LoopX pins its validated connector surface | Current upstream package/runtime APIs differ from historical integration assumptions | Pin upgrades separately; do not compare an installed DSH against an unqualified latest Pi | + +These are integration-cost and contract observations, not claims that Pi lacks +events or DSH cannot support other models. Both expose control-capable APIs; +passivity is a property of the selected adapter and its loaded dependencies. + +## Data and Authority Flow + +The operator needs to distinguish missing evidence, unhealthy execution and +an invalid observation treatment before deciding what to do: + +```text +native session publication + -> isolated observer: compact, validate, count, append + -> independent ledger validation + -> integrity receipt + diagnostic projection + -> operator presentation only + +canonical eligibility -> Desktop supervisor -> bounded Turn -> validation/writeback +``` + +There is no arrow from diagnostics back to eligibility. `valid` means the +observation contract passed, not that a task succeeded. A stall signal is not +permission to retry. Observer errors must not become worker failures. + +## Implemented Readback Increment + +The existing CLI now supports an explicit combined read: + +```bash +loopx reliability-diagnostics status --goal-id --with-receipt --format json --as-of "$(date -u +%Y-%m-%dT%H:%M:%SZ)" +``` + +The POSIX-shell example evaluates age against the current UTC time. Other +clients must supply a timezone-aware current timestamp. Omit `--as-of` only +for historical replay: it defaults to the last event time, producing zero +last-event age, not a live liveness check. Display the observation and evaluation +times separately; advancing the evaluation clock does not change integrity. + +The receipt and projection derive from the same in-memory ledger reading, +avoiding two CLI calls observing different append states. Omitting the flag +preserves the original response. This does **not** make concurrent file append +atomic: a partial last line remains an invalid-input signal rather than being +silently dropped. The command does not activate an observer, discover a binding, +write a ledger, call a model, or change a Goal/Todo/lease. + +This is an executable readback seam, **not a shipped Mode B panel or supervisor**. +A future panel must bind exact goal/session/run identity, show observation age +and integrity independently of task status, and refuse to label a multi-run or +stale goal ledger as the current session's health. It must remain operator-only, +with no diagnostic input passed into prompts or scheduler decisions. Existing +CLI ledger reads are unbounded; do not put this command on an automatic polling +loop before adding an owner-reviewed read budget/snapshot strategy. + +## Qualification Plan and Stop Conditions + +1. **C0 adapter fidelity:** compare native execution with the managed adapter, + observer disabled. Pin model, route, tool definitions, prompts, environment, + budget, package/adapter revisions and initial session state. Account for all + retries and interruptions. Reject comparisons with unequal treatments. +2. **C1 passive arm:** enable only the observer on that qualified adapter. Record + eligible run identity, persisted/accepted/rejected/drop counts, receipt status, + endpoint/worker-context/scheduler influence and all failed runs. Fixture success + does not establish this gate; non-valid receipt is not eligible C1 evidence. +3. **Overhead:** measure baseline and observer wall time, process CPU, peak RSS, + bytes written, event throughput and flush latency using paired repeated runs. + Report sample count, distributions, uncertainty and warm/cold conditions. + Declare acceptance thresholds before running; no threshold is invented here. +4. **Retention/deletion:** owner chooses maximum age/bytes, active-writer handling, + export/support access, backup scope and delete verification. Dry-run inventory + must precede deletion; never truncate an active ledger to meet a size cap. +5. **Mode B acceptance:** separately exercise start/resume/interrupt/close, + process crash, stale session identity, duplicate completion, timeout and + provider failure in a disposable runtime. Verify one Turn at a time and + canonical validation/writeback before spending quota or requesting another. + +Keep raw logs and credentials owner-local. Public evidence should contain only +generalized methodology, pinned revisions, aggregate results and safe references. +No live model execution or retention deletion is authorized by this document. + +## Delivery Sequence + +This comparison plus combined CLI readback can be reviewed now. A Mode B panel +requires the exact-session read contract and bounded refresh path first; it must +not be a second generic monitoring subsystem. Run C0/C1 and overhead experiments +as separately budgeted work, then submit only reusable fixes and safe evidence. +Implement deletion only after the owner selects the retention profile. Revisit +runtime preference if Pi satisfies the same isolation/lifecycle tests at lower +measured integration and operational cost, or DSH fails them. Do not introduce +L2 advice, retry control or a new scheduler to make an L1 experiment pass. diff --git a/docs/architecture/rfcs/harness-selection-dsh-pi-v0.zh-CN.md b/docs/architecture/rfcs/harness-selection-dsh-pi-v0.zh-CN.md new file mode 100644 index 0000000000..d91cf53bc6 --- /dev/null +++ b/docs/architecture/rfcs/harness-selection-dsh-pi-v0.zh-CN.md @@ -0,0 +1,123 @@ +# DSH / Pi:L1 观察与 Managed Runtime 选型 + +状态:有证据的实现评估,不是运行时晋级声明。 +范围:[Reliability Diagnostics](./long-running-agent-reliability-diagnostics-governed-delivery-v0.zh-CN.md) +与 [Desktop Execution Frontends](./desktop-execution-frontends-v0.zh-CN.md) 的共同目标。 +[English](./harness-selection-dsh-pi-v0.md) + +## 决策 + +保留 **DSH 作为 L1 首个事件源**,但不据此宣布它已成为生产 Mode B 的最终首选。 +Pi 保留为 managed runtime 候选。前者利用已存在的被动 observer 降低验证成本;后者 +必须证明生命周期、provider、崩溃恢复和真实结果,插件事件 fixture 不能替代这些证据。 +本评估不提供缺乏测量依据的评分或性能排名。 + +## 证据基线 + +LoopX 检查基线为 `bf217e1e01bec79f357c9ecbd580cf2dfa73db8b`: + +- `packages/dsh-loopx-plugin/src/observer.ts`:完整身份激活、事件压缩、首次落盘安全、 + 有界 buffer 和 flush 隔离。 +- `loopx/capabilities/reliability_diagnostics/{receipt,projection}.py`:独立验证、 + integrity 分类和无控制权限的诊断输出。 +- `loopx/dsh_goal_mode/turn_host_adapter.py`:有界 Turn、session lineage、SDK 调用和 + 失败映射,并不是完整 Desktop 外循环。 +- `loopx/pi_goal_mode/{loopx-goal.ts,pi-goal-loop-runtime.mjs}`:有绑定及 continuation + 行为的可见宿主集成,不是被动 observer。 +- `apps/desktop/loopx-control-plane/src-tauri/src/services.rs`:已有服务进程管理不等于 + RFC 所要求的完整 managed Agent 生命周期。 + +2026-09-06 独立检查的上游版本,不等同于 LoopX 已验证的安装版本: + +- [DSH d347e703 README](https://github.com/deepseek-ai/deepseek-harness/blob/d347e703908d0406b7a7ef80e3a0e594d86b2215/README.md): + Cordis/plugin 架构,明确处于可能不兼容升级的 developer preview。 +- [Pi 9767ba27 SDK](https://github.com/earendil-works/pi/blob/9767ba275f3e9a5ee0f5c5342249b629ab1b2282/packages/coding-agent/docs/sdk.md): + subscribe、session 操作及 runtime replacement API。 +- [Pi 9767ba27 extensions](https://github.com/earendil-works/pi/blob/9767ba275f3e9a5ee0f5c5342249b629ab1b2282/packages/coding-agent/docs/extensions.md): + 部分 hook 可以注入上下文、阻止工具调用、修改结果。 + +历史 Pi 仓库地址目前跳转至 `earendil-works/pi`,本次 SDK 文档使用 +`@earendil-works/pi-coding-agent`。这是升级时要核对的差异,不是立即替换本地依赖 +或假定新旧 API 兼容的理由。 + +## 按产品要求对比 + +| 要求 | DSH 证据 | Pi 证据 | 对选型的影响 | +| --- | --- | --- | --- | +| 被动观察 | 已有独立 observer entry、三个 session publication hook、首次落盘拒绝 | SDK 提供 subscribe,extensions 还提供干预型 hook | DSH 已有可验证切片;Pi 应优先订阅而非拦截,并证明隔离 | +| 身份与恢复 | Turn connector 派生 lineage;observer 另外要求精确 goal/session/run | SDK 将 AgentSession 与负责 replacement/resume 的 AgentSessionRuntime 分开 | 两边都要测重启、fork 后身份;有 API 不等于恢复可靠 | +| 单次有界执行 | 已有 timeout 和失败映射 | 当前 Pi Goal 集成含 continuation/pause | 不允许 native loop 与 Desktop supervisor 同时充当外循环 | +| 打包 | 独立 export/bundle、packed smokes | SDK resource loading 会发现 extensions | 检查实际加载的包及 profile;二者都不是 OS 进程隔离 | +| Provider | connector 版本与 SDK 约束明确 | SDK 暴露 runtime/model 构造 | 同 route/model/tools/budget 验证,harness 选择不代表 provider 兼容 | +| 数据安全 | producer/consumer 独立校验,共享反事实 | 工具/context hook 可接触和修改原文 | Pi 需补首次落盘安全及负向测试,不能复制 transcript | +| 开销 | 已有 buffer/count/flush 统计,没有本次匹配实测 | 有订阅接口,没有本次 LoopX observer 测量 | 未实测前不作数字排名 | +| 维护 | 上游明确可能 breaking,LoopX connector 有固定验证版本 | 当前包名与 runtime API 不能直接套用旧集成假设 | 两边升级分别固定版本,不拿已安装 DSH 对比未验证最新 Pi | + +这是接入成本和合同差异,不是说 Pi 没有事件,或 DSH 不能使用其他模型。 +两个 harness 都有控制 API;“被动”是具体 adapter 和实际加载依赖的性质。 + +## 数据流与权限 + +用户需要区分“没有证据”“执行有异常”“观察过程不可信”,而不是只得到一个绿灯: + +```text +native session publication + -> isolated observer: compact / validate / count / append + -> independent ledger validation + -> integrity receipt + diagnostic projection + -> 仅供操作者展示 + +canonical eligibility -> Desktop supervisor -> bounded Turn -> validation/writeback +``` + +诊断不得反向进入 eligibility。`valid` 只代表观察合同通过,不代表任务成功;stall +信号不是重试授权,observer 故障也不能被当成 worker 故障。 + +## 本次落地的读取增量 + +```bash +loopx reliability-diagnostics status --goal-id --with-receipt --format json --as-of "$(date -u +%Y-%m-%dT%H:%M:%SZ)" +``` + +上述 POSIX shell 示例使用当前 UTC 时间评估年龄;其它客户端应传入带时区的当前时间。 +仅在历史重放时省略 `--as-of`:默认使用最后事件时间,因此最后事件年龄为零,不能 +作为实时存活检查。分别显示观察时间和评估时间;推进评估时钟不改变 integrity。 + +显式选项让 receipt 与 projection 来自同一次 ledger 读取的内存结果,避免分别执行 +两次 CLI 时观察到不同追加状态。不加选项保持原输出。这不提供文件并发追加的原子 +快照;末尾半行仍按无效输入报告,不能悄悄丢掉。命令不激活 observer、不发现绑定、 +不写 ledger、不调用模型,也不改变 Goal/Todo/lease。 + +这是可执行的读取接口,**不是已交付的 Mode B 面板或 supervisor**。未来面板必须 +绑定精确 goal/session/run,分别展示观察时间、integrity 与任务状态;多 run 或过期 +goal ledger 不得被标成当前 session 健康。输出只供操作者,不得进入 prompt 或调度。 +现有 CLI 全量 ledger 读取没有大小上限,在引入经过评审的读取预算/快照策略之前, +不能直接拿这个命令做自动轮询。 + +## 验收方案与停止条件 + +1. **C0 保真**:比较 native 与 observer 关闭的 managed adapter。固定 model、route、 + tools、prompt、环境、预算、包/adapter 版本及起始 session,计入失败和重试; + treatment 不一致则不采纳比较结果。 +2. **C1 被动观察**:在通过 C0 的 adapter 上只开启 observer。记录完整身份、 + accepted/persisted/rejected/drop 数、receipt、endpoint 和 worker/scheduler influence。 + fixture 通过不等于 C1;非 valid receipt 不作为 eligible C1。 +3. **开销**:成对重复测 baseline/observer 的 wall time、CPU、peak RSS、写入字节、 + 吞吐、flush latency;报告样本数、分布、不确定性、冷/热启动条件。预算及验收 + 阈值在运行前约定,本次不虚构阈值或性能结果。 +4. **保留/删除**:owner 选择最大年龄/字节、活跃 writer 处理、支持访问、备份范围 + 和删除验证。先 dry-run 盘点再删除,不能为满足大小限制截断活跃 ledger。 +5. **Mode B**:在可丢弃 runtime 验证 start/resume/interrupt/close、进程崩溃、过期身份、 + 重复完成、超时及 provider 失败;同一时间一个 Turn,canonical validation/writeback + 通过后才扣 quota 或请求下一 Turn。 + +原始日志、凭据留在 owner-local;公共材料只保留通用方法、固定版本、聚合结果和 +安全引用。本文件不授权真实模型执行或删除现有记录。 + +## 后续交付次序 + +先评审对比结论和 CLI 读取增量;Mode B 面板必须先具备精确 session 读取与有界刷新, +而不是新造一套通用监控。C0/C1 与开销作为单独预算实验,仅把复用修复和安全证据 +提交仓库。删除功能等 retention profile 决定后再做。若 Pi 在相同隔离及生命周期 +验收下具有更低的实测接入/运维成本,或 DSH 无法通过,再调整偏好。 +不得为了让 L1 实验通过而加入 L2 建议、重试权限或新 scheduler。 diff --git a/docs/architecture/rfcs/long-running-agent-reliability-diagnostics-governed-delivery-v0.md b/docs/architecture/rfcs/long-running-agent-reliability-diagnostics-governed-delivery-v0.md index 2de76224da..7529a87597 100644 --- a/docs/architecture/rfcs/long-running-agent-reliability-diagnostics-governed-delivery-v0.md +++ b/docs/architecture/rfcs/long-running-agent-reliability-diagnostics-governed-delivery-v0.md @@ -609,8 +609,9 @@ required before making a stronger commercial claim. read-only `session/event`, `agent/status`, `agent/error`, and `session/disposed` hooks let the observer be proven non-interfering inside an existing packaged boundary. Pi remains the comparison candidate; the - harness-selection evaluation shared with the Desktop Execution Frontends - RFC is a follow-up deliverable and will be recorded here. + [shared harness-selection assessment](./harness-selection-dsh-pi-v0.md) + retains DSH for L1 and keeps Mode B selection conditional on lifecycle and + matched-run evidence; it is not a runtime promotion. 3. Should the first two-to-four-week offer stop at L1 diagnostics by default, or include an optional L2 advisory week before any L3 seam? 4. Which data-retention, deletion, and support profiles belong in the first diff --git a/docs/architecture/rfcs/long-running-agent-reliability-diagnostics-governed-delivery-v0.zh-CN.md b/docs/architecture/rfcs/long-running-agent-reliability-diagnostics-governed-delivery-v0.zh-CN.md index 1df5f918f7..9a5deb3ef3 100644 --- a/docs/architecture/rfcs/long-running-agent-reliability-diagnostics-governed-delivery-v0.zh-CN.md +++ b/docs/architecture/rfcs/long-running-agent-reliability-diagnostics-governed-delivery-v0.zh-CN.md @@ -516,7 +516,8 @@ advantage 与 sustainable delivery evidence。 **已决定(2026-09):DeepSeek Harness(`dsh`)session events。** LoopX 已有 typed `dsh` Turn host 与 same-session plugin,其只读 `session/event`、`agent/status`、`agent/error`、 `session/disposed` hook 让 observer 能在既有打包边界内被证明 non-interfering。Pi 仍是 - 对比候选;与 Desktop Execution Frontends RFC 共享的 harness 选型评估是后续交付物,结论将记录在此。 + 对比候选;[共享选型评估](./harness-selection-dsh-pi-v0.zh-CN.md) 保留 DSH 为 L1 首个来源, + Mode B 的选择仍取决于生命周期与匹配实跑证据,不构成运行时晋级。 3. 第一份两到四周 offer 默认应停在 L1 diagnostic,还是在进入任何 L3 seam 前增加可选 L2 advisory week? 4. 第一份 local/private/BYOC deployment pack 应包含哪些 data-retention、deletion 与 support profile? 5. 第一份 promotion packet 必须使用哪个 benchmark family 与 non-benchmark canary? diff --git a/loopx/capabilities/reliability_diagnostics/README.md b/loopx/capabilities/reliability_diagnostics/README.md index 588d1cc99c..c8700da262 100644 --- a/loopx/capabilities/reliability_diagnostics/README.md +++ b/loopx/capabilities/reliability_diagnostics/README.md @@ -159,9 +159,25 @@ export LOOPX_DSH_SHADOW_OBSERVER_RUN_IDENTITY_JSON='{"worker_id":"","mod loopx reliability-diagnostics receipt --goal-id --format json loopx reliability-diagnostics status --goal-id --format json +loopx reliability-diagnostics status --goal-id --with-receipt --format json --as-of "$(date -u +%Y-%m-%dT%H:%M:%SZ)" loopx reliability-diagnostics ingest --goal-id --input observer.ndjson --format json ``` +For live age/stall evaluation, pass the current timezone-aware `--as-of` as in +the POSIX-shell example. Without it, historical replay uses the last event time +and reports zero last-event age. This does not establish current liveness. + +Explicit `--as-of` values must be ISO-8601 timestamps with `Z` or a UTC offset. +Empty, malformed, or timezone-free values exit with code 2, even when the ledger +is missing, empty, or corrupt; this applies with or without `--with-receipt`. + +`status --with-receipt` returns both existing contracts from one ledger read. +Omit the option to retain projection-only output. It grants no control authority +and does not enable the observer. Concurrent appends are not atomic snapshots; +partial records remain integrity failures. See the +[DSH/Pi assessment](../../../docs/architecture/rfcs/harness-selection-dsh-pi-v0.md) +before wiring this full-ledger CLI into a polling surface. + The ledger lives at `/reliability_diagnostics/.ndjson`; the default runtime root is the same one the rest of LoopX uses and the CLI prints only the relative `ledger_ref`. `ingest` re-validates every line. A diff --git a/loopx/capabilities/reliability_diagnostics/README.zh-CN.md b/loopx/capabilities/reliability_diagnostics/README.zh-CN.md index d934977518..75d1535cd7 100644 --- a/loopx/capabilities/reliability_diagnostics/README.zh-CN.md +++ b/loopx/capabilities/reliability_diagnostics/README.zh-CN.md @@ -136,9 +136,23 @@ export LOOPX_DSH_SHADOW_OBSERVER_RUN_IDENTITY_JSON='{"worker_id":"","mod loopx reliability-diagnostics receipt --goal-id --format json loopx reliability-diagnostics status --goal-id --format json +loopx reliability-diagnostics status --goal-id --with-receipt --format json --as-of "$(date -u +%Y-%m-%dT%H:%M:%SZ)" loopx reliability-diagnostics ingest --goal-id --input observer.ndjson --format json ``` +实时年龄/stall 评估应像上述 POSIX shell 示例一样,向 `--as-of` 传入带时区的当前时间。 +省略时按最后事件时间进行历史重放,最后事件年龄为零,不能证明当前仍存活。 + +显式 `--as-of` 必须是含 `Z` 或 UTC 偏移的 ISO-8601 时间戳。空字符串、格式错误或 +无时区的值均返回退出码 2,即使 ledger 缺失、为空或损坏;是否开启 `--with-receipt` +不影响此校验。 + +`status --with-receipt` 从同一次 ledger 读取返回两个现有合同;省略该选项保留原来的 +projection-only 输出,不授予控制权限,也不激活 observer。并发追加不是原子快照, +末尾半行仍会导致 integrity 失败。接入轮询前应阅读 +[DSH/Pi 评估](../../../docs/architecture/rfcs/harness-selection-dsh-pi-v0.zh-CN.md) +中的全量读取与身份边界。 + ledger 位于 `/reliability_diagnostics/.ndjson`;默认 runtime root 与 LoopX 其它部分一致,CLI 只打印相对的 `ledger_ref`。`ingest` 会重新校验每一行;干净的 ingest 是透明拷贝。任何损坏或被拒绝的输入都会追加持久化 diff --git a/loopx/capabilities/reliability_diagnostics/projection.py b/loopx/capabilities/reliability_diagnostics/projection.py index 311b5dcb7a..b776aa9d3e 100644 --- a/loopx/capabilities/reliability_diagnostics/projection.py +++ b/loopx/capabilities/reliability_diagnostics/projection.py @@ -191,6 +191,15 @@ def build_diagnostic_projection( stall_threshold_ms: int = DEFAULT_STALL_THRESHOLD_MS, repetition_threshold: int = DEFAULT_REPETITION_THRESHOLD, ) -> dict[str, Any]: + if as_of is not None: + message = "as_of must be a timezone-aware ISO-8601 timestamp" + try: + parsed_as_of = parse_observed_at(as_of) + except ValueError as exc: + raise ValueError(message) from exc + if parsed_as_of.utcoffset() is None: + raise ValueError(message) + receipt = build_integrity_receipt(reading) envelopes = reading.ordered_envelopes diff --git a/loopx/cli_commands/reliability_diagnostics.py b/loopx/cli_commands/reliability_diagnostics.py index 0e30177bd5..bb14c9d962 100644 --- a/loopx/cli_commands/reliability_diagnostics.py +++ b/loopx/cli_commands/reliability_diagnostics.py @@ -84,6 +84,11 @@ def register_reliability_diagnostics_commands( status.add_argument( "--as-of", help="Timezone-aware ISO-8601 time used for stall age." ) + status.add_argument( + "--with-receipt", + action="store_true", + help="Include integrity evidence from the same ledger read; grants no control authority.", + ) def _render(payload: dict[str, Any]) -> str: @@ -252,6 +257,8 @@ def handle_reliability_diagnostics_command( payload["projection"] = build_diagnostic_projection( reading, as_of=args.as_of ) + if args.with_receipt: + payload["receipt"] = build_integrity_receipt(reading) except (OSError, ValueError) as exc: print(f"error: {CAPABILITY_ID}: {exc}", file=sys.stderr) return 2 diff --git a/tests/capabilities/test_reliability_diagnostics_readback.py b/tests/capabilities/test_reliability_diagnostics_readback.py new file mode 100644 index 0000000000..05eba0aec4 --- /dev/null +++ b/tests/capabilities/test_reliability_diagnostics_readback.py @@ -0,0 +1,116 @@ +from datetime import datetime +import json +import os +from pathlib import Path +import subprocess +import sys + +import pytest + +from loopx.capabilities.reliability_diagnostics import ( + FIXTURE_GOAL_ID, + append_ledger_records, + ledger_path, + run_dsh_fixture, +) + + +def run_status(runtime, *extra, as_of="2026-09-01T12:02:00+00:00", raw=False): + root = Path(__file__).resolve().parents[2] + result = subprocess.run( + [sys.executable, "-m", "loopx.cli", "--runtime-root", str(runtime), + "--format", "json", "reliability-diagnostics", "status", "--goal-id", + FIXTURE_GOAL_ID, *(["--as-of", as_of] if as_of is not None else []), *extra], + cwd=root, + env={**os.environ, "PYTHONPATH": str(root)}, + capture_output=True, + text=True, + check=not raw, + timeout=30, + ) + return result if raw else json.loads(result.stdout) + + +def test_receipt_opt_in_preserves_projection_and_real_ledger(tmp_path): + path = ledger_path(tmp_path, FIXTURE_GOAL_ID) + append_ledger_records(path, run_dsh_fixture()["ledger_records"]) + before = path.read_bytes() + plain = run_status(tmp_path) + combined = run_status(tmp_path, "--with-receipt") + assert "receipt" not in plain + assert combined["projection"] == plain["projection"] + assert combined["receipt"]["status"] == "degraded" + assert combined["receipt"]["status"] == combined["projection"]["integrity"]["status"] + assert combined["projection"]["authority"] == "none" + assert combined["receipt"]["observation_entered_scheduler_inputs"] is False + assert path.read_bytes() == before + assert str(tmp_path) not in json.dumps(combined) + + +def test_missing_or_corrupt_ledger_never_becomes_healthy(tmp_path): + absent = run_status(tmp_path, "--with-receipt") + assert absent["receipt"]["status"] == "invalid" + path = ledger_path(tmp_path, FIXTURE_GOAL_ID) + assert not path.exists() + path.parent.mkdir(parents=True) + path.write_text("not-json\n", encoding="utf-8") + corrupt = run_status(tmp_path, "--with-receipt") + assert corrupt["receipt"]["status"] == "invalid" + assert corrupt["receipt"]["ledger_invalid_record_count"] == 1 + assert path.read_text(encoding="utf-8") == "not-json\n" + + +def test_live_as_of_detects_silence_without_changing_integrity(tmp_path): + path = ledger_path(tmp_path, FIXTURE_GOAL_ID) + append_ledger_records(path, run_dsh_fixture()["ledger_records"]) + before = path.read_bytes() + historical = run_status(tmp_path, "--with-receipt", as_of=None) + live = run_status(tmp_path, "--with-receipt", as_of="2026-09-06T00:00:00+00:00") + assert historical["projection"]["stall"]["last_event_age_ms"] == 0 + assert historical["projection"]["stall"]["detected"] is False + assert live["projection"]["stall"]["last_event_age_ms"] > 300000 + assert live["projection"]["stall"]["detected"] is True + assert live["receipt"] == historical["receipt"] + assert path.read_bytes() == before + + +@pytest.mark.parametrize("ledger_state", ["missing", "empty", "corrupt", "events"]) +@pytest.mark.parametrize("with_receipt", [False, True]) +@pytest.mark.parametrize("as_of,valid", [ + ("2026-09-06T00:00:00Z", True), + ("2026-09-06T08:00:00+08:00", True), + ("2026-09-06T00:00:00", False), + ("not-a-date", False), + ("", False), +]) +def test_explicit_as_of_validation_is_independent_of_ledger( + tmp_path, ledger_state, with_receipt, as_of, valid, +): + path = ledger_path(tmp_path, FIXTURE_GOAL_ID) + if ledger_state == "events": + append_ledger_records(path, run_dsh_fixture()["ledger_records"]) + elif ledger_state != "missing": + path.parent.mkdir(parents=True) + path.write_text("not-json\n" if ledger_state == "corrupt" else "") + before = path.read_bytes() if path.exists() else None + result = run_status( + tmp_path, *(["--with-receipt"] if with_receipt else []), + as_of=as_of, raw=True, + ) + assert result.returncode == (0 if valid else 2) + assert "Traceback" not in result.stderr + if valid: + payload = json.loads(result.stdout) + assert ("receipt" in payload) is with_receipt + assert payload["projection"]["evidence"]["as_of"] == as_of + if ledger_state == "events": + replay = run_status(tmp_path, as_of=None)["projection"] + observed = datetime.fromisoformat(replay["evidence"]["observed_until"]) + expected_age = int((datetime.fromisoformat("2026-09-06T00:00:00+00:00") - observed).total_seconds() * 1000) + assert payload["projection"]["stall"]["last_event_age_ms"] == expected_age + else: + assert payload["projection"]["integrity"]["status"] == "invalid" + else: + assert "as_of must be a timezone-aware ISO-8601 timestamp" in result.stderr + assert not result.stdout + assert (path.read_bytes() if path.exists() else None) == before