diff --git a/apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.tsx b/apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.tsx
index 6e3b0c6412..ab3541b923 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.tsx
+++ b/apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.tsx
@@ -112,7 +112,13 @@ export function StateRecoverySettings() {
{audit.measurements ?
{zh ? "归档字节" : "Archive bytes"}: {audit.measurements.archive_bytes.toLocaleString(locale)} · {zh ? "普通文件字节" : "Regular file bytes"}: {audit.measurements.regular_file_bytes.toLocaleString(locale)}
{zh ? "归档检查与复制" : "Archive checks and copy"}: {audit.measurements.archive_processing_ms.toLocaleString(locale)} ms · {zh ? "记录读取" : "Owner reads"}: {audit.measurements.owner_read_ms.toLocaleString(locale)} ms
: {zh ? "此历史报告未记录数据量和阶段耗时。" : "This historical report has no recorded size or phase durations."}
}
- {audit.components.map(row => {row.component_id}: {row.reason_code} )}
+
: null}
{history ?
diff --git a/docs/reference/configuration-backup.md b/docs/reference/configuration-backup.md
index 6de9c45516..cccb04497c 100644
--- a/docs/reference/configuration-backup.md
+++ b/docs/reference/configuration-backup.md
@@ -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 |
| --- | --- | --- |
-|  |  |  |
+|  |  |  |
Use **Previous inspections** after reloading to read the saved, digest-checked
report. A historical report describes its recorded observation time; reading it
diff --git a/docs/reference/images/state-recovery/authority-checkpoint.png b/docs/reference/images/state-recovery/authority-checkpoint.png
new file mode 100644
index 0000000000..67f281e5b9
Binary files /dev/null and b/docs/reference/images/state-recovery/authority-checkpoint.png differ
diff --git a/examples/personal-workspace-browser/configuration-backup.mjs b/examples/personal-workspace-browser/configuration-backup.mjs
index a720bad937..57d89eeb11 100644
--- a/examples/personal-workspace-browser/configuration-backup.mjs
+++ b/examples/personal-workspace-browser/configuration-backup.mjs
@@ -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 = {
@@ -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();
@@ -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();
@@ -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`};
diff --git a/loopx/control_plane/state_recovery.ts b/loopx/control_plane/state_recovery.ts
index 89f24d401e..6ea7e4af15 100644
--- a/loopx/control_plane/state_recovery.ts
+++ b/loopx/control_plane/state_recovery.ts
@@ -172,6 +172,7 @@ async function inspectGoal(root: string, goalId: string, standaloneSqlite: Set 0
+ && facts.cursor === String(facts.history_commits));
+ }
ids.add(row.component_id);
counts[row.state as RecoveryComponentState]++;
}
diff --git a/loopx/control_plane/state_recovery_io.py b/loopx/control_plane/state_recovery_io.py
index 60d56f6be9..18883f80b8 100644
--- a/loopx/control_plane/state_recovery_io.py
+++ b/loopx/control_plane/state_recovery_io.py
@@ -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)
diff --git a/tests/control_plane_ts/state_recovery.test.ts b/tests/control_plane_ts/state_recovery.test.ts
index f8ecc23265..36349a281c 100644
--- a/tests/control_plane_ts/state_recovery.test.ts
+++ b/tests/control_plane_ts/state_recovery.test.ts
@@ -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) => ({...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));
+});
diff --git a/tests/test_state_recovery.py b/tests/test_state_recovery.py
index 41dda7c982..3b14d6530c 100644
--- a/tests/test_state_recovery.py
+++ b/tests/test_state_recovery.py
@@ -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")
@@ -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()