diff --git a/benchmark/tests/test_native_checkpoint.py b/benchmark/tests/test_native_checkpoint.py index 311d9699f6..09dbf8db14 100644 --- a/benchmark/tests/test_native_checkpoint.py +++ b/benchmark/tests/test_native_checkpoint.py @@ -194,6 +194,7 @@ def test_real_native_interrupt_retains_branch_but_does_not_qualify_checkpoint(tm boundary; a successful native fork alone must not loosen that contract. """ import subprocess + from pathlib import Path from loopx.capabilities.benchmark_toolkit.native_codex_goal import StdioNativeGoalTransport workspace = tmp_path / "work" @@ -261,6 +262,9 @@ def wait_terminal(native, session, turn, status): with pytest.raises(ValueError, match="completed final cursor"): native_session_checkpoint(env, source, timeout=15) with native_host(env) as native: + source_rollouts = list((Path(env["CODEX_HOME"]) / "sessions").rglob(f"*{source}.jsonl")) + assert len(source_rollouts) == 1 + original_rollout = source_rollouts[0].read_bytes() fork = native.request("thread/fork", {"threadId": source, "cwd": str(workspace), "lastTurnId": turn, "persistExtendedHistory": True})["thread"]["id"] assert fork != source and history(native, fork) == retained @@ -269,7 +273,17 @@ def wait_terminal(native, session, turn, status): wait_terminal(native, fork, next_turn, "completed") assert (workspace / "continued.txt").read_text() == "continued-candidate\n" assert "retained-candidate" in json.dumps(requests[2]) - assert history(native, source) == retained + assert len(requests) == 4 + # Extended history may hydrate the interrupted command's terminal + # metadata later. The persisted original and all model input must + # remain unchanged; that derived metadata is not a new source Turn. + assert source_rollouts[0].read_bytes() == original_rollout + original = history(native, source) + assert [row["id"] for row in original] == [row["id"] for row in retained] + assert original[:-1] == retained[:-1] + assert original[-1]["status"] == retained[-1]["status"] == "interrupted" + assert [item for item in original[-1]["items"] if item["type"] == "userMessage"] == [ + item for item in retained[-1]["items"] if item["type"] == "userMessage"] @pytest.mark.skipif(not os.environ.get("LOOPX_NATIVE_CHECKPOINT_BIN"), diff --git a/docs/architecture/rfcs/STATUS.md b/docs/architecture/rfcs/STATUS.md index 76b8757020..67a340c3ce 100644 --- a/docs/architecture/rfcs/STATUS.md +++ b/docs/architecture/rfcs/STATUS.md @@ -68,7 +68,7 @@ appendix may keep dated history, but no dated log heading may precede it. | [RFC: Shared Goal Alignment and Governed Amendment Protocol (v0)](shared-goal-alignment-and-governed-amendment-v0.md) | Accepted | none | [2 entries](ledger/shared-goal-alignment-and-governed-amendment-v0/) | | [RFC: LoopX Shared Control-Plane Authority and Pluggable State Providers (v0)](shared-goal-authority-state-provider-v0.md) | Accepted | none | [24 entries](ledger/shared-goal-authority-state-provider-v0/) | | [RFC: Single-Owner Local Daemon (v0)](single-owner-local-daemon-v0.md) | Accepted | none | — | -| [RFC: TypeScript Control-Plane Migration Direction v0](typescript-control-plane-migration-v0.md) | Accepted | none | [16 entries](ledger/typescript-control-plane-migration-v0/) | +| [RFC: TypeScript Control-Plane Migration Direction v0](typescript-control-plane-migration-v0.md) | Accepted | none | [18 entries](ledger/typescript-control-plane-migration-v0/) | ## Superseded (1) diff --git a/docs/architecture/rfcs/STATUS.zh-CN.md b/docs/architecture/rfcs/STATUS.zh-CN.md index 5686b66d77..7059783a1e 100644 --- a/docs/architecture/rfcs/STATUS.zh-CN.md +++ b/docs/architecture/rfcs/STATUS.zh-CN.md @@ -65,7 +65,7 @@ | [RFC:共享 Goal 对齐与受治理 Amendment 协议(v0)](shared-goal-alignment-and-governed-amendment-v0.zh-CN.md) | 已接受 | 无 | [2 条](ledger/shared-goal-alignment-and-governed-amendment-v0/) | | [RFC:LoopX 共享控制面权威与可插拔状态 Provider(v0)](shared-goal-authority-state-provider-v0.zh-CN.md) | 已接受 | 无 | [24 条](ledger/shared-goal-authority-state-provider-v0/) | | [RFC: Single-Owner Local Daemon (v0)](single-owner-local-daemon-v0.md) | 已接受 | none | — | -| [RFC:LoopX 控制面 TypeScript 渐进迁移方向 v0](typescript-control-plane-migration-v0.zh-CN.md) | 已接受 | 无 | [16 条](ledger/typescript-control-plane-migration-v0/) | +| [RFC:LoopX 控制面 TypeScript 渐进迁移方向 v0](typescript-control-plane-migration-v0.zh-CN.md) | 已接受 | 无 | [18 条](ledger/typescript-control-plane-migration-v0/) | ## 已被替代 (1) diff --git a/loopx/control_plane/todos/context_projection.ts b/loopx/control_plane/todos/context_projection.ts index 8a3cb02ffa..95cde9ca0d 100644 --- a/loopx/control_plane/todos/context_projection.ts +++ b/loopx/control_plane/todos/context_projection.ts @@ -11,6 +11,7 @@ import { requireBoolean, requireInteger, requireJsonObject } from "../runtime_de import { canonicalAuthorityJson } from "../coordination/authority_store_codec.ts"; import {compactThinTodoRead, compactTodoReadSummary, compactThinTodoReadSummary} from "./read_presentation.ts"; import {TODO_DECISION_SCOPE_OUTCOME_SCHEMA_VERSION} from "./user_completion_types.ts"; +import {removedTodoContinuationPolicy} from "./legacy_continuation_policy_migration.ts"; // A read lens over canonical records, shared by global and Goal conversations. // These are existing Todo facts, not dependency evaluation or execution grants. @@ -222,7 +223,7 @@ function compactSummaryRecord(item: JsonObject, body: unknown): JsonObject { decision_scope: readScope(row.decision_scope), required_decision_scopes: readScopes(row.required_decision_scopes), decision_outcome: outcome(token("decision_outcome")), decision_scope_outcomes: outcomes, excluded_agents: readExcludedAgents(row.excluded_agents), - removed_continuation_policy: ["primary_review", "review_handoff"].includes(token("removed_continuation_policy").toLowerCase()) ? token("removed_continuation_policy").toLowerCase() : null, + removed_continuation_policy: removedTodoContinuationPolicy(token("removed_continuation_policy")), replan_obligation_id: /^replan-[a-f0-9]{16}$/.test(token("replan_obligation_id")) ? token("replan_obligation_id") : null}; for (const [key, value] of Object.entries(normalized)) { if (value !== null && (!Array.isArray(value) || value.length)) row[key] = value; else delete row[key]; diff --git a/tests/control_plane_ts/authority_store.test.ts b/tests/control_plane_ts/authority_store.test.ts index 4eaec54fd4..f70ab808c7 100644 --- a/tests/control_plane_ts/authority_store.test.ts +++ b/tests/control_plane_ts/authority_store.test.ts @@ -376,12 +376,16 @@ test("concurrent cold reads share only the same exact-byte proof and recover aft const corrupt = JSON.parse(valid); corrupt.committed[0].provider_revision = "corrupt"; await writeFile(store.path, JSON.stringify(corrupt)); assert.ok((await Promise.all(readers.map(s => s.loadAuthority()))).every(r => r.status === "failed")); - assert.equal(CountingStore.validations, 2); + // Only overlapping proofs coalesce. A slower file read may enter after a + // rejection has already been evicted and must then validate again. + const rejectedValidations = CountingStore.validations; + assert.ok(rejectedValidations > 1, "changed corrupt bytes require a new proof"); assert.equal((await readers[0]!.loadAuthority()).status, "failed"); - assert.equal(CountingStore.validations, 3, "a rejected promise must not remain in the in-flight registry"); + assert.equal(CountingStore.validations, rejectedValidations + 1, + "a rejected promise must not remain in the in-flight registry"); await writeFile(store.path, valid + "\n\n"); assert.equal((await readers[0]!.loadAuthority()).status, "loaded"); - assert.equal(CountingStore.validations, 4); + assert.equal(CountingStore.validations, rejectedValidations + 2); }); test("alternating File stores reuse their own exact-byte proofs across handles", async t => {