Keep the contradiction alive across the rotation the reopen causes

The clearing rule sat below the reducer switch, so it ran for every event
rather than for the correction it was written for. TaskReleased found the
task unblocked and erased the contradiction, which is the rotation the
reopen itself causes: the planning session launched one lease later and was
told nothing again.

Scoped to TaskCorrected, and the store test walks the real sequence
(mismatch, reopen, release, lease) rather than reading the projection at
the moment it is written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
This commit is contained in:
2026-08-29 21:05:01 +04:00
parent edf00761fd
commit 75396963ef
2 changed files with 77 additions and 3 deletions
+8 -3
View File
@@ -612,9 +612,14 @@ func (s *Store) apply(e domain.Event) error {
if t.State != domain.StateBlocked {
t.DecisionRequest = nil
t.Blocker, t.BlockReason = "", ""
// The human's answer outranks the plan and stands as an ordinary
// decision, so the contradiction it settled is no longer live.
t.PlanMismatch = nil
// The contradiction is cleared only by the correction that answers the
// stop, never by any other event that happens to find the task
// unblocked. This block runs for every event, and clearing it here
// unconditionally erased the contradiction at the very rotation the
// reopen causes, which is exactly when the planner needs it.
if e.Type == "TaskCorrected" {
t.PlanMismatch = nil
}
}
if phase, ok := p["lifecycle_phase"].(string); ok && phase != "" {
t.LifecyclePhase = phase
+69
View File
@@ -723,3 +723,72 @@ func TestBlockRetainsLeaseEpochForALaterReopen(t *testing.T) {
t.Fatalf("last lease epoch %q, want %q", after.LastLeaseEpoch, epoch)
}
}
// F66's projection has to survive the rotation the reopen causes. The session
// that reported the contradiction hands off, a successor leases, and only then
// is the planning context rendered. Live on run 21, the field was gone by
// then: the reopen recorded it and the rotation lost it.
func TestTheContradictionSurvivesTheRotationItCauses(t *testing.T) {
s, err := Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
b := []byte(`{"source":"s","external_id":"x","project":"p"}`)
if err := s.Append(domain.Event{ID: "create", Type: "TaskCreated", TaskID: "t", Version: 1, Payload: b, Surface: string(authz.System)}); err != nil {
t.Fatal(err)
}
if _, err := s.Lease("t", "h", time.Minute); err != nil {
t.Fatal(err)
}
// Frame to implement, the shortest legal route to the phase a mismatch
// can be reported from.
task, _ := s.Task("t")
toImplement, _ := json.Marshal(map[string]any{"phase": "implement", "from": "frame"})
if err := s.Append(domain.Event{ID: "impl", Type: domain.EventWorkPhaseChanged, TaskID: "t", Version: task.Version + 1, Payload: toImplement, Surface: string(authz.System)}); err != nil {
t.Fatal(err)
}
task, _ = s.Task("t")
m, _ := json.Marshal(map[string]any{
"plan_ref": "plan-a", "phase_id": "phase-2",
"at_sha": "0123456789012345678901234567890123456789",
"observed": "the body is assembled inline",
"contradicts": "the plan says one helper returns it",
"harness_id": task.Lease.HarnessID,
"lease_epoch": task.Lease.Epoch,
"requested_action": "replan",
})
if err := s.Append(domain.Event{ID: "mismatch", Type: domain.EventPlanMismatchRecorded, TaskID: "t", Version: task.Version + 1, Payload: m, Surface: string(authz.System)}); err != nil {
t.Fatal(err)
}
if got, _ := s.Task("t"); got.PlanMismatch == nil {
t.Fatal("the contradiction was not projected at all")
}
// The reopen, then the rotation it causes.
task, _ = s.Task("t")
ph, _ := json.Marshal(map[string]any{"phase": "plan", "from": "implement", "reopen": domain.EventPlanMismatchRecorded, "reopen_phase_id": "phase-2"})
if err := s.Append(domain.Event{ID: "reopen", Type: domain.EventWorkPhaseChanged, TaskID: "t", Version: task.Version + 1, Payload: ph, Surface: string(authz.System)}); err != nil {
t.Fatal(err)
}
ref, err := s.PutArtifact([]byte("handoff"))
if err != nil {
t.Fatal(err)
}
task, _ = s.Task("t")
rel, _ := json.Marshal(map[string]any{"handoff_ref": ref, "anchor_sha": "0123456789012345678901234567890123456789", "harness_id": task.Lease.HarnessID, "lease_epoch": task.Lease.Epoch, "expected_version": task.Version})
if err := s.Append(domain.Event{ID: "release", Type: "TaskReleased", TaskID: "t", Version: task.Version + 1, Payload: rel, Surface: string(authz.System)}); err != nil {
t.Fatal(err)
}
if _, err := s.Lease("t", "h", time.Minute); err != nil {
t.Fatal(err)
}
// This is the moment the planning context is rendered.
got, _ := s.Task("t")
if got.PlanMismatch == nil {
t.Fatal("the planner is convened to settle a contradiction it is no longer told about")
}
if got.PlanMismatch.PhaseID != "phase-2" {
t.Fatalf("the contradiction changed across the rotation: %+v", got.PlanMismatch)
}
}