Skip to content

Commit fbf36b3

Browse files
Narrow LAD-003: the run adopts step identities it made up itself
Ruled out the last standing hypothesis. A run cannot hold two plan bindings — run_plan_bindings.run_id is the table's primary key — so the query's "count(*) != 1" can only ever mean zero, and zero means the step really is not in the revision the run is bound to. Reading the record path for that turned up a better-supported cause. agent_plan_record.go builds its local-to-durable step map from the result of the local BuildAgentPlan call rather than from the value the store returned, so a run adopts step identities it generated itself and assumes the store wrote the same ones. Nothing checks that. If RecordPlanRevision ever returns a revision whose steps carry different identities, every later step read is refused — and refused with a message saying the step does not belong, when the truth is that the run holds identities the store never issued. No behaviour change. The ticket now says where to start and what not to re-test. Change-Log: CL-20260803-176 Dev-Log: DL-20260803-186 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 6186d18 commit fbf36b3

3 files changed

Lines changed: 48 additions & 2 deletions

File tree

CHANGELOG

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

5151
Entries
5252
-------
53-
Change-ID: CL-20260803-175
53+
Change-ID: CL-20260803-176
5454
Commit: pending
5555
Date: 2026-08-04
56+
Type: Investigation -- LAD-003 narrowed; one hypothesis refuted and a better one
57+
found
58+
Request-or-TODO: LAD-003
59+
Outcome: Ruled out the last standing explanation for the plan-binding failure. A
60+
run cannot hold two plan bindings: run_plan_bindings.run_id is the table's
61+
PRIMARY KEY, so the query's "count(*) != 1" can only ever mean zero, and zero
62+
means the step really is not in the revision the run is bound to. Reading the
63+
record path for that turned up a better-supported cause:
64+
agent_plan_record.go builds its local-to-durable step map from the result of
65+
the local BuildAgentPlan call rather than from the value the store returned,
66+
so a run adopts step identities it generated itself and assumes the store
67+
wrote the same ones. Nothing checks that. If RecordPlanRevision ever returns a
68+
revision whose steps carry different identities -- an idempotent hit on an
69+
existing revision being the obvious way -- every later step read is refused,
70+
with a message saying the step does not belong when the truth is that the run
71+
holds identities the store never issued.
72+
Affected-behavior: None. This edits a ticket.
73+
Compatibility-or-migration: None.
74+
Verification: Not applicable; no behaviour changed. The refutation is read
75+
directly from migrations/000014_agent_plans.sql.
76+
Dev-Log: DL-20260803-186
77+
78+
Change-ID: CL-20260803-175
79+
Commit: 6186d18
80+
Date: 2026-08-04
5681
Type: Revert -- a fixture edit of mine caused a hard regression I cannot explain
5782
Request-or-TODO: Ladder rung 18
5883
Outcome: CL-20260803-173 added one sentence to rung 18's requirement defining

DEVLOG

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

2626
Entries
2727
-------
28+
Dev-Log: DL-20260803-186
29+
Date: 2026-08-04
30+
Status: Committed as CL-20260803-176
31+
Change-ID: CL-20260803-176
32+
Request-or-TODO: LAD-003
33+
Goal: Leave the ticket saying where to start rather than what I guessed
34+
Assumptions: None. The primary key is read from the migration
35+
Decisions: Recorded the finding without attempting the fix. Rung 18 passes on
36+
the reverted fixture and rung 19 is running; changing how plan identities are
37+
adopted is a storage-boundary change that deserves its own verification rather
38+
than being slipped in mid-climb
39+
Files-or-schemas: TODOS.md -- LAD-003
40+
Validation: None needed; no behaviour changed
41+
Failures-or-discarded-approaches: The double-binding hypothesis, refuted by the
42+
primary key. That is now recorded in the ticket so it is not re-tested
43+
Known-limitations: Still unproven. The new reading explains the symptom and the
44+
message, and nothing yet shows RecordPlanRevision returning different
45+
identities in this scenario
46+
Next-safe-step: Rung 19; LAD-003 as its own piece of work afterwards
47+
2848
Dev-Log: DL-20260803-185
2949
Date: 2026-08-04
3050
Status: Committed as CL-20260803-175

TODOS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5520,5 +5520,6 @@ can actually produce, not because this work is for the ladder.
55205520
- [ ] `LAD-003` A requirement's wording must not be able to break plan-step binding. On 2026-08-04 rung 18's requirement gained one sentence defining "pure" (the same definition rung 17 states). Every pass after that edit died inside two minutes on `load plan step state: database constraint: step does not belong to run plan`, having produced nothing; every pass before it reached 31 of 37 stages satisfied. The edit is reverted and the mechanism is unknown.
55215521
- Three explanations were tested and refuted. The new sentence lists `database/sql` and `math/rand`, so the first guess was that `AnalyzeTaskRequirement` reads them as paths — it does not; `requirementNamesNoFile` still reports true and `filesNamedIn` still returns the fallback pair. The second was that the durable plan was refused outright — "not be diagrammed" appears zero times in every failing pass. The third was that a step went unmapped in `adoptDurablePlanSteps` — that path now traces its drops and traced none.
55225522
- Behaviour count does not correlate: pass 1 named 18 behaviours and was clean, pass 6 named 16 and failed.
5523-
- `currentPlanStepState` (`internal/storage/agent_execution_repository.go:1946`) refuses on `count(*) != 1`, so **two** matching rows fail exactly as zero do. A run bound to two plan revisions, or a step matching two, would produce this. `recordDurablePlan` has one caller and is keyed `agent-plan:<task>:<attempt>` with attempt fixed at 1, so a second binding should not arise — worth confirming rather than assuming, since it is the only reading left that fits.
5523+
- **Ruled out since:** a double binding. `run_plan_bindings.run_id` is the table's PRIMARY KEY (`migrations/000014_agent_plans.sql:376`), so a run cannot hold two. `count(*) != 1` can therefore only mean zero, and zero means the step genuinely is not in the plan revision the run is bound to.
5524+
- **The reading that fits best now.** `agent_plan_record.go:141` builds the local-to-durable map from `plan.Steps[index].ID`, where `plan` is the result of the *local* `storage.BuildAgentPlan` call — not from `recorded`, the value the store returned. So the run adopts step identities it generated itself and assumes the store wrote the same ones. Nothing checks that. If `RecordPlanRevision` ever returns a revision whose steps carry different identities — an idempotent hit on an existing revision is the obvious way — every subsequent step read is refused, and refused with a message that says the step does not belong when the truth is that the run is holding identities the store never issued. Start by making that mapping read `recorded` rather than the local build, and assert the two agree.
55245525
- The mapping in `agent_plan_record.go:141` pairs local steps to durable ones **by position** under `if index < len(plan.Steps)`. That is safe only while `BuildAgentPlan` neither reorders nor changes the count; nothing asserts it does neither.

0 commit comments

Comments
 (0)