Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions docs/architecture/rfcs/complete-state-recovery-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,57 @@ NoKV or another provider is unsupported until its profile independently proves
the same logical contract. A generic filesystem copy cannot stand in for a
provider profile.

### Capture-owner inventory for the first local profile

The source inventory below targets the first single-Goal File/SQLite profile.
It identifies where M2 must obtain evidence; it does **not** certify that a
particular backup contains every required component. An actual capture must
resolve the selected Goal's routes and dependency closure, distinguish an
owner-confirmed absence from an unreadable or unrecognized source, and retain
unresolved required owners as holds. Optional installation does not make an
active Goal dependency optional.

| Mutable boundary and existing owner | Current capture/readback | Remaining capture or adoption evidence |
| --- | --- | --- |
| Registry routing and machine/Goal configuration: [configuration capture adapter](../../../loopx/capabilities/configuration_backup.py), [typed checkpoint envelope](../../../loopx/control_plane/configuration_backup.ts), [machine configuration store](../../../loopx/capabilities/machine_configuration/store.py) | Raw source registry and discovered routes are copied; a separate configuration envelope is captured later. Its digest verifies its own contents. | Bind the actual source configuration revision to the capture interval and dependent components. A valid envelope does not prove agreement with the earlier raw registry. |
| Goal narrative, vision and authority material references: registry-resolved active state, [vision checkpoint owner](../../../loopx/control_plane/goals/vision_checkpoint.ts) | Discovered active-state paths and files beneath included roots are copied. External referenced material is not recursively resolved. | Preserve the full Goal and revision-bound required artifacts, classify unavailable dependencies, and keep derived Markdown projections subordinate to their owners. |
| Canonical work, leases and retained operation history: [local provider selection](../../../loopx/control_plane/coordination/local_authority_provider.ts), [authority archive owner](../../../loopx/control_plane/coordination/authority_archive.ts), [legacy writer fence](../../../loopx/control_plane/coordination/legacy_writer_fence.ts) | File documents are traversed; SQLite uses the [existing physical snapshot owner](../../../loopx/control_plane/coordination/sqlite_backup.ts). M1 opens existing stores and verifies head/history without selecting a live destination. | Bind provider/store identity, selector, fence, revision and cursor; obtain an owner export or qualified snapshot. Component-local consistency cannot establish cross-owner consistency or make historical leases valid. |
| Quota accounting and Turn settlement: [accounting artifact transaction](../../../loopx/control_plane/quota/accounting_artifact_transaction.ts), [spend owner](../../../loopx/control_plane/quota/spend_commit.ts), [Turn journal commit](../../../loopx/control_plane/turn_driver/turn_journal_effects.ts) | Files under included runtime/project roots are retained; M1 does not reconcile their transaction lineage. | Capture committed receipts, indexes and in-flight journal inputs together with their exact settlement identity; let the original owner resolve replay or ambiguity without a second debit. |
| Goal lifetime and session/execution binding: [source-session lifetime owner](../../../loopx/control_plane/goals/source_session_lifetime.ts), [Chat session lifecycle](../../../loopx/control_plane/goals/chat_session_lifecycle.ts), [execution identity](../../../loopx/control_plane/runtime/execution_identity.ts) | Raw routing/journal bytes may be present. Native Host session stores outside discovered roots are not implicitly captured or adopted. | Resolve the actual entrypoint's owner, lifetime and generation. Prove stale executor exclusion and a qualified session recovery path before the original entrypoint can continue. |
| Pending effects, delivery and derived projection outboxes: [source Turn effect gate](../../../loopx/control_plane/goals/source_session_lifetime.ts), [return-delivery classifier](../../../loopx/control_plane/collaboration/return_delivery.ts), [authority shadow outbox](../../../loopx/control_plane/coordination/local_authority_shadow_outbox.ts) | Included journals/outboxes are retained as bytes; neither their external outcome nor their replay eligibility is inferred. | Each owning effect/transport resolves committed, absent or unknown outcomes and exact receipts. Distinguish a derived projection from an external effect; an unknown external result stays held. |
| Scheduler and optional Host automation: [scheduler state owner](../../../loopx/control_plane/scheduler/state_store.ts), [backup discovery](../../../loopx/state_backup.py) | Runtime scheduler files and explicitly included Codex automation/skill directories can be copied. A saved timer is not an observed active Host schedule. | Bind cadence/reset identity, reconcile actual Host state and exclude duplicate admission. Other Host/provider state needs its own explicit capture contract. |
| Knowledge/artifacts, extension state and credentials: the selected Goal's material and provider owners, including [decision-context recovery](../../../loopx/capabilities/decision_context/capture_recovery.py) where applicable | Only discovered roots are included; a physical SQLite snapshot proves no semantic provider completeness. Credentials may be present in private raw bytes. | Enumerate actual required sources and provider versions; obtain their own export/verify evidence. Credential custody, rotation/revocation and external access remain with their owners; copied bytes grant no access. |

A concrete route-coverage gap is the [source-session lifecycle journal
root](../../../loopx/control_plane/goals/source_session_registry_state.py): it
is adjacent to the source registry under `.loopx/lifecycle/goal-instance`.
Copying an external registry file does not include that sibling directory when
it falls outside every included root. M2 must resolve this owner's actual
storage, rather than infer completeness from the registry file or archive size.

**Observed ordering counterexample.** The real producer copies roots and the raw
registry before constructing `configuration-backup.json`. The File/SQLite
regression in [the recovery suite](../../../tests/test_state_recovery.py)
performs a real Todo transaction and a registry update between those steps.
The resulting archive contains the earlier work/history and raw registry plus
the later configuration; archive/member digests and the configuration check
still pass. After all source routes disappear, M1 correctly reads the earlier
work and returns `held`, `legacy_best_effort`, `legacy_manifest_incomplete` and
no execution authority. The unchanged-source cases remain the positive control.
This demonstrates the boundary of v0 byte verification, not a newly granted
execution path or a qualified `online` profile.

The next implementation slice belongs to the existing backup/recovery workflow:
resolve one Goal's required owner closure, capture configuration and canonical
authority through those owners, and record their source intervals/dependencies
before any profile can be qualified. Reuse the typed authority export/snapshot
and configuration envelope; Python remains the filesystem/transport adapter.
Exercise concurrent writes, a missing required owner and moved dependencies
through the real producer and inert verifier. Other rows remain explicit holds
until their owner evidence exists. M2's complete-inventory entry and
profile-specific exits, M3 admission, and the frozen RPO/RTO criteria stay open;
this inventory neither adds a parallel coordinator nor changes their criteria.

### Implementation ownership and reuse evidence

M3 implementation records a decision-owner matrix before code is admitted. For
Expand Down
41 changes: 41 additions & 0 deletions docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,47 @@ grant。协调器记录这些 receipt,但不能铸造或扩大它们。
identity;Agent 永远不获得数据库访问权。NoKV 或其他 provider 在独立证明同一
逻辑契约前不受支持。通用 filesystem copy 不能代替 provider profile。

### 首个本地 profile 的捕获 owner 清单

下列源码清单面向首个单 Goal File/SQLite profile,标明 M2 应从哪里取得证据,
**不证明**某份备份已经包含全部必要组件。实际捕获必须解析所选 Goal 的 route 与
dependency closure,区分 owner 确认不存在、来源不可读和来源无法识别,并让尚未
解决的必要 owner 保持 hold。可选安装不意味着活跃 Goal 的依赖也是可选的。

| 可变边界与既有 owner | 当前捕获/读回 | 尚缺的捕获或 adoption 证据 |
| --- | --- | --- |
| Registry route 与 machine/Goal 配置:[配置捕获 adapter](../../../loopx/capabilities/configuration_backup.py)、[类型化 checkpoint envelope](../../../loopx/control_plane/configuration_backup.ts)、[machine 配置 store](../../../loopx/capabilities/machine_configuration/store.py) | 先复制原始 source registry 与发现的 route,稍后再捕获独立配置 envelope;其 digest 验证自身内容。 | 将真实配置来源 revision 绑定到捕获 interval 与依赖组件;有效 envelope 不证明它与先前复制的原始 registry 一致。 |
| Goal 正文、vision 与权威材料引用:registry 解析的 active state、[vision checkpoint owner](../../../loopx/control_plane/goals/vision_checkpoint.ts) | 复制发现的 active-state 路径及 included root 内文件,不递归解析外部材料引用。 | 保留完整 Goal 与绑定 revision 的必要 artifact,分类不可用依赖,并让派生 Markdown 投影服从其 owner。 |
| Canonical 工作、租约及保留的 operation history:[本地 provider selection](../../../loopx/control_plane/coordination/local_authority_provider.ts)、[authority archive owner](../../../loopx/control_plane/coordination/authority_archive.ts)、[legacy writer fence](../../../loopx/control_plane/coordination/legacy_writer_fence.ts) | 遍历 File 文档;SQLite 使用[既有 physical snapshot owner](../../../loopx/control_plane/coordination/sqlite_backup.ts)。M1 只打开已有 store 并验证 head/history,不选定 live 目标。 | 绑定 provider/store identity、selector、fence、revision 与 cursor,取得 owner export 或合格 snapshot。组件本地一致性不证明跨 owner 一致性,也不使历史租约有效。 |
| Quota 记账与 Turn settlement:[accounting artifact transaction](../../../loopx/control_plane/quota/accounting_artifact_transaction.ts)、[spend owner](../../../loopx/control_plane/quota/spend_commit.ts)、[Turn journal commit](../../../loopx/control_plane/turn_driver/turn_journal_effects.ts) | 保留 included runtime/project root 内文件;M1 不 reconcile 其 transaction lineage。 | 将已提交 receipt、index 与进行中 journal input 按精确 settlement identity 一起捕获;由原 owner 处置 replay 或歧义,避免第二次扣账。 |
| Goal lifetime 与 session/execution binding:[source-session lifetime owner](../../../loopx/control_plane/goals/source_session_lifetime.ts)、[Chat session lifecycle](../../../loopx/control_plane/goals/chat_session_lifecycle.ts)、[execution identity](../../../loopx/control_plane/runtime/execution_identity.ts) | 原始 route/journal 字节可能存在;发现范围外的原生 Host session store 不会被隐式捕获或采用。 | 解析真实入口的 owner、lifetime 与 generation,在原入口继续之前证明 stale executor 排除与合格 session recovery 路径。 |
| Pending effect、delivery 与派生投影 outbox:[source Turn effect gate](../../../loopx/control_plane/goals/source_session_lifetime.ts)、[return-delivery classifier](../../../loopx/control_plane/collaboration/return_delivery.ts)、[authority shadow outbox](../../../loopx/control_plane/coordination/local_authority_shadow_outbox.ts) | Included journal/outbox 只保留为字节,不推断外部结果或 replay 资格。 | 每个 effect/transport owner 处置 committed、absent 或 unknown 结果及精确 receipt;区分派生投影与外部 effect,未知外部结果保持 hold。 |
| Scheduler 与可选 Host automation:[scheduler state owner](../../../loopx/control_plane/scheduler/state_store.ts)、[备份发现](../../../loopx/state_backup.py) | 可复制 runtime scheduler 文件及显式纳入的 Codex automation/skill 目录;保存的 timer 不是观察到的活跃 Host schedule。 | 绑定 cadence/reset identity,reconcile 真实 Host 状态并排除重复 admission;其他 Host/provider 状态需要独立、显式的捕获契约。 |
| Knowledge/artifact、extension 状态与 credential:所选 Goal 的 material/provider owner,适用时包括 [decision-context recovery](../../../loopx/capabilities/decision_context/capture_recovery.py) | 只纳入发现的 root;physical SQLite snapshot 不证明 provider 语义完整性。私有原始字节可能含 credential。 | 枚举实际必要来源与 provider version,取得各自 export/verify 证据。Credential custody、轮换/撤销及外部访问仍归各自 owner;复制字节不授予访问权。 |

一个具体 route 覆盖缺口是 [source-session lifecycle journal
root](../../../loopx/control_plane/goals/source_session_registry_state.py):它位于
source registry 旁的 `.loopx/lifecycle/goal-instance`。外部 registry 的这一相邻目录
若不在任何 included root 内,复制 registry 文件并不会纳入它。M2 必须解析该 owner
的实际 storage,不能从 registry 文件存在或归档大小推断完整性。

**已观察到的顺序反例。** 真实 producer 在构造 `configuration-backup.json` 前,
已经复制 root 与原始 registry。[恢复测试](../../../tests/test_state_recovery.py)
中的 File/SQLite 回归在两步之间执行真实 Todo transaction 和 registry 更新。
归档因此包含较早的工作/历史与原始 registry,以及较晚的配置;archive/member
digest 和配置验证仍然通过。所有来源 route 消失后,M1 正确读回较早的工作,返回
`held`、`legacy_best_effort`、`legacy_manifest_incomplete`,且不授予执行权。
来源不变的测试保留为正向对照。这证明 v0 字节验证的边界,不代表新增执行路径,
也不代表 `online` profile 合格。

下一实现切片归属既有 backup/recovery workflow:解析一个 Goal 的必要 owner
closure,经各自 owner 捕获配置与 canonical authority,在任何 profile 合格前记录
source interval/dependency。复用类型化 authority export/snapshot 与配置 envelope,
Python 保留 filesystem/transport adapter 角色。通过真实 producer 与惰性 verifier
覆盖并发写入、必要 owner 缺失和 dependency 移动;其他行在取得 owner 证据前显式
保持 hold。M2 完整清单准入及各 profile 出口、M3 admission、冻结的 RPO/RTO 条件
仍开放;本清单不新增平行 coordinator,也不改变这些条件。

### 实现归属与复用证据

M3 实现必须在代码准入前记录 decision-owner matrix。对于每个 Goal identity、
Expand Down
44 changes: 44 additions & 0 deletions tests/test_state_recovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,50 @@ def _rewrite(archive: Path, manifest: Path, extra: tarfile.TarInfo | None = None
manifest.write_text(json.dumps(data))


@pytest.mark.parametrize("provider", ["file", "sqlite"])
def test_valid_backup_with_capture_skew_stays_inert(tmp_path, monkeypatch, provider):
original_add = tarfile.TarFile.add
writes = []

def add_then_write(tar, source, *args, **kwargs):
result = original_add(tar, source, *args, **kwargs)
if kwargs.get("arcname") == "configuration/registry.source.json":
# Real source writes after the earlier store/registry copy, before
# the producer's later configuration capture. Do not mock verifiers.
registry = tmp_path / "registry.json"
code, added = _cli("--registry", registry, "--runtime-root", tmp_path / "runtime",
"todo", "add", "--goal-id", "goal-a", "--role", "agent",
"--claimed-by", "agent-a", "--operation-id", "during-capture",
"--text", "Later work belongs to a later capture")
assert code == 0, added
writes.append(added)
configuration = json.loads(registry.read_text())
configuration["goals"][0]["description"] = "Later configuration"
registry.write_text(json.dumps(configuration))
return result

with monkeypatch.context() as capture:
capture.setattr(tarfile.TarFile, "add", add_then_write)
archive, manifest = _backup(tmp_path, provider)
assert len(writes) == 1
with tarfile.open(archive, "r:gz") as tar:
earlier = json.load(tar.extractfile("configuration/registry.source.json"))
later = json.load(tar.extractfile("configuration-backup.json"))
assert "description" not in earlier["goals"][0]
assert later["data"]["goals"][0]["goal_configuration"]["description"] == "Later configuration"
for name in ("project", "runtime", "registry.json"):
(tmp_path / name).rename(tmp_path / f"unavailable-{name}")
audit = verify_state_backup(archive=archive, manifest=manifest,
destination=tmp_path / "inert", execute=True)
assert audit["manifest_verified"] is True and audit["file_witnesses_verified"] is True
[goal] = [row for row in audit["components"] if row["owner_kind"] == "coordination_authority"]
assert goal["state"] == "readable" and goal["facts"]["todo_count"] == 1
assert goal["facts"]["history_commits"] == 2
assert audit["status"] == "held" and audit["capture_profile"] == "legacy_best_effort"
assert any(row["reason_code"] == "legacy_manifest_incomplete" for row in audit["findings"])
assert audit["activation_eligible"] is False and audit["execution_authority_granted"] is False


@pytest.mark.parametrize("provider", ["file", "sqlite"])
def test_real_backup_recovers_readable_authority_after_original_sources_disappear(tmp_path, provider):
archive, manifest = _backup(tmp_path, provider)
Expand Down
Loading