fabro/lib
fabro-sh-0530[bot] 61e26a6bff
fix: Prefer root stage TODO projection over child OpenAI plan events (#384)
## Summary

The stage insights sidebar could show a stale child TODO list (e.g.
`0/3`) even after the root agent plan completed, because child OpenAI
subagent sessions emit their own `todo.created` events on the same
`stage_id`, replacing the root projection and causing later root
`todo.updated` completions to be ignored.

## What changed and why

OpenAI `update_plan` lists are scoped per agent session as
`openai_plan:<session_id>`. A stage can have both a root session and
child/subagent sessions, but `StageProjection.todos` is a single slot —
whichever session emitted `TodoCreated` last won. The stage sidebar is a
root-agent summary, so the projection should always reflect the root
session's list.

**The fix** adds a `should_project_stage_todo_event` guard before each
of the `TodoCreated`, `TodoUpdated`, and `TodoDeleted` match arms in
`RunProjectionReducer`. It drops an event when `list_kind == OpenAiPlan`
and `stored.parent_session_id.is_some()` (i.e. the event came from a
child session). Root OpenAI events and all Anthropic task events pass
through unchanged.

This is intentionally the minimal change: no schema additions, no
frontend changes, no multi-list projection. Historical event logs replay
correctly because `parent_session_id` is already persisted on child
event envelopes.

### Plan Summary

- Add `should_project_stage_todo_event(stored, list_kind) -> bool`
helper in `run_state.rs`
- Gate all three TODO event arms on this helper
- Add three regression tests in the existing `todo_reducer` module: root
list survives child plan events, child events alone do not create a
projection, Anthropic child-session events still project

### Bundled in this diff (unrelated to the core fix)

- Removed retired OpenAI models `gpt-5-mini` and `gpt-5.2-codex` from
catalog, docs, and tests
- Added `2026-05-23` and `2026-05-24` changelog entries; backfilled
`2026-05-22` items
- `EventExportActions` in `run-stages.tsx` now only renders on the
`debug` tab
- `listLifecycleStatusLabel` suppresses the lifecycle label for
`initializing` runs
- `Provider` gains an `expected_secret_name` optional field; round-trip
tests updated
- CLI snapshot: cost column removed from stage completion display


### Fabro Details

<details>
<summary>Ran 10 stages in 37m 7s for $9.70</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 4s | – | 0 |
| preflight_lint | 1m 58s | – | 0 |
| fix_lints | 1m 29s | $0.32 | 0 |
| implement | 3m 27s | $3.01 | 0 |
| simplify_opus | 3m 15s | $0.80 | 0 |
| simplify_gpt | 2m 3s | $1.97 | 0 |
| verify | 14m 20s | – | 0 |
| fixup | 7m 35s | $3.61 | 0 |
| **Total** | **37m 7s** | **$9.70** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>

```dot
digraph ImplementPlan {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { model: claude-opus-4-7; }
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=succeeded"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=succeeded"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=succeeded"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_opus -> simplify_gpt -> verify
    verify -> exit  [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-24 12:30:41 -04:00
..
crates fix: Prefer root stage TODO projection over child OpenAI plan events (#384) 2026-05-24 12:30:41 -04:00
packages/fabro-api-client Link unconfigured providers to prefilled secret form 2026-05-24 09:34:24 -04:00