Local diagnostic ledger retention rehearsal (v0)¶
English · 中文 · Capability · Owning RFC
English¶
This is an operator-run, offline rehearsal profile, using the existing diagnostic ledger and CLI. It makes export, deletion readback and recovery reproducible before a pilot. It introduces no automatic expiry, retention scheduler, new command, managed backup or production qualification. The P0 exit gate still needs C0/C1 and measured observer overhead. A deployment's data owner must accept its retention period and deletion scope separately.
Declare the boundary before collecting events¶
Keep a private operator note with the exact goal/session and run identity, data owner, permitted storage location, retention deadline (timezone-aware), deletion scope and recovery requirement. Choose a finite period before the pilot; this reference does not invent a universal period. Apply it to positive, null, degraded, quarantined and invalid runs equally. An unresolved integrity failure remains a failure; expiration must not improve a reported denominator.
The envelope/stats allowlist remains the collection boundary. A byte-preserving archive can include corrupt bytes or invalid records already in the source; copying them does not certify public safety. Keep the ledger, readbacks, hash and operator note private. Share only reviewed public-safe aggregates.
| Lifecycle | Required operator evidence |
|---|---|
| Collect | Declared identity and finite retention deadline; default-off observer explicitly enabled for that session |
| Freeze | Observer hooks detached, pending flush settled, and no observer/ingest writer remains for this ledger |
| Export | Whole ledger, SHA-256, fixed-time receipt/projection and pinned installed revision; no filtering of negative records |
| Verify | Same bytes/hash and same receipt/projection from an isolated copy; source still matches the export |
| Delete | Scoped authorization, deadline/hold decision and verified export when recovery is required; missing-ledger readback is invalid |
| Restore | Authorization to retain the data again; same goal, no active writer, absent destination, exclusive creation and identical readback |
| Expire all copies | Account for source, exports, copy runtimes, readback files and external backups; never claim secure erasure from unlink alone |
The deadline and hold decision are manual obligations in this reference, not machine-enforced settings. If automatic TTL, per-session pruning, concurrent rotation, cross-host/BYOC backup or tenancy is required, hold that deployment until its lifecycle owner supplies and qualifies those behaviors.
Stop collection without changing worker authority¶
Unset the three required variables before the next harness launch:
unset LOOPX_DSH_SHADOW_OBSERVER_GOAL_ID
unset LOOPX_DSH_SHADOW_OBSERVER_SESSION_ID
unset LOOPX_DSH_SHADOW_OBSERVER_RUN_IDENTITY_JSON
This leaves the observer row on its feature-off path at the next launch; it does not detach hooks in an already running process. Use the harness owner's approved lifecycle to dispose/detach that observer and wait for its final flush. If this cannot be proven without disrupting an active worker, postpone the offline operation. Do not let a retention operation stop, resume or retry the worker. Unsetting optional variables alone is insufficient.
Export, verify, delete and restore¶
The POSIX recipe below operates on one frozen, owner-authorized ledger.
Set diagnostic_runtime to its explicit runtime root, diagnostic_goal to its
exact goal id and diagnostic_as_of to one timezone-aware replay timestamp.
Set diagnostic_provider_ledger_dir to the resolved ledger directory recorded
in the frozen provider configuration; the current shell environment is not
evidence of what an already running observer used.
Use the same installed loopx revision throughout and record loopx --version.
This v0 recipe supports the canonical layout only:
<runtime-root>/reliability_diagnostics/by-goal/<sha256>.ndjson for new ledgers,
or the original direct child for legacy readback. The provider's
resolved directory must match <runtime-root>/reliability_diagnostics, including
when LOOPX_DSH_SHADOW_OBSERVER_LEDGER_DIR was explicitly set. An arbitrary
custom directory's parent does not supply this mapping: the CLI always inserts
reliability_diagnostics. The preflight below rejects that mismatch before
CLI readback or export. Do not move, re-ingest or delete an unsupported ledger
to make this recipe pass; retain it privately for an owner-approved recovery
path. Do not infer a ledger from the current project or default runtime.
New filenames use the lowercase SHA-256 of the exact UTF-8 Goal id in a separate
by-goal directory. Python and the DSH producer share this mapping, so colon/underscore
and case variants no longer alias; the directory also separates new files from
legacy ids that happen to look like a digest. This is file identity, not a tenant
security boundary. Legacy files may still contain mixed ownership: the check
below refuses mixed, foreign or unparseable records before deletion.
set -eu
: "${diagnostic_runtime:?set the frozen ledger runtime root}"
: "${diagnostic_goal:?set the exact goal id}"
: "${diagnostic_as_of:?set a fixed timezone-aware replay timestamp}"
: "${diagnostic_provider_ledger_dir:?set the frozen provider ledger directory}"
python3 - "$diagnostic_runtime" "$diagnostic_provider_ledger_dir" <<'PY'
import sys
from pathlib import Path
expected = Path(sys.argv[1]).expanduser() / "reliability_diagnostics"
provider = Path(sys.argv[2]).expanduser()
if provider.resolve() != expected.resolve():
raise SystemExit("unsupported provider ledger directory: canonical runtime layout required")
if provider.is_symlink():
raise SystemExit("symlink ledger directory: hold offline operations")
if (provider / "by-goal").is_symlink():
raise SystemExit("symlink ledger namespace: hold offline operations")
PY
umask 077
diagnostic_archive=$(mktemp -d "${TMPDIR:-/tmp}/loopx-diagnostics.XXXXXX")
loopx --version > "$diagnostic_archive/version.txt"
loopx --runtime-root "$diagnostic_runtime" --format json reliability-diagnostics \
status --goal-id "$diagnostic_goal" --with-receipt --as-of "$diagnostic_as_of" \
> "$diagnostic_archive/before.json"
diagnostic_ref=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["ledger_ref"])' \
"$diagnostic_archive/before.json")
diagnostic_ledger="$diagnostic_runtime/$diagnostic_ref"
test ! -L "$diagnostic_ledger" || { printf '%s\n' 'symlink ledger: hold offline operations' >&2; exit 1; }
test -f "$diagnostic_ledger" || { printf '%s\n' 'regular ledger required: hold offline operations' >&2; exit 1; }
python3 - "$diagnostic_ledger" "$diagnostic_goal" <<'PY'
import json, sys
from pathlib import Path
rows = [json.loads(line) for line in Path(sys.argv[1]).read_text().splitlines() if line.strip()]
if not rows or any(not isinstance(row, dict) or row.get("goal_id") != sys.argv[2] for row in rows):
raise SystemExit("ownership uncertain: hold deletion and investigate privately")
PY
diagnostic_copy_runtime="$diagnostic_archive/readback-runtime"
mkdir -p "$(dirname "$diagnostic_copy_runtime/$diagnostic_ref")"
cp "$diagnostic_ledger" "$diagnostic_copy_runtime/$diagnostic_ref"
shasum -a 256 < "$diagnostic_ledger" > "$diagnostic_archive/ledger.sha256"
cmp "$diagnostic_ledger" "$diagnostic_copy_runtime/$diagnostic_ref"
loopx --runtime-root "$diagnostic_copy_runtime" --format json reliability-diagnostics \
status --goal-id "$diagnostic_goal" --with-receipt --as-of "$diagnostic_as_of" \
> "$diagnostic_archive/export.json"
python3 -c 'import json,sys; assert json.load(open(sys.argv[1])) == json.load(open(sys.argv[2])), "export readback changed"' \
"$diagnostic_archive/before.json" "$diagnostic_archive/export.json"
# Continue only after the owner's deletion/hold decision and retention deadline check.
shasum -a 256 < "$diagnostic_ledger" > "$diagnostic_archive/pre-delete.sha256"
cmp "$diagnostic_archive/ledger.sha256" "$diagnostic_archive/pre-delete.sha256"
rm -- "$diagnostic_ledger"
loopx --runtime-root "$diagnostic_runtime" --format json reliability-diagnostics \
receipt --goal-id "$diagnostic_goal" > "$diagnostic_archive/after-delete.json"
python3 - "$diagnostic_archive/after-delete.json" <<'PY'
import json, sys
receipt = json.load(open(sys.argv[1]))["receipt"]
assert receipt["status"] == "invalid" and "no_observations" in receipt["reason_codes"]
assert receipt["persisted_event_count"] == 0
PY
# Optional recovery rehearsal: requires permission to retain the data again.
shasum -a 256 < "$diagnostic_copy_runtime/$diagnostic_ref" > "$diagnostic_archive/pre-restore.sha256"
cmp "$diagnostic_archive/ledger.sha256" "$diagnostic_archive/pre-restore.sha256"
python3 - "$diagnostic_copy_runtime/$diagnostic_ref" "$diagnostic_ledger" <<'PY'
import sys
from pathlib import Path
data = Path(sys.argv[1]).read_bytes()
with Path(sys.argv[2]).open("xb") as destination:
destination.write(data) # refuse any existing destination, including a restarted writer's file
PY
cmp "$diagnostic_ledger" "$diagnostic_copy_runtime/$diagnostic_ref"
loopx --runtime-root "$diagnostic_runtime" --format json reliability-diagnostics \
status --goal-id "$diagnostic_goal" --with-receipt --as-of "$diagnostic_as_of" \
> "$diagnostic_archive/restored.json"
python3 -c 'import json,sys; assert json.load(open(sys.argv[1])) == json.load(open(sys.argv[2])), "restored readback changed"' \
"$diagnostic_archive/before.json" "$diagnostic_archive/restored.json"
For permanent deletion, omit recovery and expire the private archive and every other retained copy under the owner's policy. A retained export makes this a source-file deletion rehearsal, not complete data deletion. These commands do not touch Goal, Todo, quota, gate or worker-session state and do not enable the observer. Keep it disabled until a separately authorized collection resumes.
Do not restore by appending accepted envelopes, resetting sequence numbers,
merging sessions or re-ingesting selected rows. ingest is validation, not a
byte-preserving restore API: it can replace invalid input with a new violation
marker. Copy the entire frozen ledger to an absent destination so stats, gaps,
failure markers and malformed bytes retain their original meaning.
Reproducible validation and remaining evidence¶
# From the source checkout; creates and deletes only its own temporary synthetic state.
uv run --extra test python examples/reliability_diagnostics/ledger-retention-smoke.py
# With a non-editable wheel installed into a disposable environment:
# use that environment's Python; --installed rejects checkout imports.
python examples/reliability_diagnostics/ledger-retention-smoke.py --installed
The smoke executes this literal shell block with the selected interpreter's real CLI against disposable state. It restores degraded and refused-control input ledgers with identical bytes and receipt/projection, including invalid missing-ledger readback after deletion. Provider, namespace and file symlinks, foreign/mixed ownership, malformed input and legacy filename collisions stop before ledger export; source/copy tampering and occupied restore destinations are rejected. Synthetic sibling state stays unchanged. The existing DSH producer's real resolver and file appender also write both canonical and arbitrary custom layouts: the canonical ledger round-trips through the CLI, while an unsupported directory is held before this recipe's operations. This proves offline recovery mechanics; it measures neither observer CPU/RSS/bytes/latency nor actual harness lifecycle or live C0/C1 non-interference. Record the revision, commands, failures/skips, operator stop evidence and policy acceptance in the owning issue before a pilot.
Upgrading a legacy ledger offline¶
Upgrade both the CLI and the DSH plugin, and freeze all old and new writers
before changing files. Ordinary receipt/status reads keep using the original
legacy file and return its actual ledger_ref; they never move or filter it.
ingest exits with code 2 while that legacy filename exists. The DSH observer
counts a flush failure and warns LegacyDiagnosticLedgerError, without changing
worker execution. If both layouts exist, CLI readback also refuses to silently
choose one history. Reconcile that condition offline; do not concatenate files
or re-ingest them merely to clear the error.
For a single-owner legacy ledger:
- Run the ownership and export verification above with frozen writers. Keep the
verified whole-byte archive outside the runtime ledger directory. If any row
is foreign, mixed, unparseable or lacks the exact
goal_id, stop and retain the original for an owner-approved recovery; do not drop negative evidence. - Obtain the new relative destination from the same installed Python package:
python3 -c 'import sys; from loopx.capabilities.reliability_diagnostics import ledger_ref; print(ledger_ref(sys.argv[1]))' "$diagnostic_goal". - Require an absent destination and an unlinked
by-goaldirectory. Create its parent, copy the complete source bytes with exclusive creation (xb), and verify the copy's hash against the retained archive and unchanged source. Do not overwrite an existing destination. A failed copy leaves the original authoritative; preserve incomplete artifacts for reconciliation. - With the verified archive retained and writers still frozen, remove only the
original legacy file. Run
status --with-receiptat the same--as-ofas the export: the receipt and projection must be identical; onlyledger_refchanges. Then allow the upgraded producer to resume.
Rollback requires the same freeze and an absent legacy destination. Restore all current canonical bytes, including any newly appended evidence, to the original filename before removing the canonical file. If the old runtime would collide with another Goal, hold rollback instead of merging their records. Downgrading only the executable is not a data rollback. Keep archives under the original retention policy. No automatic migration, mixed-record repair, or concurrent old/new writer compatibility is provided.
中文¶
这是复用现有 ledger 和 CLI 的人工执行、离线演练方案,让 pilot 之前的导出、删除读回与 恢复可复现。它没有新增自动过期、retention scheduler、命令或托管备份,也不构成生产验收。 P0 exit 仍需 C0/C1 与 observer 开销实测;部署的数据 owner 须另行接受保留期限和删除范围。
收集前在私有操作记录里固定 goal/session/run identity、数据 owner、存储位置、带时区的有限 保留截止时间、删除范围与恢复要求。正面、空结果、degraded、quarantined 和 invalid 运行采用 同一规则;到期删除不能美化已报告的分母。这里的截止时间和 hold 决策是人工义务,并非已实现 的机器配置。自动 TTL、按 session 裁剪、并发轮转、跨 host/BYOC 备份与多租户须由其 lifecycle owner 实现并独立验收,不能从此演练推导。
envelope/stats allowlist 仍是采集边界。完整字节副本可能保留源文件已有的损坏或非法记录, 不能因此宣称 public-safe;ledger、读回、hash 和操作记录均保留在私有存储,只分享审核后的 public-safe 聚合。保留全部失败标记,不筛选“成功”行。
离线步骤和证据与上面的 lifecycle 表及 POSIX recipe 相同:
- 在下一次 harness 启动前 unset 三个必需变量;这不会卸载已运行进程的 hooks。由 harness owner 按已授权生命周期 detach/dispose observer,等待最后 flush,并确认无 observer/ingest writer。若会干扰活跃 worker,则延期;retention 不能取得 stop/resume/retry worker 的权限。
- 明确
diagnostic_runtime、准确的diagnostic_goal和固定带时区diagnostic_as_of,并把 冻结 provider 配置中记录的真实目录填入diagnostic_provider_ledger_dir,记录同一安装版本。 本 v0 只支持 canonical 布局:该目录须与<runtime-root>/reliability_diagnostics对应。 任意 custom directory 的 parent 无法建立映射,因为 CLI 会固定添加reliability_diagnostics; preflight 会在 CLI 读回和导出前拒绝不匹配、provider 或by-goalsymlink directory。保留原件供另行授权的恢复 路径使用,不要移动、重新 ingest 或删除文件来绕过此 hold,也不能依赖当前 shell 的 env、 当前项目或默认路径猜测实际目录。 - 新文件使用精确 Goal id 的 UTF-8 SHA-256 小写摘要,放在独立的
by-goal子目录; 冒号/下划线与大小写不同的 id 不再共用文件。旧文件仍可能混写,使用 recipe 的归属 检查,遇到混合、外来或不可解析行暂停删除。本变化不构成安全租户隔离。 - 完整导出、记录 SHA-256 和固定时间 receipt/projection,在隔离副本里读回并比较;删除前 再比较源 hash。只有 owner 已授权、期限/hold 决策已核实才执行单文件删除。
- 删除后 receipt 必须为
invalid、包含no_observations且 persisted count 为 0。 - 如需恢复演练,先取得再次保留数据的许可,验证副本 hash,并用 exclusive creation 写入 不存在的目标,拒绝覆盖重新启动的 writer 文件;完整字节和 receipt/projection 必须一致。
- 永久删除应跳过恢复,并按 owner policy 处理原件、导出、副本 runtime、读回及外部备份。 留有导出只能称源文件删除演练,unlink 不能证明安全擦除。整个过程不修改 Goal/Todo/quota/ gate/worker session,也不启用 observer;继续采集须另行授权。
不要用追加合法 envelope、重置 sequence、合并 session 或筛选行后 ingest 来恢复。ingest
不是字节保真恢复 API:它可能用新的 violation marker 替代非法输入。恢复整个冻结文件到
不存在的目标,才能保留 stats、缺口、失败标记及损坏字节的原始意义。
上面的两条验证命令在临时合成状态中执行实际 shell block 和所选解释器的真实 CLI。
--installed 要求非 editable 的 wheel 安装并拒绝 checkout import;演练 degraded 和拒绝
控制输入的 invalid ledger,验证导出副本、丢失后的 invalid 证据和恢复前后完全一致。
symlink、foreign/mixed ownership、损坏行和文件名碰撞在导出 ledger 前拒绝;源/副本篡改、
已占用恢复目标也被拒绝,其它合成状态不变。真实 DSH producer 的 resolver 与 file appender
分别写入 canonical 和任意 custom 布局:前者经 CLI 完整读回,后者在 recipe 操作前 hold。它只证明
离线恢复机制,没有测 observer CPU/RSS/bytes/latency,也未验真实 harness 停机或 live C0/C1。
pilot 前须在所属 issue 记录 revision、命令、通过/失败/跳过、停写证据与 policy 接受决定。
旧账本离线升级¶
CLI 与 DSH 插件须一起升级,并冻结所有新旧 writer。只读 receipt/status 保留原文件与
实际 ledger_ref,不搬迁或过滤;存在旧文件时 ingest 返回 2,DSH flush 计数失败并警告
LegacyDiagnosticLedgerError,不改变 worker 行为。两种布局并存时 CLI 也拒绝静默选择历史。
按上方离线升级步骤执行:先验证每行精确归属并在 runtime 外保留完整字节备份;用同版本
ledger_ref 获取新相对路径,在无 symlink 的目录中以 xb 独占创建目的文件,比较备份、
原件、副本 hash。验证成功且 writer 仍冻结后,仅移除旧文件,再用固定 --as-of 比较
receipt/projection(只有 ledger_ref 改变),最后恢复新版 observer。混写、损坏或归属
不明时保留原件,禁止筛选行来消除失败。回退也须停写,保留新增记录,拒绝覆盖与碰撞;
仅降级可执行文件不等于数据回退。不提供自动迁移或新旧 writer 并发兼容。