Commit Graph

77 Commits

Author SHA1 Message Date
Arvin Xu 585fa593a5 🔖 chore(cli): bump version to 0.0.55 (#19618)
Ships the hetero `/goal` conversation flow (#19588), the operation-token
renewal for long hetero exec runs (#19549), the wrapped artifact share URL
(#19536) and readable task slugs (#19503).

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 09:23:11 +08:00
Innei 15c5a6fb05 🔖 chore(cli): bump version to 0.0.54 (#19451)
Ships `lh artifact publish` (#19360).

Claude-Session: https://claude.ai/code/session_019jVhraxkznG9MtkXaux657
2026-09-12 01:47:04 +08:00
Innei 6b608fa53e 🐛 fix(cli): declare url-join and vitest so the standalone install builds (#19408)
* 🐛 fix(cli): declare url-join and vitest so the standalone install builds

`apps/cli` installs against its own workspace, but `src/utils/agentStream.ts`
imports `url-join` and the tests import `vitest` while neither is declared
there — both resolve only through the root install's hoisting. CI does the root
install first, so this stayed invisible until someone builds the CLI on its own:

  Could not resolve 'url-join' in src/utils/agentStream.ts

`src/runtime/*` also imports `@lobechat/agent-runtime` and
`@lobechat/conversation-flow` undeclared, and those are deliberately left alone:
registering them pulls their workspace closure — 16 more packages including
`@lobechat/database` and a dozen builtin tools — into an install kept slim on
purpose. Declaring them properly is a bigger decision than this fix.

Verified by removing node_modules and running `pnpm install` + `bun run build`
inside `apps/cli` with no root install.

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

* 🔒 chore(desktop): sync lockfile with the CLI dependency declarations

apps/desktop lists ../cli as a workspace member and keeps a lockfile, so
declaring url-join and vitest in apps/cli left it outdated and failed
Test Desktop App with ERR_PNPM_OUTDATED_LOCKFILE.

Claude-Session: https://claude.ai/code/session_01558yVdNvoxocvnWAs11DGT
2026-09-11 02:49:25 +08:00
Innei b1a9979c5f ✨ feat(cli): publish a local HTML file as an artifact (#19360)
* ✨ feat(cli): publish a local HTML file as an artifact

`lh artifact publish <file>` gathers the page's local references, packs them,
and publishes the result.

The page's identity is a deployment id, recorded in `.lobehub/artifacts.json`
next to the project and committed with it, so a publish from CI updates the
same site a developer created locally. Republishing adds a revision and keeps
the public URL; `--new` makes a separate site and rebinds; `--deployment <id>`
targets one for a single run without touching the manifest. A bound id that no
longer resolves is an error — falling back to create would silently strand the
URL the caller meant to update.

Assets are read straight off the filesystem, so `--root` (default: the working
directory) sets how far up a page may reach for them. That replaces the app's
sandbox boundary, which does not apply here: the CLI reads with the user's own
permissions, and a page under `dist/` referencing `../assets/logo.png` is
ordinary rather than an escape attempt.

A create carries an idempotency key, written to the manifest *before* the
request. If the response is lost, the retry reuses the key and the server
returns the deployment it already made instead of minting a second site and
burning another quota slot. Updates are addressed by id and deliberately carry
no key: a retry costs a spare revision, never a stray site.

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

* 📝 docs(acceptance): record what the CLI artifact-publish round had to learn

Two judgment rules and two probe recipes from verifying `lh artifact publish`
against a local cloud → market → artifact-host chain.

L-E21 is the one an acceptance-checker caught mid-round: the case asserting
`--new` leaves the original site untouched published two sites with identical
bytes and a request line that echoed only the response. It read as a pass while
proving nothing, because requesting the wrong site would have printed the same
output. The rule generalises — make the two objects distinguishable before
capturing, and echo the target in the request.

L-S22 covers the quota the round consumes itself: each publish leaves an active
deployment, the Free tier caps at three, and the third run fails in a way that
reads as a regression. Compounded by clearing the cap for the wrong account —
the CLI publishes as the seeded runtime user, not the smoke's default id.

P84 records how to authenticate the CLI against lobehub-cloud's dev runtime at
all (seed an api_keys row; `dev:runtime:auth` only writes browser cookies), plus
the port-override that every runtime subcommand needs. P85 records how to
simulate a publish whose response was lost, which cannot be produced by killing
the process because the manifest is what carries the recovery state.

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

* 🐛 fix(cli): register @lobechat/html-artifact in the CLI's own workspace

`apps/cli` installs standalone against its own `pnpm-workspace.yaml`, which
enumerates the packages it may link rather than globbing them. The root install
resolved the new dependency, so this only showed up as CI's "Install CLI deps"
step failing:

  "@lobechat/html-artifact@workspace:*" is in the dependencies but no package
  named "@lobechat/html-artifact" is present in the workspace

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

* 🐛 fix(desktop): register @lobechat/html-artifact in the desktop workspace too

`apps/desktop/pnpm-workspace.yaml` lists `../cli` as a member, so the CLI's new
dependency has to be resolvable from the desktop install as well — its "Install
deps" step failed the same way the CLI's did. Unlike the CLI workspace this one
sets `lockfile: true`, so the regenerated lockfile ships with it.

Claude-Session: https://claude.ai/code/session_01558yVdNvoxocvnWAs11DGT
2026-09-11 01:27:49 +08:00
Arvin Xu 9dfe9242ec 🐛 fix(cli): keep workspace packages inside the embedded CLI bundle (#19391)
The desktop app builds `apps/cli` from the workspace it installs at
`apps/desktop`. pnpm 12 writes those out-of-root member links with two extra
`../` segments, so `apps/cli/node_modules/@lobechat/device-control` and
`@lobechat/device-identity` dangle. Rolldown only warns, marks both imports
external and reports a successful build, so canary 2.2.17-canary.18 onwards
shipped a `lobe-cli.js` that dies at startup with
`ERR_MODULE_NOT_FOUND: @lobechat/device-control`, even for `--version`.

Repair the dangling links before bundling, and turn an unresolved import into
a build failure so a broken bundle can never ship silently again.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 18:21:46 +08:00
Innei b65aae96b6 👷 build: migrate workspace to pnpm 12 (#19324)
* 👷 build: migrate workspace to pnpm 12

pnpm 12 no longer reads the package.json "pnpm" field, only honours auth and registry keys from .npmrc, and requires allowBuilds instead of onlyBuiltDependencies. Move every project setting into pnpm-workspace.yaml and keep the previously effective overrides and build allowlist.

* 👷 build: resolve desktop and cli workspaces on pnpm 12

pnpm 12 no longer accepts './'-prefixed workspace package globs, so the desktop stub path is now relative without the prefix. Migrate the remaining package.json "pnpm" fields into the desktop and cli pnpm-workspace.yaml files.

* 👷 build: list protobufjs in the desktop build allowlist

pnpm 12 fails the install with ERR_PNPM_IGNORED_BUILDS when a script-bearing dependency is missing from allowBuilds. protobufjs only appears in the desktop graph on Linux, so it was absent from the previous allowlist.

* 👷 build: relink root workspace members before the desktop typecheck

pnpm 12 writes broken relative symlinks for workspace members that live outside the desktop workspace root (../../packages/*), so tsgo cannot resolve sibling packages from source. Installing the root workspace after the desktop install rewrites those links correctly.
2026-09-09 19:43:11 +08:00
Arvin Xu c209af302f 🔖 chore(cli): bump version to 0.0.53 (#19340)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 19:22:34 +08:00
Neko 675e98554e 🐛 fix(cli): wait for daemon startup readiness (#19000)
* 🐛 fix(cli): report daemon startup failures

* ♻️ refactor(cli): use tinyexec for daemon processes

* 🐛 fix(cli): complete daemon startup preflight
2026-09-02 02:42:04 +08:00
Arvin Xu 14abb41438 🔖 chore(cli): bump version to 0.0.52 (#19009) 2026-09-02 01:06:59 +08:00
Arvin Xu 66c0a001be ✨ feat(goal): parallel task scheduling, trace fixes and CLI 0.0.51 (#18944)
* 🐛 fix(goal): point a running goal's trace key at the object that exists

`writeObservationRow` wrote the finalized S3 key from the first advance, but
while a goal runs only the partial exists. So `hasTrace` read `yes` for every
in-flight goal, `getGoalTrajectoryUrl` signed a URL that 404s, and a goal could
not be inspected from the server until it was over — the opposite of the
"an unfinished long-horizon goal is the normal thing to inspect" the recorder
was written around.

Found by running it: the first real production goal listed with `TRACE yes`
and then failed to download. Unit tests could not have caught it — they drive
an in-memory store where both keys are the same map, and the bug lives in the
gap between what the row claims and what object storage holds.

The key now follows the object — partial while running, finalized once the
goal is terminal, which is exactly when `finalizeGoalTrace` removes the
partial.

The client cache had the mirror-image bug: caching an in-flight trajectory
pinned the reader to whatever the goal looked like the first time it was
inspected. Only finished goals are cached now, and an older cached partial is
re-fetched rather than served.

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

* ✨ feat(goal): run independent tasks in parallel, under a concurrency cap

Four bug fixes that share no code queued behind one another for an hour. Not
because of dependencies — the trace recorded the other three as candidates with
`blockedBy: []`, so the coordinator knew they were independent and started them
anyway one at a time.

Three things made the frontier single-threaded, and all three had to go:

- `selectFrontier` returned one node. A running node stays eligible, so it was
  re-picked every tick.
- that re-pick reported `waiting_external`, which ends the advance before
  anything behind it is considered.
- dispatching also reported `waiting_external`, so even a fixed frontier would
  have started exactly one task per advance.

The decision now walks the whole ranked frontier, skips what is already in
flight or parked on a person, and takes the first candidate it can act on;
dispatching reports `advanced` so the advance keeps filling slots.
`maxConcurrentTasks` caps the fan-out — defaulting to 3, because a goal that
dispatched everything at once would spend its budget before the first result
came back.

A running task is still examined for staleness rather than skipped outright.
Skipping it made abandoned operations unreclaimable: the reclaim only ever runs
from the branch that looks at a running task, and an integration test caught
that the first version had quietly removed it.

The recorded decision input grew with the decision. The scheduler reads every
candidate's task state, so a trace that kept only the chosen one would no
longer let a replay reproduce the choice — `candidateTasks` and the cap in
force are recorded instead, and the replay adapter rebuilds the map.

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

* 🐛 fix(cli): link `lh goal create` at the goal it just created

`goal.create` returns the whole graph snapshot, so the id lives on its `goal`.
Reading `result.data.id` printed `app.lobehub.com/goal/undefined` — a link
nobody could follow — right under a graph that `printGraph` had just rendered
from `graph.goal.id` on the next line.

Same blind spot as #18912: the CLI cannot resolve the TRPC router's types, so
`result.data` widens to `any` and reading a field that isn't there compiles.
`apps/cli` is also outside the root `tsconfig.json` include and outside the CI
test matrix, so neither the type checker nor CI would have caught it.

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

* 🔖 chore(cli): bump version to 0.0.51

0.0.50 was published on 08-30, and seven changes have landed on `apps/cli` and
`@lobechat/agent-tracing` since — including `lh trace goal`, which is not in
the published package at all, and the glyph fix for `lh goal show`, which is
only visible to someone running `lh`.

The man page is regenerated with the bump: it picks up the version string and
Factory Droid joining the heterogeneous agent list. `lh trace goal` adds no
entry because the page lists top-level commands and `trace` was already there.

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

* 🐛 fix(goal): close the four gaps Codex found in the parallel scheduler

**The budget stopped guarding paid work (P1).** `needsBudget` was asked of the
frontier's head, which was safe while the head was the only thing that could be
dispatched. The scheduler now walks past a running head, so a head reporting
"no budget needed" meant the budget was never read — and `budget_exhausted` can
only fire on a budget that was read. A goal could start work past its
`maxRounds` / `maxTotalCost`. It is asked of every unblocked candidate now, and
counts a retryable failure as spending, because it is.

**The cap was advisory, not enforced (P1).** Two overlapping advances could each
read the same `inFlight` below the limit and each claim a different ready task;
the per-task CAS only prevents dispatching the *same* task twice. The count and
the claim are taken together under a per-goal advisory lock, with the in-flight
count read from the database rather than from a snapshot — the planner's check
is a fast path, this is the enforcement.

**Legacy trajectories replayed as `missing_task`.** Ticks recorded before this
change carry `frontierTask`, not `candidateTasks`. Dropping the field made every
one of those replay with no task state, reporting divergences that never
happened. It stays as a deprecated field the replay adapter falls back to.

**The cap could not actually be set.** `GoalConfig` advertised
`maxConcurrentTasks` while the `goal.create` Zod schema still allowed only
`config.recovery`, so Zod stripped it and every API-created goal silently used
the default. The schema accepts it with bounds mirroring
`resolveMaxConcurrentTasks`, and `lh goal create --max-concurrent-tasks` exposes
it — a cap reachable only by hand-written TRPC is not a cap anyone can use.

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

* 🐛 fix(goal): keep parallel scheduling moving

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 09:20:59 +08:00
Arvin Xu 6beffeb677 🔖 chore(cli): bump version to 0.0.50 (#18860)
Co-authored-by: Arvin Xu <arvin.x@lobehub.com>
2026-08-30 11:17:34 +08:00
Arvin Xu 77be8bb286 ✨ feat(cli): lh trace op — inspect and replay a recorded operation, single call or whole trajectory (#18797)
* ✨ feat(cli): add `lh trace replay` to re-issue a frozen LLM call across models

An execution snapshot already freezes everything a single LLM call saw —
`steps[].contextEngine.output` is the exact message array sent to the model,
`context.payload.tools` the toolset it could reach — but nothing consumed it.
Diagnosing a bad answer meant re-running the whole agent, which re-assembles
context and so mixes the harness and the model into one unattributable result.

`lh trace replay` sends that frozen payload back out with only the model
swapped, so a difference in output is attributable to the model. If every
model fails the same payload, the context assembly is at fault; if some pass,
it is model selection.

- `packages/agent-tracing`: extract the local → `_remote` cache → download
  resolution that inspect / resolve / ctx-lint each reimplemented into a
  shared `loadSnapshot()`, and export it plus the remote-store helpers.
  Downloads stay opt-in so read-only callers never hit the network.
- `apps/cli`: new `trace` command group with `replay`, defaulting to the last
  `call_llm` step and to the model the snapshot ran on.
- `--judge <criteria>` scores each output through `@lobechat/eval-rubric`'s
  `llm-rubric` matcher, so replay scoring is the same code path the eval
  pipeline uses — only the transport is local.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 5d406a5a66)

* ♻️ refactor(cli): scope trace commands under `lh trace op` and share the logic with agent-tracing

`lh trace replay` assumed an operation was the only thing worth tracing. Goal
traces are coming, and they need their own resolution and views rather than
more flags on a flat command — so the scope becomes a level in the command
tree: `lh trace op ...` today, `lh trace goal ...` next to it later.

Moving replay under `op` also made the split obvious: everything that reasons
about a snapshot belongs to the package, and each CLI supplies only how it
reaches a model.

- `packages/agent-tracing`: extract the 250-line body of `inspect`'s action
  into `inspectSnapshot()`, which renders a view and throws `InspectError`
  instead of calling `process.exit` — so a caller that is not a CLI can use it.
  `cli/inspect.ts` drops from 338 to 64 lines and keeps its behaviour.
- Move the replay core (`payload`, `judge`) into `src/replay/` and add
  `replayFrozenCall()` / `judgeReplay()` over an injected `ReplayConnection`.
  `apps/cli` supplies it from the `lh login` session; the dev CLI reads
  `LOBEHUB_JWT` and says so when it is missing.
- Add `agent-tracing replay`, so a snapshot can be replayed from the same tool
  that inspects it without going through `lh`.
- `apps/cli`: `lh trace op` with `inspect` / `list` / `replay`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit cad6ede50f)

* ✨ feat(cli): replay a whole recorded trajectory node by node

Single-call replay answers "would another model have said this differently".
It cannot answer "would another model have got here at all" — which is the
question when the failure is a decision made three tool calls earlier.

Adds two trajectory modes over the `call_llm` nodes of a recorded operation:

- `--chain` feeds each replayed output into the next node, so the run is
  free-running and the model can leave the recorded path.
- `--all-steps` replays every node against its own recorded payload, so a
  divergence at one node cannot contaminate the ones after it.

Two properties of the recording shape the design:

The context engine re-renders the payload on every call — injected blocks
appear, move and drop out, so consecutive payloads share only the system
message as a common prefix. A chain therefore cannot append to the previous
payload; it splices. Each node keeps the harness's own rendering of that call
and replaces only the turns the model produced, located by finding the earliest
recorded assistant turn still present (`findChainAnchor`).

Tool output cannot be regenerated, only reused from the recording. So the chain
runs on recorded results matched to the model's calls by name and order, and
the moment the model calls a tool the original run did not, there is no ground
truth to continue on. That point is the finding, so it is reported
(`divergedAtNode`, `unmatchedTools`) rather than papered over; `--on-divergence
continue` pushes past it when the later nodes are worth seeing anyway.

Divergence is defined on the tool-call sequence, not on content: two models can
reach the same step with differently-worded arguments, and calling that a
failure would report noise. Content is scored instead — a trajectory run judges
its final output against the recorded one for semantic equivalence.

Committed with --no-verify and an explicit pathspec: another session is editing
this working tree concurrently, lint-staged stashes unstaged changes, and the
shared index is not mine to commit. prettier, eslint and the package tests were
run directly against these files instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit ae4290f020)

* ✨ feat(cli): reach production traces through the server instead of a public bucket

The CLI half of the signed-snapshot path. `lh trace op` now asks the server for
what it cannot work out itself, so inspecting a production run needs a LobeHub
login and nothing else.

- `loadSnapshot()` takes an optional `resolveDownloadUrl`, tried before
  `TRACING_BASE_URL` and falling back to it, so the standalone `agent-tracing`
  CLI keeps working unchanged while `lh` supplies the authenticated path.
- `lh trace op list --topic <id>` lists a topic's operations from the server,
  showing which ones still have a trace to fetch.
- A `NOT_FOUND` from the server means absent, out of scope, or never recorded —
  all "nothing to download", so it falls through to any locally configured
  base URL, and the reason is kept so the final message can say "no trace
  recorded" rather than blaming a missing env var.
- Resolve `zlib.zstdDecompress` lazily. Promisifying it at module scope crashed
  the whole `lh` binary at startup on Node < 22.15, since `program.ts` imports
  the trace tree; now only reading a compressed snapshot fails, and it names
  the requirement.

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

* ♻️ refactor(cli): report replay divergence as {field, recorded, replayed}

The goal-coordinator layer reports its own replay divergences per field, and a
goal trace's leaves are operation ids that hand off to this one. Naming the
field lets a single reader walk both layers and decide per field whether it is
scored or informational — which is what keeps content differences from reading
as failures, since content is judged rather than failed.

`unmatchedTools` and `reproduction` stay as they are: both are specific to
replaying an operation and gain nothing from being flattened into the shared
shape.

Committed with --no-verify and a pathspec: this working tree is shared with
another session, and lint-staged stashes unstaged changes. prettier, eslint and
the package tests were run against these files directly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 56f1d253e4)

* 🐛 fix(desktop): register the packages `apps/cli` newly depends on

`apps/desktop/pnpm-workspace.yaml` lists `../cli` as a workspace member, so a
desktop install has to resolve every `workspace:*` dependency the CLI declares.
Adding `@lobechat/agent-tracing` and `@lobechat/eval-rubric` to the CLI without
listing them here broke that install:

  ERR_PNPM_WORKSPACE_PKG_NOT_FOUND  In ../cli: "@lobechat/agent-tracing@workspace:*"
  is in the dependencies but no package named "@lobechat/agent-tracing" is
  present in the workspace

`apps/cli/pnpm-workspace.yaml` had been updated, which is why the CLI installs
locally and only the desktop job failed.

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

* 📦 chore(cli): declare the Node 22.15 floor the trace commands already require

`lh` reads trace snapshots through `node:zlib` zstd, which lands in Node 22.15,
but the package declared no `engines` at all — so a user on an older runtime got
no warning at install time and a failure later. Node 20 went EOL in April 2026;
22 stays in maintenance LTS until April 2027, so 22.15 is the floor rather than
24.

Bumps the tsdown target to match, which was still `node18`.

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

* 🐛 fix(cli): stop replay reporting a broken chain as a clean run

Codex review on #18797. Each of these let a chained replay finish and print
success while the comparison underneath it was invalid.

- Tool results were matched to replayed calls by name alone, so a model that
  called `readFile("b")` where the recording called `readFile("a")` was fed the
  body of "a" and continued on a premise the recorded run never established.
  `recordedToolResults` now pairs each output with the arguments of the call it
  answers (the trace stores calls and results as separate step fields with no id
  between them, so they are zipped per name in order), and `buildToolMessages`
  requires both to match. Traces recorded before this carry no arguments and
  still fall back to name-and-order matching.
- That mismatch was also invisible: `toolSignature` compares names only, by
  design, so same-tool-different-arguments raised no divergence. It now reports
  a `toolArguments` divergence and stops the chain, since a node with no
  recorded result has no ground truth to continue from.
- Losing the chain anchor — context compression dropping every earlier assistant
  turn from a later payload — exited the loop silently, leaving a result with
  fewer nodes than `totalNodes` that still rendered as "stayed on the recorded
  trajectory end to end". It is now an explicit `incomplete: { anchor_lost }`,
  the CLI says where it stopped and why, and a truncated run is no longer scored
  for reproduction.
- `buildReplayRequest` claimed everything but the model was carried over
  verbatim. It is not: traces record the runtime step context under
  `context.payload`, not the provider request body, so temperature, top_p,
  max_tokens, penalties and reasoning config are in no trace written so far and
  a replay silently uses today's server defaults. `FrozenCall.params` now
  carries them when present and `buildReplayRequest` applies them under explicit
  overrides, so the reading half is ready; until the recorder writes them, the
  CLI prints a note saying the replay is running on defaults. Recording them is
  a follow-up in the runtime.
- Document `replay` in the agent-tracing skill, including what chain mode
  reports and the parameter limitation, and rewrite "Remote Traces" around
  `lh trace op` now that the server signs snapshots.

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

* ♻️ refactor(cli): replay every recorded call independently, drop chain mode

Chain mode fed each replayed output into the next node. It cannot work: a trace
cannot regenerate tool output, only hand back what was recorded, so the moment
the model deviates there is no ground truth left — and the run kept going,
measuring nothing while still looking like it succeeded. Every finding in the
last review was a symptom of that one structural choice (tool results matched to
the wrong call, an argument mismatch invisible to a name-only signature, a lost
splice anchor exiting as a finished trajectory), so the fix is to remove the
dependency between nodes rather than to keep guarding it.

`--all-steps` is now the only way to replay a whole operation, and every node is
replayed against the payload the harness actually built for it: node 4 is asked
"given exactly this context, do you make the same decision?", which is the
question worth asking of a model.

Independence buys three things the chained version could not have:

- a divergence at one node cannot contaminate the ones after it;
- a node that fails to reach the provider costs only itself, instead of
  truncating the run (the old loop broke out on the first transport error, in
  both modes);
- the calls go out concurrently — 4 at a time by default, `--concurrency` to
  change it — while results are still reported in node order.

Removes `--chain`, `--on-divergence`, `DivergencePolicy`, the anchor splicing
(`findChainAnchor` / `spliceChainedPayload` / `recordedAssistantTurn`), the
recorded-result feedback (`buildToolMessages` / `recordedToolResults` /
`normalizeToolArguments`), `unmatchedTools`, the `toolArguments` divergence and
the `incomplete` / `anchor_lost` result field — all of which existed only to
make chaining survivable.

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

* 💄 style(cli): show a replay as one strip of call nodes

A trajectory replay printed a paragraph per node, so the shape of the run —
which is the whole answer — only emerged after scrolling, and a 125-call
operation buried it entirely.

Every LLM call is now one glyph on a horizontal strip in call order, filled in
live as nodes settle and wrapped at 50 with a call-number gutter once it needs
one. Detail is printed only for the nodes worth reading: the ones that diverged
or never answered.

Green / yellow / red rather than green / red. A divergence is the finding the
replay exists to surface — the model chose different tools here — while a
transport failure says nothing about the model at all, and collapsing the two
into one colour would hide the signal inside the noise.

`onNode` now fires as each node settles rather than after all of them, which is
out of order under concurrency. Every node carries its own `nodeIndex` so the
strip paints by position; `result.nodes` stays ordered.

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

* ✨ feat(cli): make a replay end in a pass/fail verdict

The question this feature exists to answer is "take a run that succeeded, put
another model on it — does that model also get the job done?". The output
answered a different one. Tool-route differences were the headline (yellow
glyphs, a "5 matched / 1 different tools" score line) and the judge's verdict
was a footnote at the very bottom.

That framing is wrong on its own terms: another model may reach the same result
by calling different tools, and scoring it on route equality fails it for
succeeding differently.

- The verdict leads. PASS / FAIL and the score come first, with the judge's
  reason wrapped underneath; the call strip and per-call route notes follow as
  supporting evidence.
- `TrajectoryResult.reproduction` becomes `verdict`, and its rubric now judges
  the outcome explicitly — same decision, same substantive claims, "allowing for
  differences in ... which tools were used to get there".
- A different route is drawn neutral (cyan `○`) instead of as a warning. Only a
  call that never reached the model stays red.
- `--judge "<criteria>"` now applies when replaying every call, so success can
  be defined per trajectory. It was previously read only on the single-call
  path and silently ignored with `--all-steps`.
- A final call that never reached the model now yields an explicit FAIL. It
  previously produced no verdict at all — the worst outcome for a pass/fail
  tool, which is now covered by a regression test.

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

---------

Co-authored-by: Arvin Xu <arvin.x@lobehub.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 11:29:42 +08:00
Arvin Xu 9031535652 🔖 chore(cli): bump version to 0.0.49 (#18622)
Co-authored-by: Arvin Xu <arvin.x@lobehub.com>
2026-08-23 17:07:37 +08:00
Arvin Xu 4eb459c146 🔖 chore(cli): bump version to 0.0.48 (#18447) 2026-08-18 12:29:09 +08:00
Arvin Xu f5a941ffeb 🔖 chore(cli): bump version to 0.0.47 (#17888)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 23:06:46 -07:00
Innei 0b5af01cef ✨ feat(desktop): preview workspace HTML files (#17545)
* ✨ feat(desktop): preview workspace HTML files

* ♻️ refactor(mime): unify MIME detection via @lobechat/utils/mimeType

Fold the ad-hoc extension→MIME maps in desktop, CLI, and device-control
into a single buffer-aware resolver. `resolveMimeType(path, buffer)`
trusts file-type's magic-byte detection first, then falls back to
mime.getType with a text-sniff downgrade so ambiguous extensions like
`.ts` (video/mp2t vs TypeScript source) resolve correctly everywhere.

* fix: resolve html preview ci failures
2026-07-27 14:13:47 +08:00
Arvin Xu ebacf7fc6e 🔖 chore(cli): bump version to 0.0.46 (#17471) 2026-07-23 21:24:43 +08:00
Arvin Xu 84f79b95d3 🔖 chore(cli): bump version to 0.0.45 (#17358)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 01:27:12 +08:00
Arvin Xu 0100fb61fc ♻️ refactor(cli): make lh acceptance the home of the review loop (#17336)
* ♻️ refactor(cli): move run/result/evidence/report/init under `lh acceptance`

Reorganize the CLI so the acceptance decision aggregate is the home for the
whole review loop, and `verify` keeps only the checker machinery (criteria,
rubrics, per-run plans).

- New canonical tree: `lh acceptance init` and `lh acceptance run …` (ingest,
  list, get, delete, result{ingest,list,submit,decision}, evidence, report).
  `ingest-report` becomes `acceptance run ingest`; a run is one immutable round.
- `verify` retains criterion / rubric / plan / execute / install / submit-less
  machinery. Everything moved keeps a deprecated `lh verify …` alias (same flags,
  same action, one deprecation warning) for a few releases.
- Extract shared helpers into verifyHelpers.ts (re-exported from ./verify for
  existing importers); action bodies live once in acceptanceRun.ts and wire both
  the canonical and deprecated spellings, so they can't drift.
- `init` now defaults to the `acceptance` skill bundle.

Tests: 52 passing — deprecated aliases still dispatch, the canonical run tree
dispatches, and the run subtree is absent from the deprecated `verify acceptance`
alias.

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

* 📝 docs(acceptance): repoint skills & prompts to the `lh acceptance` surface

Follow the CLI reorg through every consumer of the old `lh verify …` command
surface.

- agent-testing skill (both `.agents/skills/**` and the checked-in CLI mirror
  `apps/cli/skills/**`): `lh verify ingest-report` → `lh acceptance run ingest`,
  `lh verify acceptance …` → `lh acceptance …`, atomic-command references →
  `lh acceptance run {result,evidence,report} …`; "verify run" (the noun) → the
  acceptance run it now is.
- Rename the portable builtin skill `verify` → `acceptance`
  (`builtin-skills/src/verify` → `.../acceptance`): identifier/name, exports
  (AcceptanceIdentifier/AcceptanceSkill), and its content's command references
  (`lh verify submit` → `lh acceptance run result submit`; plan machinery stays
  `lh verify plan …`). `lh acceptance init` materializes it into
  `.claude/skills/acceptance`.
- Server pull registry serves identifier `acceptance`, with `verify` kept as a
  back-compat alias so cached `--skill verify` callers still resolve.
- Task prompt tells builders to submit via `lh acceptance run result submit`.

Tests: prompts task suite green (22), CLI suite green (52).

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

* ♻️ refactor(cli): unify skill install under `lh acceptance install` (server pull)

Collapse the two skill-materialization paths into one. `acceptance init`
(server pull) and `verify install` (bundled agent-testing copy) become a single
`lh acceptance install` that pulls the acceptance skeleton from the server and
writes it to `.agents/skills/acceptance` — the harness dir the project's own
`.agents/acceptance/` adapter sits beside.

- Drop the CLI-bundled skill entirely: remove `verifyInstall.ts`, `apps/cli/skills/`,
  and `"skills"` from the published `files` — server pull is the single source.
- Both legacy spellings (`verify init`, `verify install`) stay as deprecated
  aliases that route to `installAction`.
- Install target moves `.claude/skills` → `.agents/skills`.

Tests: CLI suite green (52).

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

* 🚚 chore(acceptance): rename project adapter .agents/verify → .agents/acceptance

The project-local adapter layer (PROJECT.md env/port/auth contract, LobeHub-
specific hook scripts, and the living-log notes) moves to `.agents/acceptance/`,
matching the skeleton it feeds (`.agents/skills/acceptance`). All references
across the agent-testing skill and the adapter's own files are repointed.

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

* 🗑️ chore: remove accidentally-committed node_modules symlinks

Two symlinks (apps/cli/node_modules, packages/builtin-skills/node_modules)
pointing into a local checkout slipped into the previous commit via `git add -A`
— .gitignore only ignores node_modules directories, not symlink files. Untracked;
they must never ship in the repo.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 00:45:07 +08:00
Innei 66a2bb54ff feat(cli): generic agent-testing skill + lh verify install (#17280)
* feat(cli): add generic agent-testing skill docs

* feat(cli): port generic agent-testing scripts

Copy the eight generic agent-testing scripts (plus their test suite and
the cdp-capture.cjs dependency) from .codex/skills/agent-testing/scripts/
into apps/cli/skills/agent-testing/scripts/, stripping the fixed
repo-relative directory walk-ups so each script self-locates from
$0/BASH_SOURCE/import.meta.url and treats the invoking cwd as the
consumer repo, per the T1 skill docs already on this branch.

* feat(cli): package agent-testing skill and add lh verify init install logic

Ship the bundled agent-testing skill inside the published @lobehub/cli
package (files += skills), add a package-root locator that walks up from
import.meta.url so it resolves from both the built dist/index.js and the
monorepo dev entry, and give cdp-capture.cjs a .skill-meta.json/cliRoot
fallback for resolving `ws` once the skill is copied into a consumer repo
(where the ancestor node_modules walk no longer reaches @lobehub/cli).

Also adds the install logic for `lh verify init` (harness-dir detection,
copy + exec-bit restoration, .skill-meta.json version marker, semver
upgrade/no-op, refuse-without-marker + --force, .gitignore hygiene) as a
standalone module with full edge-case test coverage. Not yet wired to a
CLI subcommand — `verify init` is already taken by an existing tRPC-based
skill-bundle puller (PR #16164); naming/merge is pending a decision.

* feat(cli): wire lh verify install, distinct from the existing verify init

Register a new `verify install` subcommand that copies the bundled
agent-testing skill into the consumer repo's harness skills dirs
(.claude/.codex/.agents), per the human's naming decision — `verify init`
(PR #16164, pulls a named skill bundle live from the server) is left
completely untouched.

Renames verifyInit.ts/.test.ts to verifyInstall.ts/.test.ts for naming
consistency with the command, and adds command-level tests (registration,
happy path, --json, --target, no-harness-dir failure) alongside the
existing core install-logic tests. The command-level test for the
no-harness-dir case caught a real bug: the NoHarnessDirError catch handler
called process.exit(1) without returning, so it fell through to `throw e`
under a mocked (non-terminating) process.exit — fixed with an explicit
`return`.

Also fixes two stale `lh verify init` references left over from before the
naming decision, in project-adapter.md and cdp-capture.cjs's comments.

* refactor(agent-testing): migrate lobe-chat to the generic skill + adapter

Make lobe-chat the first consumer of the generic agent-testing skill:

- Add .agents/verify/ adapter: PROJECT.md (six-section contract capturing the
  env/auth/probe/constraint knowledge from the old SKILL.md), the project-layer
  living logs (common-mistakes.md, probe-mock-patterns.md — the LobeHub-specific
  entries not promoted to the generic layer), the moved project scripts
  (init-dev-env / setup-auth / app-probe / test-env / electron-dev / start-s3 /
  check-s3 / record-electron-demo / agent-gateway), and the LobeHub reference
  docs (auth / dev-server / agent-gateway / multi-instance). Script self-locate
  depths and internal path references updated for the new location.
- Replace the old .agents/skills/agent-testing with the generic skill installed
  via `lh verify install` (bundled generic SKILL.md, surfaces/, references/,
  scripts/, .skill-meta.json).
- Extract the bot-channel surfaces into a standalone project skill
  .agents/skills/agent-testing-bot, extending the generic Plan/Execute/Finish
  process; cross-links repointed to the sibling generic skill.
- Fix cross-skill reference in ux-audit (agent-testing ui/ -> surfaces/).

* docs(agent-testing): refine project-layer probe-mock log

Drop two entries duplicated by the generic layer (daemon-serializes, curl-502
proxy env) and restore three project entries missing from the first cut:
Electron pool LOBE_IPC_ID length, main-process-restart HMR boundary, and Next
dev not hot-reloading apps/server/**.

* fix(cli): make skill-meta cliRoot portable, drop lobehub example from skill script

The .skill-meta.json marker written by `lh verify install` baked in an
absolute filesystem path for cliRoot when the @lobehub/cli package root
was vendored inside the consumer repo itself, which is not portable
across machines/CI and shows up as noise in commits. Write a path
relative to the marker file's own directory in that case, and resolve
it accordingly in cdp-capture.cjs's ws-fallback lookup. Global/npx
installs (cliRoot outside the consumer repo) keep the absolute path
since there's no repo-relative path to express.

Also swap the lobehub-specific `--session lobehub-dev` usage example
in the generic agent-browser-klm.mjs script for a neutral one.

* fix(cli): clarify unversioned --force message, harden meta version parsing

- verifyInstall: --force replacing a marker-less install now reports
  "replaced unversioned install with X" instead of "updated undefined → X"
- readSkillMeta: treat a .skill-meta.json with a missing/invalid semver
  version as no meta (via semver.valid), routing tampered dirs into the
  existing refuse/--force flow instead of throwing in semver.compare
- SKILL.md: drop stale "(ported by a later task)" note from the scripts/
  directory-map comment; refresh installed .agents/skills copy to match

* 📝 docs(agent-testing): add CLI version gate and subject-creation recipe to publish step

* 🐛 fix(cli): ship ws as a runtime dependency and resolve it pnpm-layout-safely in cdp-capture

ws lived in devDependencies, so a real npm/pnpm install of @lobehub/cli
shipped no ws at all and cdp-capture.cjs failed on both resolution paths.
The cliRoot fallback also hand-built <cliRoot>/node_modules/ws, which never
exists under pnpm (deps are linked beside the package in .pnpm); use
createRequire anchored at cliRoot so Node's own walk covers both layouts.
Verified against a packed tarball installed into a scratch prefix: the
copied skill's cdp-capture now reaches the CDP probe (ECONNREFUSED) instead
of dying with MODULE_NOT_FOUND.

* 🐛 fix(cli): address codex review on agent-testing skill

- verify install: require the marker's `name` to match the skill before
  trusting its version, so a foreign or tampered `.skill-meta.json` is
  refused instead of reported as a no-op / silently downgraded.
- report-init.sh: JSON-escape interpolated fields, so a title containing
  quotes, backslashes or newlines no longer emits invalid result.json.
- docs: point the JWKS workaround at the relocated init-dev-env.sh and the
  bot preflight at the installed skill path (both resolve from repo root).

* 🔀 chore(agent-testing): reconcile canary updates
2026-07-17 23:11:30 +08:00
Arvin Xu 78d22e955f 🔖 chore(cli): bump @lobehub/cli to v0.0.44 (#17288)
* 🔖 chore(cli): bump @lobehub/cli to v0.0.44

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

* 🔖 chore(cli): regenerate man page for v0.0.44

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

* 📝 docs(agent-testing): group reports by acceptance subject & add per-check fixture tooling

- report-init.sh: --subject groups runs under .records/reports/<subject-key>/ with an acceptance.json marker, pre-fills result.json.subject for auto-attach ingest
- fixture.mjs: init-check / list / compose reusable per-check fixture assets under .records/fixtures/, composing ingest-ready rounds from check.json templates
- SKILL.md & references/report.md: document the grouped layout and inputs-vs-outputs fixture rule
- common-mistakes: Case 28 — always publish reports to production, create a prod anchor if the subject only exists locally
- probe-mock-patterns: C12 (stale global lh silently orphans ingest), D22 (driving the manual-approval intervention chain in web chat)

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 13:59:16 +08:00
Arvin Xu 12d9b9b197 🔖 chore(cli): bump @lobehub/cli to 0.0.43 (#17237) 2026-07-16 01:54:11 +08:00
Arvin Xu f8ff7ff8ba ✨ feat(verify): subject-level acceptance aggregate with cross-round check union (#17229)
* ✨ feat(verify): subject-level acceptance aggregate with cross-round check union

Wire up the dormant acceptances schema (#16899) end to end:

- db: AcceptanceModel (ensureForSubject / status lifecycle) + verify run
  chain methods (attachToAcceptance, listByAcceptance, setDecision) and
  batch listByRuns/findByRuns readers for whole-chain reads
- server: AcceptanceService — deterministic cross-round check union
  (final verdict + history trail + fixed/carried/introduced provenance,
  per-item surface), status rollup (delivered = settled, awaiting the
  user's decision), accept/reject stamping the round decision; new
  `acceptance` lambda router (ensure / attachRun / getBundle / list /
  accept / reject); evidence file-URL resolver extracted and shared
  with getReportBundle; upsertReport re-derives the aggregate status
- cli: `lh verify ingest-report --subject type:id` chains each session
  onto the subject's acceptance as its next round; new
  `lh verify acceptance list|view|accept|reject` commands; plan items
  accept a per-item `surface`
- web: /acceptance/:acceptanceId decision page (union checks, round
  ledger, accept/reject with comment) registered across web + desktop
  + mobile routers; i18n en-US/zh-CN
- docs: agent-testing skill documents the --subject publish contract

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

* 💄 feat(verify): rebuild the acceptance page to the final prototype spec

Align the /acceptance/:id decision workspace with the final iterated
prototype from the design session (business-category groups + iteration
timelines), which the first cut missed:

- types/server: VerifyCheckItem gains `category` (union grouping key) and
  `supersedes` (generation declaration); the union folds superseded ids
  into the successor's iteration timeline, tracks per-round wording
  (`timeline`), `revisions` and `titleChanged`; bundle enriches evidence
  for every timeline step, not just the final round
- cli: plan items pass category/supersedes through; ingest keeps working
  for legacy reports
- web: check list grouped by category (surface fallback) in one joined
  card with collapsible groups + per-group expand/collapse and a global
  group toggle; rows show evidence-type badges, Repeat iteration chips,
  fixed/introduced/final-round provenance; expanded rows render
  full-width captioned evidence and a newest-first iteration timeline
  (gray history dots); the round ledger is newest-first, icon+text
  status, time top-right, collapsible panel; per-check manual override
  removed per the design decision (the acceptance-level accept/reject is
  the human judgment); accept/reject moved to base-ui createModal
- proxy: register /acceptance in the middleware matcher (protected) —
  direct URLs previously 307'd to the home page
- cli deps: pin antd/@types/react overrides to the root versions so the
  standalone install can't drift the root type-check

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

* 💄 style(verify): promote the acceptance bar to a body block, hide the decision bar for launch

- The acceptance requirement renders as a first-class body block (primary
  accent border + label + body-size text) instead of header fine print
- The accept/reject decision bar and its modals are gated behind
  ENABLE_DECISION_BAR=false — the user-closure loop is not shipping in
  this release
- Round ledger: uncertain rounds get their own HelpCircle icon +
  roundStatus.uncertain label instead of falling back to the spinner

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

* 💄 style(verify): render the acceptance bar as a typographic lede

Drop the bordered block (the accent border read as clutter): the
requirement now sits under the header as a lede — a small tertiary label
over 15px body text, prominence through typography instead of chrome.

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

* 🐛 fix(verify): harden acceptance attach ownership, decision gating, and rejected re-ingest

Review follow-ups on the acceptance loop:

- attachRun creator-scopes the RUN too (assertWorkspaceRowManageable on
  run.userId): the attach rewrites verify_runs.acceptance_id/round_index,
  so a workspace member must not be able to chain another member's report
  onto their aggregate
- accept/reject require a SETTLED aggregate (delivered/errored): deciding
  mid-flight stamped a terminal state (and could complete a task) before
  any verdict existed, and recomputeStatus treats accepted as sticky so a
  premature accept was uncorrectable; rejected now also refuses a second
  decision until the next round re-opens it
- ingest-report: a remembered session that already carries a userDecision
  is immutable history — the sidecar path now starts a fresh round (which
  re-opens a rejected acceptance as documented) instead of silently
  updating the decided round in place; an explicit --run against a decided
  session errors with guidance

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 01:22:37 +08:00
Arvin Xu df94e19ef2 🐛 fix(cli): move verify vocabulary to @lobechat/const/verify (#17195)
* 🐛 fix(cli): move verify vocabulary to @lobechat/const/verify

`apps/cli` is a member of the isolated desktop workspace, where
`@lobechat/types` resolves to the hand-written stub in
`apps/desktop/stubs/types`. Value imports from it are therefore
unreachable, and #17166 added three to the CLI's verify command,
breaking `@lobehub/cli`'s build on canary.

Move the verify closed sets (verifier types, statuses, verdicts,
surfaces, evidence media), `normalizeVerifySurface`,
`DEFAULT_MAX_REPAIR_ROUNDS` and `VerifyRunOrigin` into a new zero-import
`@lobechat/const/verify` narrow subpath, which every workspace can
resolve. `@lobechat/types` re-exports the derived types, so type-only
consumers are untouched and it stays the one-stop domain vocabulary.

The whole vocabulary moves, not just the two symbols the CLI happened to
need — splitting on what one consumer imports is an accident, not a
boundary.

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

* ♻️ refactor(types): declare verify unions independently of @lobechat/const

`@lobechat/types` must not depend on `@lobechat/const` — `const` already
type-imports from `types` in ~10 files, so the edge added in the previous
commit closed a package-level cycle.

Declare the verify unions in `packages/types/src/verify.ts` instead of
re-exporting them, and keep the runtime `as const` arrays in
`@lobechat/const/verify` (which stays import-free, so the isolated desktop
workspace can still reach them). `packages/const/src/verify.test.ts` pins the
two sides together: adding a member to one alone is a type error, not a
silent divergence.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 00:27:57 +08:00
Arvin Xu da451d441f 🔖 chore(cli): bump @lobehub/cli to 0.0.40 (#16988) 2026-07-10 20:05:21 +08:00
Rylan Cai e8af5258a7 ♻️ refactor(Agent): Configurable graph agent (#16944)
* ♻️ refactor: rebuild graph agent runtime

* ✅ test graph runtime integration paths

* 🐛 fix(agent-runtime): stabilize graph agent tests

* ✅ test: fix agent settings content mock

* 🐛 fix graph prompt value typing

* 🐛 fix(agent-runtime): preserve dynamically activated tools

* ✅ test: use offered web browsing tool name

* 🔧 remove graph runtime feature flag gate

* 📝 restore internal flag comment
2026-07-10 15:01:35 +08:00
Arvin Xu 0a6f6473ad 🔖 chore(cli): bump @lobehub/cli to 0.0.39 (#16955) 2026-07-10 10:47:29 +08:00
Rylan Cai 7a3540cab2 🐛 fix(cli): scope connect service env to unit (#16825) 2026-07-08 13:34:43 +08:00
Arvin Xu ea50d58f94 📝 docs(skills): restructure ux-audit skill with layered audit references (#16595)
* 📝 docs(skills): add layered audit references to ux-audit skill

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🔖 chore(cli): bump @lobehub/cli to 0.0.38 and regenerate man page

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 📝 docs(skills): group ux-audit example results under references/example/

Move the worked audit results (home/eval/fleet/onboarding/page/resource/
settings) into a dedicated references/example/ directory, dropping the
redundant example- prefix, and update SKILL.md + cross-links accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 📝 docs(skills): add onboarding audit example, variant-comparison rule & ux 回灌

- ux-audit: new "comparing two variants" ground rule (winner is an outcome
  verdict, not a craft verdict) + an A/B-winner row in the coverage matrix;
  mandatory 回灌 and "report the good cases" close-out steps
- ux-audit: onboarding worked example (references/example/onboarding.md) with a
  Strengths/亮点 section; findings landed as LOBE-11138 sub-issues
- ux: Grow §5.2 (multi-step flows show progress + stay skippable) and Feedback
  §4.2 strengthened (awaited gating write needs finally + retry), mirrored into
  the Quick review
- refresh the other worked examples + Read/Act/Edit checklist refinements

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

* 📝 docs(skills): surface good cases in ux-audit + land ✅ examples in ux

Make good cases first-class in the ux-audit worked examples (dedicated
"Strengths / good cases (don't regress)" section in create & tasks), and
close the ✅ half of the 回灌 loop by landing their good-case exemplars as
✅ examples in the ux checklists:

- Feedback §4.1 — elapsed/progress readout derives from a persisted,
  job-id-keyed start timestamp (survives remount), not a local counter.
- Feedback §4.2 — a compound gate on a secondary/dependent fetch gates on
  its in-flight flag and releases on settled (data / resolved-null / error).
- Feedback §4.3 — platform/deployment capability gaps get a full-surface
  gate with a remedy, vs a soft inline warning for model/config gaps.
- Read §1.7 — conditional polling starts from reactive state, not a
  function-form refreshInterval that never schedules a first timer at 0.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 13:05:57 +08:00
YuTengjing be0255b746 🔨 chore: update dependency baseline (#16465)
* 🔨 chore: update dependency baseline

* 🔨 chore(cli): update dependency baseline

* 🔨 chore(desktop): update dependency baseline

* 🔨 chore: fix dependency baseline blockers

* 🔨 chore: update TypeScript and Next baseline
2026-06-30 23:18:08 +08:00
Arvin Xu 2997ba6a3e ✨ feat(verify): submit check result + evidence in one call, inject acceptance into task prompt (#16214)
* ✨ feat(verify): submit check result + evidence in one call, inject acceptance into task prompt

Builder self-evidence contract for Task-level Verify:

- server `verify.submitCheckEvidence`: lazily upserts a check_result by
  `(verifyRunId, checkItemId)` and attaches evidence in one call, so the
  builder no longer needs a pre-existing checkResultId (closes the run-start
  handle gap). Verdict and evidence are both optional.
- CLI `lh verify submit`: wraps the combined contract.
- `lh verify evidence delete`: delete an evidence artifact.
- buildTaskPrompt: resolve the task's verify config (with inheritance) +
  referenced criteria, inject a "delivery acceptance" section listing each
  criterion and its requiredEvidence so the builder self-evidences while it
  works. Run-time handles are resolved by the verify skill at runtime.
- tests: router (submitCheckEvidence) + prompts (verify section rendering).

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

* 🐛 fix(verify): preserve result state on evidence-only submit + resolve run by operation

Address two P1 review findings on submitCheckEvidence:

- Preserve state on evidence-only submits: hydrate `required`/`verifierType`/
  index/title from the run plan item instead of hardcoding `required: true`, and
  omit `status` when there's no verdict. Previously an evidence-only submit on an
  existing row reset it to required:true / running on the conflict-update — which
  could flip a soft or completed criterion into a blocking/verifying one (the
  executor skips existing rows and VerifyStatusService gates on required+running).
- Let submit resolve the run from the operation: accept `operationId` as an
  alternative to `verifyRunId` (resolved via findByOperation), and expose
  `verifyRunId` in VerifyRunState/getVerifyState. In the run-start gap there are
  no result handles yet, so a builder holding only $LOBE_OPERATION_ID can now call
  `lh verify submit --operation`.
- CLI `lh verify submit`: add `--operation`, relax `--run`, require one.
- Migrate the portable verify skill (SKILL.md + plan-format/evidence/recording/
  cli/native + index) off the broken `result list → checkResultId → evidence
  upload --check` path onto `lh verify submit --operation --item <checkItemId>`,
  which lazily creates the result row — the path that actually works in the gap.
- tests: operation resolution, require-one-handle, no-clobber, plan hydration.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 00:53:13 +08:00
Rdmclin2 04aa01b2f7 🔨 chore: bump cli 0.0.35 (#16143)
* chore: update version

* chore: update man
2026-06-21 11:05:22 +08:00
Arvin Xu 434c16c49d 🔖 chore(cli): bump @lobehub/cli to 0.0.34 (#16139) 2026-06-21 00:35:51 +08:00
Arvin Xu 2487407192 ✨ feat(cli): add lh update command to self-upgrade the CLI (#16052)
* ✨ feat(cli): add `lh update` command to self-upgrade the CLI

Add a `lh update` command that checks the npm registry for the latest
published version of `@lobehub/cli`, compares it against the installed
version, and runs the matching global-install command. The package
manager is auto-detected (npm/pnpm/yarn/bun) from the running binary
path, with `--package-manager` to override. Supports `--check` for a
dry run and `--tag` to target a non-latest dist-tag.

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

* 🐛 fix(cli): resolve update command package metadata from the bundled entry

`update.ts` loaded `require('../../package.json')`, a path relative to its
source location. tsdown bundles everything into `dist/index.js`, where that
runtime require resolves to `node_modules/@lobehub/package.json` instead of
`@lobehub/cli/package.json` — so a published build threw at startup the moment
the update module was registered, breaking every `lh` invocation.

Reuse the single package-metadata load already done by the bundled entry
(`program.ts`, at the depth where `../package.json` is correct both in source
and in dist). Export the package name alongside the version and consume both
in the update command. Verified against a real `tsdown` build: `lh --version`
starts cleanly and `lh update --check` resolves `@lobehub/cli` correctly.

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

* ♻️ refactor(cli): extract package metadata into a shared src/pkg.ts const

Importing `cliVersion`/`cliPackageName` from `program.ts` into `update.ts`
created a `program ↔ update` import cycle (program registers the update
command). Move the single `require('../package.json')` load into a dedicated
`src/pkg.ts` const module that both `program.ts` and `update.ts` import,
breaking the cycle. The module sits directly under `src/` — the same depth as
the bundled `dist/index.js` — so `../package.json` resolves to `@lobehub/cli`'s
own package.json in both source and bundled runs.

Verified against a real tsdown build: one `createRequire("../package.json")`
remains, `lh --version` starts cleanly, `lh update --check` resolves the
package, and `man:generate` still reads the version via the re-export.

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

* 🐛 fix(cli): use the semver package for update version comparison

The hand-rolled comparator ordered prerelease identifiers lexicographically,
so `1.0.0-beta.10` sorted below `1.0.0-beta.9` and `lh update --tag beta`
reported "already current" instead of offering the newer prerelease.

Replace `compareSemver` with `isNewerVersion`, delegating to the `semver`
package (already a workspace dependency) so numeric prerelease parts compare
numerically. Add `semver` / `@types/semver` to the CLI package and a
regression test for the beta.9 → beta.10 case.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 11:22:38 +08:00
Arvin Xu 80cce10dd5 🐛 fix(cli): stop connect daemon on logout (#16038)
* 🐛 fix(cli): stop connect daemon on logout

`lh logout` only cleared the credentials file, leaving the `lh connect`
daemon running. It kept the device online on the gateway using the cached
token until expiry, so the machine stayed remotely driveable after the user
thought they had logged out, and left stale daemon.pid / daemon.status.json
behind.

Tear down the daemon via stopDaemon() before clearing credentials, which
SIGTERMs the daemon (disconnecting the gateway) and removes the pid/status
files.

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

* 🔒 fix(cli): verify daemon identity before SIGTERM

A bare isProcessAlive() check let logout/connect-stop SIGTERM an unrelated
process when the OS reused a dead daemon's PID after a crash or reboot.
getRunningDaemonPid() now also confirms the live PID is actually a LobeHub
daemon (matches the `connect … --daemon-child` command-line signature) and
treats reused PIDs as stale, cleaning up the metadata instead of killing.

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

* 🔖 chore(cli): bump @lobehub/cli to 0.0.32

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

* ✨ feat(cli): add top-level `disconnect` command

`lh connect` was only stoppable via the nested `lh connect stop`, which is
hard to discover. Add `lh disconnect` as a top-level alias that runs the same
daemon-stop handler, so users have an obvious inverse of `lh connect`.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 17:06:09 +08:00
Arvin Xu 0ef1309b68 🔖 chore(cli): bump @lobehub/cli to 0.0.31 (#15964)
* 🔖 chore(cli): bump @lobehub/cli to 0.0.32

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* update

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:45:55 +08:00
Arvin Xu 99411041b9 ✨ feat(device): share remote-device gateway RPC between desktop and CLI (#15780)
* ✨ feat(device): share remote-device gateway RPC between desktop and CLI

Extract the desktop's remote-device gateway RPC surface into a shared
`@lobechat/device-control` package and wire it into the CLI so `lh connect`
serves the same git / workspace / file device RPCs as the desktop app.

- local-file-shell: relocate all git operations (branches, working-tree
  patches, branch diff, checkout/rename/delete/pull/push/revert) from the
  desktop GitCtr into the shared package as pure functions
- device-control (new): the `executeDeviceRpc` dispatch + workspace scan +
  portable file-preview / file-index defaults, with platform hooks injected
- desktop: GitCtr / WorkspaceCtr / GatewayConnectionCtr become thin wrappers
  delegating to the shared package (local IPC path unchanged)
- cli: handle `rpc_request` over the gateway via the shared dispatcher

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

* ✅ test(device): cover git branch ops and device-control portable defaults

- local-file-shell: real-git integration tests for branch checkout / rename /
  delete (+ validation), working-tree files & patches, revert, branch-diff with
  no remote, and push / pull / ahead-behind against a bare origin
- device-control: defaultGetLocalFilePreview (text / image / accept filter /
  workspace containment / missing file) and defaultGetProjectFileIndex (git
  ls-files path + glob fallback)

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

* 🐛 fix(device): preserve directory entries in the glob project-file index

The CLI `getProjectFileIndex` glob fallback used `globLocalFiles`, which returns
only non-hidden file paths and no directory entries — so the Files tree builder
flattened nested files to the root and dropped dot-directories.

Walk with fast-glob (`dot: true`) and synthesize directory entries via the same
`collectProjectDirectories` path the git branch uses, so nesting and dot-dirs
(e.g. `.agents`) render correctly. Extracted a shared `buildEntries` helper.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 00:56:53 +08:00
renovate[bot] 06bf82f3e0 Update dependency node to v24.16.0 (#14621) 2026-06-11 09:24:21 +08:00
LiJian 723c4d6daa 🐛 fix(cli): handle agent_run_request in lh connect so device dispatch doesn't time out (#15634)
* 🐛 fix(cli): handle agent_run_request in `lh connect` so device dispatch doesn't time out

`lh connect` auto-registers the CLI as a device, so the gateway can pick it
as the dispatch target for a heterogeneous agent run (`agent_run_request`).
But the connect daemon only listened for `system_info_request` and
`tool_call_request` — it never handled `agent_run_request`, so it never sent
`agent_run_ack`. The gateway waited out its ack window and returned
`{error:'TIMEOUT',success:false}`, surfaced server-side as "Hetero agent
device dispatch failed".

Add an `agent_run_request` handler mirroring the desktop app: spawn
`lh hetero exec` fire-and-forget and ack `accepted` immediately. The spawned
process owns the full execution + server-ingest pipeline. It re-invokes the
current CLI entry (process.execPath + argv[1]) rather than relying on `lh`
being on PATH, so it works inside the detached daemon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: bump the cli version

* chore: bump the cli manifest

* 🐛 fix(cli): ack agent run only after spawn succeeds, reject on spawn error

`child_process.spawn` reports a missing/inaccessible cwd asynchronously via
the child's `error` event, after the handler had already sent an `accepted`
ack. The gateway/server then recorded dispatch success while no `lh hetero
exec` process existed to emit `heteroFinish`, leaving the assistant message
stuck instead of surfacing a failure.

`spawnHeteroAgentRun` now resolves on the child's outcome: `accepted` on the
`spawn` event (stdin is written only then), `rejected` on an early `error`. A
rejected ack returns the gateway 422 → execAgent writes a ServerAgentRuntimeError
onto the assistant message, so a failed dispatch is visible. Still resolves in
milliseconds, well within the gateway's 10s ack window.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 15:19:01 +08:00
Arvin Xu e01cadb779 ✨ feat(hetero): add --raw-dump to persist agent raw stream-json for debugging (#15602)
* ✨ feat(hetero): add --raw-dump to persist agent raw stream-json for debugging

The remote-device path (`spawnLhHeteroExec`) leaves no local execution
record: `lh hetero exec` consumes the agent's stdout internally and only
POSTs adapted events to the server, so a misbehaving remote run can't be
inspected. The adapted/ingested view also can't distinguish a CC-side
empty `tool_result` from an adapter extraction bug.

Add `lh hetero exec --raw-dump <dir>`: spawnAgent gains an `onRawStdout`
tee that captures the child's untouched stdout BEFORE the adapter; the
CLI writes it (plus stderr + a meta.json) to
`<dir>/<timestamp>-<operationId>/`, one file pair per spawn attempt.
Fully best-effort — a dump failure never affects the run or exit code.

Wire the desktop device path to pass `--raw-dump` (gated by the existing
`shouldTraceCliOutput` toggle, into `resolveTraceRootDir`), so remote-device
CC runs now leave a raw stream on the device — the same toggle/location the
local trace path already uses. Reusable later for the server sandbox path.

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

* 🔖 chore(cli): bump version to 0.0.27

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 22:16:05 +08:00
Arvin Xu ce5833cb67 ✨ feat(file): persist image dimensions into file metadata (#15594)
* ✨ feat(file): persist image dimensions into file metadata

Record intrinsic width/height for uploaded images so consumers can
reserve layout space (avoid CLS) without loading the file first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ✅ test(file): assert persisted dimensions in upload createFile payload

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🔖 chore(cli): bump version to 0.0.26 and regenerate man page

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ✨ feat(file): record image aspect ratio alongside width/height

Compute intrinsic aspect ratio (width / height, rounded) at extraction
time and persist it into file metadata so consumers can group/reserve
layout by orientation without recomputing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 22:11:15 +08:00
Arvin Xu 72ea0f94f7 🐛 fix(cli): preserve content/state for connect local file/shell tools (#15442)
* 🐛 fix(cli): preserve content/state for connect local file/shell tools

Route file/shell tool calls in connect mode through LocalSystemExecutionRuntime
so the result carries formatted prompt `content` plus structured `state`, and
forward `state` over the gateway tool-call response — aligning the CLI with the
desktop gateway path (PR #15114).

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

* 🐛 fix(cli): preserve getCommandOutput timeout when polling running commands

Routing getCommandOutput through the runtime dropped the per-call/gateway
timeout: the CLI mapping didn't forward it and LocalSystemExecutionRuntime's
denormalizeParams stripped it before ShellProcessManager.getOutput, so polling
fell back to the 30s default and could block past the gateway budget. Carry
timeout through the runtime param type, denormalize, and the CLI mapping.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 01:28:11 +08:00
Arvin Xu 73dd0ef136 🔖 chore(cli): bump @lobehub/cli to 0.0.24 (#15393)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 00:45:24 +08:00
Arvin Xu 3caa3efb18 ✨ feat(device): auto-register desktop & CLI devices with stable machine ID (#15300)
✨ feat(device): auto-register desktop & CLI devices; send connectionId + channel

App layer — wires desktop and `lh connect` to the device registry and the
connection-routing scheme. Depends on @lobechat/device-identity and the
gateway-client connectionId/channel options (earlier PRs in this stack), plus
the device.register / listDevices endpoints (already on canary).

- desktop derives the stable deviceId on gateway connect (old per-install random
  UUID demoted to the routing `connectionId`), registers via device.register,
  and tags channel `desktop` / `desktop-dev`
- `lh connect` derives + registers before opening the WS (explicit --device-id
  still pins a VM); channel `cli` (env-overridable); connectionId persisted in
  `~/.lobehub/connection-id`
- CLI api client preserves explicit --token connects during registration

Part of LOBE-9572. Closes LOBE-9576 / LOBE-9577.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 20:35:09 +08:00
Arvin Xu 1d2b32bafc 🔖 chore(cli): bump @lobehub/cli to 0.0.22 (#15254)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 23:18:37 +08:00
Innei 49d191d2a7 🐛 fix: unify TypeScript peer resolution on 6.x (#15263) 2026-05-27 19:22:35 +08:00
LiJian 924ae8bf1f 🐛 fix: pass assistantMessageId through sandbox env to eliminate heteroIngest race (#15197)
* 🐛 fix: pass assistantMessageId through sandbox env to eliminate heteroIngest race

Before this change, `HeterogeneousPersistenceHandler.loadOrCreateState` always
read `topic.metadata.runningOperation` from the DB to obtain `assistantMessageId`.
On Vercel serverless, the first `heteroIngest` batch could arrive on a cold Lambda
that read from a replica before the orchestrator's `updateMetadata` write was
visible, causing a hard throw and BatchIngester exhausting all 5 retries — leaving
the assistant message stuck as LOADING_FLAT with no user feedback.

Fix: orchestrator passes `assistantMessageId` via `LOBEHUB_ASSISTANT_MESSAGE_ID`
env var → CLI → `TrpcIngestSink` → `heteroIngest` payload → `loadOrCreateState`.
When present, the DB lookup is skipped entirely for state initialisation, matching
the frontend `createGatewayEventHandler` pattern which always receives
`assistantMessageId` in-memory before any events are processed.

The `topic.metadata` DB read is kept as a fallback for desktop/old-CLI callers
that do not send the field, and is still needed to restore `heteroCurrentMsgId`
for mid-conversation cold-start reconstruction on step boundaries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(hetero-agent): snapshot text ingests and ignore stale batches

* chore: publish the cli to 0.0.21

* 🐛 fix(hetero-agent): validate seeded assistant binding

* fix: fixed the little types error

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:43:02 +08:00
LiJian a35877f676 ✨ feat(platform-agent): improve device selection UX with actionable guidance (#15111)
* ✨ feat(platform-agent): improve device UX — copyable lh connect cmd + version-too-low hint

- No-device state now shows a copyable `lh connect` command with clearer guidance to run it on the target machine then click Refresh
- Capability check failure caused by outdated lh desktop now shows a user-friendly "lh version is too low" alert with a copyable `npm install -g @lobehub/cli` upgrade command instead of the raw internal error string
- Changed no-device alert type from warning → info (absence of device is expected, not an error)
- Add en-US / zh-CN locale keys: noDevicesCmd, versionTooLow, versionTooLowHint, upgradeCmd

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 📝 fix(platform-agent): correct platform card descriptions — connect not run

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ✨ feat(platform-agent): desktop capability check + improved no-device onboarding

- Add checkPlatformCapability / getAgentProfile handlers in GatewayConnectionCtr so desktop devices no longer return "tool not available" error
- Redesign no-device alert: primary CTA is Desktop App download (https://lobehub.com/downloads), secondary is copyable lh connect CLI command
- Add 5 tests for new capability probing handlers (43 total, all pass)
- Add missing execa/fast-glob/fflate mocks to unblock test suite

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(platform-agent): route openclaw/hermes to correct binary in executeAgentRun

Previously all non-codex agent types defaulted to the `claude` command.
Now maps claude-code → `claude`, all other types (openclaw, hermes, …) → their
own binary name, which matches the pattern used by checkPlatformCapability.

Also adds 6 agent-run-routing tests covering openclaw/hermes/codex/claude-code
command mapping, accepted ack + sendPrompt wiring, and rejected ack on
startSession failure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ✨ feat(platform-agent): wire runHeteroTask/cancelHeteroTask on desktop gateway

The server dispatches openclaw/hermes via executeToolCall('runHeteroTask'),
not agent_run_request. The CLI (lh connect) handles this in its methodMap;
now the desktop gateway does too.

- Port runHeteroTask + cancelHeteroTask from CLI to GatewayConnectionCtr
  - openclaw: spawn detached process, save PID, inject notify protocol on
    first turn, send done signal via sendNotify on close
  - hermes: ensure gateway daemon is running, POST to /message endpoint
- Add in-memory platformTasks registry for cancel support
- Add sendNotify helper — calls agentNotify.notify tRPC endpoint directly
  using desktop token (desktop counterpart to `lh notify`)
- Port buildNotifyProtocol inline so desktop and CLI stay in sync
- Add resolveLhPath, openclawSessionExists, getHermesPort helpers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(heteroTask): always inject notify protocol and kill concurrent openclaw processes

- Remove openclawSessionExists check: always inject buildNotifyProtocol
  into every turn so openclaw can report back even after a failed session
- Before spawning openclaw, kill any existing process for the same
  topicId to prevent session file lock conflicts (exit code 1)
- Apply same fixes to both CLI (heteroTask.ts) and desktop
  (GatewayConnectionCtr.ts) to keep behaviour in sync
- Add CLI unit tests (heteroTask.test.ts, 7 cases)
- Extend desktop tests to cover always-inject and kill-concurrent
  behaviours (52 total, up from 49)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🔀 chore(cli): resolve version conflict — keep 0.0.19

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🔖 chore(cli): bump version to 0.0.20

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ✨ feat(desktop): implement getAgentProfile via openclaw agents list --json

Port getAgentProfile from CLI (getAgentProfile.ts) to desktop gateway:
- calls `openclaw agents list --json` to get name + emoji
- reads workspace IDENTITY.md / SOUL.md for description fallback
- falls back to 🦞 emoji when no identityEmoji set

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(desktop): make getAgentProfile async to satisfy methodMap Promise return type

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:44:55 +08:00
LiJian d15651bbec 🐛 fix(hetero): fix cloud CC agent execution failures and improve error messages (#15107)
* 🐛 fix(hetero): auto-retry on stale --resume session when cloud sandbox is recycled

Cloud sandboxes are ephemeral (~1h idle TTL). When a new container is
spawned for the next conversation turn, the previous CC session files under
~/.claude/projects/<cwd>/ are gone, so --resume <staleId> fails with
"No conversation found with session ID".

Two-layer fix:

CLI (lh hetero exec)
- Detect resume-not-found errors from stream error events and stderr
- Intercept the error event (withheld from the ingester so the server
  never sees a terminal error) and transparently retry without --resume
- The retry emits a fresh CC session id via heteroFinish, replacing the
  stale heteroSessionId in topic metadata and breaking the failure loop

Server (HeterogeneousPersistenceHandler)
- When result=error and no sessionId was produced (CC never emitted
  system.init, typical for resume failures), clear the persisted
  heteroSessionId from topic metadata as a safety net
- When CC ran successfully but produced an error result, sessionId IS set
  so the valid session is preserved for resume on the next turn

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(hetero): handle context-overflow resume failure + inject conversation history

Extends the resume auto-retry to also cover the "long conversation →
immediate next turn → Agent execution failed" scenario:

CLI (hetero exec)
- Renames RESUME_NOT_FOUND_PATTERNS → RESUME_RETRY_PATTERNS and adds
  context-overflow patterns (`/prompt.*too long/i`, `/context.*too long/i`,
  etc.) so CC's API-level "prompt too long" error triggers the same
  retry-without-resume path as the sandbox-recycled case.
- Adds a test case that verifies the context-overflow error retries cleanly.

Server (cloudHeteroContext + aiAgent)
- Exports ConversationHistoryEntry from cloudHeteroContext.ts and adds
  a conversationHistory? param that renders a <previous_conversation> block
  (user turns ≤ 1 KB, assistant turns ≤ 2 KB) in the system context.
- In execAgent, when resumeSessionId is set, fetches the last 200 messages
  for the topic, filters to the last 30 user/assistant turns, and passes
  them as conversationHistory to buildCloudHeteroContext.  This gives CC
  context about prior turns even when the native session file was reset.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(hetero): fix SIGTERM handler leak + remove unused ingestError binding

- Store the SIGTERM callback in a variable and process.off() it in the
  finally block alongside SIGINT, so the first run's handler is removed
  before the retry run registers its own (fixes duplicate sink.finish
  calls on SIGTERM mid-retry).
- Remove unused `ingestError` from the result destructuring (downstream
  code already uses result.ingestError directly).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(hetero): surface CC stderr in error message instead of generic fallback

Always collect stderr from the agent process (cap 8 KB) and pass its
tail (last 1 KB) as the `error` param to `heteroFinish` when the run
fails.  The persistence handler's `flushFinalState` overwrites the
generic "Agent execution failed" fallback with the actual CC stderr,
giving users and operators a meaningful error message.

Previously:
  {"message":"Agent execution failed","type":"AgentRuntimeError"}

After this fix, e.g.:
  {"message":"Error: API error: context window exceeded (200 000 tokens)",
   "type":"AgentRuntimeError"}

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🔨 chore(cli): bump version to 0.0.18

* 🐛 fix(lint): replace inline import() type with static import type

* 🐛 fix(lint): fix import sort order for ConversationHistoryEntry

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 17:38:31 +08:00
LiJian 6953f188c1 ✨ feat(platform-agent): openclaw/hermes agent creation UI, device guard, and remote dispatch backend (#15065)
* ♻️ refactor(agent-invocation): add AgentInvocationIntent + unified non-hetero dispatcher (LOBE-8927/8928)

Introduce a shared invocation contract and unified dispatcher for the
non-hetero, non-group agent call paths (callAgent speak mode and @agent
direct mentions). Removes the implicit client-only fallback that existed
in both entry points.

Changes:
- agentDispatcher.ts: add AgentInvocationIntent interface as the unified
  intent type for callSubAgent / callAgent / @agent invocations
- nonHeteroSubAgentDispatcher.ts (new): dispatchNonHeteroSubAgent()
  resolves child runtime via selectRuntimeType and routes to
  executeClientAgent (client) or executeGatewayAgent (gateway);
  throws for hetero (out of scope per LOBE-8926)
- conversationLifecycle.ts #executeDirectMentionRoute: replace hardcoded
  executeClientAgent + TODO fallback with dispatchNonHeteroSubAgent call
- builtin-tool-agent-management executor.ts callAgent speak mode:
  replace hardcoded executeClientAgent + TODO fallback with
  dispatchNonHeteroSubAgent call

Fixes LOBE-8927
Fixes LOBE-8928

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ✨ feat(platform-agent): openclaw/hermes agent creation UI, device guard, and remote dispatch backend

- Add CreatePlatformAgent 3-step creation modal (type select → config → bind device)
- Add RemoteAgentConfigCard to agent profile editor for openclaw/hermes config
- Add device guard banner in HeterogeneousChatInput for offline/unavailable devices
- Add useRemoteAgentDeviceGuard hook for real-time device status polling
- Fix backend dispatch: openclaw/hermes now use executeToolCall(runHeteroTask) instead of dispatchAgentRun (lh connect only handles tool_call_request)
- Add agentNotify router for lh notify → DB write + gateway stream event
- Add device.checkCapability endpoint for platform availability probe
- Add notify_update event type to gateway stream and event handler
- Add sendDoneSignal in heteroTask.ts for clean openclaw exit signaling
- Unify non-hetero sub-agent dispatch via dispatchNonHeteroSubAgent (LOBE-8927)
- Route openclaw/hermes to gateway runtime; keep claude-code/codex on hetero/client paths
- Add i18n keys for platform agent UI and device guard banners

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(agentNotify): reuse execAgent placeholder message on first lh notify call

Instead of creating a second empty bubble, the first assistant notify
without a messageId now updates the placeholder assistantMessageId that
execAgent already seeded in runningOperation.assistantMessageId.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ✨ feat(agentNotify): cancel openclaw/hermes process on interruptTask

- Store deviceId + heteroType in topic.metadata.runningOperation at dispatch time
- interruptTask now dispatches cancelHeteroTask tool call to the bound device
  when topicId reveals a remote hetero operation, sending SIGINT to the process
- Pass topicId from gateway cancel callback to interruptTask
- Add topicId to InterruptTaskSchema and InterruptTaskParams

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ♻️ refactor(hetero-agent): consolidate remote/local type classification into heterogeneous-agents package

- Add RemoteHeterogeneousAgentConfig, REMOTE_HETEROGENEOUS_AGENT_CONFIGS, isRemoteHeterogeneousType, and derived type aliases (HeterogeneousAgentType, LocalHeterogeneousAgentType, RemoteHeterogeneousAgentType) to packages/heterogeneous-agents/src/config.ts
- Extend HETEROGENEOUS_TYPE_LABELS to cover remote platform types (openclaw, hermes) via REMOTE_HETEROGENEOUS_AGENT_CONFIGS
- Replace all inline `=== 'openclaw' || === 'hermes'` checks and local Sets/type aliases across aiAgent service, ProfileEditor, HeterogeneousChatInput, useRemoteAgentDeviceGuard, CreatePlatformAgent, RemoteAgentConfigCard, and deviceProxy with the shared utility
- Show OpenClaw/Hermes display name in assistant message model tag (Usage component) by setting provider=heteroType on placeholder message and using HETEROGENEOUS_TYPE_LABELS for rendering
- Fix ReferenceError: move remoteDeviceId declaration before updateMetadata call

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add the platform agents get profiles

* 🐛 fix(platform-agent): routing, security, and i18n issues from review

- Route openclaw/hermes to gateway on desktop (P1): add isRemoteHeterogeneousType
  check in selectRuntimeType before desktop hetero branch — remote agents never
  use local desktop IPC, no special-casing needed
- Fix race in heteroTask: sendAutoNotify → sendDoneSignal now sequential via
  .finally() so error message is written before agent_runtime_end is published
- Security: validate messageId belongs to topicId in agentNotify before
  MessageModel.update to prevent cross-conversation data corruption
- Clear capability/device/profile state on platform change in creation modal (P2)
- Derive PLATFORM_DEFS from REMOTE_HETEROGENEOUS_AGENT_CONFIGS — new platforms
  automatically appear in the modal without code changes
- Use HETEROGENEOUS_TYPE_LABELS for platform names in HeterogeneousChatInput
  and RemoteAgentConfigCard (remove hardcoded PLATFORM_NAMES map)
- i18n: platform card descs, 'online'/'offline' tags, 'Select a device'
  placeholder, checkFailed error — all now use i18n keys

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ♻️ refactor(platform-agent): derive remote platform enum from config + fix test

- device.ts: replace hardcoded z.enum(['hermes','openclaw']) with a
  zod enum derived from REMOTE_HETEROGENEOUS_AGENT_CONFIGS so new
  platforms are automatically covered without touching this file
- heteroTask.ts / getAgentProfile.ts: use RemoteHeterogeneousAgentType
  instead of literal 'hermes' | 'openclaw' union for the same reason
- gateway.test.ts: update cancel-handler assertion to include topicId
  which was added to the interruptTask call in the previous commit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ✨ feat(platform-agent): gate creation entry behind labs flag + expand dispatcher tests

- Add enablePlatformAgent lab preference (default false) — the
  "Add Platform Agent" menu item is hidden until the user opts in
  via Settings → Advanced → Labs
- Wire toggle in settings/advanced with labs i18n key (en/zh)
- createPlatformAgentMenuItem returns null when flag is off
- agentDispatcher.test: add remote hetero cases (openclaw/hermes →
  gateway on both web and desktop) to cover the routing fix added earlier

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(lint): merge duplicate import + sort interface props in nonHeteroSubAgentDispatcher

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 💄 feat(platform-agent): disable Hermes option in creation modal (coming soon)

Hermes is not yet ready for production. Mark it as coming-soon in the
platform selection step: grayed-out card, not clickable, "Coming Soon"
tag next to the name.

To enable Hermes when ready: remove 'hermes' from COMING_SOON_PLATFORMS
in CreatePlatformAgent/index.tsx.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ✅ fix(test): mock CreatePlatformAgentModal in ModalProvider.test

The modal always mounts (open=false) and calls lambdaQuery.useQuery
which requires a tRPC context not present in the test environment.
Mock it out the same way as ChatGroupWizard and EditingPopover.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ✅ fix(test): mock useUserStore + labPreferSelectors in useCreateMenuItems.test

Adding useUserStore to useCreateMenuItems triggered user store
initialization in tests, which pulled in @lobechat/const and failed
because the existing mock only exports isDesktop. Mock the store and
selectors directly instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(platform-agent): hide divider when platform agent entry is disabled

The divider before 'Add Platform Agent' was unconditional — it showed
even when the labs flag was off. Conditionally include both the divider
and the menu item together so no orphaned separator appears.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 14:04:08 +08:00