Files
Coooolfan aacb88fba7 ✨ feat(heterogeneous-agent): support Devin CLI as a heterogeneous agent (#18554)
* ✨ feat(heterogeneous-agent): support Devin CLI

* 🐛 fix(heterogeneous-agent): reuse cached Devin authentication

* ✨ feat(heterogeneous-agent,cli): default Devin ACP to bypass permission mode

- Add `permissionMode` to `DevinAcpSessionOptions` and `SpawnAgentOptions`.
- Update `buildDevinAcpArgs` to place `--permission-mode` before the `acp`
  subcommand, since it is a global `devin` flag.
- Default the CLI `lh hetero exec` to start Devin with `permissionMode: 'bypass'`
  so headless connected-device runs avoid blocking on permission prompts.
- Support user-supplied `--permission-mode` in `extraArgs`, which takes
  precedence and is moved to the correct position.

* ✨ feat(heterogeneous-agent): set Devin ACP session mode via session/set_config_option

The `--permission-mode` global flag only affects the Devin CLI process;
the ACP session's mode still defaults to `accept-edits`, so shell/edit
tools still trigger `session/request_permission` and show askUserQuestion.

After `session/new` or `session/load`, call `session/set_config_option`
with `configId: 'mode'` and the resolved permission mode to actually put
the ACP session into bypass (or user-override) mode. The resolved mode is
derived from the built argv so an explicit `--permission-mode` in extra
args still wins over the default.

Add regression tests for new sessions, resumed sessions, and user override.

* ✨ feat(heterogeneous-agent): apply Devin ACP initial model via session/set_config_option

Previously `initialModel` was only used as a fallback when `session/new`
returned no model. Devin's `session/new` always reports a default model
(e.g. `swe-1-7-medium`), so a user-selected `glm-5-2` was ignored and the
default was used.

Add `applyInitialModel()` to resolve the requested model against the
`configOptions` catalog and call `session/set_config_option` with
`configId: 'model'`. It matches by raw model value and by normalized
human-readable name (so 'glm 5.2' maps to 'glm-5-2'). When only a raw
`initialModel` is provided and the catalog is unavailable (e.g. resume),
it attempts to set the value directly.

Also preserve the updated model after setting the permission mode so the
final `devin_session` event reports the selected model.

Add regression tests for model selection, label matching, default
preservation, and combined permission mode + model setup. Update the
`spawnAgent` fake to expose a 'sonnet' option for the new behavior.

* ♻️ refactor(heterogeneous-agent): stop injecting --permission-mode bypass CLI flag

Devin ACP permission mode is now applied through `session/set_config_option`
(configId: 'mode'), which is the only reliable way to change the active ACP
session mode. The global `--permission-mode` CLI flag before the `acp`
subcommand does not change the ACP session's effective mode.

- `buildDevinAcpArgs` no longer injects a default `--permission-mode`.
- It still moves a user-supplied `--permission-mode` in `extraArgs` before
  the `acp` subcommand for backwards compatibility.
- `DevinAcpSession.resolvedPermissionMode` falls back to
  `options.permissionMode` so the default CLI `bypass` is still sent via ACP.
- Updated tests to expect `acp` without the default flag and assert the
  `session/set_config_option` mode request.

* fix(devin): do not default to bypass permission mode and protect selected model from mode response

- Remove the hard-coded `permissionMode: 'bypass'` default for Devin in
  `lh hetero exec`. Users can still pass `--permission-mode` via
  `--agent-arg` when needed.
- Stop using the `session/set_config_option` mode response to overwrite the
  model resolved from `applyInitialModel`. Permission-mode negotiation can
  return config-options whose `model` value is stale/default, causing a
  freshly selected model (e.g. `glm-5-2`) to be replaced with the Devin
  default (`swe-1-7`) on `stream_start`.
- Update CLI tests to no longer expect the bypass default.

Refs LOBE-xxx (feat/devin-cli).

* fix(devin): keep default bypass permission mode while protecting selected model

Re-introduce the default `permissionMode: 'bypass'` for Devin ACP so
headless connected-device runs still do not block on permission prompts.
The previous change that removed it was too aggressive; the real bug is
that the `session/set_config_option` mode response was overwriting the
selected model. That guard in `DevinAcpSession` remains in place.

Refs LOBE-xxx (feat/devin-cli).

* ✨ feat(heterogeneous-agents): capture and emit Devin ACP usage as turn_metadata

- Forward `session/prompt` result `usage` from `DevinAcpSession` into the
  `devin_prompt_completed` synthetic event.
- Add generic ACP `Usage` parsing to `TraeAcpAdapter`, plus `usage_update`
  handling with a protected `extractUsageFromUsageUpdate` hook.
- Override `extractUsageFromUsageUpdate` in `DevinAcpAdapter` to parse
  `_meta.cognition.ai/{input,output,cachedRead,cachedWrite,thought}Tokens`.
- Emit `step_complete` with `phase: 'turn_metadata'` so the frontend reducer
  writes `usage` (and optional `cost`) onto the assistant message.
- Add `UsageData.cost` to mirror `MessageMetadata.usage`.
- Add `devinAcp.test.ts` covering prompt-result usage, `usage_update` _meta,
  and fallback paths.

* ✅ test(heterogeneous-agents): add missing request params type in spawnAgent test fixture

* 🐛 fix(heterogeneous-agent): cancel device-side CLI process on user stop

Previously only remote-task hetero types (openclaw / hermes) triggered a
`cancelHeteroTask` dispatch to the device when the user clicked Stop.
Local-cli types (claude-code / codex / devin / trae / qoder) that also
run on a remote device via `dispatchAgentRun` were silently skipped —
the UI showed cancelled but the CLI process kept running.

Two fixes:
- Server `interruptTask`: drop the `isRemoteHeterogeneousType` guard so
  any hetero type with a deviceId in runningOperation triggers the cancel
  dispatch.
- Desktop `spawnLhHeteroExec`: register the spawned child process into
  `platformTasks` via a new `onChildSpawned` callback so `cancelHeteroTask`
  can find and kill it by operationId. Also add `detached: true` so the
  process-group kill reaches the CLI without signalling the desktop app.

* 🐛 fix(cli): cancel lh connect device CLI process on user stop

The previous cancel fix only covered the desktop device path. When a
local CLI agent (devin/claude-code/codex/…) is dispatched through
`lh connect`, the spawned `lh hetero exec` child was never registered
into the task registry, so `cancelHeteroTask` dispatched from the
server returned "No task found" and the remote process kept running.

- `agentRun.ts`: spawn with `detached: true` and register the child PID
  into `taskRegistry` on spawn, keyed by operationId. Remove the entry
  on exit only if the exiting PID still owns the task.
- `taskRegistry.ts`: widen `TaskEntry.agentType` from
  `'hermes' | 'openclaw'` to `string` so local CLI agent types can be
  persisted.
- `heteroTask.ts`: `cancelHeteroTask` now signals the whole process
  group via negative-PID kill (Unix) or `taskkill /T /F` (Windows),
  with a 2s SIGINT→SIGKILL escalation — mirroring the desktop path.

* 🐛 fix(cli): add cancel-trace logging to diagnose device CLI stop path

Add [cancel-trace] logs at every hop of the cancel path:
- server interruptTask: logs when dispatching cancelHeteroTask, when
  condition is not met (with runningOp/targetOperation details), and
  the dispatch result/error
- agentRun.ts: logs when task is registered into taskRegistry, and
  when it is removed on exit (or skipped due to stale PID)
- heteroTask.ts cancelHeteroTask: logs when received, when task is
  found or not found, and each kill attempt

* 🐛 fix(server): seed deviceId/heteroType into runningOperation for local CLI hetero agents

Local CLI hetero agents (devin / claude-code / codex / …) dispatch to a
device via `dispatchAgentRun`, but `childOperation` only embedded
`deviceId`/`heteroType` for remote platform agents (openclaw / hermes)
guarded by `isRemoteHetero`. Without these fields in
`topic.metadata.runningOperation`, `interruptTask` could not find the
target operation and never dispatched `cancelHeteroTask` — so clicking
Stop in the web UI did nothing for `lh connect` devices.

Patch `runningOperation` with the resolved `dispatchDeviceId` and
`heteroType` after a successful local CLI hetero dispatch, handling
both root and child (group supervisor) operation shapes.

* 🔥 chore: remove [cancel-trace] debug logs

Clean up the temporary diagnostic logs added to trace the cancel path.
Keep only the production-level `interruptTask` and `execAgent` logs that
were already present or are useful for long-term observability.

* 🐛 fix(heterogeneous-agent): preserve cancel lifecycle state

* ♻️ refactor(heterogeneous-agent): simplify Devin ACP integration

* 🐛 fix Devin intervention review flow

* 🐛 fix heterogeneous agent cancellation

* 🐛 fix heterogeneous agent cancellation confirmation

* 🐛 fix(heterogeneous-agent): preserve Devin permission option IDs

* 🐛 fix: resolve CI failures after rebase

* 🐛 fix: route Devin verification and permissions

* 🧪 test(cli): isolate agent run cancellation registry

* 🐛 fix(cli): cancel full live agent process group
2026-09-10 16:39:30 +08:00
..
…