Skip to content

fix(llc): resolve the run transcript from the adapter state file (#13614) - #14755

Merged
mrveiss merged 6 commits into
Dev_new_guifrom
issue-13614-replay-transcript
Aug 22, 2026
Merged

mrveiss merged 6 commits into
Dev_new_guifrom
issue-13614-replay-transcript

Conversation

@mrveiss

@mrveiss mrveiss commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Closes #13614.

Thinking Path

A claude_code run executed real work end to end — 5 turns, real tool calls,
is_error: false — and its 37 KB transcript sat on disk while
llc_run_replay_logs.output_text stayed empty and recorded_events NULL.

Two places derive the same path from different inputs:

adapter    _output_path(dir, agent, "0/<session>")   -> ..._0_<session>.jsonl
adapter    return f"{proc.pid}/{session_id}"         -> "<pid>/<session>"
recorder   _output_path(dir, agent, external_run_id) -> ..._<pid>_<session>.jsonl   <- never exists

The placeholder is not an oversight that can simply be corrected: the file is
the child's stdout, so it must be created before Popen, and the pid does
not exist until after. Any fix that keeps two derivations has to keep them in
step forever.

The adapter already records the truth. Its state file — written after spawn,
keyed by the id it returns — carries the real output_file. So the fix is to
read it and delete the second derivation entirely.

What Changed

heartbeat_scheduler.py gains _resolve_adapter_output_file, which reads the
adapter's state file for the returned run id and takes output_file from it.

It falls back to the computed path when the state file is missing or unreadable
— runs predating the state file still resolve as well as they used to, and the
caller already reports a missing transcript rather than silently recording an
empty one.

Verification

Reproduced the defect and the fix against a stand-in that mirrors the adapter
exactly — placeholder-named file, pid-bearing returned id, state file written:

transcript on disk : llc_agent_agent7_0_<session>.jsonl
OLD resolver       : llc_agent_agent7_4242_<session>.jsonl  exists=False   <- the bug
NEW resolver       : llc_agent_agent7_0_<session>.jsonl     exists=True
no state file      : falls back to the computed path

A regression test drives _record_run_for_replay itself, with the file
named by the placeholder and a state file keyed by the returned id. It asserts
up front that the computed path does not exist — otherwise the test would
pass through the fallback and prove nothing — then asserts the transcript is
attached and recorded_events is non-empty.

The existing H1 test still passes, through the fallback: it creates the file
at the computed path with no state file, which is now the fallback's exact
case. So both paths are covered, and the older behaviour is preserved rather
than replaced.

Not addressed

The 0/ placeholder remains in the adapter. Renaming the file post-spawn would
remove the oddity but adds a failure mode (a rename that half-completes) to a
path that currently cannot lose data. The state file is the contract; this makes
the recorder use it.

Model Used

Claude Opus 5

@github-actions

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No hardcoded values detected that have SSOT config equivalents!

Applied Black, isort, and autoflake to match code-quality checks.
Triggered by workflow auto-fix-formatting.yml.
mrveiss and others added 4 commits August 22, 2026 09:14
…it (#13614)

The fallback recomputed the transcript path from the returned run id
`<pid>/<session>`, but an adapter must name that file before spawning the
child — it is the child's stdout — so the name always carries the placeholder
`0/<session>`. A live process cannot have pid 0, so the fallback named a file
no run has ever written and missed every time, in exactly the case it exists
to cover: a missing or unreadable state file.

The placeholder was being rebuilt by hand in four adapters, which is what let
the two derivations drift apart. It now has one definition in subprocess_base
and every adapter imports it, with a guard test so it cannot regrow.

An ImportError of the path helpers is a wiring fault, not an absent
transcript; the caller cannot tell those apart from a None, so it is logged.

Copilot adapters use a different filename scheme and stay unresolved; that
pre-existing gap is filed as #14760 and referenced from the docstring.
Applied Black, isort, and autoflake to match code-quality checks.
Triggered by workflow auto-fix-formatting.yml.
…ites (#13614)

test_uses_exact_output_path_from_external_run_id wrote its transcript at the
path derived from the returned run id `<pid>/<session>`. No adapter has ever
written there — the file is named before the child exists, so it always carries
the placeholder `0/<session>`. The test was passing on a shape no producer
emits, which is the same derivation #13614 is about.

Its guarantee — resolve one file outright, never a glob or an mtime ordering —
is worth keeping, so it now builds the path through the adapter's own helpers
and keeps the later-mtime decoy.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant