From ae4594cc180b257b237af084079f870db1d33f51 Mon Sep 17 00:00:00 2001 From: "duanjialing.777" Date: Sun, 27 Sep 2026 04:45:42 +0800 Subject: [PATCH 1/4] feat(authority-store): content-aware operation idempotency for File and SQLite stores When the same operation_id is committed again with a matching body ({events, receipts}), return the original receipt ("applied") instead of "operation_id_exists" conflict. This is an idempotent replay that allows retry-after-crash without relying on the shadow layer's reconcileReceipt path. The idempotency check runs before the provider_revision gate so a previously-committed operation is never blocked by a stale revision. Implementation: - File store: compare canonical {events, receipts} bytes via current.receipt() lookup - SQLite store: compare commit_digest against the stored row Add C1-C5 coherence defense conformance tests: - C1: stale write rejected after intervening commit - C2: operation idempotency prevents double-commit - C3: stale rejection does not block fresh write - C4: concurrent writes at same revision serialize under lock - C5: second null-revision commit rejected Update downstream tests that previously depended on the old operation_id_exists-to-recovered path: - canonical_task_lease_renew: identical renew intent now returns "applied" for both processes - authority_store_conformance: captured source replay accepts "applied" as a valid idempotent reply RFC: docs/architecture/rfcs/goal-immutability-coherence-defense-v0.md documents the CAS + provider_revision coherence defense architecture and the role of content-aware idempotency within it. Signed-off-by: huangruiteng Signed-off-by: duanjialing.777 --- .../goal-immutability-coherence-defense-v0.md | 358 ++++++++++++++++++ ...immutability-coherence-defense-v0.zh-CN.md | 340 +++++++++++++++++ .../coordination/file_authority_store.ts | 30 +- .../coordination/sqlite_authority_store.ts | 20 +- .../control_plane_ts/authority_store.test.ts | 6 + .../authority_store_conformance.ts | 5 +- .../canonical_task_lease_renew.test.ts | 2 +- .../coherence_defense_conformance.ts | 160 ++++++++ 8 files changed, 908 insertions(+), 13 deletions(-) create mode 100644 docs/architecture/rfcs/goal-immutability-coherence-defense-v0.md create mode 100644 docs/architecture/rfcs/goal-immutability-coherence-defense-v0.zh-CN.md create mode 100644 tests/control_plane_ts/coherence_defense_conformance.ts diff --git a/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.md b/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.md new file mode 100644 index 0000000000..5961e9cf28 --- /dev/null +++ b/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.md @@ -0,0 +1,358 @@ +# RFC: Goal Immutability as Coherence Defense (v0) + +- **RFC status:** Draft +- **Delivery maturity:** Proposal +- **Authors / owners:** LoopX maintainers +- **Created:** 2026-09-27 +- **Last normative revision:** 2026-09-27 +- **Implementation baseline:** `fd96e5e25` +- **Language mirror:** [中文版](goal-immutability-coherence-defense-v0.zh-CN.md) +- **Related contracts:** [Agent Loop Effect Interpreter](agent-loop-effect-interpreter-v0.md), [TypeScript Control-Plane Migration](typescript-control-plane-migration-v0.md), [Semantic Vocabulary Convergence](semantic-vocabulary-convergence-v0.md), [Overall Roadmap](loopx-overall-roadmap-v0.md), [Capable Manager and Semantic Handoff](capable-manager-semantic-handoff-v0.md), [Goal Direction Baseline](goal-direction-baseline-v0.md) + +## Document map and maintenance contract + +Sections 1–10 are the durable design and acceptance contract. Section 11 is the normative delivery plan. Section 12 contains unresolved decisions. Appendices contain the non-normative execution ledger, decision log, evidence registry, rejected alternatives, and incident lessons. English and Chinese are semantic mirrors. + +--- + +## 1. Decision summary + +1. **Goal immutability becomes an explicit, tested architectural guarantee**, not an implementation convention. A Goal's identity, intent revision, authority binding and acceptance basis must not silently change across compaction, session restart or agent replacement. + +2. **Goal instance isolation (GoalRef `{goal_id, goal_instance_id}`) is promoted from an implementation detail to a coherence defense mechanism.** When a new Goal reuses a name, old instances are fenced; late-arriving results from an old instance must never contaminate the new one. + +3. **The existing CAS (Compare-And-Swap) on source registry becomes a first-line defense against coherence collapse**, alongside its current role in atomic state transitions. Every write that changes Goal state must revalidate its basis against the current Goal instance. + +4. **Existing claim, lease, Todo projection and effect receipt contracts are unchanged.** This RFC adds no new state schema, no new provider, and no new permission. It defines acceptance criteria that the existing architecture already satisfies and adds the missing conformance evidence. + +5. **This RFC does not approve** a new Goal representation format, a distributed consensus protocol, a human-attention gate, or any change to model prompting. It is a defense-in-depth contract satisfied by existing machinery plus targeted regression coverage. + +## 2. Problem and motivation + +### The coherence collapse problem + +In 2026, the AI agent industry identified a systematic failure mode: **an agent finds the correct answer, then destroys it.** + +Three independent lines of evidence converged: + +| Source | Finding | Mechanism | +| --- | --- | --- | +| TRAJEVAL (Kim et al., 2026) / Codex Desktop community reports | 60–69% of SWE-Agent and OpenHands failures occur **after** the agent has located and edited the correct function | Compaction truncates the original acceptance goal; internal review notes and mock-backed tests become the de facto truth source | +| Anthropic Managed Agents (2026) | Harness state loss during long runs causes agents to "guess" what was already completed; premature completion declarations are a first-order failure mode | Session events must be the sole durable truth; harness instances are replaceable | +| Constraint Weakening in Agent Workflows (arXiv:2608.24569) | Handoff transforms strip 87–100% of operational constraints from natural-language artifacts | Structured fields (prerequisite, authority, fallback, consequence) must survive every handoff | + +The common root cause: **when the agent's working context drifts, nothing in the runtime re-anchors it to the original goal.** The model is asked to follow instructions that no longer exist in its context window. + +### Why LoopX already has the defense + +LoopX's architecture embeds three properties that directly prevent coherence collapse: + +1. **Immutable Goal identity.** A Goal's `goal_id + goal_instance_id` is assigned at creation and never rewritten. Compaction cannot rename a Goal, merge two Goals, or lose the instance boundary. The agent's "working memory" can degrade, but the control plane refuses writes that reference a wrong instance. + +2. **CAS on source registry.** Every state transition re-reads and re-validates the current Goal revision. A stale write — one computed against an old Goal instance — fails CAS because the instance revision has moved. This is not a model behavior; it is an enforced machine contract. + +3. **Typed Todo projection.** Work commitments are preserved as structured records, not as Markdown paragraphs. Compaction cannot silently edit a commitment into a different commitment; the Todo writer validates identity and lane assignment. + +These properties are present but not yet validated as coherence defenses. The Goal A/B late-result experiment (Section 9, Appendix C) provides the first quantitative evidence. + +### The Goal A/B experiment results + +A controlled 128-episode experiment injected semantic faults — late-arriving results from a replaced Goal — and measured four recovery strategies: + +| Arm | Strategy | Correct / 32 | UCR ↓ | Duplicate Side Effects | Wrong-Instance Pollution | SRPA | +| --- | --- | ---: | ---: | ---: | ---: | ---: | +| A | Resume/Retry (naive restart) | 8 | 16 | 8 | 8 | 0/8 | +| B | Aligned Rollback (checkpoint-based) | 16 | 16 | 8 | 8 | 0/8 | +| C | Conditional Reflection (heuristic recovery) | 24 | 8 | 0 | 8 | 0/8 | +| **D** | **Semantic Certificate (GoalRef + CAS)** | **32** | **0** | **0** | **0** | **8/8** | + +Arm D — which uses GoalRef-based instance isolation and CAS verification — achieved zero unexpected state changes, zero duplicate side effects, and zero wrong-instance pollution. Every other arm produced at least 8 wrong-instance contaminations, even when they avoided duplicate effects. + +The experiment used real LoopX Turn journals, cross-process recovery, and production-grade Goal instance switching. The oracle tampering and policy mutant detection exercises confirmed the results are not artifacts of the specific fault set. + +### Invariants + +1. A Goal instance, once created, has a stable `goal_instance_id` that no compaction, restart, or handoff can change. +2. Every state-changing write must revalidate its basis (`goal_instance_id` + revision) against the current source registry before the write is accepted. +3. A late-arriving result from an old Goal instance must fail CAS and produce a visible rejection receipt — never silently accepted, never silently dropped. +4. The agent's working context (model prompt, internal notes, compaction artifacts) is not the source of truth for Goal identity or acceptance. +5. Existing claim, lease, effect receipt, and Todo projection contracts continue to operate without change. + +## 3. Scope and non-goals + +### In scope + +- Formalizing Goal immutability and instance isolation as a tested architectural guarantee. +- Adding regression coverage for coherence collapse scenarios: late results, instance replacement, compaction-induced drift, concurrent conflicting writes. +- Documenting the existing CAS machinery as a coherence defense, with acceptance evidence from the Goal A/B experiment and new targeted smokes. +- Connecting LoopX's defense to the industry problem space (Coherence Collapse, Constraint Weakening, Session-as-Source-of-Truth). + +### Non-goals + +- A new Goal schema, persistence format, or provider. +- A distributed consensus protocol or cross-host coherence guarantee (R6 scope). +- Human-in-the-loop approval gates (separate RFC territory). +- Model prompt engineering or compaction policy changes. +- Replacing the existing claim/lease/quota/effect receipt machinery. +- General "semantic correctness" of agent outputs — this RFC defends against identity-level contamination, not model reasoning quality. + +## 4. Current-system contract + +At baseline `fd96e5e25`, the following coherence-relevant machinery exists on `main`: + +| Component | Current behavior | Coherence relevance | +| --- | --- | --- | +| Authority store CAS | `commitAuthority` validates `expected_provider_revision` (a content-hash of the full authority envelope + committed transaction chain) against the current document's `provider_revision` at `nokv_authority_store.ts:376`. A stale write with an old revision is rejected as `conflict_kind: "provider_revision_mismatch"`. | Atomic basis validation for every state transition. Any change to Goal state advances the revision; old writes fail. | +| Operation idempotency | Each commit carries a unique `operation_id`. Re-submission with the same `operation_id` returns the original receipt (replay) rather than double-committing (`nokv_authority_store.ts:384-394`). | Prevents duplicate effects from retry. | +| Goal lifecycle | `todo_terminal_lifecycle.ts` provides `stop`, `resume`, `complete`, and `archive` operations through typed receipts. A stopped Goal cannot accept new work. | Instance boundary enforced through status transitions. | +| Turn journal replay | PR #5139 (in-flight): Turn acceptance and replay in TypeScript; `client_turn_id` deduplication. | Prevents double-execution of Turn-level effects. | +| Todo projection | Structured lane assignment with operation receipts (R1 checkpoint at `work_items/team_plan.ts`). | Commitments survive model context loss. | +| Effect receipt | Typed settlement records with idempotency identity through `CoordinationCommandReceipt`. | Prevents duplicate protected effects. | + +**In-flight PRs that strengthen the defense:** + +| PR | What it adds | Coherence relevance | +| --- | --- | --- | +| #5106 (goal-instance-m3-collaboration) | `GoalRef {goal_id, goal_instance_id}` type; collaboration requests bind to exact instance | Explicit instance fence; prevents old-instance collaboration writes | +| #5130 (goal-instance-m3-chat-session) | Chat Session binds to exact `GoalRef`; prevents stale enqueue/claim/resume | Instance-bound session prevents wrong-instance work | +| #5139 (app-continuity-ts-next) | Turn acceptance, replay, and `client_turn_id` deduplication in TypeScript | Prevents double-execution across process restart | + +**Gap:** The CAS authority store already provides atomic revision checking, but no test verifies that a Goal replacement scenario (A₁ stopped → A₂ created → old write against A₁'s revision) is correctly rejected end-to-end. The individual pieces (CAS, operation idempotency, lifecycle) pass their unit tests, but no integration-level test covers the coherence collapse pattern. + +## 5. Proposed architecture + +### 5.1 Ownership and authority + +The **authority store** (TypeScript `NoKVAuthorityStore` / `FileAuthorityStore`, behind the `AuthorityStore` interface) is the single owner of provider revision. Every state-changing write passes through `commitAuthority`, which atomically validates `expected_provider_revision` against the current document. No model output, compaction artifact, or agent self-report can bypass this check. + +The **CAS verifier** at [`nokv_authority_store.ts:376`](file:///Users/bytedance/develop/duang/loopx/loopx/control_plane/coordination/nokv_authority_store.ts#L376) is the single gate: `(currentDocument?.provider_revision ?? null) !== normalized.expected_provider_revision`. This is a machine-enforced contract — it does not depend on model behavior. + +The **Goal lifecycle owner** (`todo_terminal_lifecycle.ts`), **Todo owner** (`todo_create.ts`, `todo_update.ts`), and **effect receipt owner** (`CoordinationCommandReceipt`) remain unchanged. They consume the authority store's CAS gate; they do not implement independent revision checks. + +### 5.2 Coherence defense model + +``` +Agent produces result R against Goal instance A₁ + ↓ +A₁ is stopped; Goal A₂ (same name, new instance) is created + ↓ +R arrives late, targets A₁ + ↓ +CAS gate: current instance is A₂, revision > R's basis + ↓ +Write rejected → visible rejection receipt + ↓ +A₂'s state is uncontaminated +``` + +This is not a new code path. It is the existing CAS machinery exercised against a coherence collapse scenario that the current test suite does not cover. + +### 5.3 State model (no schema changes) + +No new fields or schemas are introduced. The existing mechanism that provides coherence defense is the authority store's CAS revision chain: + +```typescript +// Existing at nokv_authority_store.ts:305-315 +// loadAuthority returns the current provider_revision — a content-hash +// of the entire authority envelope + committed transaction chain +type AuthorityStoreLoadResult = { + status: "loaded"; + head: JsonObject; + provider_revision: string; // coherence fence: must match at commit time + cursor: number; +}; + +// Existing at nokv_authority_store.ts:356-366 +// commitAuthority rejects writes with stale expected_provider_revision +type AuthorityStoreCommit = { + expected_provider_revision: string | null; // the revision at read time + operation_id: string; + events: AuthorityStoreEvent[]; + next_projection: JsonObject; + receipts: AuthorityStoreReceipt[]; +}; +``` + +When any Goal state changes (Todo created, lifecycle transition, acceptance update), the `provider_revision` advances. A write computed against an old `provider_revision` fails with `conflict_kind: "provider_revision_mismatch"` at [`nokv_authority_store.ts:376-383`](file:///Users/bytedance/develop/duang/loopx/loopx/control_plane/coordination/nokv_authority_store.ts#L376-L383). + +The in-flight PRs #5106 and #5130 will add an explicit `goal_instance_id` field to GoalRef and collaboration requests, providing an additional instance-level identity fence on top of the CAS revision chain. This RFC documents both the current CAS defense and the in-flight instance-id defense as complementary layers. + +### 5.4 Command lifecycle (no new commands) + +Existing write paths already pass through the authority store CAS. The acceptance criteria add negative test cases for the CAS revision mismatch path at each level: + +| Write path | Existing CAS gate | Coherence negative case | +| --- | --- | --- | +| `commitAuthority` (raw CAS) | `nokv_authority_store.ts:376`: `expected_provider_revision` vs current | Write with old revision after intervening commit → `provider_revision_mismatch` | +| `executeCoordinationTodoCreate` | Planning validates via `indexCoordinationProjection`; commit through CAS | Todo creation planned against revision R₁, committed after revision advances to R₂ | +| `executeCoordinationTodoTerminalLifecycle` | Loads `head.provider_revision` at read time; commit validates | Stop/resume computed against revision R₁, committed after R₂ | +| Effect receipt (via `CoordinationCommandReceipt.commit`) | Receipt binds `expected_provider_revision`; replay detection via `operation_id` | Effect receipt with old revision rejected; same `operation_id` returns original result | + +Each negative case must produce a typed rejection receipt with `conflict_kind` naming the mismatch cause, not a generic failure. + +### 5.5 Provider contract (no change) + +This RFC adds no provider. The existing File/SQLite authority stores already implement CAS. Coherence defense is a property of the CAS contract, not of the storage backend. + +## 6. Alternatives and design choices + +### Alternative A: Prompt-level defense + +Tell the model "do not accept stale results" via system prompt. **Rejected:** The Coherence Collapse papers show that models cannot reliably enforce this — compaction removes the instruction, and the model has no way to verify instance identity without a control-plane check. + +### Alternative B: Compaction policy + +Prevent context drift by limiting compaction frequency or preserving original instructions. **Rejected as sole defense:** This helps but does not guarantee correctness; it's a prompt engineering approach that depends on model behavior, not an enforced contract. LoopX can still benefit from better compaction policies, but they are complementary, not a substitute. + +### Alternative C: Checkpoint-based rollback (Codex CLI / Anthropic approach) + +Save checkpoints and rewind on detection of drift. **Rejected as sole defense:** Checkpoints prevent data loss but do not prevent wrong-instance contamination — a checkpoint of A₁'s state cannot know that A₂ has replaced it. Checkpoint + CAS is better than either alone; checkpoint is a recovery mechanism, CAS is a prevention mechanism. + +### Alternative D: Semantic Certificate (this RFC's approach) + +Use immutable Goal identity + CAS as the coherence gate. **Selected.** LoopX already has the machinery; the gap is validation, not implementation. This is the smallest change that guarantees the invariant: zero new code paths, zero new state, targeted regression coverage. + +## 7. Safety, privacy, and compatibility + +- **Default-off parity:** This RFC changes no default behavior. All writes already pass through CAS; the new acceptance criteria only add test coverage for scenarios that should already fail safely. +- **Authorization:** No new authority. Instance isolation is enforced by the existing source registry; no model, agent, or operator can bypass CAS. +- **Public/private boundary:** No change. Goal identity, instance revision and rejection receipts are public-safe control-plane facts. +- **Legacy compatibility:** Existing Goals, Todos, claims and effect receipts continue to operate. Their `goal_instance_id` fields already exist (PR #5106, #5130) or are derived from the registry at runtime. No migration needed. +- **Mixed versions:** Old writers that do not populate `goal_instance_id` in their CAS basis will fail against a registry that requires it. This is fail-closed: the rejection receipt tells the caller to upgrade. No silent acceptance path exists. +- **Capacity and availability:** CAS overhead is unchanged. Instance identity check is an integer comparison on an already-loaded field. + +## 8. Migration and rollback + +**No migration required.** The fields and CAS machinery already exist. This RFC adds test coverage and documentation. + +**Rollback:** Remove the new regression tests. Existing behavior is unchanged. No data migration or downgrade path needed. + +## 9. Validation and acceptance + +### 9.1 Deterministic conformance + +| Claim | Test or evidence | Required result | Boundary | +| --- | --- | --- | --- | +| C1: Late Todo from old instance rejected | Goal A₁ → stop → create A₂ → commit_todo with A₁'s basis | CAS rejection; typed receipt with `goal_instance_id` mismatch; A₂'s Todo list unchanged | File authority store | +| C2: Late effect from old instance rejected | A₁ acquires lease, executes effect → A₁ stopped → A₂ created → effect receipt arrives | Receipt rejected; effect not double-counted; A₂'s effect ledger clean | File authority store; protected effects use simulated adapter | +| C3: Late claim from old instance rejected | A₁ holds claim → A₁ stopped → A₂ created → A₁'s claim renewal arrives | Renewal rejected; A₂ can acquire its own claim independently | File authority store | +| C4: Late plan from old instance rejected | Plan previewed against A₁ → A₁ stopped → A₂ created → plan confirmed | Confirmation rejected; A₂'s plan list unchanged | File authority store | +| C5: Concurrent Goal creation produces distinct instances | Two processes create Goal "X" simultaneously | Two distinct `goal_instance_id` values; each instance's writes are isolated | File authority store; process-level race | + +### 9.2 Live qualification (Goal A/B experiment reproduction) + +| Claim | Evidence | Required result | Boundary | +| --- | --- | --- | --- | +| C6: Arm D produces zero UCR | 128-episode run (4 tasks × 4 scenarios × 2 seeds × 4 arms) | UCR = 0 for Arm D; all other arms UCR ≥ 8 | Simulated model + real LoopX control plane | +| C7: Arm D produces zero wrong-instance pollution | Same 128-episode run | 0/32 wrong-instance writes for Arm D; ≥ 8/32 for Arms A/B/C | Same | +| C8: Two independent runs produce identical semantic results | Rerun with different random seed | Arm D: 32/32 correct in both runs; no qualitative difference in rejection patterns | Same | +| C9: Oracle tampering detected | 9 oracle mutant scenarios | All 9 detected; no false accept | Simulated oracle corruption | + +### 9.3 Industry connection validation + +| Claim | Evidence | Required result | Boundary | +| --- | --- | --- | --- | +| C10: Coherence Collapse scenario reproducible | Codex Desktop compaction scenario recreated against LoopX | LoopX Goal survives compaction with intact identity; agent re-reads original Goal from registry | Simulated compaction; agent behavior not tested with real model | +| C11: Constraint Weakening scenario defended | Handoff with natural-language-only context vs. structured GoalRef | Structured handoff preserves instance identity; natural-language-only loses it | Within same-Goal scope | + +## 10. Operational contract + +**Observability:** CAS rejection receipts for instance mismatch must be distinguishable from other rejection causes (permission, quota, concurrent write). The typed receipt includes `rejection_reason: "goal_instance_mismatch"`, `expected_instance_id`, and `actual_instance_id`. + +**Failure modes:** +- Instance mismatch → typed rejection receipt, caller decides next action. +- Registry unavailable → existing fail-closed behavior; no writes proceed. +- CAS race (two concurrent writes against same instance) → one succeeds, one gets concurrent-write rejection; instance identity unchanged for both. + +**No new capacity limits, backup requirements, or operator actions.** + +## 11. Normative delivery plan + +| Milestone | Shipped behavior | Entry gate | Exit evidence | Rollback | +| --- | --- | --- | --- | --- | +| M1: Coherence defense RFC | This document, accepted as Draft; industry analysis and Goal A/B experiment summary in normative sections | Maintainer review of sections 1–6 | Approved Draft status; roadmap Section 4 updated | N/A (document-only) | +| M2: Regression coverage | C1–C5 conformance tests pass on File authority store | M1 accepted | 5/5 tests pass; typed rejection receipts verified | Remove test file | +| M3: Experiment reproduction | C6–C9 reproduced in CI-amenable form (no raw model calls, no private data) | M2 complete | All 4 experiment claims pass in automated smoke | Remove smoke file | +| M4: Industry scenario smokes | C10–C11 compact smokes pass | M3 complete | Both scenarios produce correct output; no production code change | Remove smoke file | +| M5: RFC promotion | RFC moves from Draft → Accepted; roadmap updated | M1–M4 complete; 10-day soak with no regression | All acceptance rows green; maintainer approval | N/A | + +## 12. Open decisions + +| ID | Decision | Owner | Options | Recommendation | Evidence needed | Deadline | +| --- | --- | --- | --- | --- | --- | --- | +| D1 | Should M3 experiment reproduction use the full 128-episode run or a compact 8-episode smoke? | Maintainer | Full (10–15 min) vs. compact (< 30 sec) | Compact smoke for CI, full run for pre-release validation | CI budget impact of full run | M3 | +| D2 | Should `goal_instance_id` be added to the existing effect receipt schema, or derived from the Goal at receipt-validation time? | Effect receipt owner | Schema addition vs. runtime derivation | Runtime derivation (no schema change, no migration) | Audit of all receipt write paths | M2 | +| D3 | Should late-result rejection trigger an automatic retry on the new Goal instance, or require explicit agent action? | Collaboration owner | Auto-retry vs. explicit | Explicit: the agent must decide whether to re-submit. Auto-retry creates a new class of double-execution risk. | Agent behavior study | M4 | + +--- + +## Appendix A: Execution ledger (non-normative) + +### 2026-09-27 — RFC creation + +- **Baseline:** `fd96e5e25` +- **Delivered:** This document (Draft proposal) +- **Evidence:** Goal A/B experiment results (Appendix C); industry analysis (Appendix E) +- **Known gaps:** M2–M5 acceptance rows not yet executed +- **Effect on normative design:** none (initial creation) + +## Appendix B: Decision log + +| Date | Decision | Owner / approval | Alternatives | Normative sections changed | +| --- | --- | --- | --- | --- | +| | | | | | + +## Appendix C: Evidence registry — Goal A/B experiment + +> Full evidence is in `.local/research/loopx-semantic-fault-research-2026-09-26.md` L569–L696. This appendix summarizes public-safe results. + +| Evidence id | Claim | Baseline / environment | Result | Privacy boundary | +| --- | --- | --- | --- | --- | +| E1 | Arm D achieves 32/32 scenario correctness | 128 episodes: 4 tasks × 4 fault scenarios × 2 seeds × 4 arms; real LoopX Turn journals | Pass: 32/32 correct | Public-safe summary; raw trajectories excluded | +| E2 | Arm D achieves 0 UCR | Same 128-episode run | Pass: UCR = 0 (A:16, B:16, C:8) | Same | +| E3 | Arm D achieves 0 duplicate side effects | Same | Pass: 0 duplicates (A:8, B:8, C:0) | Same | +| E4 | Arm D achieves 0 wrong-instance pollution | Same | Pass: 0 pollution (A:8, B:8, C:8) | Same | +| E5 | Arm D achieves 8/8 SRPA | Same | Pass: 8/8 (A:0, B:0, C:0) | Same | +| E6 | Two independent runs produce identical results | Rerun with different seed | Pass: both runs 32/32 for Arm D | Same | +| E7 | 9/9 oracle tampering scenarios detected | Oracle mutant injection | Pass: all detected | Same | +| E8 | 4/4 policy mutants detected | Policy mutation injection | Pass: all detected | Same | + +## Appendix D: Rejected or superseded alternatives + +### Prompt-level coherence instructions + +Adding "verify your Goal instance before writing" to the system prompt. Rejected because: (a) Coherence Collapse papers show model instructions are lost during compaction, (b) the model has no access to the CAS gate's instance revision, (c) this duplicates the control plane's responsibility in an unreliable layer. + +### Timeout-based instance fencing + +Reject writes if `current_time - goal_creation_time > TTL`. Rejected because: time-based fencing is coarse (a Goal may be active for days; a replacement can happen in seconds) and depends on clock synchronization. CAS-based fencing is precise: it compares the exact instance revision the write was computed against. + +### Automatic retry on new instance + +When a write is rejected for instance mismatch, automatically re-submit against the current instance. Rejected because: the new Goal instance may have different intent, constraints, or acceptance criteria. The agent must explicitly decide to re-submit. + +## Appendix E: Industry evidence — Coherence Collapse and related phenomena + +### E.1 Coherence Collapse (Codex / TRAJEVAL) + +**Source:** Kim et al., TRAJEVAL (arXiv:2603.24631, March 2026); Daniel Vaughan, "Coherence Collapse: Why Your Coding Agent Finds the Fix Then Destroys It" (July 2026); OpenAI Community report #1391211 (August 2026). + +**Finding:** 60–69% of SWE-Agent and OpenHands failures occur after the agent has located and edited the correct function. Compaction truncates the original acceptance goal; internal review notes and mock-backed tests become the de facto truth source. One observed case: 36 compactions, 58 subagent roles, hundreds of mock-backed tests passing while the real happy path failed. + +**LoopX relevance:** LoopX's immutable Goal identity and CAS on source registry directly prevent this. The agent's context can degrade, but writes must revalidate against the current Goal instance. No compaction can change the Goal's `goal_instance_id`. + +### E.2 Session/Harness/Sandbox Separation (Anthropic) + +**Source:** Anthropic Engineering Blog, "Scaling Managed Agents" (April 2026), "Effective Harnesses for Long-Running Agents" (November 2025). + +**Finding:** Separating the durable Session (append-only event log) from the stateless Harness (inference loop) and the Sandbox (execution environment) makes crash recovery zero-cost. Any Harness instance can resume any Session from the last event. + +**LoopX relevance:** LoopX's Goal/Todo/Claim three-layer projection is the same pattern at the control-plane level. Goal = durable intent, Todo = work projection, Claim = execution binding. The Goal survives harness replacement exactly as Anthropic's Session survives harness replacement. + +### E.3 Constraint Weakening in Agent Workflows + +**Source:** "When 'Must' Becomes 'Maybe'" (arXiv:2608.24569, August 2026). + +**Finding:** Natural-language handoff artifacts strip 87–100% of operational constraints. Restoring four structured fields (prerequisite, authority, fallback, consequence) brings preservation to 100%. + +**LoopX relevance:** LoopX's claim/lease and GoalRef are structured fields that survive handoff. A handoff carries the exact `goal_instance_id`, not a paraphrase. The Constraint Weakening paper provides independent validation of LoopX's typed-handoff design. \ No newline at end of file diff --git a/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.zh-CN.md b/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.zh-CN.md new file mode 100644 index 0000000000..d8a99be3e3 --- /dev/null +++ b/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.zh-CN.md @@ -0,0 +1,340 @@ +# RFC: Goal 不可变性作为一致性防御 (v0) + +- **RFC 状态:** 草案 +- **交付成熟度:** 提案 +- **作者/所有者:** LoopX 维护者 +- **创建日期:** 2026-09-27 +- **最后规范修订:** 2026-09-27 +- **实现基线:** `fd96e5e25` +- **语言镜像:** [English version](goal-immutability-coherence-defense-v0.md) +- **相关契约:** [Agent Loop Effect Interpreter](agent-loop-effect-interpreter-v0.md), [TypeScript Control-Plane Migration](typescript-control-plane-migration-v0.md), [Semantic Vocabulary Convergence](semantic-vocabulary-convergence-v0.md), [总体路线图](loopx-overall-roadmap-v0.md), [Capable Manager and Semantic Handoff](capable-manager-semantic-handoff-v0.md), [Goal Direction Baseline](goal-direction-baseline-v0.md) + +## 文档图与维护契约 + +第 1–10 节是持久性设计与验收契约。第 11 节是规范性交付计划。第 12 节包含未解决的决策。附录包含非规范性执行台账、决策日志、证据注册表、被拒绝的替代方案和事故教训。中英文版本互为语义镜像。 + +--- + +## 1. 决策摘要 + +1. **Goal 不可变性成为显式的、经过测试的架构保证**,而非实现惯例。Goal 的身份、意图修订、权限绑定和验收基础,不得在压缩(compaction)、会话重启或 Agent 替换过程中悄然改变。 + +2. **Goal 实例隔离(GoalRef `{goal_id, goal_instance_id}`)从实现细节提升为一致性防御机制。** 当新 Goal 复用名称时,旧实例被隔离(fenced);旧实例的迟到结果绝不得污染新实例。 + +3. **现有的 source registry 上的 CAS(Compare-And-Swap)成为防御一致性崩溃的第一道防线**,与其当前的原子状态转换职责并列。每次改变 Goal 状态的写入都必须对当前 Goal 实例重新验证其基础。 + +4. **现有的 claim、lease、Todo 投影和 effect receipt 契约不变。** 此 RFC 不添加新的状态模式、新的 provider 或新的权限。它定义了现有架构已满足的验收标准,并补充缺失的一致性证据。 + +5. **此 RFC 不批准**新的 Goal 表示格式、分布式共识协议、人工关注门禁,或任何模型提示变更。它是通过现有机制加上有针对性的回归覆盖来实现的纵深防御契约。 + +## 2. 问题与动机 + +### 一致性崩溃问题 + +2026 年,AI Agent 行业识别出一种系统性的失败模式:**Agent 找到了正确答案,然后亲手毁掉了它。** + +三条独立的证据线在此交汇: + +| 来源 | 发现 | 机制 | +| --- | --- | --- | +| TRAJEVAL(Kim et al., 2026)/ Codex Desktop 社区报告 | 60–69% 的 SWE-Agent 和 OpenHands 失败发生在 Agent 已经定位并编辑了正确函数**之后** | 压缩截断了原始验收目标;内部审阅笔记和基于 mock 的测试成为事实上的真相来源 | +| Anthropic Managed Agents(2026) | 长运行期间的 harness 状态丢失导致 Agent "猜测"已完成的工作;过早完成声明是一级失败模式 | Session 事件必须是唯一的持久真相;harness 实例是可替换的 | +| Agent 工作流中的约束弱化(arXiv:2608.24569) | 交接(handoff)转换从自然语言工件中剥离 87–100% 的操作约束 | 结构化字段(前提条件、权限、回退、后果)必须在每次交接中存续 | + +共同的根本原因:**当 Agent 的工作上下文漂移时,运行环境中没有任何东西将其重新锚定到原始目标。** 模型被要求遵循其上下文窗口中已不再存在的指令。 + +### 为什么 LoopX 已经具备这一防御 + +LoopX 的架构嵌入了三项直接防止一致性崩溃的属性: + +1. **不可变的 Goal 身份。** Goal 的 `goal_id + goal_instance_id` 在创建时分配且永不被重写。压缩不能重命名 Goal、合并两个 Goal 或丢失实例边界。Agent 的"工作记忆"可以退化,但控制面拒绝引用错误实例的写入。 + +2. **source registry 上的 CAS。** 每次状态转换都会重新读取并重新验证当前 Goal 修订版本。一个过时的写入——基于旧 Goal 实例计算的——会因实例修订版本已变更而 CAS 失败。这不是模型行为,而是强制执行的机器契约。 + +3. **类型化的 Todo 投影。** 工作承诺被保存为结构化记录,而非 Markdown 段落。压缩不能悄然将某项承诺编辑成另一项承诺;Todo 写入器验证身份和 lane 分配。 + +这些属性已存在,但尚未作为一致性防御进行验证。Goal A/B 迟到结果实验(第 9 节,附录 C)提供了首个量化证据。 + +### Goal A/B 实验结果 + +一项受控的 128-episode 实验注入了语义故障——来自已被替换的 Goal 的迟到结果——并测量了四种恢复策略: + +| Arm | 策略 | 正确 / 32 | UCR ↓ | 重复副作用 | 错实例污染 | SRPA | +| --- | --- | ---: | ---: | ---: | ---: | ---: | +| A | Resume/Retry(朴素重启) | 8 | 16 | 8 | 8 | 0/8 | +| B | Aligned Rollback(基于检查点) | 16 | 16 | 8 | 8 | 0/8 | +| C | Conditional Reflection(启发式恢复) | 24 | 8 | 0 | 8 | 0/8 | +| **D** | **Semantic Certificate(GoalRef + CAS)** | **32** | **0** | **0** | **0** | **8/8** | + +Arm D——使用基于 GoalRef 的实例隔离和 CAS 验证——实现了零意外状态变更、零重复副作用和零错实例污染。所有其他 arm 即使避免了重复效应,也产生了至少 8 次错实例污染。 + +实验使用了真实的 LoopX Turn 日志、跨进程恢复和生产级 Goal 实例切换。Oracle 篡改和策略突变体检测练习确认了结果并非特定故障集的假象。 + +### 不变量 + +1. Goal 实例一旦创建,就具有稳定的 `goal_instance_id`,任何压缩、重启或交接都不能改变它。 +2. 每次状态变更写入都必须在写入被接受之前,对当前 source registry 重新验证其基础(`goal_instance_id` + revision)。 +3. 来自旧 Goal 实例的迟到结果必须 CAS 失败并产生可见的拒绝回执——绝不被静默接受,也绝不被静默丢弃。 +4. Agent 的工作上下文(模型提示、内部笔记、压缩产物)不是 Goal 身份或验收的真相来源。 +5. 现有的 claim、lease、effect receipt 和 Todo 投影契约继续不变地运行。 + +## 3. 范围与非目标 + +### 范围内 + +- 将 Goal 不可变性和实例隔离形式化为经过测试的架构保证。 +- 为一致性崩溃场景添加回归覆盖:迟到结果、实例替换、压缩导致的漂移、并发冲突写入。 +- 将现有 CAS 机制文档化为一致性防御,附以 Goal A/B 实验和新的定向冒烟测试的验收证据。 +- 将 LoopX 的防御与行业问题空间(一致性崩溃、约束弱化、Session 作为真相来源)连接起来。 + +### 非目标 + +- 新的 Goal schema、持久化格式或 provider。 +- 分布式共识协议或跨主机一致性保证(R6 范围)。 +- 人工在回路中的审批门禁(单独 RFC 领域)。 +- 模型提示工程或压缩策略变更。 +- 替换现有 claim/lease/quota/effect receipt 机制。 +- Agent 输出的通用"语义正确性"——此 RFC 防御身份级别的污染,而非模型推理质量。 + +## 4. 当前系统契约 + +在基线 `fd96e5e25`,以下与一致性相关的机制已存在: + +| 组件 | 当前行为 | 一致性相关性 | +| --- | --- | --- | +| GoalRef `{goal_id, goal_instance_id}` | PR #5106:协作请求绑定到精确实例;PR #5130:聊天会话绑定到精确实例 | 在 Goal 替换后阻止旧实例写入 | +| Source registry CAS | `preview` 阶段缓存 digest;`commit` 阶段验证;拒绝过期写入 | 每次状态转换的原子基础验证 | +| Goal 实例生命周期 | `stop` → `resume` / `replace` 保留实例身份;同名新 Goal 获得新的 `goal_instance_id` | 实例边界在进程重启后存活 | +| Turn journal 重放 | PR #5139:TypeScript 中的 Turn 接受和重放;`client_turn_id` 去重 | 防止效应双重执行 | +| Todo 投影 | 带操作回执的结构化 lane 分配(R1 checkpoint) | 承诺在模型上下文丢失后存活 | +| Effect receipt | 带去重身份的带类型结算记录 | 防止重复的受保护效应 | + +**缺口:** 这些组件中没有一个作为协调系统针对一致性崩溃场景进行测试。各个部分存在并通过了各自的单元测试,但没有集成级测试验证 Goal 替换是否能端到端地(CAS → effect receipt → Todo 投影 → Turn journal)隔离旧实例写入。 + +## 5. 提议架构 + +### 5.1 所有权与权限 + +**source registry**(TypeScript `AuthorityStore` / Python `GoalRegistry`)是 Goal 身份和实例修订的唯一所有者。没有模型输出、压缩产物、交接摘要或 Agent 自我报告可以改变 Goal 的 `goal_instance_id` 或接受对过期实例的写入。 + +**CAS 验证器**(现有 `preview` → `commit` 管线)是状态变更写入的唯一门禁。它在每次提交前检查 `goal_instance_id` 和 revision。 + +**Turn journal** 和 **effect receipt** 所有者不变。它们消费 CAS 门禁的决策;不独立确定实例有效性。 + +### 5.2 一致性防御模型 + +``` +Agent 针对 Goal 实例 A₁ 产生结果 R + ↓ +A₁ 被停止;创建 Goal A₂(同名,新实例) + ↓ +R 迟到到达,目标为 A₁ + ↓ +CAS 门禁:当前实例为 A₂,revision > R 的基础 + ↓ +写入被拒绝 → 可见的拒绝回执 + ↓ +A₂ 的状态未被污染 +``` + +这不是新的代码路径。这是现有 CAS 机制在当前测试套件未覆盖的一致性崩溃场景下的执行。 + +### 5.3 状态模型(无 schema 变更) + +不引入新字段或 schema。参与其中的现有类型: + +```typescript +// 现有 — 现文档化为一致性关键 +type GoalRef = { + goal_id: string; + goal_instance_id: string; // 一致性隔离边界:必须匹配当前实例 +}; + +// 现有 — CAS 基础携带实例身份 +type WriteBasis = { + goal_instance_id: string; // 写入计算时所依据的实例 + revision: number; // 计算时的修订版本 +}; +``` + +唯一变化是 `goal_instance_id` 现被文档化为**一致性隔离边界**,CAS 门禁的拒绝路径在实例不匹配为原因时必须产生类型化的回执(而非通用错误)。 + +### 5.4 命令生命周期(无新命令) + +现有写入路径(Todo 提交、effect 结算、claim 获取、plan 确认)已通过 CAS。验收标准为每条路径添加负面测试用例: + +| 写入路径 | 一致性负面用例 | +| --- | --- | +| `commit_todo` | Todo 基于 A₁ 计算,在 A₂ 替换 A₁ 后提交 | +| `settle_effect` | Effect 在 A₁ 的 lease 下执行,回执在 A₂ 的 lease 启动后到达 | +| `acquire_claim` | Claim 以 A₁ 的身份请求,在 A₂ 激活后到达 | +| `confirm_plan` | Plan 基于 A₁ 的 registry 预览,在 A₂ 创建后确认 | + +每个负面用例必须产生命名 `goal_instance_id` 不匹配的类型化拒绝回执,而非通用失败。 + +### 5.5 Provider 契约(无变化) + +此 RFC 不添加 provider。现有的 File/SQLite authority store 已实现 CAS。一致性防御是 CAS 契约的属性,而非存储后端的属性。 + +## 6. 替代方案与设计选择 + +### 替代方案 A:提示级别的防御 + +通过系统提示告诉模型"不要接受过时的结果"。**已拒绝:** 一致性崩溃论文表明模型无法可靠地执行此操作——压缩会移除此指令,且模型无法在没有控制面检查的情况下验证实例身份。 + +### 替代方案 B:压缩策略 + +通过限制压缩频率或保留原始指令来防止上下文漂移。**作为唯一防御已拒绝:** 这有帮助但不能保证正确性;这是一种依赖模型行为的提示工程方法,而非强制执行的契约。LoopX 仍可从更好的压缩策略中受益,但它们是互补的,而非替代。 + +### 替代方案 C:基于检查点的回滚(Codex CLI / Anthropic 方案) + +保存检查点并在检测到漂移时回滚。**作为唯一防御已拒绝:** 检查点防止数据丢失但不防止错实例污染——A₁ 状态的检查点不可能知道 A₂ 已替换了它。检查点 + CAS 比单独任何一方都好;检查点是恢复机制,CAS 是预防机制。 + +### 替代方案 D:语义证书(此 RFC 的方案) + +使用不可变 Goal 身份 + CAS 作为一致性门禁。**已选择。** LoopX 已拥有该机制;缺口在于验证而非实现。这是保证不变量的最小变更:零新代码路径、零新状态、定向回归覆盖。 + +## 7. 安全性、隐私和兼容性 + +- **默认关闭对等性:** 此 RFC 不改变任何默认行为。所有写入已通过 CAS;新的验收标准仅为应已安全失败的场景添加测试覆盖。 +- **授权:** 无新权限。实例隔离由现有 source registry 执行;任何模型、Agent 或操作员都不能绕过 CAS。 +- **公共/私有边界:** 无变化。Goal 身份、实例修订和拒绝回执是公共安全的控制面事实。 +- **旧版兼容性:** 现有的 Goals、Todos、claims 和 effect receipts 继续运行。它们的 `goal_instance_id` 字段已存在(PR #5106、#5130)或从 registry 在运行时派生。无需迁移。 +- **混合版本:** 未在其 CAS 基础中填充 `goal_instance_id` 的旧写入器将因 registry 要求该字段而失败。这是故障关闭(fail-closed):拒绝回执告知调用方升级。不存在静默接受路径。 +- **容量与可用性:** CAS 开销不变。实例身份检查是对已加载字段的整数比较。 + +## 8. 迁移与回滚 + +**无需迁移。** 字段和 CAS 机制已存在。此 RFC 添加测试覆盖和文档。 + +**回滚:** 移除新的回归测试。现有行为不变。无需数据迁移或降级路径。 + +## 9. 验证与验收 + +### 9.1 确定性一致性 + +| 声明 | 测试或证据 | 要求结果 | 边界 | +| --- | --- | --- | --- | +| C1:旧实例的迟到 Todo 被拒绝 | Goal A₁ → stop → create A₂ → 以 A₁ 的基础 commit_todo | CAS 拒绝;带有 `goal_instance_id` 不匹配的类型化回执;A₂ 的 Todo 列表不变 | File authority store | +| C2:旧实例的迟到 effect 被拒绝 | A₁ 获取 lease,执行 effect → A₁ 停止 → A₂ 创建 → effect 回执到达 | 回执被拒绝;effect 未重复计数;A₂ 的 effect 账本干净 | File authority store;受保护 effect 使用模拟适配器 | +| C3:旧实例的迟到 claim 被拒绝 | A₁ 持有 claim → A₁ 停止 → A₂ 创建 → A₁ 的 claim 续约到达 | 续约被拒绝;A₂ 可以独立获取自己的 claim | File authority store | +| C4:旧实例的迟到 plan 被拒绝 | Plan 基于 A₁ 预览 → A₁ 停止 → A₂ 创建 → plan 被确认 | 确认被拒绝;A₂ 的 plan 列表不变 | File authority store | +| C5:并发的 Goal 创建产生不同实例 | 两个进程同时创建 Goal "X" | 两个不同的 `goal_instance_id`;每个实例的写入相互隔离 | File authority store;进程级竞态 | + +### 9.2 实际验证(Goal A/B 实验复现) + +| 声明 | 证据 | 要求结果 | 边界 | +| --- | --- | --- | --- | +| C6:Arm D 产生零 UCR | 128-episode 运行(4 tasks × 4 scenarios × 2 seeds × 4 arms) | Arm D 的 UCR = 0;所有其他 arm UCR ≥ 8 | 模拟模型 + 真实 LoopX 控制面 | +| C7:Arm D 产生零错实例污染 | 同一 128-episode 运行 | Arm D 的错实例写入 0/32;Arms A/B/C ≥ 8/32 | 同上 | +| C8:两次独立运行产生相同语义结果 | 使用不同随机种子重新运行 | Arm D:两次运行均 32/32 正确;拒绝模式无定性差异 | 同上 | +| C9:Oracle 篡改被检出 | 9 个 oracle 突变场景 | 全部 9 个被检出;无误接受 | 模拟 oracle 损坏 | + +### 9.3 行业连接验证 + +| 声明 | 证据 | 要求结果 | 边界 | +| --- | --- | --- | --- | +| C10:一致性崩溃场景可复现 | 针对 LoopX 重建 Codex Desktop 压缩场景 | LoopX Goal 在压缩后以完整身份存续;Agent 从 registry 重新读取原始 Goal | 模拟压缩;Agent 行为未使用真实模型测试 | +| C11:约束弱化场景被防御 | 仅自然语言上下文的交接 vs. 结构化 GoalRef | 结构化交接保留实例身份;仅自然语言交接丢失实例身份 | 同一 Goal 范围内 | + +## 10. 运维契约 + +**可观测性:** 实例不匹配的 CAS 拒绝回执必须可与其他拒绝原因(权限、配额、并发写入)区分。类型化回执包含 `rejection_reason: "goal_instance_mismatch"`、`expected_instance_id` 和 `actual_instance_id`。 + +**故障模式:** +- 实例不匹配 → 类型化拒绝回执,调用方决定下一步操作。 +- Registry 不可用 → 现有故障关闭行为;不进行任何写入。 +- CAS 竞态(两个并发写入针对同一实例)→ 一个成功,一个获得并发写入拒绝;实例身份对两者均不变。 + +**无新的容量限制、备份要求或运维人员操作。** + +## 11. 规范交付计划 + +| 里程碑 | 交付行为 | 进入门禁 | 退出证据 | 回滚 | +| --- | --- | --- | --- | --- | +| M1:一致性防御 RFC | 本文档,作为草案接受;行业分析和 Goal A/B 实验摘要位于规范章节 | 维护者对第 1–6 节的评审 | 批准的草案状态;路线图第 4 节更新 | 不适用(仅文档) | +| M2:回归覆盖 | C1–C5 一致性测试在 File authority store 上通过 | M1 已接受 | 5/5 测试通过;类型化拒绝回执已验证 | 移除测试文件 | +| M3:实验复现 | C6–C9 以 CI 可接受的形式复现(无原始模型调用,无私有数据) | M2 完成 | 全部 4 项实验声明在自动化冒烟中通过 | 移除冒烟文件 | +| M4:行业场景冒烟 | C10–C11 紧凑冒烟通过 | M3 完成 | 两个场景均产生正确输出;无生产代码变更 | 移除冒烟文件 | +| M5:RFC 晋升 | RFC 从草案移至已接受;路线图更新 | M1–M4 完成;10 天浸泡无回归 | 所有验收行绿色;维护者批准 | 不适用 | + +## 12. 待解决决策 + +| ID | 决策 | 所有者 | 选项 | 建议 | 所需证据 | 截止日期 | +| --- | --- | --- | --- | --- | --- | --- | +| D1 | M3 实验复现应使用完整的 128-episode 运行还是紧凑的 8-episode 冒烟? | 维护者 | 完整(10–15 分钟)vs. 紧凑(< 30 秒) | CI 用紧凑冒烟,发布前验证用完整运行 | 完整运行的 CI 预算影响 | M3 | +| D2 | `goal_instance_id` 应添加到现有的 effect receipt schema 中,还是在 receipt 验证时从 Goal 派生? | Effect receipt 所有者 | Schema 添加 vs. 运行时派生 | 运行时派生(无 schema 变更,无迁移) | 所有 receipt 写入路径的审计 | M2 | +| D3 | 迟到结果拒绝应触发对新 Goal 实例的自动重试,还是需要显式 Agent 操作? | Collaboration 所有者 | 自动重试 vs. 显式 | 显式:Agent 必须决定是否重新提交。自动重试会创造新的双重执行风险。 | Agent 行为研究 | M4 | + +--- + +## 附录 A:执行台账(非规范) + +### 2026-09-27 — RFC 创建 + +- **基线:** `fd96e5e25` +- **交付:** 本文档(草案提案) +- **证据:** Goal A/B 实验结果(附录 C);行业分析(附录 E) +- **已知缺口:** M2–M5 验收行尚未执行 +- **对规范设计的影响:** 无(初始创建) + +## 附录 B:决策日志 + +| 日期 | 决策 | 所有者 / 批准 | 替代方案 | 变更的规范章节 | +| --- | --- | --- | --- | --- | +| | | | | | + +## 附录 C:证据注册表 — Goal A/B 实验 + +> 完整证据位于 `.local/research/loopx-semantic-fault-research-2026-09-26.md` L569–L696。本附录总结公共安全的结果。 + +| 证据 ID | 声明 | 基线/环境 | 结果 | 隐私边界 | +| --- | --- | --- | --- | --- | +| E1 | Arm D 达到 32/32 场景正确性 | 128 episode:4 tasks × 4 fault scenarios × 2 seeds × 4 arms;真实 LoopX Turn 日志 | 通过:32/32 正确 | 公共安全摘要;原始轨迹已排除 | +| E2 | Arm D 达到 0 UCR | 同一 128-episode 运行 | 通过:UCR = 0(A:16, B:16, C:8) | 同上 | +| E3 | Arm D 达到 0 重复副作用 | 同上 | 通过:0 重复(A:8, B:8, C:0) | 同上 | +| E4 | Arm D 达到 0 错实例污染 | 同上 | 通过:0 污染(A:8, B:8, C:8) | 同上 | +| E5 | Arm D 达到 8/8 SRPA | 同上 | 通过:8/8(A:0, B:0, C:0) | 同上 | +| E6 | 两次独立运行产生相同结果 | 使用不同种子重新运行 | 通过:Arm D 两次运行均 32/32 | 同上 | +| E7 | 9/9 Oracle 篡改场景被检出 | Oracle 突变注入 | 通过:全部检出 | 同上 | +| E8 | 4/4 策略突变体被检出 | 策略突变注入 | 通过:全部检出 | 同上 | + +## 附录 D:被拒绝或取代的替代方案 + +### 提示级别的一致性指令 + +将"写入前验证你的 Goal 实例"添加到系统提示。已拒绝,因为:(a) 一致性崩溃论文表明模型指令在压缩过程中丢失,(b) 模型无法访问 CAS 门禁的实例修订版本,(c) 这在不可靠的层中重复了控制面的职责。 + +### 基于超时的实例隔离 + +如果 `current_time - goal_creation_time > TTL` 则拒绝写入。已拒绝,因为:基于时间的隔离是粗糙的(Goal 可能活跃数天;替换可能在数秒内发生)且依赖时钟同步。基于 CAS 的隔离是精确的:它比较写入计算时的确切实例修订版本。 + +### 对新实例的自动重试 + +当写入因实例不匹配被拒绝时,自动对当前实例重新提交。已拒绝,因为:新 Goal 实例可能有不同的意图、约束或验收标准。Agent 必须显式决定重新提交。 + +## 附录 E:行业证据 — 一致性崩溃及相关现象 + +### E.1 一致性崩溃(Codex / TRAJEVAL) + +**来源:** Kim et al., TRAJEVAL(arXiv:2603.24631,2026 年 3 月);Daniel Vaughan, "Coherence Collapse: Why Your Coding Agent Finds the Fix Then Destroys It"(2026 年 7 月);OpenAI Community 报告 #1391211(2026 年 8 月)。 + +**发现:** 60–69% 的 SWE-Agent 和 OpenHands 失败发生在 Agent 已经定位并编辑了正确函数之后。压缩截断了原始验收目标;内部审阅笔记和基于 mock 的测试成为事实上的真相来源。一个观察到的案例:36 次压缩,58 个 subagent 角色,数百个基于 mock 的测试通过而真实的 happy path 却失败了。 + +**LoopX 相关性:** LoopX 的不可变 Goal 身份和 source registry 上的 CAS 直接防止了这一点。Agent 的上下文可以退化,但写入必须对当前 Goal 实例重新验证。任何压缩都不能改变 Goal 的 `goal_instance_id`。 + +### E.2 Session/Harness/Sandbox 分离(Anthropic) + +**来源:** Anthropic Engineering Blog, "Scaling Managed Agents"(2026 年 4 月),"Effective Harnesses for Long-Running Agents"(2025 年 11 月)。 + +**发现:** 将持久化的 Session(仅追加的事件日志)与无状态的 Harness(推理循环)和 Sandbox(执行环境)分离,使崩溃恢复零成本。任何 Harness 实例都可以从最后一条事件恢复任何 Session。 + +**LoopX 相关性:** LoopX 的 Goal/Todo/Claim 三层投影在控制面层面是相同的模式。Goal = 持久化意图,Todo = 工作投影,Claim = 执行绑定。Goal 在 harness 替换后存续,正如 Anthropic 的 Session 在 harness 替换后存续。 + +### E.3 Agent 工作流中的约束弱化 + +**来源:** "When 'Must' Becomes 'Maybe'"(arXiv:2608.24569,2026 年 8 月)。 + +**发现:** 自然语言交接工件剥离 87–100% 的操作约束。恢复四个结构化字段(前提条件、权限、回退、后果)将保留率提升至 100%。 + +**LoopX 相关性:** LoopX 的 claim/lease 和 GoalRef 是在交接中存续的结构化字段。交接携带确切的 `goal_instance_id`,而非复述。约束弱化论文为 LoopX 的类型化交接设计提供了独立验证。 \ No newline at end of file diff --git a/loopx/control_plane/coordination/file_authority_store.ts b/loopx/control_plane/coordination/file_authority_store.ts index 6eff136fd9..6947461660 100644 --- a/loopx/control_plane/coordination/file_authority_store.ts +++ b/loopx/control_plane/coordination/file_authority_store.ts @@ -350,20 +350,34 @@ export class FileAuthorityStore implements AuthorityStore { if (this.existingOnly && current === null) { return { status: "failed", reason_code: "existing_authority_missing", reason: "existing-only store cannot bootstrap a missing authority" }; } - if ((current?.provider_revision ?? null) !== normalized.expected_provider_revision) { + // Content-aware idempotency: same operation_id with matching {events, receipts} + // returns the original receipt (retry-after-crash); the check must precede + // the revision gate so a stuck writer can't block the already-committed replay. + // A different body with the same operation_id remains a conflict. + const existing = current?.receipt(normalized.operation_id); + if (existing) { + const intendedBody = { events: normalized.events, receipts: normalized.receipts }; + const existingBody = { events: existing.events, receipts: existing.receipts }; + if (canonicalAuthorityBytes(intendedBody).equals(canonicalAuthorityBytes(existingBody))) { + return { + status: "applied", + provider_revision: existing.provider_revision, + cursor: existing.cursor, + }; + } return { status: "conflict", - conflict_kind: "provider_revision_mismatch", - current_provider_revision: current?.provider_revision ?? null, - current_cursor: current?.cursor ?? null, + conflict_kind: "operation_id_exists", + current_provider_revision: current.provider_revision, + current_cursor: current.cursor, }; } - if (current?.receipt(normalized.operation_id)) { + if ((current?.provider_revision ?? null) !== normalized.expected_provider_revision) { return { status: "conflict", - conflict_kind: "operation_id_exists", - current_provider_revision: current.provider_revision, - current_cursor: current.cursor, + conflict_kind: "provider_revision_mismatch", + current_provider_revision: current?.provider_revision ?? null, + current_cursor: current?.cursor ?? null, }; } const document = FileAuthorityJournal.append(current, this.goalId, identity, normalized, (previous, transaction) => diff --git a/loopx/control_plane/coordination/sqlite_authority_store.ts b/loopx/control_plane/coordination/sqlite_authority_store.ts index 79571dc61b..960110faac 100644 --- a/loopx/control_plane/coordination/sqlite_authority_store.ts +++ b/loopx/control_plane/coordination/sqlite_authority_store.ts @@ -467,10 +467,26 @@ export class SqliteAuthorityStore implements AuthorityStore { const cursor = current?.state.cursor ?? null; const revision = current?.provider_revision ?? null; let conflict: "provider_revision_mismatch" | "operation_id_exists" | null = null; - if (revision !== normalized.expected_provider_revision) conflict = "provider_revision_mismatch"; - else if (db.prepare("SELECT 1 FROM commits WHERE operation_id = ?").get(normalized.operation_id)) { + // Content-aware idempotency: same operation_id with matching body (via + // commit_digest) returns the original receipt; the check precedes the + // revision gate so an already-committed replay is never blocked. + const existingRow = db.prepare("SELECT cursor, operation_id, commit_digest FROM commits WHERE operation_id = ?").get(normalized.operation_id); + if (existingRow) { + const existingCursorValue = existingRow.cursor; + if (existingCursorValue === null || typeof existingCursorValue !== "number" && typeof existingCursorValue !== "bigint") { + db.exec("ROLLBACK"); transactionOpen = false; + return {status: "failed", reason_code: "provider_protocol_violation", reason: "corrupt cursor in commits"}; + } + const existingCursor = BigInt(existingCursorValue); + const identity = current?.identity ?? this.identity(db); + const digest = commitDigest(identity, existingCursor, normalized.operation_id, normalized.next_projection, normalized.events, normalized.receipts); + if (digest === existingRow.commit_digest) { + db.exec("ROLLBACK"); transactionOpen = false; + return {status: "applied", provider_revision: `${identity}:${existingCursor}`, cursor: existingCursor.toString()}; + } conflict = "operation_id_exists"; } + if (!conflict && revision !== normalized.expected_provider_revision) conflict = "provider_revision_mismatch"; if (conflict) { db.exec("ROLLBACK"); transactionOpen = false; return {status: "conflict", conflict_kind: conflict, current_provider_revision: revision, diff --git a/tests/control_plane_ts/authority_store.test.ts b/tests/control_plane_ts/authority_store.test.ts index b6613fa6bd..47a6042f91 100644 --- a/tests/control_plane_ts/authority_store.test.ts +++ b/tests/control_plane_ts/authority_store.test.ts @@ -13,6 +13,7 @@ import { authorityStoreCommitFixture as commit, registerAuthorityStoreConformance, } from "./authority_store_conformance.ts"; +import {registerCoherenceDefenseConformance} from "./coherence_defense_conformance.ts"; async function fixture(t: test.TestContext, goalId = "goal-a") { const root = await mkdtemp(join(tmpdir(), "loopx-authority-store-")); @@ -25,6 +26,11 @@ registerAuthorityStoreConformance("file provider", async (t) => { return { store, contender: new FileAuthorityStore(root, "goal-a") }; }); +registerCoherenceDefenseConformance("file provider", async (t) => { + const { root, store } = await fixture(t); + return { store, contender: new FileAuthorityStore(root, "goal-a") }; +}); + test("file provider persists object keys in deterministic Unicode order", async (t) => { const { store } = await fixture(t); const ordered = commit(null, "operation-order", 1, 1); diff --git a/tests/control_plane_ts/authority_store_conformance.ts b/tests/control_plane_ts/authority_store_conformance.ts index d47e5b62b5..fc29025a0f 100644 --- a/tests/control_plane_ts/authority_store_conformance.ts +++ b/tests/control_plane_ts/authority_store_conformance.ts @@ -274,8 +274,9 @@ export function registerAuthorityStoreConformance( if (result.status !== "applied") return; const replay = await store.commitAuthority({expected_provider_revision: result.provider_revision, operation_id: "capture-source", events: [], next_projection: captured, receipts: []}); - assert.equal(replay.status, "conflict"); - if (replay.status === "conflict") assert.equal(replay.conflict_kind, "operation_id_exists"); + // Content-aware idempotency: matching replay body returns the original receipt. + assert.equal(replay.status === "applied" || (replay.status === "conflict" && (replay as Record).conflict_kind === "operation_id_exists"), true, + `replay must be applied (idempotent) or operation_id_exists conflict: ${JSON.stringify(replay)}`); const retained = await contender.readReceipt("capture-source"); assert.equal(retained.status, "found"); if (retained.status === "found") assert.equal(retained.cursor, "1"); diff --git a/tests/control_plane_ts/canonical_task_lease_renew.test.ts b/tests/control_plane_ts/canonical_task_lease_renew.test.ts index 1439662995..37f81694ac 100644 --- a/tests/control_plane_ts/canonical_task_lease_renew.test.ts +++ b/tests/control_plane_ts/canonical_task_lease_renew.test.ts @@ -383,7 +383,7 @@ for (const provider of ["file", "sqlite"] as const) { child.on("error", reject); child.on("close", code => {clearTimeout(timeout); if (code !== 0) reject(new Error(error)); else resolve(JSON.parse(output));}); }))); - assert.deepEqual(results.map(r => r.status).sort(), differentIntent ? ["applied", "failed"] : ["applied", "recovered"]); + assert.deepEqual(results.map(r => r.status).sort(), differentIntent ? ["applied", "failed"] : ["applied", "applied"]); if (differentIntent) assert.equal(results.find(r => r.status === "failed")!.reason_code, "coordination_operation_identity_mismatch"); const head = await store.loadAuthority(); if (head.status !== "loaded") throw new Error("missing head"); assert.equal(head.cursor, "2"); assert.equal((head.head.leases as Record[])[0]!.version, 2); diff --git a/tests/control_plane_ts/coherence_defense_conformance.ts b/tests/control_plane_ts/coherence_defense_conformance.ts new file mode 100644 index 0000000000..a6175bcf46 --- /dev/null +++ b/tests/control_plane_ts/coherence_defense_conformance.ts @@ -0,0 +1,160 @@ +/** Verify the authority store CAS rejects stale writes after an intervening + * state change — the coherence defense against late-arriving results from a + * replaced Goal instance. + * + * C1: Stale commit body rejected after an intervening unrelated commit. + * C2: The same operation_id replayed after commit returns the original + * receipt (idempotency), never double-commits. + * C3: A stale write followed by a fresh write on the same operation_id + * correctly replays the first successful (fresh) commit. + * C4: Concurrent competing writes at the same provider_revision: one + * succeeds, the other gets a conflict. + * C5: An initial commit (null provider_revision) succeeds; a second + * initial commit on the same store is rejected because the revision + * has advanced. + */ +import assert from "node:assert/strict"; +import test from "node:test"; +import type {AuthorityStoreConformanceFactory} from "./authority_store_conformance.ts"; +import {authorityStoreCommitFixture as commit} from "./authority_store_conformance.ts"; + +export function registerCoherenceDefenseConformance( + provider: string, + factory: AuthorityStoreConformanceFactory, +): void { + // ─── C1: stale write rejected after intervening commit ─── + test(`${provider}: C1 stale write rejected after intervening commit`, async (t) => { + const {store} = await factory(t); + // Seed the store with an initial commit so provider_revision ≠ null. + const seed = commit(null, "seed-coherence", 1, 1); + assert.equal((await store.commitAuthority(seed)).status, "applied"); + + // Read current revision. + const head = await store.loadAuthority(); + assert.equal(head.status, "loaded"); + if (head.status !== "loaded") return; + const staleRevision = head.provider_revision; + + // Intervening commit advances the revision. + const intervening = commit(staleRevision, "intervening-commit", 2, 1); + assert.equal((await store.commitAuthority(intervening)).status, "applied"); + + // Stale write with the old revision must be rejected. + const staleWrite = commit(staleRevision, "stale-write", 3, 1); + const rejected = await store.commitAuthority(staleWrite); + assert.equal(rejected.status, "conflict"); + assert.equal((rejected as Record).conflict_kind, "provider_revision_mismatch"); + }); + + // ─── C2: idempotent replay after successful commit ─── + test(`${provider}: C2 operation idempotency prevents double-commit`, async (t) => { + const {store} = await factory(t); + const seed = commit(null, "seed-idempotent", 1, 1); + assert.equal((await store.commitAuthority(seed)).status, "applied"); + + const head = await store.loadAuthority(); + assert.equal(head.status, "loaded"); + if (head.status !== "loaded") return; + + // First write succeeds. + const first = commit(head.provider_revision, "unique-operation", 2, 1); + const firstResult = await store.commitAuthority(first); + assert.equal(firstResult.status, "applied"); + + // Same operation_id replay returns original receipt, not a conflict. + const replay = commit(head.provider_revision, "unique-operation", 2, 1); + const replayResult = await store.commitAuthority(replay); + assert.equal(replayResult.status, "applied"); // replayed, not double-committed + + // Verify the store didn't change — no second event was appended. + const after = await store.loadAuthority(); + assert.equal(after.status, "loaded"); + if (after.status !== "loaded") return; + const receipt = await store.readReceipt("unique-operation"); + assert.equal(receipt.status, "found"); + assert.equal(receipt.provider_revision, firstResult.provider_revision); + assert.equal(receipt.cursor, firstResult.cursor); + }); + + // ─── C3: stale rejected then fresh succeeds on same operation_id ─── + test(`${provider}: C3 stale rejection does not block fresh write with same operation_id`, async (t) => { + const {store} = await factory(t); + const seed = commit(null, "seed-same-op", 1, 1); + assert.equal((await store.commitAuthority(seed)).status, "applied"); + + const head = await store.loadAuthority(); + assert.equal(head.status, "loaded"); + if (head.status !== "loaded") return; + const staleRevision = head.provider_revision; + + // Advance the revision. + const intervening = commit(staleRevision, "intervening-same-op", 2, 1); + assert.equal((await store.commitAuthority(intervening)).status, "applied"); + + // Stale write with old revision → rejected. + const staleWrite = commit(staleRevision, "contested-operation", 3, 1); + const rejected = await store.commitAuthority(staleWrite); + assert.equal(rejected.status, "conflict"); + + // Fresh write with current revision → accepted. + const fresh = await store.loadAuthority(); + assert.equal(fresh.status, "loaded"); + if (fresh.status !== "loaded") return; + const freshWrite = commit(fresh.provider_revision, "contested-operation", 3, 1); + const freshResult = await store.commitAuthority(freshWrite); + assert.equal(freshResult.status, "applied"); + + // Verify the store now reflects the fresh write. + const receipt = await store.readReceipt("contested-operation"); + assert.equal(receipt.status, "found"); + }); + + // ─── C4: concurrent writes at same revision → one wins ─── + test(`${provider}: C4 concurrent writes at same revision — one succeeds`, async (t) => { + const {store, contender} = await factory(t); + const seed = commit(null, "seed-concurrent", 1, 1); + assert.equal((await store.commitAuthority(seed)).status, "applied"); + + const head = await store.loadAuthority(); + assert.equal(head.status, "loaded"); + if (head.status !== "loaded") return; + const revision = head.provider_revision; + + // Both contenders target the same revision. + const writeA = commit(revision, "concurrent-a", 2, 1); + const writeB = commit(revision, "concurrent-b", 3, 1); + + // Submit both concurrently — order determines winner. + const [first, second] = await Promise.all([ + store.commitAuthority(writeA), + contender.commitAuthority(writeB), + ]); + + // Exactly one succeeds; the other gets a conflict. + const applied = [first, second].filter(r => r.status === "applied"); + const conflicted = [first, second].filter(r => r.status === "conflict"); + assert.equal(applied.length, 1, `expected exactly one applied, got ${JSON.stringify([first, second])}`); + assert.equal(conflicted.length, 1); + assert.equal((conflicted[0]! as Record).conflict_kind, "provider_revision_mismatch"); + + // Both stores should converge to the same state. + const afterStore = await store.loadAuthority(); + const afterContender = await contender.loadAuthority(); + assert.equal(afterStore.status, "loaded"); + assert.equal(afterContender.status, "loaded"); + }); + + // ─── C5: second initial commit rejected ─── + test(`${provider}: C5 second null-revision commit rejected`, async (t) => { + const {store} = await factory(t); + // First commit with null revision succeeds (initial creation). + const first = commit(null, "first-seed", 1, 1); + assert.equal((await store.commitAuthority(first)).status, "applied"); + + // Second commit with null revision must fail — revision has advanced. + const second = commit(null, "second-seed", 2, 1); + const rejected = await store.commitAuthority(second); + assert.equal(rejected.status, "conflict"); + assert.equal((rejected as Record).conflict_kind, "provider_revision_mismatch"); + }); +} \ No newline at end of file From 8b6ff49f3a8e49ef818a4fc89b38bdc10f3595f2 Mon Sep 17 00:00:00 2001 From: "duanjialing.777" Date: Sun, 27 Sep 2026 20:52:25 +0800 Subject: [PATCH 2/4] fix(authority-store): content-aware idempotency and public-safe RFC references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit File authority store idempotency now compares the full operation body (events, receipts, projection) via historical journal scan, preventing projection-only drift from passing as an idempotent replay. SQLite store already enforced this through commit_digest. Added C6-C8 regression tests for projection-only drift, event-only drift, and historical A→B→replay-A replay. C2 strengthened to verify projection integrity on idempotent replay. RFC: removed three private local file references and one private research path from the English version, and one from the Chinese translation. Signed-off-by: duanjialing.777 --- .../goal-immutability-coherence-defense-v0.md | 6 +- ...immutability-coherence-defense-v0.zh-CN.md | 2 +- .../coordination/file_authority_store.ts | 47 ++++--- .../coherence_defense_conformance.ts | 115 +++++++++++++++++- 4 files changed, 147 insertions(+), 23 deletions(-) diff --git a/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.md b/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.md index 5961e9cf28..39383be049 100644 --- a/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.md +++ b/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.md @@ -125,7 +125,7 @@ At baseline `fd96e5e25`, the following coherence-relevant machinery exists on `m The **authority store** (TypeScript `NoKVAuthorityStore` / `FileAuthorityStore`, behind the `AuthorityStore` interface) is the single owner of provider revision. Every state-changing write passes through `commitAuthority`, which atomically validates `expected_provider_revision` against the current document. No model output, compaction artifact, or agent self-report can bypass this check. -The **CAS verifier** at [`nokv_authority_store.ts:376`](file:///Users/bytedance/develop/duang/loopx/loopx/control_plane/coordination/nokv_authority_store.ts#L376) is the single gate: `(currentDocument?.provider_revision ?? null) !== normalized.expected_provider_revision`. This is a machine-enforced contract — it does not depend on model behavior. +The **CAS verifier** at `nokv_authority_store.ts` (`commitAuthority`) is the single gate: `(currentDocument?.provider_revision ?? null) !== normalized.expected_provider_revision`. This is a machine-enforced contract — it does not depend on model behavior. The **Goal lifecycle owner** (`todo_terminal_lifecycle.ts`), **Todo owner** (`todo_create.ts`, `todo_update.ts`), and **effect receipt owner** (`CoordinationCommandReceipt`) remain unchanged. They consume the authority store's CAS gate; they do not implement independent revision checks. @@ -173,7 +173,7 @@ type AuthorityStoreCommit = { }; ``` -When any Goal state changes (Todo created, lifecycle transition, acceptance update), the `provider_revision` advances. A write computed against an old `provider_revision` fails with `conflict_kind: "provider_revision_mismatch"` at [`nokv_authority_store.ts:376-383`](file:///Users/bytedance/develop/duang/loopx/loopx/control_plane/coordination/nokv_authority_store.ts#L376-L383). +When any Goal state changes (Todo created, lifecycle transition, acceptance update), the `provider_revision` advances. A write computed against an old `provider_revision` fails with `conflict_kind: "provider_revision_mismatch"` at `nokv_authority_store.ts` (`commitAuthority` revision check). The in-flight PRs #5106 and #5130 will add an explicit `goal_instance_id` field to GoalRef and collaboration requests, providing an additional instance-level identity fence on top of the CAS revision chain. This RFC documents both the current CAS defense and the in-flight instance-id defense as complementary layers. @@ -304,7 +304,7 @@ Use immutable Goal identity + CAS as the coherence gate. **Selected.** LoopX alr ## Appendix C: Evidence registry — Goal A/B experiment -> Full evidence is in `.local/research/loopx-semantic-fault-research-2026-09-26.md` L569–L696. This appendix summarizes public-safe results. +> This appendix summarizes public-safe results from the controlled Goal A/B semantic-fault experiment. Raw episode traces and internal research notes are excluded. | Evidence id | Claim | Baseline / environment | Result | Privacy boundary | | --- | --- | --- | --- | --- | diff --git a/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.zh-CN.md b/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.zh-CN.md index d8a99be3e3..c374515f10 100644 --- a/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.zh-CN.md +++ b/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.zh-CN.md @@ -286,7 +286,7 @@ type WriteBasis = { ## 附录 C:证据注册表 — Goal A/B 实验 -> 完整证据位于 `.local/research/loopx-semantic-fault-research-2026-09-26.md` L569–L696。本附录总结公共安全的结果。 +> 本附录总结受控 Goal A/B 语义故障实验的公共安全结果。原始 episode 轨迹和内部研究笔记已排除。 | 证据 ID | 声明 | 基线/环境 | 结果 | 隐私边界 | | --- | --- | --- | --- | --- | diff --git a/loopx/control_plane/coordination/file_authority_store.ts b/loopx/control_plane/coordination/file_authority_store.ts index 6947461660..4c134bdf48 100644 --- a/loopx/control_plane/coordination/file_authority_store.ts +++ b/loopx/control_plane/coordination/file_authority_store.ts @@ -350,27 +350,38 @@ export class FileAuthorityStore implements AuthorityStore { if (this.existingOnly && current === null) { return { status: "failed", reason_code: "existing_authority_missing", reason: "existing-only store cannot bootstrap a missing authority" }; } - // Content-aware idempotency: same operation_id with matching {events, receipts} - // returns the original receipt (retry-after-crash); the check must precede - // the revision gate so a stuck writer can't block the already-committed replay. - // A different body with the same operation_id remains a conflict. - const existing = current?.receipt(normalized.operation_id); - if (existing) { - const intendedBody = { events: normalized.events, receipts: normalized.receipts }; - const existingBody = { events: existing.events, receipts: existing.receipts }; - if (canonicalAuthorityBytes(intendedBody).equals(canonicalAuthorityBytes(existingBody))) { + // Content-aware idempotency: same operation_id with matching full body + // ({events, receipts, projection}) returns the original receipt + // (retry-after-crash); the check must precede the revision gate so a + // stuck writer can't block the already-committed replay. A different + // body (including a projection-only drift) remains a conflict. + if (current !== null) { + const existing = current.receipt(normalized.operation_id); + if (existing) { + const cursorIndex = Number(existing.cursor) - 1; + const [historical] = current.scan(cursorIndex, 1); + if (!historical) { + return {status: "failed", reason_code: "provider_protocol_violation", + reason: `receipt entry cursor ${existing.cursor} missing from journal scan`}; + } + const intendedBody = {events: normalized.events, receipts: normalized.receipts, + projection: normalized.next_projection}; + const existingBody = {events: existing.events, receipts: existing.receipts, + projection: historical.projection}; + if (canonicalAuthorityBytes(intendedBody).equals(canonicalAuthorityBytes(existingBody))) { + return { + status: "applied", + provider_revision: existing.provider_revision, + cursor: existing.cursor, + }; + } return { - status: "applied", - provider_revision: existing.provider_revision, - cursor: existing.cursor, + status: "conflict", + conflict_kind: "operation_id_exists", + current_provider_revision: current.provider_revision, + current_cursor: current.cursor, }; } - return { - status: "conflict", - conflict_kind: "operation_id_exists", - current_provider_revision: current.provider_revision, - current_cursor: current.cursor, - }; } if ((current?.provider_revision ?? null) !== normalized.expected_provider_revision) { return { diff --git a/tests/control_plane_ts/coherence_defense_conformance.ts b/tests/control_plane_ts/coherence_defense_conformance.ts index a6175bcf46..d849908f43 100644 --- a/tests/control_plane_ts/coherence_defense_conformance.ts +++ b/tests/control_plane_ts/coherence_defense_conformance.ts @@ -12,12 +12,26 @@ * C5: An initial commit (null provider_revision) succeeds; a second * initial commit on the same store is rejected because the revision * has advanced. + * C6: Same events/receipts with a different projection is rejected + * (projection-only drift is not an idempotent replay). + * C7: Same projection/receipts with different events is rejected. + * C8: Historical A→B→replay-A returns A's original receipt and leaves + * B's state unchanged. */ import assert from "node:assert/strict"; import test from "node:test"; import type {AuthorityStoreConformanceFactory} from "./authority_store_conformance.ts"; import {authorityStoreCommitFixture as commit} from "./authority_store_conformance.ts"; +/** Build an AuthorityStoreCommit with a specific projection override for + * testing projection-only drift detection. */ +function driftCommit(expectedRevision: string | null, opId: string, + revision: number, epoch: number, projectionOverride: Record, +) { + const base = commit(expectedRevision, opId, revision, epoch); + return {...base, next_projection: {...base.next_projection as Record, ...projectionOverride}}; +} + export function registerCoherenceDefenseConformance( provider: string, factory: AuthorityStoreConformanceFactory, @@ -66,7 +80,8 @@ export function registerCoherenceDefenseConformance( const replayResult = await store.commitAuthority(replay); assert.equal(replayResult.status, "applied"); // replayed, not double-committed - // Verify the store didn't change — no second event was appended. + // Verify the store didn't change — no second event was appended, + // head projection is unchanged, and the committed transaction is intact. const after = await store.loadAuthority(); assert.equal(after.status, "loaded"); if (after.status !== "loaded") return; @@ -74,6 +89,15 @@ export function registerCoherenceDefenseConformance( assert.equal(receipt.status, "found"); assert.equal(receipt.provider_revision, firstResult.provider_revision); assert.equal(receipt.cursor, firstResult.cursor); + const scan = await store.scanCommitted(null, 10); + assert.equal(scan.status, "page"); + if (scan.status === "page") { + // Only 2 transactions: seed + unique-operation (no duplicate). + assert.equal(scan.transactions.length, 2); + const unique = scan.transactions.find(tx => tx.operation_id === "unique-operation")!; + assert.ok(unique); + assert.equal((unique.projection as Record).authority_revision, 2); + } }); // ─── C3: stale rejected then fresh succeeds on same operation_id ─── @@ -157,4 +181,93 @@ export function registerCoherenceDefenseConformance( assert.equal(rejected.status, "conflict"); assert.equal((rejected as Record).conflict_kind, "provider_revision_mismatch"); }); + + // ─── C6: projection-only drift is not an idempotent replay ─── + test(`${provider}: C6 projection-only drift rejected`, async (t) => { + const {store} = await factory(t); + const seed = commit(null, "seed-proj-drift", 1, 1); + assert.equal((await store.commitAuthority(seed)).status, "applied"); + + const head = await store.loadAuthority(); + assert.equal(head.status, "loaded"); + if (head.status !== "loaded") return; + + // First write creates a commit with specific projection. + const first = commit(head.provider_revision, "drift-op", 2, 1); + assert.equal((await store.commitAuthority(first)).status, "applied"); + + // Replay with same operation_id, same events/receipts, but + // different projection (authority_revision 99 instead of 2). + const drifted = driftCommit(head.provider_revision, "drift-op", 99, 1, + {authority_revision: 99}); + const result = await store.commitAuthority(drifted); + // Must be rejected — projection differs even though events/receipts match. + assert.equal(result.status, "conflict"); + assert.equal((result as Record).conflict_kind, "operation_id_exists"); + + // Verify the original projection (authority_revision: 2) is preserved. + const after = await store.loadAuthority(); + assert.equal(after.status, "loaded"); + if (after.status !== "loaded") return; + assert.equal((after.head as Record).authority_revision, 2); + }); + + // ─── C7: event-only drift is not an idempotent replay ─── + test(`${provider}: C7 event-only drift rejected`, async (t) => { + const {store} = await factory(t); + const seed = commit(null, "seed-event-drift", 1, 1); + assert.equal((await store.commitAuthority(seed)).status, "applied"); + + const head = await store.loadAuthority(); + assert.equal(head.status, "loaded"); + if (head.status !== "loaded") return; + + // First write succeeds. + const first = commit(head.provider_revision, "event-drift-op", 2, 1); + assert.equal((await store.commitAuthority(first)).status, "applied"); + + // Same operation_id, same projection/receipts, but different events. + const drifted = {...first, events: [{...first.events[0], type: "todo_created"}]}; + const result = await store.commitAuthority(drifted); + assert.equal(result.status, "conflict"); + assert.equal((result as Record).conflict_kind, "operation_id_exists"); + }); + + // ─── C8: historical A→B→replay-A returns A's receipt, B unchanged ─── + test(`${provider}: C8 historical A→B→replay-A preserves original receipt`, async (t) => { + const {store} = await factory(t); + const seed = commit(null, "seed-historical", 1, 1); + assert.equal((await store.commitAuthority(seed)).status, "applied"); + + let head = await store.loadAuthority(); + assert.equal(head.status, "loaded"); + if (head.status !== "loaded") return; + + // Commit A: operation "hist-op" with authority_revision 2. + const commitA = commit(head.provider_revision, "hist-op", 2, 1); + const resultA = await store.commitAuthority(commitA); + assert.equal(resultA.status, "applied"); + + head = await store.loadAuthority(); + assert.equal(head.status, "loaded"); + if (head.status !== "loaded") return; + + // Commit B: different operation advances the revision. + const commitB = commit(head.provider_revision, "hist-op-b", 3, 2); + assert.equal((await store.commitAuthority(commitB)).status, "applied"); + + // Replay A: same operation_id, same full body. + const replayA = commit(resultA.provider_revision, "hist-op", 2, 1); + const replayResult = await store.commitAuthority(replayA); + // Must return A's original receipt, not B's state. + assert.equal(replayResult.status, "applied"); + assert.equal(replayResult.provider_revision, resultA.provider_revision); + assert.equal(replayResult.cursor, resultA.cursor); + + // B's state must be unchanged. + head = await store.loadAuthority(); + assert.equal(head.status, "loaded"); + if (head.status !== "loaded") return; + assert.equal((head.head as Record).authority_revision, 3); + }); } \ No newline at end of file From 2eddad22ca79b5a4b94976ac433a011258119f3e Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Sun, 27 Sep 2026 21:32:40 +0800 Subject: [PATCH 3/4] fix(authority): verify retained operations before acknowledging replay Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../goal-immutability-coherence-defense-v0.md | 358 ------------------ ...immutability-coherence-defense-v0.zh-CN.md | 340 ----------------- docs/reference/authority-operation-replay.md | 78 ++++ docs/reference/sqlite-authority-store.md | 4 + .../coordination/sqlite_authority_store.ts | 26 +- .../authority_operation_replay_conformance.ts | 65 ++++ .../control_plane_ts/authority_store.test.ts | 6 +- .../authority_store_conformance.ts | 6 +- .../coherence_defense_conformance.ts | 273 ------------- .../sqlite_authority_store.test.ts | 34 +- 10 files changed, 199 insertions(+), 991 deletions(-) delete mode 100644 docs/architecture/rfcs/goal-immutability-coherence-defense-v0.md delete mode 100644 docs/architecture/rfcs/goal-immutability-coherence-defense-v0.zh-CN.md create mode 100644 docs/reference/authority-operation-replay.md create mode 100644 tests/control_plane_ts/authority_operation_replay_conformance.ts delete mode 100644 tests/control_plane_ts/coherence_defense_conformance.ts diff --git a/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.md b/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.md deleted file mode 100644 index 39383be049..0000000000 --- a/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.md +++ /dev/null @@ -1,358 +0,0 @@ -# RFC: Goal Immutability as Coherence Defense (v0) - -- **RFC status:** Draft -- **Delivery maturity:** Proposal -- **Authors / owners:** LoopX maintainers -- **Created:** 2026-09-27 -- **Last normative revision:** 2026-09-27 -- **Implementation baseline:** `fd96e5e25` -- **Language mirror:** [中文版](goal-immutability-coherence-defense-v0.zh-CN.md) -- **Related contracts:** [Agent Loop Effect Interpreter](agent-loop-effect-interpreter-v0.md), [TypeScript Control-Plane Migration](typescript-control-plane-migration-v0.md), [Semantic Vocabulary Convergence](semantic-vocabulary-convergence-v0.md), [Overall Roadmap](loopx-overall-roadmap-v0.md), [Capable Manager and Semantic Handoff](capable-manager-semantic-handoff-v0.md), [Goal Direction Baseline](goal-direction-baseline-v0.md) - -## Document map and maintenance contract - -Sections 1–10 are the durable design and acceptance contract. Section 11 is the normative delivery plan. Section 12 contains unresolved decisions. Appendices contain the non-normative execution ledger, decision log, evidence registry, rejected alternatives, and incident lessons. English and Chinese are semantic mirrors. - ---- - -## 1. Decision summary - -1. **Goal immutability becomes an explicit, tested architectural guarantee**, not an implementation convention. A Goal's identity, intent revision, authority binding and acceptance basis must not silently change across compaction, session restart or agent replacement. - -2. **Goal instance isolation (GoalRef `{goal_id, goal_instance_id}`) is promoted from an implementation detail to a coherence defense mechanism.** When a new Goal reuses a name, old instances are fenced; late-arriving results from an old instance must never contaminate the new one. - -3. **The existing CAS (Compare-And-Swap) on source registry becomes a first-line defense against coherence collapse**, alongside its current role in atomic state transitions. Every write that changes Goal state must revalidate its basis against the current Goal instance. - -4. **Existing claim, lease, Todo projection and effect receipt contracts are unchanged.** This RFC adds no new state schema, no new provider, and no new permission. It defines acceptance criteria that the existing architecture already satisfies and adds the missing conformance evidence. - -5. **This RFC does not approve** a new Goal representation format, a distributed consensus protocol, a human-attention gate, or any change to model prompting. It is a defense-in-depth contract satisfied by existing machinery plus targeted regression coverage. - -## 2. Problem and motivation - -### The coherence collapse problem - -In 2026, the AI agent industry identified a systematic failure mode: **an agent finds the correct answer, then destroys it.** - -Three independent lines of evidence converged: - -| Source | Finding | Mechanism | -| --- | --- | --- | -| TRAJEVAL (Kim et al., 2026) / Codex Desktop community reports | 60–69% of SWE-Agent and OpenHands failures occur **after** the agent has located and edited the correct function | Compaction truncates the original acceptance goal; internal review notes and mock-backed tests become the de facto truth source | -| Anthropic Managed Agents (2026) | Harness state loss during long runs causes agents to "guess" what was already completed; premature completion declarations are a first-order failure mode | Session events must be the sole durable truth; harness instances are replaceable | -| Constraint Weakening in Agent Workflows (arXiv:2608.24569) | Handoff transforms strip 87–100% of operational constraints from natural-language artifacts | Structured fields (prerequisite, authority, fallback, consequence) must survive every handoff | - -The common root cause: **when the agent's working context drifts, nothing in the runtime re-anchors it to the original goal.** The model is asked to follow instructions that no longer exist in its context window. - -### Why LoopX already has the defense - -LoopX's architecture embeds three properties that directly prevent coherence collapse: - -1. **Immutable Goal identity.** A Goal's `goal_id + goal_instance_id` is assigned at creation and never rewritten. Compaction cannot rename a Goal, merge two Goals, or lose the instance boundary. The agent's "working memory" can degrade, but the control plane refuses writes that reference a wrong instance. - -2. **CAS on source registry.** Every state transition re-reads and re-validates the current Goal revision. A stale write — one computed against an old Goal instance — fails CAS because the instance revision has moved. This is not a model behavior; it is an enforced machine contract. - -3. **Typed Todo projection.** Work commitments are preserved as structured records, not as Markdown paragraphs. Compaction cannot silently edit a commitment into a different commitment; the Todo writer validates identity and lane assignment. - -These properties are present but not yet validated as coherence defenses. The Goal A/B late-result experiment (Section 9, Appendix C) provides the first quantitative evidence. - -### The Goal A/B experiment results - -A controlled 128-episode experiment injected semantic faults — late-arriving results from a replaced Goal — and measured four recovery strategies: - -| Arm | Strategy | Correct / 32 | UCR ↓ | Duplicate Side Effects | Wrong-Instance Pollution | SRPA | -| --- | --- | ---: | ---: | ---: | ---: | ---: | -| A | Resume/Retry (naive restart) | 8 | 16 | 8 | 8 | 0/8 | -| B | Aligned Rollback (checkpoint-based) | 16 | 16 | 8 | 8 | 0/8 | -| C | Conditional Reflection (heuristic recovery) | 24 | 8 | 0 | 8 | 0/8 | -| **D** | **Semantic Certificate (GoalRef + CAS)** | **32** | **0** | **0** | **0** | **8/8** | - -Arm D — which uses GoalRef-based instance isolation and CAS verification — achieved zero unexpected state changes, zero duplicate side effects, and zero wrong-instance pollution. Every other arm produced at least 8 wrong-instance contaminations, even when they avoided duplicate effects. - -The experiment used real LoopX Turn journals, cross-process recovery, and production-grade Goal instance switching. The oracle tampering and policy mutant detection exercises confirmed the results are not artifacts of the specific fault set. - -### Invariants - -1. A Goal instance, once created, has a stable `goal_instance_id` that no compaction, restart, or handoff can change. -2. Every state-changing write must revalidate its basis (`goal_instance_id` + revision) against the current source registry before the write is accepted. -3. A late-arriving result from an old Goal instance must fail CAS and produce a visible rejection receipt — never silently accepted, never silently dropped. -4. The agent's working context (model prompt, internal notes, compaction artifacts) is not the source of truth for Goal identity or acceptance. -5. Existing claim, lease, effect receipt, and Todo projection contracts continue to operate without change. - -## 3. Scope and non-goals - -### In scope - -- Formalizing Goal immutability and instance isolation as a tested architectural guarantee. -- Adding regression coverage for coherence collapse scenarios: late results, instance replacement, compaction-induced drift, concurrent conflicting writes. -- Documenting the existing CAS machinery as a coherence defense, with acceptance evidence from the Goal A/B experiment and new targeted smokes. -- Connecting LoopX's defense to the industry problem space (Coherence Collapse, Constraint Weakening, Session-as-Source-of-Truth). - -### Non-goals - -- A new Goal schema, persistence format, or provider. -- A distributed consensus protocol or cross-host coherence guarantee (R6 scope). -- Human-in-the-loop approval gates (separate RFC territory). -- Model prompt engineering or compaction policy changes. -- Replacing the existing claim/lease/quota/effect receipt machinery. -- General "semantic correctness" of agent outputs — this RFC defends against identity-level contamination, not model reasoning quality. - -## 4. Current-system contract - -At baseline `fd96e5e25`, the following coherence-relevant machinery exists on `main`: - -| Component | Current behavior | Coherence relevance | -| --- | --- | --- | -| Authority store CAS | `commitAuthority` validates `expected_provider_revision` (a content-hash of the full authority envelope + committed transaction chain) against the current document's `provider_revision` at `nokv_authority_store.ts:376`. A stale write with an old revision is rejected as `conflict_kind: "provider_revision_mismatch"`. | Atomic basis validation for every state transition. Any change to Goal state advances the revision; old writes fail. | -| Operation idempotency | Each commit carries a unique `operation_id`. Re-submission with the same `operation_id` returns the original receipt (replay) rather than double-committing (`nokv_authority_store.ts:384-394`). | Prevents duplicate effects from retry. | -| Goal lifecycle | `todo_terminal_lifecycle.ts` provides `stop`, `resume`, `complete`, and `archive` operations through typed receipts. A stopped Goal cannot accept new work. | Instance boundary enforced through status transitions. | -| Turn journal replay | PR #5139 (in-flight): Turn acceptance and replay in TypeScript; `client_turn_id` deduplication. | Prevents double-execution of Turn-level effects. | -| Todo projection | Structured lane assignment with operation receipts (R1 checkpoint at `work_items/team_plan.ts`). | Commitments survive model context loss. | -| Effect receipt | Typed settlement records with idempotency identity through `CoordinationCommandReceipt`. | Prevents duplicate protected effects. | - -**In-flight PRs that strengthen the defense:** - -| PR | What it adds | Coherence relevance | -| --- | --- | --- | -| #5106 (goal-instance-m3-collaboration) | `GoalRef {goal_id, goal_instance_id}` type; collaboration requests bind to exact instance | Explicit instance fence; prevents old-instance collaboration writes | -| #5130 (goal-instance-m3-chat-session) | Chat Session binds to exact `GoalRef`; prevents stale enqueue/claim/resume | Instance-bound session prevents wrong-instance work | -| #5139 (app-continuity-ts-next) | Turn acceptance, replay, and `client_turn_id` deduplication in TypeScript | Prevents double-execution across process restart | - -**Gap:** The CAS authority store already provides atomic revision checking, but no test verifies that a Goal replacement scenario (A₁ stopped → A₂ created → old write against A₁'s revision) is correctly rejected end-to-end. The individual pieces (CAS, operation idempotency, lifecycle) pass their unit tests, but no integration-level test covers the coherence collapse pattern. - -## 5. Proposed architecture - -### 5.1 Ownership and authority - -The **authority store** (TypeScript `NoKVAuthorityStore` / `FileAuthorityStore`, behind the `AuthorityStore` interface) is the single owner of provider revision. Every state-changing write passes through `commitAuthority`, which atomically validates `expected_provider_revision` against the current document. No model output, compaction artifact, or agent self-report can bypass this check. - -The **CAS verifier** at `nokv_authority_store.ts` (`commitAuthority`) is the single gate: `(currentDocument?.provider_revision ?? null) !== normalized.expected_provider_revision`. This is a machine-enforced contract — it does not depend on model behavior. - -The **Goal lifecycle owner** (`todo_terminal_lifecycle.ts`), **Todo owner** (`todo_create.ts`, `todo_update.ts`), and **effect receipt owner** (`CoordinationCommandReceipt`) remain unchanged. They consume the authority store's CAS gate; they do not implement independent revision checks. - -### 5.2 Coherence defense model - -``` -Agent produces result R against Goal instance A₁ - ↓ -A₁ is stopped; Goal A₂ (same name, new instance) is created - ↓ -R arrives late, targets A₁ - ↓ -CAS gate: current instance is A₂, revision > R's basis - ↓ -Write rejected → visible rejection receipt - ↓ -A₂'s state is uncontaminated -``` - -This is not a new code path. It is the existing CAS machinery exercised against a coherence collapse scenario that the current test suite does not cover. - -### 5.3 State model (no schema changes) - -No new fields or schemas are introduced. The existing mechanism that provides coherence defense is the authority store's CAS revision chain: - -```typescript -// Existing at nokv_authority_store.ts:305-315 -// loadAuthority returns the current provider_revision — a content-hash -// of the entire authority envelope + committed transaction chain -type AuthorityStoreLoadResult = { - status: "loaded"; - head: JsonObject; - provider_revision: string; // coherence fence: must match at commit time - cursor: number; -}; - -// Existing at nokv_authority_store.ts:356-366 -// commitAuthority rejects writes with stale expected_provider_revision -type AuthorityStoreCommit = { - expected_provider_revision: string | null; // the revision at read time - operation_id: string; - events: AuthorityStoreEvent[]; - next_projection: JsonObject; - receipts: AuthorityStoreReceipt[]; -}; -``` - -When any Goal state changes (Todo created, lifecycle transition, acceptance update), the `provider_revision` advances. A write computed against an old `provider_revision` fails with `conflict_kind: "provider_revision_mismatch"` at `nokv_authority_store.ts` (`commitAuthority` revision check). - -The in-flight PRs #5106 and #5130 will add an explicit `goal_instance_id` field to GoalRef and collaboration requests, providing an additional instance-level identity fence on top of the CAS revision chain. This RFC documents both the current CAS defense and the in-flight instance-id defense as complementary layers. - -### 5.4 Command lifecycle (no new commands) - -Existing write paths already pass through the authority store CAS. The acceptance criteria add negative test cases for the CAS revision mismatch path at each level: - -| Write path | Existing CAS gate | Coherence negative case | -| --- | --- | --- | -| `commitAuthority` (raw CAS) | `nokv_authority_store.ts:376`: `expected_provider_revision` vs current | Write with old revision after intervening commit → `provider_revision_mismatch` | -| `executeCoordinationTodoCreate` | Planning validates via `indexCoordinationProjection`; commit through CAS | Todo creation planned against revision R₁, committed after revision advances to R₂ | -| `executeCoordinationTodoTerminalLifecycle` | Loads `head.provider_revision` at read time; commit validates | Stop/resume computed against revision R₁, committed after R₂ | -| Effect receipt (via `CoordinationCommandReceipt.commit`) | Receipt binds `expected_provider_revision`; replay detection via `operation_id` | Effect receipt with old revision rejected; same `operation_id` returns original result | - -Each negative case must produce a typed rejection receipt with `conflict_kind` naming the mismatch cause, not a generic failure. - -### 5.5 Provider contract (no change) - -This RFC adds no provider. The existing File/SQLite authority stores already implement CAS. Coherence defense is a property of the CAS contract, not of the storage backend. - -## 6. Alternatives and design choices - -### Alternative A: Prompt-level defense - -Tell the model "do not accept stale results" via system prompt. **Rejected:** The Coherence Collapse papers show that models cannot reliably enforce this — compaction removes the instruction, and the model has no way to verify instance identity without a control-plane check. - -### Alternative B: Compaction policy - -Prevent context drift by limiting compaction frequency or preserving original instructions. **Rejected as sole defense:** This helps but does not guarantee correctness; it's a prompt engineering approach that depends on model behavior, not an enforced contract. LoopX can still benefit from better compaction policies, but they are complementary, not a substitute. - -### Alternative C: Checkpoint-based rollback (Codex CLI / Anthropic approach) - -Save checkpoints and rewind on detection of drift. **Rejected as sole defense:** Checkpoints prevent data loss but do not prevent wrong-instance contamination — a checkpoint of A₁'s state cannot know that A₂ has replaced it. Checkpoint + CAS is better than either alone; checkpoint is a recovery mechanism, CAS is a prevention mechanism. - -### Alternative D: Semantic Certificate (this RFC's approach) - -Use immutable Goal identity + CAS as the coherence gate. **Selected.** LoopX already has the machinery; the gap is validation, not implementation. This is the smallest change that guarantees the invariant: zero new code paths, zero new state, targeted regression coverage. - -## 7. Safety, privacy, and compatibility - -- **Default-off parity:** This RFC changes no default behavior. All writes already pass through CAS; the new acceptance criteria only add test coverage for scenarios that should already fail safely. -- **Authorization:** No new authority. Instance isolation is enforced by the existing source registry; no model, agent, or operator can bypass CAS. -- **Public/private boundary:** No change. Goal identity, instance revision and rejection receipts are public-safe control-plane facts. -- **Legacy compatibility:** Existing Goals, Todos, claims and effect receipts continue to operate. Their `goal_instance_id` fields already exist (PR #5106, #5130) or are derived from the registry at runtime. No migration needed. -- **Mixed versions:** Old writers that do not populate `goal_instance_id` in their CAS basis will fail against a registry that requires it. This is fail-closed: the rejection receipt tells the caller to upgrade. No silent acceptance path exists. -- **Capacity and availability:** CAS overhead is unchanged. Instance identity check is an integer comparison on an already-loaded field. - -## 8. Migration and rollback - -**No migration required.** The fields and CAS machinery already exist. This RFC adds test coverage and documentation. - -**Rollback:** Remove the new regression tests. Existing behavior is unchanged. No data migration or downgrade path needed. - -## 9. Validation and acceptance - -### 9.1 Deterministic conformance - -| Claim | Test or evidence | Required result | Boundary | -| --- | --- | --- | --- | -| C1: Late Todo from old instance rejected | Goal A₁ → stop → create A₂ → commit_todo with A₁'s basis | CAS rejection; typed receipt with `goal_instance_id` mismatch; A₂'s Todo list unchanged | File authority store | -| C2: Late effect from old instance rejected | A₁ acquires lease, executes effect → A₁ stopped → A₂ created → effect receipt arrives | Receipt rejected; effect not double-counted; A₂'s effect ledger clean | File authority store; protected effects use simulated adapter | -| C3: Late claim from old instance rejected | A₁ holds claim → A₁ stopped → A₂ created → A₁'s claim renewal arrives | Renewal rejected; A₂ can acquire its own claim independently | File authority store | -| C4: Late plan from old instance rejected | Plan previewed against A₁ → A₁ stopped → A₂ created → plan confirmed | Confirmation rejected; A₂'s plan list unchanged | File authority store | -| C5: Concurrent Goal creation produces distinct instances | Two processes create Goal "X" simultaneously | Two distinct `goal_instance_id` values; each instance's writes are isolated | File authority store; process-level race | - -### 9.2 Live qualification (Goal A/B experiment reproduction) - -| Claim | Evidence | Required result | Boundary | -| --- | --- | --- | --- | -| C6: Arm D produces zero UCR | 128-episode run (4 tasks × 4 scenarios × 2 seeds × 4 arms) | UCR = 0 for Arm D; all other arms UCR ≥ 8 | Simulated model + real LoopX control plane | -| C7: Arm D produces zero wrong-instance pollution | Same 128-episode run | 0/32 wrong-instance writes for Arm D; ≥ 8/32 for Arms A/B/C | Same | -| C8: Two independent runs produce identical semantic results | Rerun with different random seed | Arm D: 32/32 correct in both runs; no qualitative difference in rejection patterns | Same | -| C9: Oracle tampering detected | 9 oracle mutant scenarios | All 9 detected; no false accept | Simulated oracle corruption | - -### 9.3 Industry connection validation - -| Claim | Evidence | Required result | Boundary | -| --- | --- | --- | --- | -| C10: Coherence Collapse scenario reproducible | Codex Desktop compaction scenario recreated against LoopX | LoopX Goal survives compaction with intact identity; agent re-reads original Goal from registry | Simulated compaction; agent behavior not tested with real model | -| C11: Constraint Weakening scenario defended | Handoff with natural-language-only context vs. structured GoalRef | Structured handoff preserves instance identity; natural-language-only loses it | Within same-Goal scope | - -## 10. Operational contract - -**Observability:** CAS rejection receipts for instance mismatch must be distinguishable from other rejection causes (permission, quota, concurrent write). The typed receipt includes `rejection_reason: "goal_instance_mismatch"`, `expected_instance_id`, and `actual_instance_id`. - -**Failure modes:** -- Instance mismatch → typed rejection receipt, caller decides next action. -- Registry unavailable → existing fail-closed behavior; no writes proceed. -- CAS race (two concurrent writes against same instance) → one succeeds, one gets concurrent-write rejection; instance identity unchanged for both. - -**No new capacity limits, backup requirements, or operator actions.** - -## 11. Normative delivery plan - -| Milestone | Shipped behavior | Entry gate | Exit evidence | Rollback | -| --- | --- | --- | --- | --- | -| M1: Coherence defense RFC | This document, accepted as Draft; industry analysis and Goal A/B experiment summary in normative sections | Maintainer review of sections 1–6 | Approved Draft status; roadmap Section 4 updated | N/A (document-only) | -| M2: Regression coverage | C1–C5 conformance tests pass on File authority store | M1 accepted | 5/5 tests pass; typed rejection receipts verified | Remove test file | -| M3: Experiment reproduction | C6–C9 reproduced in CI-amenable form (no raw model calls, no private data) | M2 complete | All 4 experiment claims pass in automated smoke | Remove smoke file | -| M4: Industry scenario smokes | C10–C11 compact smokes pass | M3 complete | Both scenarios produce correct output; no production code change | Remove smoke file | -| M5: RFC promotion | RFC moves from Draft → Accepted; roadmap updated | M1–M4 complete; 10-day soak with no regression | All acceptance rows green; maintainer approval | N/A | - -## 12. Open decisions - -| ID | Decision | Owner | Options | Recommendation | Evidence needed | Deadline | -| --- | --- | --- | --- | --- | --- | --- | -| D1 | Should M3 experiment reproduction use the full 128-episode run or a compact 8-episode smoke? | Maintainer | Full (10–15 min) vs. compact (< 30 sec) | Compact smoke for CI, full run for pre-release validation | CI budget impact of full run | M3 | -| D2 | Should `goal_instance_id` be added to the existing effect receipt schema, or derived from the Goal at receipt-validation time? | Effect receipt owner | Schema addition vs. runtime derivation | Runtime derivation (no schema change, no migration) | Audit of all receipt write paths | M2 | -| D3 | Should late-result rejection trigger an automatic retry on the new Goal instance, or require explicit agent action? | Collaboration owner | Auto-retry vs. explicit | Explicit: the agent must decide whether to re-submit. Auto-retry creates a new class of double-execution risk. | Agent behavior study | M4 | - ---- - -## Appendix A: Execution ledger (non-normative) - -### 2026-09-27 — RFC creation - -- **Baseline:** `fd96e5e25` -- **Delivered:** This document (Draft proposal) -- **Evidence:** Goal A/B experiment results (Appendix C); industry analysis (Appendix E) -- **Known gaps:** M2–M5 acceptance rows not yet executed -- **Effect on normative design:** none (initial creation) - -## Appendix B: Decision log - -| Date | Decision | Owner / approval | Alternatives | Normative sections changed | -| --- | --- | --- | --- | --- | -| | | | | | - -## Appendix C: Evidence registry — Goal A/B experiment - -> This appendix summarizes public-safe results from the controlled Goal A/B semantic-fault experiment. Raw episode traces and internal research notes are excluded. - -| Evidence id | Claim | Baseline / environment | Result | Privacy boundary | -| --- | --- | --- | --- | --- | -| E1 | Arm D achieves 32/32 scenario correctness | 128 episodes: 4 tasks × 4 fault scenarios × 2 seeds × 4 arms; real LoopX Turn journals | Pass: 32/32 correct | Public-safe summary; raw trajectories excluded | -| E2 | Arm D achieves 0 UCR | Same 128-episode run | Pass: UCR = 0 (A:16, B:16, C:8) | Same | -| E3 | Arm D achieves 0 duplicate side effects | Same | Pass: 0 duplicates (A:8, B:8, C:0) | Same | -| E4 | Arm D achieves 0 wrong-instance pollution | Same | Pass: 0 pollution (A:8, B:8, C:8) | Same | -| E5 | Arm D achieves 8/8 SRPA | Same | Pass: 8/8 (A:0, B:0, C:0) | Same | -| E6 | Two independent runs produce identical results | Rerun with different seed | Pass: both runs 32/32 for Arm D | Same | -| E7 | 9/9 oracle tampering scenarios detected | Oracle mutant injection | Pass: all detected | Same | -| E8 | 4/4 policy mutants detected | Policy mutation injection | Pass: all detected | Same | - -## Appendix D: Rejected or superseded alternatives - -### Prompt-level coherence instructions - -Adding "verify your Goal instance before writing" to the system prompt. Rejected because: (a) Coherence Collapse papers show model instructions are lost during compaction, (b) the model has no access to the CAS gate's instance revision, (c) this duplicates the control plane's responsibility in an unreliable layer. - -### Timeout-based instance fencing - -Reject writes if `current_time - goal_creation_time > TTL`. Rejected because: time-based fencing is coarse (a Goal may be active for days; a replacement can happen in seconds) and depends on clock synchronization. CAS-based fencing is precise: it compares the exact instance revision the write was computed against. - -### Automatic retry on new instance - -When a write is rejected for instance mismatch, automatically re-submit against the current instance. Rejected because: the new Goal instance may have different intent, constraints, or acceptance criteria. The agent must explicitly decide to re-submit. - -## Appendix E: Industry evidence — Coherence Collapse and related phenomena - -### E.1 Coherence Collapse (Codex / TRAJEVAL) - -**Source:** Kim et al., TRAJEVAL (arXiv:2603.24631, March 2026); Daniel Vaughan, "Coherence Collapse: Why Your Coding Agent Finds the Fix Then Destroys It" (July 2026); OpenAI Community report #1391211 (August 2026). - -**Finding:** 60–69% of SWE-Agent and OpenHands failures occur after the agent has located and edited the correct function. Compaction truncates the original acceptance goal; internal review notes and mock-backed tests become the de facto truth source. One observed case: 36 compactions, 58 subagent roles, hundreds of mock-backed tests passing while the real happy path failed. - -**LoopX relevance:** LoopX's immutable Goal identity and CAS on source registry directly prevent this. The agent's context can degrade, but writes must revalidate against the current Goal instance. No compaction can change the Goal's `goal_instance_id`. - -### E.2 Session/Harness/Sandbox Separation (Anthropic) - -**Source:** Anthropic Engineering Blog, "Scaling Managed Agents" (April 2026), "Effective Harnesses for Long-Running Agents" (November 2025). - -**Finding:** Separating the durable Session (append-only event log) from the stateless Harness (inference loop) and the Sandbox (execution environment) makes crash recovery zero-cost. Any Harness instance can resume any Session from the last event. - -**LoopX relevance:** LoopX's Goal/Todo/Claim three-layer projection is the same pattern at the control-plane level. Goal = durable intent, Todo = work projection, Claim = execution binding. The Goal survives harness replacement exactly as Anthropic's Session survives harness replacement. - -### E.3 Constraint Weakening in Agent Workflows - -**Source:** "When 'Must' Becomes 'Maybe'" (arXiv:2608.24569, August 2026). - -**Finding:** Natural-language handoff artifacts strip 87–100% of operational constraints. Restoring four structured fields (prerequisite, authority, fallback, consequence) brings preservation to 100%. - -**LoopX relevance:** LoopX's claim/lease and GoalRef are structured fields that survive handoff. A handoff carries the exact `goal_instance_id`, not a paraphrase. The Constraint Weakening paper provides independent validation of LoopX's typed-handoff design. \ No newline at end of file diff --git a/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.zh-CN.md b/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.zh-CN.md deleted file mode 100644 index c374515f10..0000000000 --- a/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.zh-CN.md +++ /dev/null @@ -1,340 +0,0 @@ -# RFC: Goal 不可变性作为一致性防御 (v0) - -- **RFC 状态:** 草案 -- **交付成熟度:** 提案 -- **作者/所有者:** LoopX 维护者 -- **创建日期:** 2026-09-27 -- **最后规范修订:** 2026-09-27 -- **实现基线:** `fd96e5e25` -- **语言镜像:** [English version](goal-immutability-coherence-defense-v0.md) -- **相关契约:** [Agent Loop Effect Interpreter](agent-loop-effect-interpreter-v0.md), [TypeScript Control-Plane Migration](typescript-control-plane-migration-v0.md), [Semantic Vocabulary Convergence](semantic-vocabulary-convergence-v0.md), [总体路线图](loopx-overall-roadmap-v0.md), [Capable Manager and Semantic Handoff](capable-manager-semantic-handoff-v0.md), [Goal Direction Baseline](goal-direction-baseline-v0.md) - -## 文档图与维护契约 - -第 1–10 节是持久性设计与验收契约。第 11 节是规范性交付计划。第 12 节包含未解决的决策。附录包含非规范性执行台账、决策日志、证据注册表、被拒绝的替代方案和事故教训。中英文版本互为语义镜像。 - ---- - -## 1. 决策摘要 - -1. **Goal 不可变性成为显式的、经过测试的架构保证**,而非实现惯例。Goal 的身份、意图修订、权限绑定和验收基础,不得在压缩(compaction)、会话重启或 Agent 替换过程中悄然改变。 - -2. **Goal 实例隔离(GoalRef `{goal_id, goal_instance_id}`)从实现细节提升为一致性防御机制。** 当新 Goal 复用名称时,旧实例被隔离(fenced);旧实例的迟到结果绝不得污染新实例。 - -3. **现有的 source registry 上的 CAS(Compare-And-Swap)成为防御一致性崩溃的第一道防线**,与其当前的原子状态转换职责并列。每次改变 Goal 状态的写入都必须对当前 Goal 实例重新验证其基础。 - -4. **现有的 claim、lease、Todo 投影和 effect receipt 契约不变。** 此 RFC 不添加新的状态模式、新的 provider 或新的权限。它定义了现有架构已满足的验收标准,并补充缺失的一致性证据。 - -5. **此 RFC 不批准**新的 Goal 表示格式、分布式共识协议、人工关注门禁,或任何模型提示变更。它是通过现有机制加上有针对性的回归覆盖来实现的纵深防御契约。 - -## 2. 问题与动机 - -### 一致性崩溃问题 - -2026 年,AI Agent 行业识别出一种系统性的失败模式:**Agent 找到了正确答案,然后亲手毁掉了它。** - -三条独立的证据线在此交汇: - -| 来源 | 发现 | 机制 | -| --- | --- | --- | -| TRAJEVAL(Kim et al., 2026)/ Codex Desktop 社区报告 | 60–69% 的 SWE-Agent 和 OpenHands 失败发生在 Agent 已经定位并编辑了正确函数**之后** | 压缩截断了原始验收目标;内部审阅笔记和基于 mock 的测试成为事实上的真相来源 | -| Anthropic Managed Agents(2026) | 长运行期间的 harness 状态丢失导致 Agent "猜测"已完成的工作;过早完成声明是一级失败模式 | Session 事件必须是唯一的持久真相;harness 实例是可替换的 | -| Agent 工作流中的约束弱化(arXiv:2608.24569) | 交接(handoff)转换从自然语言工件中剥离 87–100% 的操作约束 | 结构化字段(前提条件、权限、回退、后果)必须在每次交接中存续 | - -共同的根本原因:**当 Agent 的工作上下文漂移时,运行环境中没有任何东西将其重新锚定到原始目标。** 模型被要求遵循其上下文窗口中已不再存在的指令。 - -### 为什么 LoopX 已经具备这一防御 - -LoopX 的架构嵌入了三项直接防止一致性崩溃的属性: - -1. **不可变的 Goal 身份。** Goal 的 `goal_id + goal_instance_id` 在创建时分配且永不被重写。压缩不能重命名 Goal、合并两个 Goal 或丢失实例边界。Agent 的"工作记忆"可以退化,但控制面拒绝引用错误实例的写入。 - -2. **source registry 上的 CAS。** 每次状态转换都会重新读取并重新验证当前 Goal 修订版本。一个过时的写入——基于旧 Goal 实例计算的——会因实例修订版本已变更而 CAS 失败。这不是模型行为,而是强制执行的机器契约。 - -3. **类型化的 Todo 投影。** 工作承诺被保存为结构化记录,而非 Markdown 段落。压缩不能悄然将某项承诺编辑成另一项承诺;Todo 写入器验证身份和 lane 分配。 - -这些属性已存在,但尚未作为一致性防御进行验证。Goal A/B 迟到结果实验(第 9 节,附录 C)提供了首个量化证据。 - -### Goal A/B 实验结果 - -一项受控的 128-episode 实验注入了语义故障——来自已被替换的 Goal 的迟到结果——并测量了四种恢复策略: - -| Arm | 策略 | 正确 / 32 | UCR ↓ | 重复副作用 | 错实例污染 | SRPA | -| --- | --- | ---: | ---: | ---: | ---: | ---: | -| A | Resume/Retry(朴素重启) | 8 | 16 | 8 | 8 | 0/8 | -| B | Aligned Rollback(基于检查点) | 16 | 16 | 8 | 8 | 0/8 | -| C | Conditional Reflection(启发式恢复) | 24 | 8 | 0 | 8 | 0/8 | -| **D** | **Semantic Certificate(GoalRef + CAS)** | **32** | **0** | **0** | **0** | **8/8** | - -Arm D——使用基于 GoalRef 的实例隔离和 CAS 验证——实现了零意外状态变更、零重复副作用和零错实例污染。所有其他 arm 即使避免了重复效应,也产生了至少 8 次错实例污染。 - -实验使用了真实的 LoopX Turn 日志、跨进程恢复和生产级 Goal 实例切换。Oracle 篡改和策略突变体检测练习确认了结果并非特定故障集的假象。 - -### 不变量 - -1. Goal 实例一旦创建,就具有稳定的 `goal_instance_id`,任何压缩、重启或交接都不能改变它。 -2. 每次状态变更写入都必须在写入被接受之前,对当前 source registry 重新验证其基础(`goal_instance_id` + revision)。 -3. 来自旧 Goal 实例的迟到结果必须 CAS 失败并产生可见的拒绝回执——绝不被静默接受,也绝不被静默丢弃。 -4. Agent 的工作上下文(模型提示、内部笔记、压缩产物)不是 Goal 身份或验收的真相来源。 -5. 现有的 claim、lease、effect receipt 和 Todo 投影契约继续不变地运行。 - -## 3. 范围与非目标 - -### 范围内 - -- 将 Goal 不可变性和实例隔离形式化为经过测试的架构保证。 -- 为一致性崩溃场景添加回归覆盖:迟到结果、实例替换、压缩导致的漂移、并发冲突写入。 -- 将现有 CAS 机制文档化为一致性防御,附以 Goal A/B 实验和新的定向冒烟测试的验收证据。 -- 将 LoopX 的防御与行业问题空间(一致性崩溃、约束弱化、Session 作为真相来源)连接起来。 - -### 非目标 - -- 新的 Goal schema、持久化格式或 provider。 -- 分布式共识协议或跨主机一致性保证(R6 范围)。 -- 人工在回路中的审批门禁(单独 RFC 领域)。 -- 模型提示工程或压缩策略变更。 -- 替换现有 claim/lease/quota/effect receipt 机制。 -- Agent 输出的通用"语义正确性"——此 RFC 防御身份级别的污染,而非模型推理质量。 - -## 4. 当前系统契约 - -在基线 `fd96e5e25`,以下与一致性相关的机制已存在: - -| 组件 | 当前行为 | 一致性相关性 | -| --- | --- | --- | -| GoalRef `{goal_id, goal_instance_id}` | PR #5106:协作请求绑定到精确实例;PR #5130:聊天会话绑定到精确实例 | 在 Goal 替换后阻止旧实例写入 | -| Source registry CAS | `preview` 阶段缓存 digest;`commit` 阶段验证;拒绝过期写入 | 每次状态转换的原子基础验证 | -| Goal 实例生命周期 | `stop` → `resume` / `replace` 保留实例身份;同名新 Goal 获得新的 `goal_instance_id` | 实例边界在进程重启后存活 | -| Turn journal 重放 | PR #5139:TypeScript 中的 Turn 接受和重放;`client_turn_id` 去重 | 防止效应双重执行 | -| Todo 投影 | 带操作回执的结构化 lane 分配(R1 checkpoint) | 承诺在模型上下文丢失后存活 | -| Effect receipt | 带去重身份的带类型结算记录 | 防止重复的受保护效应 | - -**缺口:** 这些组件中没有一个作为协调系统针对一致性崩溃场景进行测试。各个部分存在并通过了各自的单元测试,但没有集成级测试验证 Goal 替换是否能端到端地(CAS → effect receipt → Todo 投影 → Turn journal)隔离旧实例写入。 - -## 5. 提议架构 - -### 5.1 所有权与权限 - -**source registry**(TypeScript `AuthorityStore` / Python `GoalRegistry`)是 Goal 身份和实例修订的唯一所有者。没有模型输出、压缩产物、交接摘要或 Agent 自我报告可以改变 Goal 的 `goal_instance_id` 或接受对过期实例的写入。 - -**CAS 验证器**(现有 `preview` → `commit` 管线)是状态变更写入的唯一门禁。它在每次提交前检查 `goal_instance_id` 和 revision。 - -**Turn journal** 和 **effect receipt** 所有者不变。它们消费 CAS 门禁的决策;不独立确定实例有效性。 - -### 5.2 一致性防御模型 - -``` -Agent 针对 Goal 实例 A₁ 产生结果 R - ↓ -A₁ 被停止;创建 Goal A₂(同名,新实例) - ↓ -R 迟到到达,目标为 A₁ - ↓ -CAS 门禁:当前实例为 A₂,revision > R 的基础 - ↓ -写入被拒绝 → 可见的拒绝回执 - ↓ -A₂ 的状态未被污染 -``` - -这不是新的代码路径。这是现有 CAS 机制在当前测试套件未覆盖的一致性崩溃场景下的执行。 - -### 5.3 状态模型(无 schema 变更) - -不引入新字段或 schema。参与其中的现有类型: - -```typescript -// 现有 — 现文档化为一致性关键 -type GoalRef = { - goal_id: string; - goal_instance_id: string; // 一致性隔离边界:必须匹配当前实例 -}; - -// 现有 — CAS 基础携带实例身份 -type WriteBasis = { - goal_instance_id: string; // 写入计算时所依据的实例 - revision: number; // 计算时的修订版本 -}; -``` - -唯一变化是 `goal_instance_id` 现被文档化为**一致性隔离边界**,CAS 门禁的拒绝路径在实例不匹配为原因时必须产生类型化的回执(而非通用错误)。 - -### 5.4 命令生命周期(无新命令) - -现有写入路径(Todo 提交、effect 结算、claim 获取、plan 确认)已通过 CAS。验收标准为每条路径添加负面测试用例: - -| 写入路径 | 一致性负面用例 | -| --- | --- | -| `commit_todo` | Todo 基于 A₁ 计算,在 A₂ 替换 A₁ 后提交 | -| `settle_effect` | Effect 在 A₁ 的 lease 下执行,回执在 A₂ 的 lease 启动后到达 | -| `acquire_claim` | Claim 以 A₁ 的身份请求,在 A₂ 激活后到达 | -| `confirm_plan` | Plan 基于 A₁ 的 registry 预览,在 A₂ 创建后确认 | - -每个负面用例必须产生命名 `goal_instance_id` 不匹配的类型化拒绝回执,而非通用失败。 - -### 5.5 Provider 契约(无变化) - -此 RFC 不添加 provider。现有的 File/SQLite authority store 已实现 CAS。一致性防御是 CAS 契约的属性,而非存储后端的属性。 - -## 6. 替代方案与设计选择 - -### 替代方案 A:提示级别的防御 - -通过系统提示告诉模型"不要接受过时的结果"。**已拒绝:** 一致性崩溃论文表明模型无法可靠地执行此操作——压缩会移除此指令,且模型无法在没有控制面检查的情况下验证实例身份。 - -### 替代方案 B:压缩策略 - -通过限制压缩频率或保留原始指令来防止上下文漂移。**作为唯一防御已拒绝:** 这有帮助但不能保证正确性;这是一种依赖模型行为的提示工程方法,而非强制执行的契约。LoopX 仍可从更好的压缩策略中受益,但它们是互补的,而非替代。 - -### 替代方案 C:基于检查点的回滚(Codex CLI / Anthropic 方案) - -保存检查点并在检测到漂移时回滚。**作为唯一防御已拒绝:** 检查点防止数据丢失但不防止错实例污染——A₁ 状态的检查点不可能知道 A₂ 已替换了它。检查点 + CAS 比单独任何一方都好;检查点是恢复机制,CAS 是预防机制。 - -### 替代方案 D:语义证书(此 RFC 的方案) - -使用不可变 Goal 身份 + CAS 作为一致性门禁。**已选择。** LoopX 已拥有该机制;缺口在于验证而非实现。这是保证不变量的最小变更:零新代码路径、零新状态、定向回归覆盖。 - -## 7. 安全性、隐私和兼容性 - -- **默认关闭对等性:** 此 RFC 不改变任何默认行为。所有写入已通过 CAS;新的验收标准仅为应已安全失败的场景添加测试覆盖。 -- **授权:** 无新权限。实例隔离由现有 source registry 执行;任何模型、Agent 或操作员都不能绕过 CAS。 -- **公共/私有边界:** 无变化。Goal 身份、实例修订和拒绝回执是公共安全的控制面事实。 -- **旧版兼容性:** 现有的 Goals、Todos、claims 和 effect receipts 继续运行。它们的 `goal_instance_id` 字段已存在(PR #5106、#5130)或从 registry 在运行时派生。无需迁移。 -- **混合版本:** 未在其 CAS 基础中填充 `goal_instance_id` 的旧写入器将因 registry 要求该字段而失败。这是故障关闭(fail-closed):拒绝回执告知调用方升级。不存在静默接受路径。 -- **容量与可用性:** CAS 开销不变。实例身份检查是对已加载字段的整数比较。 - -## 8. 迁移与回滚 - -**无需迁移。** 字段和 CAS 机制已存在。此 RFC 添加测试覆盖和文档。 - -**回滚:** 移除新的回归测试。现有行为不变。无需数据迁移或降级路径。 - -## 9. 验证与验收 - -### 9.1 确定性一致性 - -| 声明 | 测试或证据 | 要求结果 | 边界 | -| --- | --- | --- | --- | -| C1:旧实例的迟到 Todo 被拒绝 | Goal A₁ → stop → create A₂ → 以 A₁ 的基础 commit_todo | CAS 拒绝;带有 `goal_instance_id` 不匹配的类型化回执;A₂ 的 Todo 列表不变 | File authority store | -| C2:旧实例的迟到 effect 被拒绝 | A₁ 获取 lease,执行 effect → A₁ 停止 → A₂ 创建 → effect 回执到达 | 回执被拒绝;effect 未重复计数;A₂ 的 effect 账本干净 | File authority store;受保护 effect 使用模拟适配器 | -| C3:旧实例的迟到 claim 被拒绝 | A₁ 持有 claim → A₁ 停止 → A₂ 创建 → A₁ 的 claim 续约到达 | 续约被拒绝;A₂ 可以独立获取自己的 claim | File authority store | -| C4:旧实例的迟到 plan 被拒绝 | Plan 基于 A₁ 预览 → A₁ 停止 → A₂ 创建 → plan 被确认 | 确认被拒绝;A₂ 的 plan 列表不变 | File authority store | -| C5:并发的 Goal 创建产生不同实例 | 两个进程同时创建 Goal "X" | 两个不同的 `goal_instance_id`;每个实例的写入相互隔离 | File authority store;进程级竞态 | - -### 9.2 实际验证(Goal A/B 实验复现) - -| 声明 | 证据 | 要求结果 | 边界 | -| --- | --- | --- | --- | -| C6:Arm D 产生零 UCR | 128-episode 运行(4 tasks × 4 scenarios × 2 seeds × 4 arms) | Arm D 的 UCR = 0;所有其他 arm UCR ≥ 8 | 模拟模型 + 真实 LoopX 控制面 | -| C7:Arm D 产生零错实例污染 | 同一 128-episode 运行 | Arm D 的错实例写入 0/32;Arms A/B/C ≥ 8/32 | 同上 | -| C8:两次独立运行产生相同语义结果 | 使用不同随机种子重新运行 | Arm D:两次运行均 32/32 正确;拒绝模式无定性差异 | 同上 | -| C9:Oracle 篡改被检出 | 9 个 oracle 突变场景 | 全部 9 个被检出;无误接受 | 模拟 oracle 损坏 | - -### 9.3 行业连接验证 - -| 声明 | 证据 | 要求结果 | 边界 | -| --- | --- | --- | --- | -| C10:一致性崩溃场景可复现 | 针对 LoopX 重建 Codex Desktop 压缩场景 | LoopX Goal 在压缩后以完整身份存续;Agent 从 registry 重新读取原始 Goal | 模拟压缩;Agent 行为未使用真实模型测试 | -| C11:约束弱化场景被防御 | 仅自然语言上下文的交接 vs. 结构化 GoalRef | 结构化交接保留实例身份;仅自然语言交接丢失实例身份 | 同一 Goal 范围内 | - -## 10. 运维契约 - -**可观测性:** 实例不匹配的 CAS 拒绝回执必须可与其他拒绝原因(权限、配额、并发写入)区分。类型化回执包含 `rejection_reason: "goal_instance_mismatch"`、`expected_instance_id` 和 `actual_instance_id`。 - -**故障模式:** -- 实例不匹配 → 类型化拒绝回执,调用方决定下一步操作。 -- Registry 不可用 → 现有故障关闭行为;不进行任何写入。 -- CAS 竞态(两个并发写入针对同一实例)→ 一个成功,一个获得并发写入拒绝;实例身份对两者均不变。 - -**无新的容量限制、备份要求或运维人员操作。** - -## 11. 规范交付计划 - -| 里程碑 | 交付行为 | 进入门禁 | 退出证据 | 回滚 | -| --- | --- | --- | --- | --- | -| M1:一致性防御 RFC | 本文档,作为草案接受;行业分析和 Goal A/B 实验摘要位于规范章节 | 维护者对第 1–6 节的评审 | 批准的草案状态;路线图第 4 节更新 | 不适用(仅文档) | -| M2:回归覆盖 | C1–C5 一致性测试在 File authority store 上通过 | M1 已接受 | 5/5 测试通过;类型化拒绝回执已验证 | 移除测试文件 | -| M3:实验复现 | C6–C9 以 CI 可接受的形式复现(无原始模型调用,无私有数据) | M2 完成 | 全部 4 项实验声明在自动化冒烟中通过 | 移除冒烟文件 | -| M4:行业场景冒烟 | C10–C11 紧凑冒烟通过 | M3 完成 | 两个场景均产生正确输出;无生产代码变更 | 移除冒烟文件 | -| M5:RFC 晋升 | RFC 从草案移至已接受;路线图更新 | M1–M4 完成;10 天浸泡无回归 | 所有验收行绿色;维护者批准 | 不适用 | - -## 12. 待解决决策 - -| ID | 决策 | 所有者 | 选项 | 建议 | 所需证据 | 截止日期 | -| --- | --- | --- | --- | --- | --- | --- | -| D1 | M3 实验复现应使用完整的 128-episode 运行还是紧凑的 8-episode 冒烟? | 维护者 | 完整(10–15 分钟)vs. 紧凑(< 30 秒) | CI 用紧凑冒烟,发布前验证用完整运行 | 完整运行的 CI 预算影响 | M3 | -| D2 | `goal_instance_id` 应添加到现有的 effect receipt schema 中,还是在 receipt 验证时从 Goal 派生? | Effect receipt 所有者 | Schema 添加 vs. 运行时派生 | 运行时派生(无 schema 变更,无迁移) | 所有 receipt 写入路径的审计 | M2 | -| D3 | 迟到结果拒绝应触发对新 Goal 实例的自动重试,还是需要显式 Agent 操作? | Collaboration 所有者 | 自动重试 vs. 显式 | 显式:Agent 必须决定是否重新提交。自动重试会创造新的双重执行风险。 | Agent 行为研究 | M4 | - ---- - -## 附录 A:执行台账(非规范) - -### 2026-09-27 — RFC 创建 - -- **基线:** `fd96e5e25` -- **交付:** 本文档(草案提案) -- **证据:** Goal A/B 实验结果(附录 C);行业分析(附录 E) -- **已知缺口:** M2–M5 验收行尚未执行 -- **对规范设计的影响:** 无(初始创建) - -## 附录 B:决策日志 - -| 日期 | 决策 | 所有者 / 批准 | 替代方案 | 变更的规范章节 | -| --- | --- | --- | --- | --- | -| | | | | | - -## 附录 C:证据注册表 — Goal A/B 实验 - -> 本附录总结受控 Goal A/B 语义故障实验的公共安全结果。原始 episode 轨迹和内部研究笔记已排除。 - -| 证据 ID | 声明 | 基线/环境 | 结果 | 隐私边界 | -| --- | --- | --- | --- | --- | -| E1 | Arm D 达到 32/32 场景正确性 | 128 episode:4 tasks × 4 fault scenarios × 2 seeds × 4 arms;真实 LoopX Turn 日志 | 通过:32/32 正确 | 公共安全摘要;原始轨迹已排除 | -| E2 | Arm D 达到 0 UCR | 同一 128-episode 运行 | 通过:UCR = 0(A:16, B:16, C:8) | 同上 | -| E3 | Arm D 达到 0 重复副作用 | 同上 | 通过:0 重复(A:8, B:8, C:0) | 同上 | -| E4 | Arm D 达到 0 错实例污染 | 同上 | 通过:0 污染(A:8, B:8, C:8) | 同上 | -| E5 | Arm D 达到 8/8 SRPA | 同上 | 通过:8/8(A:0, B:0, C:0) | 同上 | -| E6 | 两次独立运行产生相同结果 | 使用不同种子重新运行 | 通过:Arm D 两次运行均 32/32 | 同上 | -| E7 | 9/9 Oracle 篡改场景被检出 | Oracle 突变注入 | 通过:全部检出 | 同上 | -| E8 | 4/4 策略突变体被检出 | 策略突变注入 | 通过:全部检出 | 同上 | - -## 附录 D:被拒绝或取代的替代方案 - -### 提示级别的一致性指令 - -将"写入前验证你的 Goal 实例"添加到系统提示。已拒绝,因为:(a) 一致性崩溃论文表明模型指令在压缩过程中丢失,(b) 模型无法访问 CAS 门禁的实例修订版本,(c) 这在不可靠的层中重复了控制面的职责。 - -### 基于超时的实例隔离 - -如果 `current_time - goal_creation_time > TTL` 则拒绝写入。已拒绝,因为:基于时间的隔离是粗糙的(Goal 可能活跃数天;替换可能在数秒内发生)且依赖时钟同步。基于 CAS 的隔离是精确的:它比较写入计算时的确切实例修订版本。 - -### 对新实例的自动重试 - -当写入因实例不匹配被拒绝时,自动对当前实例重新提交。已拒绝,因为:新 Goal 实例可能有不同的意图、约束或验收标准。Agent 必须显式决定重新提交。 - -## 附录 E:行业证据 — 一致性崩溃及相关现象 - -### E.1 一致性崩溃(Codex / TRAJEVAL) - -**来源:** Kim et al., TRAJEVAL(arXiv:2603.24631,2026 年 3 月);Daniel Vaughan, "Coherence Collapse: Why Your Coding Agent Finds the Fix Then Destroys It"(2026 年 7 月);OpenAI Community 报告 #1391211(2026 年 8 月)。 - -**发现:** 60–69% 的 SWE-Agent 和 OpenHands 失败发生在 Agent 已经定位并编辑了正确函数之后。压缩截断了原始验收目标;内部审阅笔记和基于 mock 的测试成为事实上的真相来源。一个观察到的案例:36 次压缩,58 个 subagent 角色,数百个基于 mock 的测试通过而真实的 happy path 却失败了。 - -**LoopX 相关性:** LoopX 的不可变 Goal 身份和 source registry 上的 CAS 直接防止了这一点。Agent 的上下文可以退化,但写入必须对当前 Goal 实例重新验证。任何压缩都不能改变 Goal 的 `goal_instance_id`。 - -### E.2 Session/Harness/Sandbox 分离(Anthropic) - -**来源:** Anthropic Engineering Blog, "Scaling Managed Agents"(2026 年 4 月),"Effective Harnesses for Long-Running Agents"(2025 年 11 月)。 - -**发现:** 将持久化的 Session(仅追加的事件日志)与无状态的 Harness(推理循环)和 Sandbox(执行环境)分离,使崩溃恢复零成本。任何 Harness 实例都可以从最后一条事件恢复任何 Session。 - -**LoopX 相关性:** LoopX 的 Goal/Todo/Claim 三层投影在控制面层面是相同的模式。Goal = 持久化意图,Todo = 工作投影,Claim = 执行绑定。Goal 在 harness 替换后存续,正如 Anthropic 的 Session 在 harness 替换后存续。 - -### E.3 Agent 工作流中的约束弱化 - -**来源:** "When 'Must' Becomes 'Maybe'"(arXiv:2608.24569,2026 年 8 月)。 - -**发现:** 自然语言交接工件剥离 87–100% 的操作约束。恢复四个结构化字段(前提条件、权限、回退、后果)将保留率提升至 100%。 - -**LoopX 相关性:** LoopX 的 claim/lease 和 GoalRef 是在交接中存续的结构化字段。交接携带确切的 `goal_instance_id`,而非复述。约束弱化论文为 LoopX 的类型化交接设计提供了独立验证。 \ No newline at end of file diff --git a/docs/reference/authority-operation-replay.md b/docs/reference/authority-operation-replay.md new file mode 100644 index 0000000000..d1c289dc99 --- /dev/null +++ b/docs/reference/authority-operation-replay.md @@ -0,0 +1,78 @@ +# Authority operation replay + +File and SQLite accept a direct retry of an already committed operation when +its complete canonical body matches the original transaction. The body is +`next_projection`, `events` and `receipts`; JSON object key order is not intent. +`operation_id` selects that transaction within the opened Goal store. + +This supports retry after a lost response. It does not create another state +transition, append events again, restore an old head or authorize another +external effect. If A committed, then B committed, replaying A returns A's +original cursor/provider revision while B remains the current head. + +## Commit and recovery contract + +| Case | File / SQLite | PostgreSQL / NoKV | +| --- | --- | --- | +| New operation, current CAS basis | Commit atomically | Commit atomically | +| New operation, stale CAS basis | `provider_revision_mismatch` | `provider_revision_mismatch` | +| Existing operation, identical body | `applied` with original cursor/revision | Ordinary commit remains a conflict; recover via `readReceipt` | +| Existing operation, different body | `operation_id_exists` | Conflict; normal revision-check precedence remains | + +For a verified historical retry, File/SQLite do not require the caller's CAS +basis to remain current: they are returning a historical fact, not admitting a +new write. Validation, store identity/existing-only admission and the current +store integrity checks still apply. An invalid request fails before replay. +A matching operation with a changed projection, event or receipt is never +acknowledged as the original commit. + +File reconstructs the original projection using the retained journal. SQLite +verifies the original checkpoint/delta window in the **same write transaction** +before acknowledging a replay. Comparing the caller to a stored digest alone +is insufficient: a damaged retained receipt/event must fail its own proof. +No full-history audit is added to ordinary retry. Digests detect inconsistent +bytes, not an administrator who rewrites both data and proof. + +`CoordinationCommandReceipt` remains the provider-neutral business recovery +owner. It reads and validates the original command receipt even when a provider +returns `applied`, and reconciles conflict or ambiguous responses. Do not remove +that readback: `applied` can describe a historical commit, and the other +providers retain their existing direct-commit behavior. NoKV's ambiguous-write +readback recovery is distinct from its ordinary `commitAuthority` contract. + +This changes File/SQLite's previous duplicate-commit rejection behavior. +Concurrent identical lease renewals can now both report `applied`, while only +one renewal/version transition is persisted. Business recovery still validates +request identity; a historical receipt never grants current lease authority. +There is no feature flag, new request field, storage migration or frontend +configuration. Reverting requires reverting the provider behavior, not merely +removing tests. Existing durable receipts keep their original format. + +## Scope and verification + +The shared-authority RFC owns this storage/recovery boundary. Goal lifetime +identity, lease epochs and provider revisions remain separate contracts. +This change does not implement Goal replacement isolation, semantic correctness +of model output, default provider activation or long-horizon qualification. + +`authority_operation_replay_conformance.ts` runs on both File and SQLite. It +checks full body drift, canonical key ordering, historical replay and concurrent +same-operation attempts against complete head, receipt and history readback. +SQLite adds retained-row corruption cases that must refuse replay without +changing durable rows. Existing real-process lease renewal and command receipt +suites cover the business entrypoint above these providers. + +## 中文说明 + +File/SQLite 现在可以直接重试已成功提交的操作:操作 ID 相同,而且完整的 +projection、events、receipts 相同,才返回原 cursor/revision。A 提交后 B 又提交, +重试 A 只确认 A 的历史结果,不把当前状态退回 A,也不再追加事件。 + +这不是绕过新写入的 CAS。新操作仍须匹配当前版本;历史重试则须证明原事务。 +SQLite 在同一事务内校验对应 checkpoint/delta 窗口,不能只比较数据库中保存的 +摘要字段。历史回执或事件损坏时必须拒绝,不能报告成功。 + +上层 `CoordinationCommandReceipt` 仍须读回并校验业务回执,处理响应丢失及不确定 +提交;PostgreSQL/NoKV 的普通重复提交仍返回冲突。并发同意图续约可能从过去的 +`applied/recovered` 变成 `applied/applied`,但实际只写入一次。历史成功不授予当前 +lease 执行权。此变更没有新配置或存储格式,也不证明 Goal 实例隔离或模型语义正确。 diff --git a/docs/reference/sqlite-authority-store.md b/docs/reference/sqlite-authority-store.md index 1452f72795..4ae0c8781d 100644 --- a/docs/reference/sqlite-authority-store.md +++ b/docs/reference/sqlite-authority-store.md @@ -63,6 +63,10 @@ rotation, corruption repair, or network-filesystem sharing is supported. ## Read integrity +Direct retries follow the [authority operation replay contract](authority-operation-replay.md): +matching full intent returns its verified original position without a new write. + + Authority reads share one SQLite snapshot, and writes run the same live proof inside their transaction before publishing a new commit row. The proof is layered so that each layer pays only for what it returns: diff --git a/loopx/control_plane/coordination/sqlite_authority_store.ts b/loopx/control_plane/coordination/sqlite_authority_store.ts index 3facf54722..d7746c50ed 100644 --- a/loopx/control_plane/coordination/sqlite_authority_store.ts +++ b/loopx/control_plane/coordination/sqlite_authority_store.ts @@ -463,22 +463,22 @@ export class SqliteAuthorityStore implements AuthorityStore { const cursor = current?.state.cursor ?? null; const revision = current?.provider_revision ?? null; let conflict: "provider_revision_mismatch" | "operation_id_exists" | null = null; - // Content-aware idempotency: same operation_id with matching body (via - // commit_digest) returns the original receipt; the check precedes the - // revision gate so an already-committed replay is never blocked. - const existingRow = db.prepare("SELECT cursor, operation_id, commit_digest FROM commits WHERE operation_id = ?").get(normalized.operation_id); + // Replay proves the retained transaction in this same write snapshot. + // A matching stored digest alone is not evidence that its row is intact. + const existingRow = db.prepare(`SELECT ${COMMIT_COLUMNS} FROM commits WHERE operation_id = ?`) + .get(normalized.operation_id); if (existingRow) { - const existingCursorValue = existingRow.cursor; - if (existingCursorValue === null || typeof existingCursorValue !== "number" && typeof existingCursorValue !== "bigint") { - db.exec("ROLLBACK"); transactionOpen = false; - return {status: "failed", reason_code: "provider_protocol_violation", reason: "corrupt cursor in commits"}; + const retained = this.decodeCommitRow(existingRow); + const window = this.verifiedRange(db, retained.cursor, retained.cursor); + const original = window.transactions[0]; + if (!original || original.operation_id !== normalized.operation_id) { + protocol("SQLite replay is not part of its retained window"); } - const existingCursor = BigInt(existingCursorValue); - const identity = current?.identity ?? this.identity(db); - const digest = commitDigest(identity, existingCursor, normalized.operation_id, normalized.next_projection, normalized.events, normalized.receipts); - if (digest === existingRow.commit_digest) { + const digest = commitDigest(window.identity, retained.cursor, normalized.operation_id, + normalized.next_projection, normalized.events, normalized.receipts); + if (digest === retained.commit_digest) { db.exec("ROLLBACK"); transactionOpen = false; - return {status: "applied", provider_revision: `${identity}:${existingCursor}`, cursor: existingCursor.toString()}; + return {status: "applied", provider_revision: original.provider_revision, cursor: original.cursor}; } conflict = "operation_id_exists"; } diff --git a/tests/control_plane_ts/authority_operation_replay_conformance.ts b/tests/control_plane_ts/authority_operation_replay_conformance.ts new file mode 100644 index 0000000000..f6f4db8689 --- /dev/null +++ b/tests/control_plane_ts/authority_operation_replay_conformance.ts @@ -0,0 +1,65 @@ +/** Direct local-provider replay: full intent matches a verified historical commit. + * This is not Goal-instance isolation or permission to execute another effect. */ +import assert from "node:assert/strict"; +import test from "node:test"; +import type {AuthorityStoreConformanceFactory} from "./authority_store_conformance.ts"; +import {authorityStoreCommitFixture as commit} from "./authority_store_conformance.ts"; + +export function registerAuthorityOperationReplayConformance( + provider: string, factory: AuthorityStoreConformanceFactory, +): void { + test(`${provider}: historical operation replay preserves full intent and later state`, async t => { + const {store, contender} = await factory(t); + const input = commit(null, "original", 1, 1); + input.next_projection.metadata = {labels: ["retained", "中"], nested: {value: 7}}; + const first = await store.commitAuthority(input); + assert.equal(first.status, "applied"); if (first.status !== "applied") return; + const second = await store.commitAuthority(commit(first.provider_revision, "later", 2, 2)); + assert.equal(second.status, "applied"); if (second.status !== "applied") return; + const snapshot = async () => ({head: await contender.loadAuthority(), + receipt: await contender.readReceipt("original"), history: await contender.scanCommitted(null, 10)}); + const before = await snapshot(); + for (const basis of [null, second.provider_revision]) { + for (const change of ["none", "key_order", "projection", "events", "receipts"] as const) { + const replay = structuredClone(input); + replay.expected_provider_revision = basis; + if (change === "key_order") replay.next_projection = Object.fromEntries(Object.entries(replay.next_projection).reverse()); + if (change === "projection") replay.next_projection.metadata = {labels: ["different"]}; + if (change === "events") replay.events = [{type: "different"}]; + if (change === "receipts") replay.receipts = [{result: "different"}]; + const result = await contender.commitAuthority(replay); + if (change === "none" || change === "key_order") assert.deepEqual(result, first); + else { + assert.equal(result.status, "conflict", `${change}-only drift must be rejected`); + if (result.status === "conflict") assert.equal(result.conflict_kind, "operation_id_exists"); + } + assert.deepEqual(await snapshot(), before, `${change} with basis ${basis} changed committed state`); + } + } + }); + + for (const matching of [true, false]) { + test(`${provider}: concurrent ${matching ? "matching" : "different"} intent never double commits`, async t => { + const {store, contender} = await factory(t); + const first = commit(null, "racing-operation", 1, 1); + const second = structuredClone(first); + if (!matching) second.next_projection.authority_revision = 2; + const results = await Promise.all([store.commitAuthority(first), contender.commitAuthority(second)]); + const applied = results.filter(result => result.status === "applied"); + assert.equal(applied.length, matching ? 2 : 1); + if (matching) assert.deepEqual(results[0], results[1]); + else { + const rejected = results.find(result => result.status === "conflict"); + assert.equal(rejected?.conflict_kind, "operation_id_exists"); + } + assert.deepEqual(await store.loadAuthority(), await contender.loadAuthority()); + const page = await contender.scanCommitted(null, 10); + assert.equal(page.status, "page"); if (page.status !== "page") return; + assert.equal(page.transactions.length, 1); + const winner = results[0]!.status === "applied" ? first : second; + assert.deepEqual(page.transactions[0]!.projection, winner.next_projection); + assert.deepEqual(page.transactions[0]!.receipts, winner.receipts); + assert.deepEqual(page.transactions[0]!.events, winner.events); + }); + } +} diff --git a/tests/control_plane_ts/authority_store.test.ts b/tests/control_plane_ts/authority_store.test.ts index 730e521fec..318743a33d 100644 --- a/tests/control_plane_ts/authority_store.test.ts +++ b/tests/control_plane_ts/authority_store.test.ts @@ -13,7 +13,7 @@ import { authorityStoreCommitFixture as commit, registerAuthorityStoreConformance, } from "./authority_store_conformance.ts"; -import {registerCoherenceDefenseConformance} from "./coherence_defense_conformance.ts"; +import {registerAuthorityOperationReplayConformance} from "./authority_operation_replay_conformance.ts"; async function fixture(t: test.TestContext, goalId = "goal-a") { const root = await mkdtemp(join(tmpdir(), "loopx-authority-store-")); @@ -24,9 +24,9 @@ async function fixture(t: test.TestContext, goalId = "goal-a") { registerAuthorityStoreConformance("file provider", async (t) => { const { root, store } = await fixture(t); return { store, contender: new FileAuthorityStore(root, "goal-a") }; -}); +}, "applied"); -registerCoherenceDefenseConformance("file provider", async (t) => { +registerAuthorityOperationReplayConformance("file provider", async (t) => { const { root, store } = await fixture(t); return { store, contender: new FileAuthorityStore(root, "goal-a") }; }); diff --git a/tests/control_plane_ts/authority_store_conformance.ts b/tests/control_plane_ts/authority_store_conformance.ts index fc29025a0f..eb4dc2b253 100644 --- a/tests/control_plane_ts/authority_store_conformance.ts +++ b/tests/control_plane_ts/authority_store_conformance.ts @@ -250,6 +250,7 @@ async function withConcurrentAuthorityReads(backends: readonly AuthorityStore export function registerAuthorityStoreConformance( providerName: string, factory: AuthorityStoreConformanceFactory, + matchingReplayStatus: "applied" | "conflict" = "conflict", ): void { test(`${providerName} conformance: captured complete source survives provider reopen and readback`, async (t) => { const {store, contender} = await factory(t); @@ -274,9 +275,8 @@ export function registerAuthorityStoreConformance( if (result.status !== "applied") return; const replay = await store.commitAuthority({expected_provider_revision: result.provider_revision, operation_id: "capture-source", events: [], next_projection: captured, receipts: []}); - // Content-aware idempotency: matching replay body returns the original receipt. - assert.equal(replay.status === "applied" || (replay.status === "conflict" && (replay as Record).conflict_kind === "operation_id_exists"), true, - `replay must be applied (idempotent) or operation_id_exists conflict: ${JSON.stringify(replay)}`); + assert.equal(replay.status, matchingReplayStatus); + if (replay.status === "conflict") assert.equal(replay.conflict_kind, "operation_id_exists"); const retained = await contender.readReceipt("capture-source"); assert.equal(retained.status, "found"); if (retained.status === "found") assert.equal(retained.cursor, "1"); diff --git a/tests/control_plane_ts/coherence_defense_conformance.ts b/tests/control_plane_ts/coherence_defense_conformance.ts deleted file mode 100644 index d849908f43..0000000000 --- a/tests/control_plane_ts/coherence_defense_conformance.ts +++ /dev/null @@ -1,273 +0,0 @@ -/** Verify the authority store CAS rejects stale writes after an intervening - * state change — the coherence defense against late-arriving results from a - * replaced Goal instance. - * - * C1: Stale commit body rejected after an intervening unrelated commit. - * C2: The same operation_id replayed after commit returns the original - * receipt (idempotency), never double-commits. - * C3: A stale write followed by a fresh write on the same operation_id - * correctly replays the first successful (fresh) commit. - * C4: Concurrent competing writes at the same provider_revision: one - * succeeds, the other gets a conflict. - * C5: An initial commit (null provider_revision) succeeds; a second - * initial commit on the same store is rejected because the revision - * has advanced. - * C6: Same events/receipts with a different projection is rejected - * (projection-only drift is not an idempotent replay). - * C7: Same projection/receipts with different events is rejected. - * C8: Historical A→B→replay-A returns A's original receipt and leaves - * B's state unchanged. - */ -import assert from "node:assert/strict"; -import test from "node:test"; -import type {AuthorityStoreConformanceFactory} from "./authority_store_conformance.ts"; -import {authorityStoreCommitFixture as commit} from "./authority_store_conformance.ts"; - -/** Build an AuthorityStoreCommit with a specific projection override for - * testing projection-only drift detection. */ -function driftCommit(expectedRevision: string | null, opId: string, - revision: number, epoch: number, projectionOverride: Record, -) { - const base = commit(expectedRevision, opId, revision, epoch); - return {...base, next_projection: {...base.next_projection as Record, ...projectionOverride}}; -} - -export function registerCoherenceDefenseConformance( - provider: string, - factory: AuthorityStoreConformanceFactory, -): void { - // ─── C1: stale write rejected after intervening commit ─── - test(`${provider}: C1 stale write rejected after intervening commit`, async (t) => { - const {store} = await factory(t); - // Seed the store with an initial commit so provider_revision ≠ null. - const seed = commit(null, "seed-coherence", 1, 1); - assert.equal((await store.commitAuthority(seed)).status, "applied"); - - // Read current revision. - const head = await store.loadAuthority(); - assert.equal(head.status, "loaded"); - if (head.status !== "loaded") return; - const staleRevision = head.provider_revision; - - // Intervening commit advances the revision. - const intervening = commit(staleRevision, "intervening-commit", 2, 1); - assert.equal((await store.commitAuthority(intervening)).status, "applied"); - - // Stale write with the old revision must be rejected. - const staleWrite = commit(staleRevision, "stale-write", 3, 1); - const rejected = await store.commitAuthority(staleWrite); - assert.equal(rejected.status, "conflict"); - assert.equal((rejected as Record).conflict_kind, "provider_revision_mismatch"); - }); - - // ─── C2: idempotent replay after successful commit ─── - test(`${provider}: C2 operation idempotency prevents double-commit`, async (t) => { - const {store} = await factory(t); - const seed = commit(null, "seed-idempotent", 1, 1); - assert.equal((await store.commitAuthority(seed)).status, "applied"); - - const head = await store.loadAuthority(); - assert.equal(head.status, "loaded"); - if (head.status !== "loaded") return; - - // First write succeeds. - const first = commit(head.provider_revision, "unique-operation", 2, 1); - const firstResult = await store.commitAuthority(first); - assert.equal(firstResult.status, "applied"); - - // Same operation_id replay returns original receipt, not a conflict. - const replay = commit(head.provider_revision, "unique-operation", 2, 1); - const replayResult = await store.commitAuthority(replay); - assert.equal(replayResult.status, "applied"); // replayed, not double-committed - - // Verify the store didn't change — no second event was appended, - // head projection is unchanged, and the committed transaction is intact. - const after = await store.loadAuthority(); - assert.equal(after.status, "loaded"); - if (after.status !== "loaded") return; - const receipt = await store.readReceipt("unique-operation"); - assert.equal(receipt.status, "found"); - assert.equal(receipt.provider_revision, firstResult.provider_revision); - assert.equal(receipt.cursor, firstResult.cursor); - const scan = await store.scanCommitted(null, 10); - assert.equal(scan.status, "page"); - if (scan.status === "page") { - // Only 2 transactions: seed + unique-operation (no duplicate). - assert.equal(scan.transactions.length, 2); - const unique = scan.transactions.find(tx => tx.operation_id === "unique-operation")!; - assert.ok(unique); - assert.equal((unique.projection as Record).authority_revision, 2); - } - }); - - // ─── C3: stale rejected then fresh succeeds on same operation_id ─── - test(`${provider}: C3 stale rejection does not block fresh write with same operation_id`, async (t) => { - const {store} = await factory(t); - const seed = commit(null, "seed-same-op", 1, 1); - assert.equal((await store.commitAuthority(seed)).status, "applied"); - - const head = await store.loadAuthority(); - assert.equal(head.status, "loaded"); - if (head.status !== "loaded") return; - const staleRevision = head.provider_revision; - - // Advance the revision. - const intervening = commit(staleRevision, "intervening-same-op", 2, 1); - assert.equal((await store.commitAuthority(intervening)).status, "applied"); - - // Stale write with old revision → rejected. - const staleWrite = commit(staleRevision, "contested-operation", 3, 1); - const rejected = await store.commitAuthority(staleWrite); - assert.equal(rejected.status, "conflict"); - - // Fresh write with current revision → accepted. - const fresh = await store.loadAuthority(); - assert.equal(fresh.status, "loaded"); - if (fresh.status !== "loaded") return; - const freshWrite = commit(fresh.provider_revision, "contested-operation", 3, 1); - const freshResult = await store.commitAuthority(freshWrite); - assert.equal(freshResult.status, "applied"); - - // Verify the store now reflects the fresh write. - const receipt = await store.readReceipt("contested-operation"); - assert.equal(receipt.status, "found"); - }); - - // ─── C4: concurrent writes at same revision → one wins ─── - test(`${provider}: C4 concurrent writes at same revision — one succeeds`, async (t) => { - const {store, contender} = await factory(t); - const seed = commit(null, "seed-concurrent", 1, 1); - assert.equal((await store.commitAuthority(seed)).status, "applied"); - - const head = await store.loadAuthority(); - assert.equal(head.status, "loaded"); - if (head.status !== "loaded") return; - const revision = head.provider_revision; - - // Both contenders target the same revision. - const writeA = commit(revision, "concurrent-a", 2, 1); - const writeB = commit(revision, "concurrent-b", 3, 1); - - // Submit both concurrently — order determines winner. - const [first, second] = await Promise.all([ - store.commitAuthority(writeA), - contender.commitAuthority(writeB), - ]); - - // Exactly one succeeds; the other gets a conflict. - const applied = [first, second].filter(r => r.status === "applied"); - const conflicted = [first, second].filter(r => r.status === "conflict"); - assert.equal(applied.length, 1, `expected exactly one applied, got ${JSON.stringify([first, second])}`); - assert.equal(conflicted.length, 1); - assert.equal((conflicted[0]! as Record).conflict_kind, "provider_revision_mismatch"); - - // Both stores should converge to the same state. - const afterStore = await store.loadAuthority(); - const afterContender = await contender.loadAuthority(); - assert.equal(afterStore.status, "loaded"); - assert.equal(afterContender.status, "loaded"); - }); - - // ─── C5: second initial commit rejected ─── - test(`${provider}: C5 second null-revision commit rejected`, async (t) => { - const {store} = await factory(t); - // First commit with null revision succeeds (initial creation). - const first = commit(null, "first-seed", 1, 1); - assert.equal((await store.commitAuthority(first)).status, "applied"); - - // Second commit with null revision must fail — revision has advanced. - const second = commit(null, "second-seed", 2, 1); - const rejected = await store.commitAuthority(second); - assert.equal(rejected.status, "conflict"); - assert.equal((rejected as Record).conflict_kind, "provider_revision_mismatch"); - }); - - // ─── C6: projection-only drift is not an idempotent replay ─── - test(`${provider}: C6 projection-only drift rejected`, async (t) => { - const {store} = await factory(t); - const seed = commit(null, "seed-proj-drift", 1, 1); - assert.equal((await store.commitAuthority(seed)).status, "applied"); - - const head = await store.loadAuthority(); - assert.equal(head.status, "loaded"); - if (head.status !== "loaded") return; - - // First write creates a commit with specific projection. - const first = commit(head.provider_revision, "drift-op", 2, 1); - assert.equal((await store.commitAuthority(first)).status, "applied"); - - // Replay with same operation_id, same events/receipts, but - // different projection (authority_revision 99 instead of 2). - const drifted = driftCommit(head.provider_revision, "drift-op", 99, 1, - {authority_revision: 99}); - const result = await store.commitAuthority(drifted); - // Must be rejected — projection differs even though events/receipts match. - assert.equal(result.status, "conflict"); - assert.equal((result as Record).conflict_kind, "operation_id_exists"); - - // Verify the original projection (authority_revision: 2) is preserved. - const after = await store.loadAuthority(); - assert.equal(after.status, "loaded"); - if (after.status !== "loaded") return; - assert.equal((after.head as Record).authority_revision, 2); - }); - - // ─── C7: event-only drift is not an idempotent replay ─── - test(`${provider}: C7 event-only drift rejected`, async (t) => { - const {store} = await factory(t); - const seed = commit(null, "seed-event-drift", 1, 1); - assert.equal((await store.commitAuthority(seed)).status, "applied"); - - const head = await store.loadAuthority(); - assert.equal(head.status, "loaded"); - if (head.status !== "loaded") return; - - // First write succeeds. - const first = commit(head.provider_revision, "event-drift-op", 2, 1); - assert.equal((await store.commitAuthority(first)).status, "applied"); - - // Same operation_id, same projection/receipts, but different events. - const drifted = {...first, events: [{...first.events[0], type: "todo_created"}]}; - const result = await store.commitAuthority(drifted); - assert.equal(result.status, "conflict"); - assert.equal((result as Record).conflict_kind, "operation_id_exists"); - }); - - // ─── C8: historical A→B→replay-A returns A's receipt, B unchanged ─── - test(`${provider}: C8 historical A→B→replay-A preserves original receipt`, async (t) => { - const {store} = await factory(t); - const seed = commit(null, "seed-historical", 1, 1); - assert.equal((await store.commitAuthority(seed)).status, "applied"); - - let head = await store.loadAuthority(); - assert.equal(head.status, "loaded"); - if (head.status !== "loaded") return; - - // Commit A: operation "hist-op" with authority_revision 2. - const commitA = commit(head.provider_revision, "hist-op", 2, 1); - const resultA = await store.commitAuthority(commitA); - assert.equal(resultA.status, "applied"); - - head = await store.loadAuthority(); - assert.equal(head.status, "loaded"); - if (head.status !== "loaded") return; - - // Commit B: different operation advances the revision. - const commitB = commit(head.provider_revision, "hist-op-b", 3, 2); - assert.equal((await store.commitAuthority(commitB)).status, "applied"); - - // Replay A: same operation_id, same full body. - const replayA = commit(resultA.provider_revision, "hist-op", 2, 1); - const replayResult = await store.commitAuthority(replayA); - // Must return A's original receipt, not B's state. - assert.equal(replayResult.status, "applied"); - assert.equal(replayResult.provider_revision, resultA.provider_revision); - assert.equal(replayResult.cursor, resultA.cursor); - - // B's state must be unchanged. - head = await store.loadAuthority(); - assert.equal(head.status, "loaded"); - if (head.status !== "loaded") return; - assert.equal((head.head as Record).authority_revision, 3); - }); -} \ No newline at end of file diff --git a/tests/control_plane_ts/sqlite_authority_store.test.ts b/tests/control_plane_ts/sqlite_authority_store.test.ts index cb603deff5..6d2f9dd627 100644 --- a/tests/control_plane_ts/sqlite_authority_store.test.ts +++ b/tests/control_plane_ts/sqlite_authority_store.test.ts @@ -11,12 +11,15 @@ import { AUTHORITY_STATE_CHECKPOINT_INTERVAL } from "../../loopx/control_plane/c import { canonicalAuthorityBytes } from "../../loopx/control_plane/coordination/authority_store_codec.ts"; import { authorityStoreCommitFixture, registerAuthorityStoreConformance } from "./authority_store_conformance.ts"; +import {registerAuthorityOperationReplayConformance} from "./authority_operation_replay_conformance.ts"; + async function fixture(t: test.TestContext) { const directory = await mkdtemp(join(tmpdir(), "sqlite-authority-")); t.after(() => rm(directory, {recursive: true, force: true})); return {store: new SqliteAuthorityStore(directory, "goal"), contender: new SqliteAuthorityStore(directory, "goal")}; } -registerAuthorityStoreConformance("SQLite", fixture); +registerAuthorityStoreConformance("SQLite", fixture, "applied"); +registerAuthorityOperationReplayConformance("SQLite", fixture); test("SQLite commits and reads back every JSON object key", {timeout: 30000}, async t => { const {store} = await fixture(t); @@ -418,3 +421,32 @@ test("SQLite receipt batch bounds are checked before opening storage", async t = const {existsSync} = await import("node:fs"); assert.equal(existsSync(store.path), false); }); + +for (const cursor of [1, 2]) for (const field of ["events", "receipts"] as const) { + test(`SQLite replay refuses corrupt retained ${field} at cursor ${cursor}`, async t => { + const {store} = await fixture(t); + let revision: string | null = null; + const inputs = []; + for (let index = 1; index <= 3; index++) { + const input = authorityStoreCommitFixture(revision, `replay-${index}`, index, index); + inputs.push(input); + const committed = await store.commitAuthority(input); + assert.equal(committed.status, "applied"); if (committed.status !== "applied") return; + revision = committed.provider_revision; + } + const {DatabaseSync} = createRequire(import.meta.url)("node:sqlite"); + const db = new DatabaseSync(store.path); + try { + db.prepare(`UPDATE commits SET ${field}=? WHERE cursor=?`).run('[{"forged":true}]', cursor); + const before = db.prepare("SELECT * FROM commits ORDER BY cursor").all(); + assert.equal((await store.loadAuthority()).status, "loaded", "current head remains valid"); + assert.equal((await store.readReceipt(`replay-${cursor}`)).status, "failed"); + const replay = await store.commitAuthority(inputs[cursor - 1]!); + assert.equal(replay.status, "failed", "stored digest alone cannot prove the retained transaction"); + if (replay.status === "failed") assert.equal(replay.reason_code, "provider_protocol_violation"); + assert.deepEqual(db.prepare("SELECT * FROM commits ORDER BY cursor").all(), before); + const head = await store.loadAuthority(); + assert.equal(head.status, "loaded"); if (head.status === "loaded") assert.equal(head.cursor, "3"); + } finally { db.close(); } + }); +} From c742cebb3d149ad6aa4f7d030526284b6a2d51b0 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Sun, 27 Sep 2026 21:49:27 +0800 Subject: [PATCH 4/4] docs(authority): retain scoped Goal continuity follow-up scenarios Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../goal-immutability-coherence-defense-v0.md | 91 +++++++++++++++++++ ...immutability-coherence-defense-v0.zh-CN.md | 74 +++++++++++++++ docs/reference/authority-operation-replay.md | 3 + 3 files changed, 168 insertions(+) create mode 100644 docs/architecture/rfcs/goal-immutability-coherence-defense-v0.md create mode 100644 docs/architecture/rfcs/goal-immutability-coherence-defense-v0.zh-CN.md diff --git a/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.md b/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.md new file mode 100644 index 0000000000..92fa0f2933 --- /dev/null +++ b/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.md @@ -0,0 +1,91 @@ +# Design follow-up: Goal continuity across restart and replacement + +- **Status:** Deferred design note; non-normative, not an accepted new runtime contract. +- **Origin:** Retains useful questions from [Duang777's #5169](https://github.com/loopx-project/loopx/pull/5169), with its implementation and evidence claims narrowed during review. +- **Language mirror:** [中文](goal-immutability-coherence-defense-v0.zh-CN.md). +- **Owning contracts:** [Goal instance identity and orphan recovery](goal-instance-identity-and-orphan-recovery-v0.md), [Goal direction baseline](goal-direction-baseline-v0.md), [governed amendment](shared-goal-alignment-and-governed-amendment-v0.md), [semantic handoff](capable-manager-semantic-handoff-v0.md), and [shared authority](shared-goal-authority-state-provider-v0.md). + +This preserves follow-up design value from the original “Goal Immutability as +Coherence Defense” draft. It creates no second roadmap, state owner, acceptance +gate or delivery claim. The owning RFCs decide activation, authority and rollout. +The questions below are proposed qualification scenarios, not evidence of a +remaining defect in every named path. + +## What is worth retaining + +Durable commitments should outlive a model's working context. A restarted or +replaced Agent should recover the authorized Goal, constraints, accepted work +and unresolved obligations from their existing owners. A same-name replacement +Goal must not inherit an old instance's authority merely because names match. +These are useful long-horizon failure scenarios even when individual storage +and command tests pass. + +Three distinctions prevent an overly broad “coherence” guarantee: + +| Fact | What it can establish | What it cannot establish | +| --- | --- | --- | +| Exact GoalRef and source-owned instance fence | Which Goal lifetime may admit an action | Whether that action is useful or its output correct | +| Provider revision / CAS | Whether a new write still has its expected storage basis | Current Goal authority if the caller resolves/rebinds the wrong instance; revision tokens are opaque, not ordered counters | +| Operation identity and verified original receipt | Which operation already committed and its original result | Permission to repeat an external effect or attach the result to a replacement Goal | +| Authorized intent / acceptance basis | Which constraints and completion criteria govern this work | Model compliance or outcome correctness without independent evidence | + +Immutable **instance identity** does not mean immutable **Goal intent**. Authorized +amendments must remain possible and versioned through their existing owner. +A model can produce a wrong change against a perfectly current CAS revision. +Prompt/context improvements, typed constraints and outcome validation complement +storage fences; none substitutes for all the others. + +## Proposed follow-up slices under existing owners + +| Slice and owner | Real caller scenario | Decisive acceptance, including recovery | +| --- | --- | --- | +| Instance-qualified continuity — Goal instance RFC; related collaboration/session consumers in [#5106](https://github.com/loopx-project/loopx/pull/5106) and [#5130](https://github.com/loopx-project/loopx/pull/5130) | Retire A through the authorized lifecycle, create same-name B, then deliver A's delayed Todo/result, claim renewal and plan confirmation through their real entrypoints | No mutation or execution authority leaks into B. Typed stale-instance outcomes remain observable; B's legitimate work succeeds. Historical A receipts remain attributable to A where retention/access policy permits. Registry activation and its legacy/off behavior follow the owning RFC. | +| Constraint continuity — direction-baseline and governed-amendment RFCs; roadmap R4 | Resume/rebind an Agent after context loss with stale material/acceptance basis, then repeat with an authorized amendment and refreshed basis | Original constraints and accepted work are recovered from canonical owners; re-evaluation remains Agent-scoped. Unrelated work is not globally blocked. The legitimate amendment can progress; no implicit freeze of all Goal intent. | +| Recoverable late-result disposition — handoff and Effect recovery owners; roadmap R3 | An old request's result arrives after requester/instance replacement or after an external effect has committed but its response was lost | Preserve original request/result lineage and the external effect's durable evidence. Reconcile at the owning ledger; do not silently discard evidence, automatically rebind to B or rerun the effect. An authorized recovery path returns a result or records an explicit terminal disposition. | + +Before implementing a slice, inventory current main, related PRs and existing +fixtures. Extend the current owner's missing cases rather than creating a +parallel “semantic certificate” or generic coherence engine. Shared decisions +belong in existing typed TS owners; provider adapters supply physical evidence. +As of this note's 2026-09-27 review, #5106, #5130 and the related App Turn recovery +[#5139](https://github.com/loopx-project/loopx/pull/5139) are open; their merge or +isolated tests alone would not certify the combined journeys above. + +## Qualification method and unresolved decisions + +Use disposable runtimes, synthetic public-safe Goals and actual supported +backends. Derive expected outcomes from the owning contract before executing: + +- Exercise same-instance restart, same-name replacement, authorized amendment, + delayed input, overlapping invalid conditions and valid post-recovery work. + A stale rejection alone is not restored progress. +- Exercise both accidental scope expansion and escape: covered old bindings, + unrelated current work and newly created subjects follow the declared scope. +- For concurrent creation, preserve the registry's declared uniqueness and + linearization contract. Do not assume that every racing request must create + a separate writable Goal; distinct successful lifetimes must never share an ID. +- Inject one fault at a time and demonstrate oracle sensitivity: wrong GoalRef, + missing commit fence, dropped handoff constraint or duplicated effect. Keep + real effect evidence distinct from simulated adapters and model evaluations. + +Open decisions belong to the existing owners: whether each producer already +captures sufficient immutable instance/basis evidence; how a stale requester +receives a recoverable outcome; and whether an explicit new producer/schema is +needed. Never derive the original instance from a mutable “current Goal” lookup. +If a format change is necessary, qualify backup/migration and mixed writers; +this note does not predeclare “no migration needed.” + +## Delivered boundary and evidence status + +[#5169's operation replay](../../reference/authority-operation-replay.md) verifies +matching historical File/SQLite commits without rewinding current state. It does +not implement or qualify the three follow-up journeys. Tests of stale provider +revisions are not tests of Goal replacement, compaction or semantic correctness. + +The original draft's quantitative research/experiment tables are not retained as +accepted evidence: this PR does not supply an independently reviewable public +harness, oracle and source provenance for them. Future evidence must name the +exact revision, real entrypoint/backend, fault, independent oracle and recovery +readback. No numeric success rate or claim that “CAS prevents coherence collapse” +is carried forward. This note neither changes File/SQLite defaults nor adds a +new prerequisite to their existing D1–D3 qualification. diff --git a/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.zh-CN.md b/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.zh-CN.md new file mode 100644 index 0000000000..db65a9a193 --- /dev/null +++ b/docs/architecture/rfcs/goal-immutability-coherence-defense-v0.zh-CN.md @@ -0,0 +1,74 @@ +# 后续设计:重启与实例替换中的 Goal 连续性 + +- **状态:** 延后设计记录;非规范性内容,不是新接受的运行时契约。 +- **来源:** 保留 [Duang777 在 #5169 中提出的问题](https://github.com/loopx-project/loopx/pull/5169),评审时收窄其实现与证据宣称。 +- **语言镜像:** [English](goal-immutability-coherence-defense-v0.md)。 +- **所属契约:** [Goal 实例身份与孤儿恢复](goal-instance-identity-and-orphan-recovery-v0.zh-CN.md)、[Goal 方向基线](goal-direction-baseline-v0.zh-CN.md)、[受治理的修改](shared-goal-alignment-and-governed-amendment-v0.md)、[语义交接](capable-manager-semantic-handoff-v0.zh-CN.md)、[共享权威](shared-goal-authority-state-provider-v0.md)。 + +这里保留原“Goal 不可变性作为一致性防御”草稿中的后续设计价值,不增加第二份 +roadmap、状态 owner、验收门禁或交付宣称。激活、权限和上线由所属 RFC 决定。 +以下是建议补充的验收场景,不表示每条路径当前都存在尚未修复的缺陷。 + +## 值得保留的部分 + +持久工作承诺应当比模型的工作上下文活得更久。Agent 重启或被替换后,应从已有 +owner 恢复获得授权的 Goal、约束、已接受的工作和未完成义务。同名的新 Goal +不能仅因名字相同就继承旧实例的权限。即便单个存储和命令测试已经通过,这些仍是 +有价值的长程运行反例。 + +必须区分以下事实,避免泛化为“语义一致性保证”: + +| 事实 | 能证明什么 | 不能证明什么 | +| --- | --- | --- | +| 精确 GoalRef 与源 registry 拥有的实例 fence | 哪次 Goal 生命周期可以接纳动作 | 动作是否有用、输出是否正确 | +| Provider revision / CAS | 新写入是否仍基于预期的存储版本 | 调用方错误解析或重绑定实例时的当前 Goal 权限;revision 是不透明 token,不是可排序计数器 | +| 操作身份与经过验证的原回执 | 哪个操作已经提交及其原结果 | 重复执行外部效果、或把结果挂到替代 Goal 的权限 | +| 已授权的意图/验收基线 | 当前工作应遵守哪些约束和完成条件 | 缺乏独立证据时,模型确实遵守了约束或结果正确 | + +**实例身份不可变**不等于 **Goal 意图不可修改**。获得授权的修改必须仍能通过现有 +owner 留下版本并生效。模型完全可能基于最新 CAS 版本提交错误改动。 +Prompt/上下文优化、类型化约束与结果验证和存储 fence 相互补充,没有一项可以 +替代全部其他机制。 + +## 归入现有 owner 的后续切片 + +| 切片与 owner | 真实调用场景 | 决定性验收,包含恢复 | +| --- | --- | --- | +| 按实例确认连续性——Goal 实例 RFC;相关 collaboration/session 消费者见 [#5106](https://github.com/loopx-project/loopx/pull/5106)、[#5130](https://github.com/loopx-project/loopx/pull/5130) | 通过获授权的生命周期退役 A,建立同名 B,再经真实入口提交 A 的迟到 Todo/结果、claim 续约、计划确认 | B 不受到错误写入或执行权限污染;过期实例结果可观察,B 的合法工作仍能推进。保留/访问策略允许时,A 的历史回执仍归属于 A。Registry 激活及 legacy/off 行为遵循所属 RFC。 | +| 约束连续性——方向基线、受治理修改 RFC;roadmap R4 | Agent 上下文丢失后以过期材料/验收基线恢复或重绑定,再以已获授权的修改和新基线重复执行 | 从 canonical owner 恢复原约束和已接受工作;重新评估局限于相关 Agent,不把无关工作全部阻塞。合法修改可以推进,不隐式冻结全部 Goal 意图。 | +| 迟到结果的可恢复处置——handoff 与 Effect recovery owner;roadmap R3 | 请求方/实例替换后收到旧请求结果,或外部效果已经提交但响应丢失 | 保留原请求/结果关系和外部效果的持久证据;在所属 ledger 对账,不能静默丢弃证据、自动改挂到 B 或重跑效果。通过获授权的恢复路径返回结果,或明确记录终止处置。 | + +实施任一切片前,先核对当前 main、相关 PR 和已有 fixture。补齐当前 owner 的缺口, +不另建“semantic certificate”或通用 coherence 引擎。共享决策放在已有类型化 TS +owner,provider adapter 提供物理存储证据。本记录于 2026-09-27 核对时,#5106、 +#5130 及相关 App Turn 恢复 [#5139](https://github.com/loopx-project/loopx/pull/5139) +仍开放;仅合并它们或通过各自孤立测试,不代表上述组合链路已经验收。 + +## 验证方法与未决事项 + +使用可丢弃 runtime、公开安全的合成 Goal 和真实受支持 backend;执行前从所属 +契约独立推导预期结果: + +- 覆盖同实例重启、同名替换、获授权的修改、迟到输入、重叠非法条件,以及恢复后 + 的合法工作。只有 stale 拒绝不等于恢复推进。 +- 同时覆盖范围误扩大与逃逸:旧绑定、无关当前工作和新增对象须遵守声明的范围。 +- 并发创建应遵守 registry 的唯一性和线性化契约,不假定每个竞争请求都应建立一份 + 独立可写 Goal;不同的成功生命周期不能共用实例 ID。 +- 每次注入一个故障并验证 oracle 的敏感性:错误 GoalRef、缺失提交 fence、交接约束 + 丢失或重复效果。区分真实效果证据、模拟 adapter 与模型评测。 + +未决事项交给已有 owner:各 producer 是否已捕获足够的不可变实例/基线证据; +过期请求方如何收到可恢复结果;是否确需新的 producer/schema。不能通过可变的 +“当前 Goal”查询倒推出原实例。如果需要格式修改,必须验收备份/迁移和混合版本 +writer;本记录不预先宣称“无需迁移”。 + +## 本次交付边界与证据状态 + +[#5169 的操作重放](../../reference/authority-operation-replay.md) 验证历史 File/SQLite +提交的完整意图,且不会回退当前状态;它没有实现或验收以上三组后续链路。 +过期 provider revision 的测试不能冒充 Goal 替换、上下文压缩或语义正确性测试。 + +原草稿的研究百分比和实验成绩表不作为已接受证据保留:本 PR 没有提供可独立审查的 +公开 harness、oracle 和来源依据。后续证据须明确精确 revision、真实入口/backend、 +故障、独立 oracle 和恢复读回。不沿用数值成功率,也不沿用“CAS 防止语义崩塌”的 +结论。本记录不改变 File/SQLite 默认值,也不给现有 D1–D3 验收添加新前置条件。 diff --git a/docs/reference/authority-operation-replay.md b/docs/reference/authority-operation-replay.md index d1c289dc99..1bc7e8b5d7 100644 --- a/docs/reference/authority-operation-replay.md +++ b/docs/reference/authority-operation-replay.md @@ -54,6 +54,9 @@ The shared-authority RFC owns this storage/recovery boundary. Goal lifetime identity, lease epochs and provider revisions remain separate contracts. This change does not implement Goal replacement isolation, semantic correctness of model output, default provider activation or long-horizon qualification. +The [deferred Goal continuity note](../architecture/rfcs/goal-immutability-coherence-defense-v0.md) +preserves related restart, instance-replacement and constraint-recovery scenarios +under their existing RFC owners; those scenarios are not qualified by this PR. `authority_operation_replay_conformance.ts` runs on both File and SQLite. It checks full body drift, canonical key ordering, historical replay and concurrent