Files
Arvin Xu edbcbe3233 ✨ feat(cli): record local hetero agent runs as execution snapshots (#18874)
* ✨ feat(cli): record local hetero agent runs as execution snapshots

A heterogeneous agent run executed by the CLI left no durable record of
itself: server-ingest runs persisted messages, standalone `lh hetero exec`
runs persisted nothing, and neither produced the `ExecutionSnapshot` a
native agent run writes. A process killed mid-run was simply gone.

Record every run — server-ingest or standalone — into the same snapshot
format the server already uses, under `~/.lobehub/traces`, so `lh trace op
list` / `inspect` read local and production runs without branching.

`HeteroTraceRecorder` folds the adapter's event stream into steps:
`stream_start` … `step_complete{turn_metadata}` becomes a `call_llm` step
carrying that turn's model and usage, `tool_start` … `tool_end` becomes its
own `call_tool` step, and `step_complete{result_usage}` supplies the session
grand total. Turns after the first open lazily, so adapters that emit
`stream_start` once per session (Claude Code) and per turn segment alike.
Writes are serialized and coalesced, and go straight through rather than
re-reading the partial on every step as `appendStepToPartial` does.

Three fixes in the tracing store that this depends on:

- `savePartial` now writes atomically (temp file + rename). A partial exists
  precisely so a killed process can be recovered from it, and a plain
  `writeFile` interrupted by SIGKILL leaves truncated JSON — unparseable at
  exactly the moment it is needed.
- `FileSnapshotStore` takes a `dirName`, so a host owning its own layout does
  not end up with a nested hidden `.agent-tracing/` inside it.
- The `latest.json` symlink no longer fails the save when two runs race on it.

`lh connect` sweeps the store on start: partials whose last step is older
than six hours become `interrupted` snapshots (otherwise a crashed run — the
one most worth finding — is the only one `list` cannot show), and snapshots
past a fourteen-day window are deleted.

Also isolates the CLI home in tests. Driving a command end to end previously
wrote into the developer's real `~/.lobehub`, which this change would have
turned into hundreds of stray trace files per test run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(cli): keep subagent work out of the main trace spine

Two issues from review.

Claude Code's Task tool emits the subagent's turns and tool calls on the
SAME stream as the main agent's, tagged with a `subagent` peer field. The
recorder consumed them as main-agent events, so the child's text was
appended to the parent's turn, the child's usage closed that turn early,
and the child's tools surfaced as top-level calls — corrupting the trace
precisely for the runs that use Agent/Task. Skip subagent-scoped events:
the parent's own `Task` step still records what the child returned, which
mirrors how the native runtime keeps a sub-agent in its own child
operation rather than the parent's.

`lh trace op inspect`/`replay` resolved the default `latest` target from
the CLI-home store alone, and each store answers `latest` with its own
newest entry — so a developer who had run both `lh hetero exec` and a
local server inspected a stale run while `lh trace op list`, which merges
and sorts both stores, showed the newer one first. Compare the two stores
for the default target; an explicit id still needs no comparison.

Both are covered by tests that fail without the fix — the subagent one
drives the real `ClaudeCodeAdapter` through a Task sequence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 01:51:19 +08:00
..