Skip to content

Commit e133f2d

Browse files
Drop a step the recorded plan does not know, and say so
Adoption maps each step's local identity onto the one the store recorded. A step with no mapping was kept with its local identity, and the store then refuses it on every read: "load plan step state: database constraint: step does not belong to run plan". That arrives mid-attempt, names no step, and reaches the coordinator as an unrecognised loop error — costing an attempt each time and, three times over, the run. Ladder rung 18 lost two entire passes to it, ending in seventy-seven and a hundred and forty-two seconds, and the trace said only "the loop refused the attempt". Such a step is dropped now, and the drop is traced with the identities. A step the plan never recorded is one no tool call can be attributed to, so keeping it only offers the run work it will be refused for doing. The trigger is still unidentified. Two readings were tested and refuted: that the requirement text was being parsed as file paths — filesNamedIn still reports the fallback pair — and that the durable plan was being refused outright, which the log contradicts. Fixing the silent mismatch is right whatever put a step there, and the next trace will name it. Change-Log: CL-20260803-174 Dev-Log: DL-20260803-184 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent b8f0891 commit e133f2d

4 files changed

Lines changed: 164 additions & 2 deletions

File tree

CHANGELOG

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,41 @@ Dev-Log:
5050

5151
Entries
5252
-------
53-
Change-ID: CL-20260803-173
53+
Change-ID: CL-20260803-174
5454
Commit: pending
5555
Date: 2026-08-04
56+
Type: Fix -- a step the recorded plan did not know was kept, and every read of
57+
it was refused
58+
Request-or-TODO: Ladder rung 18
59+
Outcome: Adoption maps each step's local identity onto the one the store
60+
recorded. A step with no mapping was kept with its local identity, and the
61+
store then refuses it on every read: "load plan step state: database
62+
constraint: step does not belong to run plan". That arrives mid-attempt, names
63+
no step, and reaches the coordinator as an unrecognised loop error -- which
64+
costs an attempt each time and, three times over, the run. Rung 18 on
65+
2026-08-04 lost two entire passes to it, ending in seventy-seven and
66+
a hundred and forty-two seconds, and the trace said only "the loop refused the
67+
attempt". Such a step is now dropped rather than kept, and the drop is traced
68+
with the identities: a step the plan never recorded is one no tool call can be
69+
attributed to, so keeping it offers the run work it will be refused for doing.
70+
Affected-behavior: A plan-adoption mismatch is visible and survivable instead of
71+
fatal three attempts later.
72+
Compatibility-or-migration: None.
73+
Verification: Three new tests: an unmapped step is dropped while the mapped ones
74+
take their durable identities, a fully recorded plan comes back whole and in
75+
order, and a run with no recorded plan keeps its own identities so a degraded
76+
run does not become no run.
77+
Considered-and-rejected: Reverting rung 18's requirement, which is the only
78+
thing that changed between the last clean pass and the two that failed. The
79+
correlation is real and unexplained -- filesNamedIn still reports the
80+
requirement as naming no file, so the layout path is unchanged, and the plan
81+
was recorded successfully in both failing passes. Fixing the silent mismatch
82+
is right whatever triggered it, and the trace will now name the step.
83+
Dev-Log: DL-20260803-184
84+
85+
Change-ID: CL-20260803-173
86+
Commit: b8f0891
87+
Date: 2026-08-04
5688
Type: Fix -- three defects rung 18 exposed in sequence: an undefined word in the
5789
requirement, a template that taught the wrong shape, and a slip priced as an
5890
attempt

DEVLOG

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,35 @@ Next-safe-step:
2525

2626
Entries
2727
-------
28+
Dev-Log: DL-20260803-184
29+
Date: 2026-08-04
30+
Status: Committed as CL-20260803-174
31+
Change-ID: CL-20260803-174
32+
Request-or-TODO: Ladder rung 18
33+
Goal: Make a plan-identity mismatch say what it is instead of ending the run
34+
Assumptions: None, and the one hypothesis I had was tested and refuted. The
35+
error appears only in the two passes after rung 18's requirement gained its
36+
purity definition, so I checked whether the new sentence -- which lists
37+
database/sql and math/rand -- was being read as naming files by the
38+
requirement analyser. It is not: requirementNamesNoFile still reports true and
39+
filesNamedIn still returns the fallback pair
40+
Decisions: Fixed the mismatch handling rather than chasing the trigger. A step
41+
the durable plan does not know cannot be the subject of a tool call, so
42+
keeping it can only produce refusals; dropping it and saying so is right
43+
whatever put it there. The trigger stays open and the next run's trace will
44+
name the step
45+
Files-or-schemas: internal/coordinator/agent_execution.go --
46+
adoptDurablePlanSteps
47+
Validation: Three new tests, including the two controls that stop this becoming
48+
a way to lose work
49+
Failures-or-discarded-approaches: Two wrong readings before this. First that the
50+
requirement text was being parsed as file paths, refuted above. Second that
51+
the durable plan was being refused outright, which the log contradicts -- "the
52+
plan could not be recorded" appears zero times in both failing passes
53+
Known-limitations: The cause is still unidentified. This makes it visible and
54+
non-fatal; it does not explain why a step went unmapped
55+
Next-safe-step: Read the plan trace on pass 8 and follow the named step
56+
2857
Dev-Log: DL-20260803-183
2958
Date: 2026-08-04
3059
Status: Committed as CL-20260803-173

internal/coordinator/agent_execution.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1922,11 +1922,31 @@ func adoptDurablePlanSteps(
19221922
return steps
19231923
}
19241924
adopted := make([]agentloop.PlanStep, 0, len(steps))
1925+
var unmapped []string
19251926
for _, step := range steps {
19261927
if durable, present := plan.Steps[step.ID]; present {
19271928
step.ID = durable
1929+
adopted = append(adopted, step)
1930+
continue
19281931
}
1929-
adopted = append(adopted, step)
1932+
// A step the durable plan does not know is dropped, and said so.
1933+
//
1934+
// It used to be kept with its local identity, which the store then
1935+
// refuses on every read: "load plan step state: database constraint:
1936+
// step does not belong to run plan". That arrives mid-attempt, names no
1937+
// step, and reaches the coordinator as an unrecognised loop error —
1938+
// which costs an attempt each time and, three times over, the run.
1939+
// Ladder rung 18 on 2026-08-04 lost two whole passes to it and the
1940+
// trace said only "the loop refused the attempt".
1941+
//
1942+
// Dropped rather than kept, because a step the plan never recorded is
1943+
// one no tool call can be attributed to: keeping it offers the run work
1944+
// it will be refused for doing.
1945+
unmapped = append(unmapped, step.ID)
1946+
}
1947+
if len(unmapped) > 0 {
1948+
tracef("plan", "%d step(s) are not in the recorded plan and were "+
1949+
"dropped: %s", len(unmapped), strings.Join(unmapped, ", "))
19301950
}
19311951
return adopted
19321952
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package coordinator
2+
3+
import (
4+
"testing"
5+
6+
agentloop "codeflux.dev/codeflux/internal/agent"
7+
)
8+
9+
// TestAStepTheRecordedPlanDoesNotKnowIsDropped is the silent mismatch that
10+
// ended two ladder passes.
11+
//
12+
// Adoption maps each step's local identity onto the one the store recorded. A
13+
// step with no mapping used to be kept with its local identity, and the store
14+
// then refuses it on every read: "load plan step state: database constraint:
15+
// step does not belong to run plan". That arrives mid-attempt, names no step,
16+
// and reaches the coordinator as an unrecognised loop error — which costs an
17+
// attempt each time and, three times over, the run.
18+
//
19+
// Ladder rung 18 on 2026-08-04 lost two whole passes to it, and the trace said
20+
// only "the loop refused the attempt".
21+
func TestAStepTheRecordedPlanDoesNotKnowIsDropped(t *testing.T) {
22+
steps := []agentloop.PlanStep{
23+
{ID: "edit-1"}, {ID: "edit-2"}, {ID: "verify"},
24+
}
25+
plan := durablePlan{Steps: map[string]string{
26+
"edit-1": "stp_one",
27+
"verify": "stp_verify",
28+
}}
29+
30+
adopted := adoptDurablePlanSteps(steps, plan)
31+
if len(adopted) != 2 {
32+
t.Fatalf("want the two recorded steps, got %d: %+v",
33+
len(adopted), adopted)
34+
}
35+
for _, step := range adopted {
36+
if step.ID == "edit-2" {
37+
t.Error("a step the plan never recorded survived adoption, so " +
38+
"every read of its state will be refused by the store")
39+
}
40+
}
41+
if adopted[0].ID != "stp_one" || adopted[1].ID != "stp_verify" {
42+
t.Errorf("the recorded steps did not take their durable identities: %+v",
43+
adopted)
44+
}
45+
}
46+
47+
// TestEveryMappedStepIsKept is the control.
48+
//
49+
// The ordinary case is that every step maps, and dropping must not become a way
50+
// to lose work: a plan that records all its steps has to come back whole and in
51+
// order.
52+
func TestEveryMappedStepIsKept(t *testing.T) {
53+
steps := []agentloop.PlanStep{
54+
{ID: "edit-1"}, {ID: "edit-2"}, {ID: "verify"},
55+
}
56+
plan := durablePlan{Steps: map[string]string{
57+
"edit-1": "stp_one", "edit-2": "stp_two", "verify": "stp_verify",
58+
}}
59+
60+
adopted := adoptDurablePlanSteps(steps, plan)
61+
if len(adopted) != 3 {
62+
t.Fatalf("a fully recorded plan lost a step: %+v", adopted)
63+
}
64+
for index, want := range []string{"stp_one", "stp_two", "stp_verify"} {
65+
if adopted[index].ID != want {
66+
t.Errorf("step %d is %q, want %q", index, adopted[index].ID, want)
67+
}
68+
}
69+
}
70+
71+
// TestAnUnrecordedPlanLeavesTheStepsAlone keeps the no-plan path working.
72+
//
73+
// When nothing was recorded at all — the store refused the plan, or there is no
74+
// store — the run proceeds on its own identities. Dropping every step there
75+
// would turn a degraded run into no run.
76+
func TestAnUnrecordedPlanLeavesTheStepsAlone(t *testing.T) {
77+
steps := []agentloop.PlanStep{{ID: "edit-1"}, {ID: "verify"}}
78+
if adopted := adoptDurablePlanSteps(steps, durablePlan{}); len(adopted) != 2 {
79+
t.Errorf("a run with no recorded plan lost its steps: %+v", adopted)
80+
}
81+
}

0 commit comments

Comments
 (0)