Skip to content

Commit 3fd2b1e

Browse files
committed
fix(qualification): verify complete historical provider replay
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
1 parent 6498262 commit 3fd2b1e

6 files changed

Lines changed: 141 additions & 19 deletions

File tree

‎docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-28-retirement-cadence.md‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -237,9 +237,25 @@ Scale characterization with 4,101 synthetic Agent Todos still hits the existing
237237
repair for File/SQLite. The repaired contract API can read that collection;
238238
this does not qualify the remaining whole-command payload boundary.
239239

240-
The next B work remains history artifact lookup and remaining public payload/
241-
cold-path costs, preserving file-change freshness, full decision inputs and
242-
corruption rejection. Contract checks and attention now share one request-local, validated canonical
240+
The next B work is SQLite admission on a frozen source/runtime profile: rerun
241+
the existing reference capacity axes, reconcile concurrency/recovery/consumer-lag
242+
evidence, and verify the applicability of retained natural-time soak results.
243+
The comparison runner's former conflict expectation contradicted merged #5169:
244+
an identical historical intent must return its original applied revision/cursor.
245+
The runner now checks that result, independently rejects projection/event/receipt
246+
drift, and walks the complete history before and after retries without retaining
247+
all expected snapshots. A failing invariant prevents report publication; checks
248+
stay outside the unchanged timing windows. This repairs the qualification tool,
249+
not a provider defect or a D2/default pass. #4224 already reports a soak started
250+
on September 14 at `e98191faa`; its final result and applicability to the current
251+
candidate still need evidence. Do not call it unstarted or restart its clock
252+
solely because an unrelated source revision changed.
253+
254+
Last-caller Python decision retirement can proceed independently where the TS
255+
replacement and affected real callers are proven. Whole Markdown writer removal
256+
still requires C's new-Goal/upgrade/recovery exits. Complete consumer metadata,
257+
freshness and decision inputs remain acceptance requirements. Contract checks
258+
and attention now share one request-local, validated canonical
243259
Todo snapshot per runtime/Goal. Standalone checks and subsequent requests read
244260
afresh; lease and projection-writeback reads do not participate. Consumer edits
245261
cannot mutate retained input, and a failed first read cannot recover midway

‎docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-28-retirement-cadence.zh-CN.md‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,18 @@ Todo 写入时的业务校验,也不重审完成/deferred 历史的授权。
183183
仍触及既有 `todo.succession.project` RPC 响应预算;修复后的合同 API 能读取该集合,
184184
不代表剩余整命令包体边界已完成验收。
185185

186-
B 下一步仍是历史 artifact 查找和剩余公共包体/冷路径,保留文件变化 freshness、
187-
完整决策输入及损坏拒绝。合同检查与 attention 现在按 runtime/Goal 共享请求内已校验的完整
186+
B 下一步聚焦冻结 source/runtime profile 下的 SQLite 准入:重新跑已有 reference
187+
容量轴,对齐并发/恢复/consumer lag 证据,并核对保留的自然时间 soak 适用性。
188+
比较 runner 原先要求历史重试返回 conflict,与已合并 #5169 矛盾:相同完整意图应
189+
返回原 applied revision/cursor。现在核对原结果,分别拒绝 projection/event/receipt
190+
漂移,在重试前后分页验证全部历史,不保留所有预期快照。不变量失败就不发布成功
191+
报告;这些检查放在既有计时窗口之外。这修复的是验证工具,不代表 provider 故障、
192+
D2 通过或默认切换。#4224 已报告在 `e98191faa` 上于 9 月 14 日开始 soak,仍需最终
193+
结果及对当前候选的适用性证据,不能称为未开始,也不能仅因无关 source 修订就重启计时。
194+
195+
已有 TS 替代且真实受影响调用方验证完成的 Python 重复决策,可以按最后调用方独立
196+
退役;整条 Markdown writer 删除仍需 C 的新 Goal/升级/恢复出口。消费者完整
197+
metadata、freshness 和决策输入继续验收。合同检查与 attention 现在按 runtime/Goal 共享请求内已校验的完整
188198
canonical Todo 快照。独立检查和下一次请求重新读取;租约与投影写回读取不参与。消费者修改
189199
不会污染保留输入,首次读取失败不会在请求中途恢复成功。这不代表 registry、Markdown、
190200
历史或多个 Goal 之间的原子快照。集成后继续核对安装态消费者,A/C 与 D2

‎docs/reference/sqlite-authority-store.md‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,15 @@ node --experimental-sqlite --experimental-strip-types \
8585
```
8686

8787
The runner creates and removes its own temporary store, checks complete
88-
projections (including Todo metadata), original receipts and reopened state,
89-
and records the source revision, runtime, runner hash and tracked source diff
90-
hash. It does not open a selected live Goal. RSS includes fixture/checking
88+
projections (including Todo metadata), events, original receipts and reopened
89+
state, and records the source revision, runtime, runner hash and tracked source
90+
diff hash. After timing, it verifies that an exact historical retry returns the
91+
original applied revision/cursor, even after later commits; projection-, event-
92+
or receipt-only drift must conflict. The later head, original receipt and entire
93+
paged history must remain unchanged. `historical_replay_and_conflict_checks`
94+
is reported only after these checks pass; a failed check prevents a successful
95+
report. These are storage guarantees, not Goal-instance isolation or permission
96+
to repeat external effects. It does not open a selected live Goal. RSS includes fixture/checking
9197
allocations; File publication bytes are application bytes, not physical disk
9298
writes. Use the existing SQLite capacity runner for WAL traffic and D2 history
9399
sizes. Keep performance experiments separate from concurrent test suites.

‎examples/coordination/local-provider-comparison.ts‎

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {performance} from "node:perf_hooks";
99
import {parseArgs} from "node:util";
1010
import {fileURLToPath} from "node:url";
1111
import type {JsonObject} from "../../loopx/control_plane/effect_program.ts";
12-
import type {AuthorityStore, AuthorityStoreCommit} from "../../loopx/control_plane/coordination/authority_store.ts";
12+
import type {AuthorityStore, AuthorityStoreCommit, AuthorityStoreCommitResult} from "../../loopx/control_plane/coordination/authority_store.ts";
13+
import {canonicalAuthorityBytes} from "../../loopx/control_plane/coordination/authority_store_codec.ts";
1314
import {FileAuthorityStore} from "../../loopx/control_plane/coordination/file_authority_store.ts";
1415
import {SqliteAuthorityStore} from "../../loopx/control_plane/coordination/sqlite_authority_store.ts";
1516
import {sqliteRuntimeIdentity} from "../../loopx/control_plane/coordination/sqlite_runtime.ts";
@@ -84,6 +85,9 @@ async function measure(root: string) {
8485
const finalProjection = projectionAt(count - 1);
8586
let revision: string | null = null;
8687
let first: AuthorityStoreCommit | undefined;
88+
let firstResult: Extract<AuthorityStoreCommitResult, {status: "applied"}> | undefined;
89+
const receiptsAt = (index: number) => [{operation_id: `op-${index}`, index,
90+
metadata: {checked: true, labels: ["synthetic", "保留"]}}];
8791
let filePublicationBytes = 0;
8892
const fileBytes = (): number => readdirSync(root).reduce((sum, name) => sum + statSync(join(root, name)).size, 0);
8993
const timed = async <T>(action: () => Promise<T>, into: number[]): Promise<T> => {
@@ -93,11 +97,11 @@ async function measure(root: string) {
9397
for (let index = 0; index < count; index++) {
9498
const input: AuthorityStoreCommit = {expected_provider_revision: revision, operation_id: `op-${index}`,
9599
next_projection: projectionAt(index), events: [{kind: "observation", index}],
96-
receipts: [{operation_id: `op-${index}`, index, metadata: {checked: true, labels: ["synthetic", "保留"]}}]};
100+
receipts: receiptsAt(index)};
97101
const result = await timed(() => store.commitAuthority(input), commits);
98102
assert.equal(result.status, "applied"); if (result.status !== "applied") throw new Error("commit rejected");
99103
revision = result.provider_revision;
100-
if (index === 0) first = input;
104+
if (index === 0) { first = input; firstResult = result; }
101105
if (values.provider === "file") filePublicationBytes += statSync((store as FileAuthorityStore).path).size;
102106
if ((index + 1) % 128 === 0) process.stderr.write(`${values.provider} ${values.workload}: ${index + 1}/${count}\n`);
103107
}
@@ -109,6 +113,10 @@ async function measure(root: string) {
109113
const receiptIndex = Math.floor(index * (count - 1) / (samples - 1));
110114
const receipt = await timed(() => store.readReceipt(`op-${receiptIndex}`), reads);
111115
assert.equal(receipt.status, "found");
116+
if (receipt.status === "found") {
117+
assert.equal(receipt.cursor, String(receiptIndex + 1));
118+
assert.deepEqual(receipt.receipts, receiptsAt(receiptIndex));
119+
}
112120
const page = await timed(() => store.scanCommitted(String(count - 100), 100), scans);
113121
assert.equal(page.status, "page");
114122
if (page.status === "page") {
@@ -117,8 +125,8 @@ async function measure(root: string) {
117125
const ordinal = count - 100 + offset;
118126
assert.equal(row.operation_id, `op-${ordinal}`);
119127
assert.deepEqual(row.projection, projectionAt(ordinal));
120-
assert.deepEqual(row.receipts, [{operation_id: `op-${ordinal}`, index: ordinal,
121-
metadata: {checked: true, labels: ["synthetic", "保留"]}}]);
128+
assert.deepEqual(row.events, [{kind: "observation", index: ordinal}]);
129+
assert.deepEqual(row.receipts, receiptsAt(ordinal));
122130
}
123131
}
124132
}
@@ -132,13 +140,62 @@ async function measure(root: string) {
132140
cold.push(performance.now() - started); assert.equal(child.status, 0, child.stderr);
133141
assert.deepEqual(JSON.parse(child.stdout).head, finalProjection);
134142
}
135-
const reopened = openStore(root), replay = await reopened.commitAuthority(first!);
136-
assert.equal(replay.status, "conflict"); // Current store contract reconciles via readReceipt.
137-
const original = await reopened.readReceipt(first!.operation_id);
143+
// Qualification is outside the timings. Walk bounded pages without retaining
144+
// N full projections, checking independently generated input and exact history.
145+
const reopened = openStore(root);
146+
const historyDigest = async () => {
147+
const digest = createHash("sha256");
148+
let cursor: string | null = null, checked = 0;
149+
for (;;) {
150+
const page = await reopened.scanCommitted(cursor, 100);
151+
assert.equal(page.status, "page"); if (page.status !== "page") throw new Error("history read rejected");
152+
assert(page.transactions.length > 0);
153+
for (const row of page.transactions) {
154+
assert(checked < count, "history includes an unexpected transaction");
155+
assert.equal(row.operation_id, `op-${checked}`);
156+
assert.equal(row.cursor, String(checked + 1));
157+
assert.deepEqual(row.projection, projectionAt(checked));
158+
assert.deepEqual(row.events, [{kind: "observation", index: checked}]);
159+
assert.deepEqual(row.receipts, receiptsAt(checked));
160+
digest.update(canonicalAuthorityBytes(row)); digest.update("\n"); checked++;
161+
}
162+
if (!page.has_more) break;
163+
assert.equal(page.next_cursor, String(checked));
164+
cursor = page.next_cursor;
165+
}
166+
assert.equal(checked, count);
167+
return digest.digest("hex");
168+
};
169+
assert(first && firstResult);
170+
const before = await reopened.loadAuthority(), original = await reopened.readReceipt(first.operation_id);
171+
assert.equal(before.status, "loaded");
172+
if (before.status === "loaded") {
173+
assert.equal(before.provider_revision, revision); assert.equal(before.cursor, String(count));
174+
assert.deepEqual(before.head, finalProjection);
175+
}
138176
assert.equal(original.status, "found");
139-
if (original.status === "found") assert.deepEqual(original.receipts, first!.receipts);
140-
const after = await reopened.loadAuthority();
141-
assert.equal(after.status, "loaded"); if (after.status === "loaded") assert.equal(after.provider_revision, revision);
177+
if (original.status === "found") {
178+
assert.equal(original.provider_revision, firstResult.provider_revision);
179+
assert.equal(original.cursor, firstResult.cursor); assert.deepEqual(original.receipts, first.receipts);
180+
}
181+
const retainedHistory = await historyDigest();
182+
// An identical historical intent returns its original result despite a stale
183+
// basis. Projection-, event- and receipt-only drift must conflict, not append.
184+
for (const change of ["none", "projection", "events", "receipts"] as const) {
185+
const input = structuredClone(first);
186+
if (change === "projection") input.next_projection.replay_marker = "different";
187+
if (change === "events") input.events = [{kind: "observation", index: -1}];
188+
if (change === "receipts") input.receipts = [{...receiptsAt(0)[0], replay_marker: "different"}];
189+
const replay = await reopened.commitAuthority(input);
190+
if (change === "none") assert.deepEqual(replay, firstResult);
191+
else {
192+
assert.equal(replay.status, "conflict", `${change}-only drift was accepted`);
193+
if (replay.status === "conflict") assert.equal(replay.conflict_kind, "operation_id_exists");
194+
}
195+
assert.deepEqual(await reopened.loadAuthority(), before, `${change} changed the later head`);
196+
assert.deepEqual(await reopened.readReceipt(first.operation_id), original, `${change} changed the original receipt`);
197+
}
198+
assert.equal(await historyDigest(), retainedHistory, "replay attempts changed retained history");
142199
assert.deepEqual(sourceIdentity(), source, "measurement source changed while running");
143200
return {schema_version: "loopx_local_provider_comparison_v0", provider: values.provider, workload: values.workload,
144201
source, node: process.version, sqlite: sqliteRuntimeIdentity(), platform: process.platform, arch: process.arch,
@@ -148,5 +205,6 @@ async function measure(root: string) {
148205
post_fill_rss_bytes: postFillRss,
149206
final_store_bytes: fileBytes(), file_document_publication_bytes: values.provider === "file" ? filePublicationBytes : null,
150207
complete_record_and_receipt_checks: "passed", original_receipt_recovery_after_reopen: "passed",
208+
historical_replay_and_conflict_checks: "passed",
151209
limits: "bounded sequential store experiment; cold process includes module loading, not cold OS cache; RSS includes fixture and verification allocations, not a steady-state qualification; no CLI, concurrent writers, crash, soak or formal D2 qualification; File publication bytes are application bytes, not physical writes; SQLite WAL traffic is measured by the separate capacity runner"};
152210
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import assert from "node:assert/strict";
2+
import {spawnSync} from "node:child_process";
3+
import {mkdir, mkdtemp, readFile, readdir, rm} from "node:fs/promises";
4+
import {tmpdir} from "node:os";
5+
import {join} from "node:path";
6+
import {fileURLToPath} from "node:url";
7+
import test from "node:test";
8+
9+
for (const provider of ["file", "sqlite"]) {
10+
test(`${provider} comparison verifies historical replay after later commits and cleans its store`,
11+
{timeout: 120000}, async t => {
12+
const directory = await mkdtemp(join(tmpdir(), "local-provider-report-"));
13+
t.after(() => rm(directory, {recursive: true, force: true}));
14+
const data = join(directory, "tmp"), output = join(directory, "report.json");
15+
await mkdir(data);
16+
const child = spawnSync(process.execPath, ["--no-warnings", "--experimental-sqlite", "--experimental-strip-types",
17+
fileURLToPath(new URL("../../examples/coordination/local-provider-comparison.ts", import.meta.url)),
18+
"--provider", provider, "--workload", "mixed", "--commits", "128", "--samples", "3", "--output", output],
19+
{encoding: "utf8", timeout: 110000, env: {...process.env, TMPDIR: data, TMP: data, TEMP: data}});
20+
assert.equal(child.status, 0, child.stderr);
21+
const report = JSON.parse(await readFile(output, "utf8"));
22+
assert.equal(report.provider, provider);
23+
assert.equal(report.commits, 128);
24+
assert.equal(report.complete_record_and_receipt_checks, "passed");
25+
assert.equal(report.original_receipt_recovery_after_reopen, "passed");
26+
assert.equal(report.historical_replay_and_conflict_checks, "passed");
27+
assert.equal(report.warm_head.n, 3);
28+
assert.deepEqual(await readdir(data), []);
29+
});
30+
}

‎tsconfig.control-plane.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,10 @@
8888
"loopx/control_plane/work_items/task_lease_acquire_cli.ts",
8989
"examples/nokv-authority-store/live-qualification.ts",
9090
"examples/coordination/sqlite-capacity.ts",
91+
"examples/coordination/local-provider-comparison.ts",
9192
"examples/coordination/sqlite-authority-migration.ts",
9293
"tests/control_plane_ts/sqlite_capacity.test.ts",
94+
"tests/control_plane_ts/local_provider_comparison.test.ts",
9395
"tests/control_plane_ts/effect_program.test.ts",
9496
"tests/control_plane_ts/monitor_metadata.test.ts",
9597
"tests/control_plane_ts/effect_runtime_errors.test.ts",

0 commit comments

Comments
 (0)