Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,13 @@ export function StateRecoverySettings() {
{audit.measurements ? <p>{zh ? "归档字节" : "Archive bytes"}: {audit.measurements.archive_bytes.toLocaleString(locale)} · {zh ? "普通文件字节" : "Regular file bytes"}: {audit.measurements.regular_file_bytes.toLocaleString(locale)}<br />
{zh ? "归档检查与复制" : "Archive checks and copy"}: {audit.measurements.archive_processing_ms.toLocaleString(locale)} ms · {zh ? "记录读取" : "Owner reads"}: {audit.measurements.owner_read_ms.toLocaleString(locale)} ms</p>
: <p>{zh ? "此历史报告未记录数据量和阶段耗时。" : "This historical report has no recorded size or phase durations."}</p>}
<ul>{audit.components.map(row => <li key={row.component_id}><code>{row.component_id}: {row.reason_code}</code></li>)}</ul>
<ul>{audit.components.map(row => <li key={row.component_id}><code>{row.component_id}: {row.reason_code}</code>
{row.owner_kind === "coordination_authority" && row.state === "readable" ? <p>
{typeof row.facts.provider_revision === "string"
? <>{zh ? "备份中的存储检查点" : "Checkpoint in backup"}: <code>{row.facts.provider_revision}</code> · {zh ? "游标" : "Cursor"}: <code>{String(row.facts.cursor)}</code></>
: (zh ? "这份历史报告未记录存储检查点。" : "This historical report did not record a storage checkpoint.")}
</p> : null}
</li>)}</ul>
</details>
</div> : null}
{history ? <div aria-label={zh ? "历史备份检查" : "Previous backup inspections"}>
Expand Down
25 changes: 20 additions & 5 deletions docs/reference/configuration-backup.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,14 +214,29 @@ pending external operation is resumed. This is the M1 inspection path in the
[complete recovery RFC](../architecture/rfcs/complete-state-recovery-v0.md),
not controlled reactivation.

The following public synthetic scene uses a built wheel and the real File
backend. Before this change, the entry only restored configuration snapshots.
The new path reads the saved Goal work after its original source was removed;
it does not register that Goal. The phone view is explicitly a historical report.
Readable File/SQLite Goal components retain the `provider_revision` and `cursor`
read from the captured authority after its complete head/history check. The JSON
audit, CLI summary and the packaged view's existing **Verification details** show
that historical checkpoint, even when the original source is unavailable or has
later commits. Saved-report readback preserves the recorded position; older
reports without it keep the checkpoint unknown. These are positions in the saved
component, not producer capture intervals, a shared consistency point or current
execution authority. Other owners and M2/M3 qualification remain unchanged.

中文:可读 File/SQLite Goal 的报告保留隔离副本实际读取的存储版本和游标,
可在 JSON、CLI 和原有“核对信息”中核对。来源之后的新提交不会成为这份备份的
恢复位置;历史报告重读保留原值,旧报告缺少该信息时仍为未知。这只标识保存组件的
历史检查点,不证明跨 owner 捕获一致性,也不授予继续执行的权限。

These public synthetic scenes use the existing offline owners after source
loss. The configuration and phone references use a built wheel and real File
backend; the current desktop checkpoint view uses the packaged bundle and real
SQLite backend. None registers the saved Goal. The phone view is explicitly a
historical report, and checkpoint identifiers stay behind Verification details.

| Before: configuration only | Inert full-archive inspection | Phone historical readback |
| --- | --- | --- |
| ![Configuration-only entry](images/state-recovery/before-configuration.png) | ![Inert backup audit](images/state-recovery/inert-audit-desktop.png) | ![Historical audit on phone](images/state-recovery/historical-audit-phone.png) |
| ![Configuration-only entry](images/state-recovery/before-configuration.png) | ![Inert backup audit with its captured checkpoint](images/state-recovery/authority-checkpoint.png) | ![Historical audit on phone](images/state-recovery/historical-audit-phone.png) |

Use **Previous inspections** after reloading to read the saved, digest-checked
report. A historical report describes its recorded observation time; reading it
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 13 additions & 5 deletions examples/personal-workspace-browser/configuration-backup.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ source_registry.write_text(json.dumps({'common_runtime_root':str(source_runtime)
projection=build_todo_runtime_shadow_projection(goal_id='recovery-fixture',todos=[{'schema_version':'todo_item_v0','todo_id':'todo_recovery_work','role':'agent','status':'open','done':False,'archive_state':'active','source_section':'Agent Todo','text':'Preserve the original commitment','task_class':'advancement_task'}],handoff_mode='soft_claim')
provider=(pathlib.Path(loopx.__file__).parent/'control_plane/coordination/local_authority_provider.ts').as_uri()
source_provider=os.environ.get('LOOPX_RECOVERY_TEST_PROVIDER','sqlite');assert source_provider in ('file','sqlite')
script='import {selectLocalSqliteAuthority,openLocalAuthorityStore} from '+json.dumps(provider)+';let input="";for await(const c of process.stdin)input+=c;const r=JSON.parse(input);if(r.provider==="sqlite")await selectLocalSqliteAuthority(r.root,r.goal,true);const s=await openLocalAuthorityStore(r.root,r.goal);const x=await s.commitAuthority({expected_provider_revision:null,operation_id:"original-work",events:[],receipts:[],next_projection:r.projection});if(x.status!=="applied")throw new Error(JSON.stringify(x));'
subprocess.run(['node','--no-warnings','--experimental-sqlite','--experimental-strip-types','--input-type=module','-e',script],input=json.dumps({'root':str(source_runtime),'goal':'recovery-fixture','provider':source_provider,'projection':projection}),text=True,check=True,capture_output=True)
script='import {selectLocalSqliteAuthority,openLocalAuthorityStore} from '+json.dumps(provider)+';let input="";for await(const c of process.stdin)input+=c;const r=JSON.parse(input);if(r.provider==="sqlite")await selectLocalSqliteAuthority(r.root,r.goal,true);const s=await openLocalAuthorityStore(r.root,r.goal);const x=await s.commitAuthority({expected_provider_revision:null,operation_id:"original-work",events:[],receipts:[],next_projection:r.projection});if(x.status!=="applied")throw new Error(JSON.stringify(x));console.log(JSON.stringify({provider_revision:x.provider_revision,cursor:x.cursor}));'
source_position=json.loads(subprocess.run(['node','--no-warnings','--experimental-sqlite','--experimental-strip-types','--input-type=module','-e',script],input=json.dumps({'root':str(source_runtime),'goal':'recovery-fixture','provider':source_provider,'projection':projection}),text=True,check=True,capture_output=True).stdout)
saved=execute_state_backup_plan(build_state_backup_plan(project=source_project,runtime_root=source_runtime,registry_path=source_registry,output_dir=r/'source-backup',backup_id='recovery-fixture',include_registry_projects=False,include_automations=False,include_skills=False))
source_project.rename(r/'unavailable-project');source_runtime.rename(r/'unavailable-runtime');source_registry.rename(r/'unavailable-registry.json')
assert not source_project.exists() and not source_runtime.exists() and not source_registry.exists()
s=ChatHTTPServer(('127.0.0.1',0),ChatRequestHandler);s.registry_path=registry;s.runtime_root=runtime;s.verbose=False
print(json.dumps({'port':s.server_address[1],'python':sys.executable,'module_path':loopx.__file__,'archive':saved['archive_path'],'manifest':saved['manifest_path'],'source_provider':source_provider,'original_sources_absent':True}),flush=True);s.serve_forever()
print(json.dumps({'port':s.server_address[1],'python':sys.executable,'module_path':loopx.__file__,'archive':saved['archive_path'],'manifest':saved['manifest_path'],'source_provider':source_provider,'source_position':source_position,'original_sources_absent':True}),flush=True);s.serve_forever()
`;

export const configurationBackupScenario = {
Expand Down Expand Up @@ -128,7 +128,10 @@ export const configurationBackupScenario = {
const verificationDetails = recovery.locator("details");
await verificationDetails.locator("summary").click();
await verificationDetails.getByText("归档字节:", {exact: false}).waitFor();
const recordedMeasurements = await verificationDetails.locator("p").innerText();
const recordedMeasurements = await verificationDetails.locator("p").filter({hasText: "归档字节:"}).innerText();
await verificationDetails.getByText(backend.source_position.provider_revision, {exact: true}).waitFor();
await verificationDetails.getByText(backend.source_position.cursor, {exact: true}).waitFor();
const recordedCheckpoint = await verificationDetails.locator("p").filter({hasText: "备份中的存储检查点"}).innerText();
await verificationDetails.scrollIntoViewIfNeeded();
await page.screenshot({path: resolve(outputDir, "state-recovery-measurements.png"), animations: "disabled"});
await verificationDetails.locator("summary").click();
Expand Down Expand Up @@ -178,8 +181,10 @@ export const configurationBackupScenario = {
await reopened.getByLabel("历史备份检查", {exact: true}).getByRole("button").click();
await reopened.getByText("已读取历史检查记录;尚未重新验证副本。", {exact: true}).waitFor();
await reopened.locator(".personal-recovery-result details summary").click();
assert.equal(await reopened.locator(".personal-recovery-result details p").innerText(), recordedMeasurements,
assert.equal(await reopened.locator(".personal-recovery-result details p").filter({hasText: "归档字节:"}).innerText(), recordedMeasurements,
"historical readback must retain original measured values, not time the reload");
assert.equal(await reopened.locator(".personal-recovery-result details p").filter({hasText: "备份中的存储检查点"}).innerText(), recordedCheckpoint,
"historical readback must retain the captured authority position");
await page.getByRole("button", {name: "语言", exact: true}).click();
await page.getByRole("radio", {name: "English", exact: true}).check();
await page.locator(".personal-settings-tabs").getByRole("button", {name: /Capabilit/}).click();
Expand All @@ -189,6 +194,9 @@ export const configurationBackupScenario = {
await english.getByRole("button", {name: "Previous inspections", exact: true}).click();
await english.getByLabel("Previous backup inspections", {exact: true}).getByRole("button").click();
await english.getByText("Historical inspection loaded; the copy has not been reverified.", {exact: true}).waitFor();
await english.locator(".personal-recovery-result details summary").click();
await english.getByText("Checkpoint in backup:", {exact: false}).waitFor();
await english.getByText(backend.source_position.provider_revision, {exact: true}).waitFor();
await english.getByRole("status").scrollIntoViewIfNeeded();
await page.screenshot({path: resolve(outputDir, "state-recovery-english.png"), animations: "disabled"});
return {coverageEntries: context.coverageEntries, errors: context.errors, backend, verified: `Packaged configuration recovery and full archive inspection; real ${backend.source_provider} head/history after original-source loss, no Goal registration, keyboard, corrupt-input recovery, immutable history reload and narrow viewport`};
Expand Down
1 change: 1 addition & 0 deletions loopx/control_plane/state_recovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ async function inspectGoal(root: string, goalId: string, standaloneSqlite: Set<s
return component(id, "coordination_authority", "readable", "historical_head_and_history_readable",
"review_goal_identity_before_adoption", {...goalFacts, provider: handle.provider,
head_digest: canonicalAuthoritySha256(loaded.head), history_commits: count,
provider_revision: loaded.provider_revision, cursor: loaded.cursor,
todo_count: collection.projection.todo_ids.length,
historical_lease_count: collection.leaseIndex?.lease_todo_ids.length ?? 0}, ["configuration"]);
} catch (error) {
Expand Down
8 changes: 8 additions & 0 deletions loopx/control_plane/state_recovery_contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ export function decodeStateRecoveryAudit(value: unknown): StateRecoveryAudit {
&& Array.isArray(row.dependency_ids) && row.dependency_ids.every(token));
const facts = requireJsonObject(row.facts, "recovery component facts");
requireValue(facts.goal_id === undefined || (typeof facts.goal_id === "string" && recoveryGoalLabel(facts.goal_id) === facts.goal_id));
// Older audits did not retain the inspected position. Never synthesize it
// from an observation time or a commit count on historical readback.
if (facts.provider_revision !== undefined || facts.cursor !== undefined) {
requireValue(row.owner_kind === "coordination_authority" && row.state === "readable"
&& token(facts.provider_revision) && typeof facts.cursor === "string"
&& Number.isSafeInteger(facts.history_commits) && Number(facts.history_commits) > 0
&& facts.cursor === String(facts.history_commits));
}
ids.add(row.component_id);
counts[row.state as RecoveryComponentState]++;
}
Expand Down
5 changes: 5 additions & 0 deletions loopx/control_plane/state_recovery_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,4 +439,9 @@ def render_state_recovery_markdown(payload: dict[str, Any]) -> str:
if isinstance(measurements, dict):
lines.append(f"Archive: {measurements['archive_bytes']} bytes; regular files: {measurements['regular_file_bytes']} bytes; "
f"archive checks/copy: {measurements['archive_processing_ms']} ms; owner reads: {measurements['owner_read_ms']} ms.")
for component in payload.get("components", []):
facts = component["facts"]
if "provider_revision" in facts:
lines.append(f"Captured authority {component['component_id']}: "
f"provider revision {facts['provider_revision']}; cursor {facts['cursor']}.")
return "\n".join(lines)
19 changes: 19 additions & 0 deletions tests/control_plane_ts/state_recovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,22 @@ test("recorded measurements reject invalid values and absent historical measurem
}
}
});

test("historical authority positions stay optional but supplied positions must agree with the inspected history", () => {
const input = audit();
input.components[0] = {...input.components[0], component_id: "goal:fixture",
owner_kind: "coordination_authority", facts: {history_commits: 2}};
assert.doesNotThrow(() => decodeStateRecoveryAudit(input));
const position = {provider_revision: `file:2:${"a".repeat(24)}`, cursor: "2"};
const positioned = (facts: Record<string, unknown>) => ({...input,
components: [{...input.components[0], facts: {history_commits: 2, ...facts}}]});
assert.doesNotThrow(() => decodeStateRecoveryAudit(positioned(position)));
for (const facts of [{provider_revision: position.provider_revision}, {cursor: "2"},
{...position, cursor: "3"}, {...position, cursor: "02"}, {...position, cursor: 2},
{...position, provider_revision: "/private/source"}, {...position, provider_revision: "a".repeat(129)}]) {
assert.throws(() => decodeStateRecoveryAudit(positioned(facts)));
}
const wrongOwner = positioned(position);
wrongOwner.components[0].owner_kind = "configuration";
assert.throws(() => decodeStateRecoveryAudit(wrongOwner));
});
18 changes: 18 additions & 0 deletions tests/test_state_recovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,19 @@ def _rewrite(archive: Path, manifest: Path, extra: tarfile.TarInfo | None = None
@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)
source_args = ("--registry", tmp_path / "registry.json", "--runtime-root", tmp_path / "runtime")
code, captured = _cli(*source_args, "todo", "list", "--goal-id", "goal-a")
assert code == 0, captured
captured_position = captured["authority_read"]
assert captured_position["cursor"] == "2"
# A later real source commit must not become this backup's recovery point.
code, later = _cli(*source_args, "todo", "add", "--goal-id", "goal-a", "--role", "agent",
"--claimed-by", "agent-a", "--operation-id", "after-backup",
"--text", "This later work is outside the captured checkpoint")
assert code == 0, later
code, current = _cli(*source_args, "todo", "list", "--goal-id", "goal-a")
assert code == 0, current
assert current["authority_read"]["provider_revision"] != captured_position["provider_revision"]
(tmp_path / "project").rename(tmp_path / "unavailable-project")
(tmp_path / "runtime").rename(tmp_path / "unavailable-runtime")
(tmp_path / "registry.json").rename(tmp_path / "unavailable-registry.json")
Expand All @@ -98,6 +111,11 @@ def test_real_backup_recovers_readable_authority_after_original_sources_disappea
assert goal["facts"]["todo_count"] == 1
assert goal["facts"]["history_commits"] == 2
assert goal["facts"]["provider"] == provider
assert goal["facts"]["provider_revision"] == captured_position["provider_revision"]
assert goal["facts"]["cursor"] == captured_position["cursor"]
assert captured_position["provider_revision"] in render_state_recovery_markdown(audit)
saved = read_state_recovery_audit(destination)
assert saved["components"] == audit["components"]
captures = json.loads(manifest.read_text())["included"]
state_path = next(row["archive_path"] for row in captures if row["key"] == "registry_active_state:goal-a")
assert "Preserve the full Goal, including unfinished acceptance." in (destination / "data" / state_path).read_text()
Expand Down
Loading