Commit Graph

2 Commits

Author SHA1 Message Date
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
Innei 3280844f5d ⚡️ perf(test): cut root suite from 397s to 287s, dedupe logger mocks, fix blind stream tests (#18804)
* ⚡️ perf(test): stop loading antd in the global vitest setup

setupFiles re-evaluate per test file, so `import { theme } from 'antd'` —
present only to set `theme.defaultConfig.hashed = false` — cost every one of
the 1836 root-suite files ~596ms of module evaluation.

Nothing asserts on unhashed antd class names, so the flag can go with it.

Full root suite: 396.95s -> 283.88s wall, setup 2027s -> 256s, no test changes.

Prebundling antd via `deps.optimizer` was measured as an alternative and
rejected: a prebundled module resolves to the optimizer chunk while
`vi.mock('<bare-specifier>')` registers on the bare id, so the mock silently
stops applying (23 suites started reading the real implementation).

Claude-Session: https://claude.ai/code/session_01K9fwqjBqMYMsYDddo9b4JN

* ✅ test(desktop,cli): lift per-file logger mocks into the setup files

52 desktop suites and 38 CLI suites each carried an identical
`vi.mock('…/utils/logger')` whose only job was silencing output — the desktop
logger also pulls electron-log and `@/env` in at module scope.

Register one stub per app instead:
- desktop `__mocks__/setup.ts` stubs `createLogger`
- new `apps/cli/tests/setup.ts` spreads the real module and replaces every `log`
  method with a spy, so suites that assert on log calls keep working

Suites needing different behavior still override with their own `vi.mock`;
`logger.test.ts` in each app opts out via `vi.unmock`.

desktop 122/122 pass. CLI unit failures go 9 -> 8 (all pre-existing;
`src/settings/index.test.ts` now passes).

Claude-Session: https://claude.ai/code/session_01K9fwqjBqMYMsYDddo9b4JN

* 🐛 fix(test): make model-runtime stream tests actually assert

Three suites drove a stream end to end and then asserted only that something
existed, so they stayed green against a broken implementation.

- zhipu `handleStream`: 8 cases built tool_call chunks, read the whole stream
  into `chunks`, then asserted `expect(chunks).toBeDefined()`. Driving
  `instance.chat()` with a mocked `create` yields `choices: []`, so the
  transform under test never ran. Rewritten to call
  `params.chatCompletion.handleStream` directly and assert the emitted SSE
  `event: tool_calls` indices — 365 -> 147 lines, and the
  all-placeholders branch is now covered.
- google `should handle text messages correctly`: the mock enqueued a bare
  string, which actually throws `Cannot use 'in' operator to search for
  'promptFeedback'` once the body is read. Now enqueues a real candidates
  chunk and asserts the SSE text payload.
- azureOpenai `should handle o1 series models without streaming` asserted only
  that `transformResponseToStream` ran, never that o1 forces `stream: false`.

Each was mutation-verified: deleting the negative-index fix, the placeholder
filter, the google thought filter, or the o1 stream override now fails these
tests and previously did not.

186 files / 4650 tests pass.

Claude-Session: https://claude.ai/code/session_01K9fwqjBqMYMsYDddo9b4JN
2026-08-29 17:39:09 +08:00