* ✨ feat(model-bank): add claude-fable-5 to Anthropic models
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(agent): allow adding directory topics on web when agent targets a bound device
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(agent): deliver sub-agent resume bridge via QStash webhook in queue mode
The callSubAgent completion bridge was a handler-only hook, which lives in
process memory: in queue mode (AGENT_RUNTIME_MODE=queue) HookDispatcher only
delivers webhook-configured hooks, so the bridge never fired — the parent op
stayed parked in waiting_for_async_tool forever after all sub-agents finished.
- Give the bridge hook a webhook config (delivery: qstash) targeting the new
/api/agent/webhooks/subagent-callback endpoint; local mode keeps the
in-process handler. Both paths converge on
AgentRuntimeService.completeSubAgentBridge (backfill + barrier/CAS resume).
- Park-time self-check: after the parked state and operation row are
persisted, re-run the resume barrier once to recover children that
completed before the parent finished parking.
- One-shot verify watchdog: when a completion finds the parent not yet
resumable, schedule a delayed verifyAsyncToolBarrier re-check (no step
lock, CAS-idempotent, never re-arms).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 📝 docs(agent): correct verify-watchdog rationale comment
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 📝 docs(agent): clarify eventFields trimming rationale
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ♻️ refactor(agent): align subagent-callback with workspace-scoped step worker
Post-rebase adaptation to canary's runtime restructure (#15609):
- Route the webhook bridge through AiAgentService (like the /run step
worker) so the runtime's models stay workspace-scoped — a bare
AgentRuntimeService would be personal-scoped and the tool-message
backfill / resume barrier could miss workspace-scoped rows.
- Extract SubAgentBridgeParams into agentRuntime/types and add the
completeSubAgentBridge passthrough next to executeStep.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(agent): fail sub-agent callback loudly on backfill or delivery failure
Address two review findings on the resume bridge:
- completeSubAgentBridge now checks updateToolMessage's { success } result
(it swallows transaction errors instead of throwing) and propagates all
infrastructure failures. The webhook endpoint then returns non-2xx so
QStash redelivers the whole bridge — previously a failed backfill was
acked with 200 and the parent stayed parked forever, since the verify
recheck only re-reads the barrier and cannot retry the backfill.
- New AgentHookWebhook.fallback: 'none' opts a qstash-delivered hook out of
the unsigned plain-fetch fallback, which can never authenticate against a
QStash-signed endpoint and only masked publish failures as silently
dropped 401s. The bridge hook uses it; dispatch escalates such delivery
failures to console.error instead of the debug namespace.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(model-runtime): emit stop:abort instead of error when stream request is aborted
When user cancels a streaming request, the provider SDK throws abort errors
(e.g. "Request was aborted"). Previously these were propagated as error chunks,
causing the client to display a provider error message. Now abort errors emit
a stop:abort event through the SSE pipeline, allowing the client to handle
cancellation gracefully.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🐛 fix(model-runtime): fix type error in abort pipeline test
Use `as const` for type literal to satisfy StreamProtocolChunk union type.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✅ test(fetch-sse): add planUpgradeAfterFinish to onFinish expectations
#15616 added planUpgradeAfterFinish to the onFinish context but missed
updating fetchSSE.test.ts, breaking 13 tests on canary.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* 🐛 fix(model-runtime): harden abort detection against non-Error throws
isAbortError assumed error.message is always a string, but catch
clauses receive unknown — a non-Error throw (string, object without
message) would make the abort check itself throw inside the stream
error handler, swallowing both ABORT_CHUNK and the first-chunk error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🐛 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>
🐛 fix: skill list/search commands returning empty results
tRPC endpoints return { data, total } but CLI was treating the result as
an array; switch to result?.data ?? [] and update mocks to match.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* ♻️ refactor(hetero-agent): shared subagent-run coordinator + fix device-mode subagent streaming
Remote-device (gateway) hetero runs corrupted SubAgent text on the wire: the
CLI `SerialServerIngester`'s main-agent text-snapshot coalescing was subagent-
unaware, so subagent full-block text got mixed into the main accumulator and
re-`append`ed as `replace` snapshots server-side. Fix: exclude `data.subagent`
text from the coalescer so it forwards raw (the server appends it once).
The deeper cause was duplication: the renderer executor and the server
persistence handler each hand-wrote the SAME subagent-run state machine (lazy
thread create, turn-boundary cut, finalize, orphan drain, chain parenting) —
the epicenter of past hetero subagent bugs. Extract it into ONE pure,
transactional reducer (`reduceSubagentRuns`) in `@lobechat/heterogeneous-agents`
that emits declarative intents; each engine keeps a thin interpreter for its
own I/O (renderer: messageService + live store dispatch; server: messageModel).
The reducer pre-allocates ids so intents carry parentId chains with no
create→backfill round-trip; this needs `messageService.createMessage` to accept
a caller id (threaded through; the model already supported it). Also widened the
message nanoid 14→18 for the higher per-run id volume.
Behavior unifications (vs the two old copies):
- transactional commit-on-success subsumes the renderer's `pendingFlushTarget`
(a failed flush leaves the run intact for the onComplete-drain retry; the
renderer keeps a local pending-flush map pinned to the original assistant).
- finalize DELETES the run (server-style); a second finalize / orphan drain is
a clean no-op with the same DB end-state.
Scoped to subagent runs only; main-agent persistence stays per-engine. A future
pass can absorb the main-agent path into a unified agent-event reducer.
Tests: reducer 13, CLI hetero 22, server hetero 84, renderer executor 58.
Refs: LOBE-10175
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ✅ test(hetero-agent): strengthen subagent flush-retry assertion
The earlier rewrite of this assertion (caused by ids moving from server-
generated to caller-pre-allocated) weakened it to "all streamed writes share
one id", which would also pass if they all wrongly hit the terminal row. Pin it
back to the test's real intent: resolve the FIRST streaming-turn assistant by
its create payload and assert every streamed write targets it AND that it
differs from the terminal assistant's id — so `resultContent` is never clobbered.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* 🐛 fix(hetero-agent): honor commit-on-success for renderer subagent intents + fix stale id-length tests
- renderer interpreter: createThread / createMessage failures now rethrow so
reduceAndApplySubagent skips the state commit — the next event retries the
lazy create / turn boundary instead of orphaning the run (review P2)
- catch around the intent loop so a failed intent can't poison persistQueue
- regression test: transient createThread failure retries on next event
- update message id length assertions 18 → 22 (nanoid widened 14→18 + msg_)
- update messageService.createMessage spy assertions for the new (params, id) call
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* ✨ feat(agent): block nested sub-agent calls
Sub-agents must not recursively spawn further sub-agents. Plumb an
`isSubAgent` flag from the spawning thread through the conversation /
operation / tool-call metadata, and refuse nested dispatch at every layer:
- streamingExecutor marks the spawned sub-agent context with `isSubAgent`
- aiAgent strips the LobeAgent tool from a sub-agent's plugin config
- client builtin-tool executor + server tool runtime return a clear error
- RuntimeExecutors blocks both single and batch sub-agent dispatch
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* 🐛 fix(test): align execSubAgentTask expectation with isSubAgent appContext
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 🐛 fix(agent): don't mark group sub-agent tasks as isSubAgent
Group sub-agents are real agent dispatches and must keep the ability to
spawn their own sub-agents; only the LobeAgent-tool virtual sub-agent
path should carry isSubAgent. Drop the flag from execSubAgentTask.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* 🐛 fix(page-agent): inject active documentId into context on send
Page-scoped conversations never carried the open document id to the
agent runtime. At send time `operationContext` only had agentId/scope/
topicId, so the gateway's `appContext.documentId` was undefined and the
server-side PageAgent runtime threw "received a tool call without
documentId in context".
Inject the live document id from the page editor runtime
(`pageAgentRuntime.getCurrentDocId()`) into `operationContext` when
scope is `page`, so it flows through `execAgentTask` → server
`state.metadata.documentId` → tool execution context.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* 🐛 fix(page-agent): pass new document id explicitly in sendAsWrite to avoid stale injection
The page-scoped documentId fallback reads the page editor runtime
singleton, which is only authoritative once the active page's editor has
mounted. `sendAsWrite` creates a document, navigates, and sends
immediately — before the new editor mounts — so the singleton may still
be bound to the previously open page, scoping server-side PageAgent
tools to the wrong document.
Thread the freshly created `newDoc.id` through the conversation context;
the existing `!context.documentId` guard then skips the singleton
fallback entirely. Document the constraint at the fallback site.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* ✅ test(database): raise model/repository coverage to 95%+ and document DB test conventions
Raise @lobechat/database client-db coverage 89.11% -> 95.36%:
- New integration tests for connector, connectorTool, workspaceMember (were 0%)
- Extend task, workspace, rbac, notification, userMemory/query, file,
agentSignal/reviewContext, verifyRubric, brief, taskTopic, dataImporter,
messengerAccountLink, home
Fix client-db (PGlite) test failures: BM25 search lacks the pg_search
extension under PGlite, so wrap session.queryByKeyword and home.searchAgents
in describe.skipIf(!isServerDB), matching the existing convention.
Document DB model/repository testing conventions so new models ship with tests:
- Rewrite testing skill's db-model-test.md (getTestDB integration pattern,
client-vs-server-db split, BM25 skipIf guard, schema gotchas, user isolation)
- Surface the rule in testing/SKILL.md, cross-link from drizzle/SKILL.md,
review-checklist/SKILL.md, and models/_template.ts
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ✅ test(database): make verifyRubric/brief ordering tests deterministic
These models order by `updatedAt`/`createdAt` desc with no id tiebreaker, and
the tests created rows back-to-back relying on default `now()` — when two rows
land in the same millisecond the order is non-deterministic, causing flaky CI
failures. Set explicit, well-separated timestamps instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
- Carry a `reason` payload on the `authorizationRequired` IPC event so the
cause behind the Session Expired modal (proxy 401, refresh non-retryable,
startup proactive refresh exception, etc.) lands in `electron-log` and the
renderer debug namespace for postmortem.
- On 401 + `X-Auth-Required`, enrich the reason with `hadToken`, the upstream
`www-authenticate` header and a truncated body snippet so OAuth/tRPC error
details are captured without consuming the forwarded stream.
- Fix returning users (token refresh failed -> active=false -> relaunch)
landing on the Welcome screen of desktop onboarding. Persist an
`everCompleted` flag in localStorage and resume at the Login screen for
anyone who has already completed onboarding once.
- Extract the screen-resolution logic into a pure `resolveInitialScreen`
helper with unit tests; cover the new storage flag and reason payload in
AuthCtr / BackendProxy tests.
* 🐛 fix(hetero): chain step boundary off tool row when tools[] backfill is unseen
On a warm replica that did not drain the prior step's `tools_calling` (or
before the assistant's `tools[]` JSONB has its `result_msg_id` backfilled),
the in-memory tool state is empty, so the step boundary falls back to the
previous assistant and forks the wire into two disconnected bubbles.
Fall back to the authoritative anchor — the `role:'tool'` rows themselves,
committed in Phase 2 independently of the JSONB mirror's Phase-3 backfill —
via a new `MessageModel.getLastChildToolMessageId`. Excludes subagent tool
rows (threadId set) so they never anchor the main-agent wire.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* 🐛 fix(hetero): write per-device cwd when adding topic from project group
The sidebar "+ new topic in this directory" action wrote the working
directory to the legacy per-agent slot (localAgentWorkingDirectoryMap),
which sits below agencyConfig.workingDirByDevice in the resolution
precedence. Once a directory had been picked via the ControlBar (which
writes workingDirByDevice), the "+" action was silently shadowed and the
new topic was created with the previously-picked directory instead.
Route the action through useCommitWorkingDirectory.commitAgentDefault so
it writes the same high-precedence per-device slot the picker uses,
keeping the two write paths from drifting again.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ✅ test(hetero): cover MessageModel.getLastChildToolMessageId
The fallback anchor query added in 599eea5bda had no DB-level test — the
persistence handler mocks it, so its real SQL was never exercised and
patch coverage dropped. Add direct PGlite tests covering all branches:
latest-tool ordering, no-tool → undefined (ignoring non-tool children),
subagent thread exclusion (threadId IS NULL), and ownership isolation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* 🐛 fix(server): restore sub-agent forking in QStash step worker
In QStash mode every agent step runs in a fresh HTTP request via the
hono `runStep` handler, which built a bare AgentRuntimeService without
the `execSubAgent` fork callback. As a result `lobe-agent.callSubAgent`
failed with SUB_AGENT_UNAVAILABLE in cloud (the in-process callback
never survives the queue boundary).
Step through AiAgentService.executeStep instead, reusing its internal
runtime that is already wired with the fork callback — no second runtime,
no manual rebinding.
Also rename the internal `execSubAgentTask` → `execSubAgent` (method,
runtime/tool context fields, options, ExecSubAgent{Params,Result} types)
to separate the "task" concept from "sub-agent", and make the method an
auto-bound arrow field so it no longer needs `.bind(this)`. The external
lambda procedure name (`execSubAgentTask`) and the client service are
left unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ♻️ refactor(server): group runtime upward-calls into an AgentRuntimeDelegate
`execSubAgent` was a loose top-level option on AgentRuntimeService, which
hid that it is not ordinary config but an upward call: the low-level
runtime, mid-step, triggering a high-level pipeline that lives in
AiAgentService (the layer above it).
Introduce `AgentRuntimeDelegate` as the single named home for these
upward-call capabilities, and inject it as `delegate: { execSubAgent }`.
The interface doc states the convention so future "runtime must trigger a
higher-layer pipeline" capabilities land in the same place instead of
sprawling as ad-hoc options.
Scope is deliberately the injection surface (options + service field +
AiAgentService wiring). The downstream executor/tool context keeps its
flat `execSubAgent` field — the tool runner wants the unpacked capability,
not the whole delegate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The lobe-agent manifest exposed `callSubAgents` (parallel multi-task
dispatch), but the server runtime only implemented `callSubAgent`. When an
agent run executed server-side and the model invoked `callSubAgents`, the
builtin executor threw "Builtin tool lobe-agent's callSubAgents is not
implemented".
The server already supports parallel sub-agents natively: a batch parks on
all deferred tools (`pendingToolsCalling`) and `tryResumeParentFromAsyncTool`
enforces a K=N barrier, resuming the parent only once every pending
tool_result is fulfilled. So emitting multiple `callSubAgent` calls in one
turn is equivalent to the old `callSubAgents` — making the plural API
redundant and the source of a server/client inconsistency.
Remove `callSubAgents` end to end (manifest, types, client executor,
Inspector/Render/Streaming components + registries, locale keys, display-name
map, dev fixture) and update the system prompt to guide the model to fan out
via multiple `callSubAgent` calls.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* 🐛 fix(desktop): preserve Error cause across IPC so renderer sees real failure reason
Electron's IPC error serialization carries an Error's message/stack/name plus
its enumerable own properties, but a standard `cause` (set via
`new Error(msg, { cause })`) is non-enumerable — so the real failure reason
(e.g. undici wrapping ENOTFOUND/ECONNREFUSED under a generic
`TypeError: fetch failed`) was dropped on the way to the renderer.
- IPC base: re-expose `cause` as an enumerable, clone-safe field in the central
handler catch (nested Errors flattened to { name, message, code }) so every
IPC method's error carries it.
- Heterogeneous agent executor: include `cause` in the ChatMessageError body so
the surfaced error structure exposes the underlying reason alongside message.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* 🐛 fix(desktop): ferry IPC error cause via a serializable envelope
Making `cause` enumerable before rethrowing didn't actually reach the renderer:
Electron's `ipcRenderer.invoke` rebuilds a thrown handler error from its *string*
form (`Error invoking remote method '<channel>': <String(error)>`), so the
original error object — and any `cause` — never crosses the boundary.
Switch to an explicit serializable envelope:
- `~common/ipcError`: `toIpcErrorEnvelope` (clone-safe plain object, recursively
captures name/message/stack/code/cause) + `isIpcErrorEnvelope` /
`fromIpcErrorEnvelope` to rebuild a real Error.
- IPC base handler: return the envelope instead of throwing.
- preload `invoke`: detect the envelope and re-throw a rebuilt Error (with
`cause`), preserving the "promise rejects on failure" contract.
- hetero executor: flatten the Error cause to a plain object for the
DB-persisted `ChatMessageError.body`.
Adds unit tests for the envelope round-trip and the preload unwrap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* ✨ 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>
* ✨ 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>
* ♻️ refactor(chat-input): rename RuntimeConfig to ControlBar
The bar below the chat input now composes mode switcher, execution
device + working directory, approval mode and context window — "runtime
config" no longer matches. Rename the directory, component, and the
showRuntimeConfig / runtimeConfigSlot props (→ showControlBar /
controlBarSlot) across all call sites. Reads as a sibling of ActionBar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ♻️ refactor(agent): rename WorkingDirectoryBar to HeteroControlBar
Make the heterogeneous chat-input bar a symmetric sibling of ControlBar:
both compose the shared WorkspaceControls, so naming should match. Rename
the file, component and displayName, and update the controlBarSlot usage.
* 🐛 fix(agent): resolve working directory by target device instead of legacy-only
The chat-input directory picker writes the selection to
`agencyConfig.workingDirByDevice[deviceId]`, but the send / regenerate /
streaming / placeholder paths resolved the agent working directory via
selectors that only read the legacy `localAgentWorkingDirectoryMap`. So a
freshly picked directory was silently dropped and execution fell back to a
default cwd (the app's own repo), losing the user's project and `--resume`.
Make both `getAgentWorkingDirectoryById` and `currentAgentWorkingDirectory`
device-aware: per-device choice > legacy > desktop/home, with the target
device resolved from a passed-in `currentDeviceId` (kept out of the selector
so hook callers stay reactive). Update all call sites to supply the device id.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* 🐛 fix(hetero): forward user images on regenerate so vision input isn't dropped
The hetero regenerate/resend path (`runHeterogeneousFromExistingMessage`)
only forwarded the text prompt to `executeHeterogeneousAgent`, never the
original user message's `imageList`. The send path reads imageList off the
persisted user message and passes it along; this path must too. Without it,
regenerating an image turn re-ran the CLI with no attachments (fully lost
when the session couldn't be resumed, e.g. cwd changed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The chat-input directory picker writes the selection to
`agencyConfig.workingDirByDevice[deviceId]`, but the send / regenerate /
streaming / placeholder paths resolved the agent working directory via
selectors that only read the legacy `localAgentWorkingDirectoryMap`. So a
freshly picked directory was silently dropped and execution fell back to a
default cwd (the app's own repo), losing the user's project and `--resume`.
Make both `getAgentWorkingDirectoryById` and `currentAgentWorkingDirectory`
device-aware: per-device choice > legacy > desktop/home, with the target
device resolved from a passed-in `currentDeviceId` (kept out of the selector
so hook callers stay reactive). Update all call sites to supply the device id.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* 🐛 fix: activator tool discovery for cloud-sandbox and local-system
- P0: Explicitly inject LocalSystemManifest when device gateway is configured
(discoverable: isDesktop is always false on server, so it never enters
the discovery loop. The explicit injection mirrors the canUseDevice guard.)
- P1: Skip CloudSandboxManifest when runtimeMode is not 'cloud'
(resolveRuntimeMode unifies executionTarget='sandbox' and legacy
chatConfig.runtimeEnv.runtimeMode paths, so agents with sandbox
disabled correctly exclude the cloud-sandbox tool.)
Both fixes operate at the manifest-map build stage, consistently affecting
all downstream consumers (activator discovery, availableTools, etc.)
* 🐛 fix: remove cloud-sandbox manifest when runtime is not sandbox
The initial manifest seed via getEnabledPluginManifests includes
defaultToolIds (which contains lobe-cloud-sandbox), so the manifest
was already in toolManifestMap before the allowedBuiltinTools loop's
continue guard. This made lobe-cloud-sandbox activatable even when
sandbox was disabled.
Add a delete right after resolveRuntimeMode to cover both the
manifestMap seed and the allowedBuiltinTools loop in one place.
Co-authored-by: chatgpt-codex-connector[bot]
* 🐛 fix: gate local-system injection by runtimeMode === 'local'
🐛 fix(hetero): reset per-message text accumulator at message boundaries
In server-ingest mode (remote-device CC and cloud sandbox both run
`lh hetero exec`), SerialServerIngester's `accumulatedText` spanned the
whole run and never reset across assistant-message boundaries. Combined
with `snapshotMode: 'replace'`, every later message's snapshot re-emitted
all prior messages' text verbatim, which the server persisted into the
new DB message — producing cross-message text duplication.
Reset `accumulatedText` on `stream_start` / `stream_end` (emitted by the
adapter's `openMainMessage`) after flushing the just-ended message's
snapshot, so each message snapshots only its own text.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* 🐛 fix(heterogeneous-agents): emit per-turn usage for batch-mode Claude Code
Device + sandbox runs spawn Claude Code via the `lh hetero exec` CLI in BATCH
mode (no `--include-partial-messages`), unlike the desktop driver which always
streams partial messages. In batch mode CC emits no `message_delta`, and the
adapter deliberately skipped usage on `assistant` events (assuming the stale
`message_start` echo that only exists in partial mode). The grand-total
`result_usage` is intentionally ignored to avoid double-counting, so batch runs
ended up persisting NO usage at all — the model tag showed no token count.
Track whether any `stream_event` was seen (partial mode); when none has been
(batch mode), emit per-turn usage from the `assistant` event as turn_metadata.
The assistant event's usage is authoritative in batch mode, not a stale echo.
This also fixes the model tag showing `claude-opus-4-8[1m]`: the `[1m]` 1M-context
beta marker only appears in the `system init` model field, while `assistant`
events report the canonical `claude-opus-4-8`. The new turn_metadata carries the
clean id, which supersedes the init-captured one (and matches the id ModelIcon /
pricing lookups expect).
Partial mode (desktop/local) is unchanged — `message_delta` still owns usage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ✅ test(heterogeneous-agents): update batch-mode E2E for assistant usage
The multi-step E2E fixture has no `stream_event` records (batch mode) and 5
assistant events with `message.usage`, so the new batch-mode path now emits 5
turn_metadata events. Update the expectation from 0 — this validates the fix on
a realistic device/sandbox session: per-turn usage lands with the canonical
model id.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* 🐛 fix(heterogeneous-agents): stop leaking host Anthropic creds into spawned CLI
The local CLI spawn forwarded the entire `process.env` to `claude`, so a
developer with `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_BASE_URL`
exported in their shell had it inherited by the CLI — overriding its own
subscription login and surfacing as a baffling "Invalid API key" + non-zero
exit on every message.
Strip those three vars from the inherited env via `buildInheritedSpawnEnv`.
`session.env` is still spread last, so an agent that explicitly configures an
API key continues to win. Adds regression tests for both the strip and the
override.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>