9 Commits
Author SHA1 Message Date
74ff8aabd7 MUL-7983: fix(agent): handle CodeBuddy startup errors without aborting active runs (#9214)
* fix(agent): fail codebuddy runs that emit a top-level error event

CodeBuddy's startup-failure shape is one top-level {"type":"error"}
line followed by a process that stays alive and silent: with an
inherited SERVER__PORT (a user-scope config shared by several tools),
a second concurrent run of the same agent fails to listen, prints
{"type":"error","error":"listen EADDRINUSE: ..."} and never emits
anything again. The parser had no branch for that event type, so the
run sat at "waiting for agent response" until the idle watchdog
stopped it - 19 of 48 runs hung this way on the reporter's machine.

- type=error is now fatal when no terminal result has been seen: the
  CLI's own message becomes the run error and the hung process is torn
  down immediately instead of idling. A result already seen keeps
  governing the outcome, so a late informational error line cannot
  flip a finished run.
- buildCodebuddyEnv now also forces SERVER__PORT=0 (OS-assigned port
  per run, so concurrent runs never share one) and
  CODEBUDDY_BASH_AUTO_BACKGROUND_DISABLED=1 (CodeBuddy's PowerShell
  tool moves timed-out commands to the background and only honors that
  variable). Both are appended last like the existing background-task
  disable so a caller-provided value cannot outvote them.

Fixes #9200

Generated-by: GLM-5.3-Flash (ZCode)

* fix(agent): keep CodeBuddy running after recoverable errors

MUL-7983

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Yushen <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-13 13:54:24 +08:00
4d4cae7753 MUL-7312 LARI-10: Desktop PATH fix + CodeBuddy Claude-parity stream-json guards (#8340)
* fix(desktop): do not prepend PATH fallbacks over login-shell Node

Prepending /usr/local/bin after fix-path let a stale system Node 12
shadow nvm/fnm, breaking shebang CLIs (CodeBuddy/OpenClaw) on --version.
Append only missing fallback dirs so recovered shell PATH keeps precedence.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(agent): align CodeBuddy stream-json guards with Claude

Pass stderr into resume rejection, fail on prompt_too_long terminal_reason,
and force foreground when Bash requests run_in_background — the same
failure modes Claude already handles for Multica-managed headless runs.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(agent): align CodeBuddy guards with real CLI protocol

Address review on #8340: force CODEBUDDY_CODE_DISABLE_BACKGROUND_TASKS,
fail on system/task_* background events, and drop Claude-only
prompt_too_long / async_launched handling that CodeBuddy 2.150.0 never emits.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(agent): update CodeBuddy handleUser test after signature change

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(agent): force CodeBuddy background-disable env last for Windows

Append CODEBUDDY_CODE_DISABLE_BACKGROUND_TASKS=1 so os/exec's
case-insensitive last-wins dedup keeps it over lowercase custom_env.
Also extract Desktop PATH fallback helper with unit tests.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-17 13:34:53 +08:00
c26da76c42 fix(codebuddy): deduplicate fallback usage by response ID (#8395)
CodeBuddy is a Claude Code fork on the same stream-json protocol, and its
adapter carried the same usage-accounting defect #8382 fixed for Claude: one
response can emit several assistant events sharing a message ID and usage, and
the adapter summed every one of them. A run that ends without usable final
usage therefore reported inflated input and cache tokens.

Count each identified main-loop response once within an execution. Keep
forwarding every text and tool block, and keep the existing precedence of the
result's modelUsage over result-level usage over the fallback.

The fallback also stops summing the assistant output_tokens placeholder and
excludes nested-agent events, matching claude.go and the documented crash
recovery procedure. It recovers main-loop input/cache usage, not a full bill.

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-14 18:24:10 +08:00
9914d51040 MUL-5846 fix(agent): stop launching CodeBuddy with MCP disabled (#6556)
* fix(agent): stop launching CodeBuddy with MCP disabled (MUL-5846)

`--strict-mcp-config` means "only use MCP servers from --mcp-config".
buildCodebuddyArgs passed it unconditionally, so every CodeBuddy agent
without a Multica-managed mcp_config was launched in strict mode with no
config at all — zero MCP servers, and the user's own ~/.codebuddy.json
silently ignored. claude.go already gates the flag on hasManagedMcpConfig;
do the same here, and gate the --mcp-config temp file on the same
three-state check so a JSON `null` no longer counts as a managed set.

Also split CodeBuddy off Claude in runtime_mcp.go. CodeBuddy is a Claude
Code fork but keeps its own config file and plugin root (~/.codebuddy.json,
~/.workbuddy/), so reading ~/.claude.json leaked Claude's servers into
CodeBuddy launches while dropping CodeBuddy's own.

Add kimi to the runtime MCP inventory (<KIMI_CODE_HOME>/mcp.json), which
previously reported this runtime as not supporting MCP. Inventory only:
`kimi acp` already merges that file with the ephemeral `mcpServers` array
the ACP backend sends in session/new, and a duplicate name spawns the
server twice, so the task-local merge stays a passthrough for kimi.

Verified against CodeBuddy 2.x and kimi-code 0.33.0 with a local stdio MCP
probe server: before, the strict launch line answered NO_MCP_TOOL and never
spawned the server; after, it returns the probe's payload.

Co-authored-by: multica-agent <[email protected]>

* fix(daemon): resolve CodeBuddy's real MCP config path (MUL-5846)

Review follow-up. The previous commit pointed CodeBuddy at ~/.codebuddy.json,
which is only the last fallback in its chain. Verified against the installed
CodeBuddy CLI:

  - the config dir is $CODEBUDDY_CONFIG_DIR, defaulting to ~/.codebuddy
  - user-scope MCP is the FIRST existing of <configDir>/.mcp.json ->
    <configDir>/mcp.json -> ~/.codebuddy.json (a fallback chain, not a merge);
    `codebuddy mcp add --scope user` writes the first
  - those files are JSONC: comments and trailing commas parse fine
  - plugins live under <configDir>/plugins, not ~/.workbuddy

This is not just a missing inventory row. When an agent has a managed
mcp_config, codebuddy.go turns on --strict-mcp-config, so the merged document
the daemon writes is the entire world CodeBuddy sees. Reading the wrong file
meant "add one cloud MCP server" silently dropped every server the user had
configured locally, which is exactly what mergeRuntimeAndAgentMcpConfig exists
to prevent.

stripJSONC rewrites comments to spaces and drops trailing commas without
touching string literals, so a `//` or a comma inside a command argument
survives.

Also adds the session/new counterpart to TestKimiResumeIncludesMcpServers:
Kimi takes MCP over ACP rather than as a CLI flag, so a bare `kimi acp`
launch line is not evidence the servers were dropped.

Regressions added: path precedence, CODEBUDDY_CONFIG_DIR, JSONC (including
comment-like text inside strings), managed + local merge, same-name override,
explicit empty set, JSON null passthrough.

Co-authored-by: multica-agent <[email protected]>

* fix(agent): let CodeBuddy merge its own MCP scopes natively (MUL-5846)

Round-2 review follow-up. Pointing the daemon at CodeBuddy's user-scope file
only fixed one of three scopes: --strict-mcp-config drops user, project AND
local config, so an agent with a managed mcp_config still lost every server
the user had configured locally.

Measured against CodeBuddy 2.x with one real MCP server registered per scope,
using process spawns as the oracle (the model's own tool list is not
trustworthy here — it happily names servers that failed to connect):

  --mcp-config only ....... managed + user + local
  --mcp-config + strict ... managed only
  strict only ............. nothing at all

So stop passing --strict-mcp-config entirely and let CodeBuddy build the
union itself. A managed entry already wins a same-name collision natively
(verified), which is exactly what mergeRuntimeAndAgentMcpConfig promises, so
codebuddy also drops out of the daemon-side merge and becomes a passthrough
like kimi.

This removes the class of bugs the previous approach kept generating: the
daemon no longer has to reproduce CodeBuddy's config-root resolution, scope
precedence, or JSONC parsing on the launch path, and the review's second
must-fix goes away with it — agent custom_env CODEBUDDY_CONFIG_DIR is now
read by the CLI itself, so the daemon resolving a different directory can no
longer desync the launch. Project-scope servers also keep their native
approval gate instead of being rewritten into daemon-generated CLI config.
(Measured: a project-scope server sits at "Needs approval" and never starts
headless, with or without our flags.)

codebuddyUserMcpConfigPath and the JSONC reader stay, now used only by the
read-only runtime inventory the Agent > MCP tab renders.

stripJSONC also tightened per the review: only the last comma before a closer
is dropped, so `[1,,,]` stays invalid instead of being silently repaired, and
output is now exactly the same length as input so parse-error offsets line up.

Co-authored-by: multica-agent <[email protected]>

* test(agent): pin kimi MCP delivery against the real CLI (MUL-5846)

Users reported that MCP configured in Multica never reaches kimi, citing the
bare `kimi acp` launch line as evidence. That line carries no MCP flags
because the CLI has none — `kimi acp --help` lists only --login and --help —
so kimi takes MCP over ACP session/new instead, and only an end-to-end run
against the real binary can settle the claim.

Drives this package's own kimi backend with a Multica-shaped agent.mcp_config.
The oracle is the MCP server process itself: a stdio fixture that appends to a
log when spawned and returns a sentinel from its one tool, so neither a
hand-written ACP fixture nor the model's own account of its tools can fake a
pass. Verified against kimi-code 0.33.0: server spawned, tools/list served,
tools/call answered, sentinel in the task output.

Behind the agentintegration tag and MULTICA_RUN_REAL_AGENT_SMOKE, so the
default suites and CI are unaffected.

Co-authored-by: multica-agent <[email protected]>

* fix(daemon): reject unterminated JSONC block comments (MUL-5846)

Review nits.

stripJSONC blanked an unterminated `/*` all the way to EOF, so
`{"mcpServers":{}} /* oops` became valid JSON plus whitespace, and the `/*/`
near-miss let the opener's own `*` double as the closer's. Either could make
the Agent > MCP tab list servers out of a file CodeBuddy itself rejects —
verified: the CLI answers "No MCP servers configured" for both. It now
consumes the opener before scanning and returns an error when no closer is
found. Only the inventory is affected; the launch path stopped depending on
this reader last commit.

Also refreshes the launch-path comment, which still described the managed file
as a "controlled set" gated by a strict-mode three-state check that
buildCodebuddyArgs no longer performs. The flag is gone; --mcp-config now adds
to CodeBuddy's own user/project/local scopes.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-07 15:22:01 +08:00
Bohan Jiang 3eff25401d MUL-5785 fix(agent): return an empty output when a stream-json run has no final text (#6557)
A successful stream-json run with no deliverable text returned the sentence "The agent completed without a final response." as its output. That string is a signal, not a message to a user, and shipping it as content defeated every no-response decision the delivery layer already owns — all three branch on the output being EMPTY: an issue task skips the synthesized fallback comment, a direct chat writes a localized no_response outcome, and a Slack/Lark channel drops the turn instead of pushing anything out.

Return the empty string instead, and classify each assistant event explicitly so the fallback answer only survives turns we positively understood: a tool-using turn clears it, an unparseable or unknown-shape event clears it (fail closed), and a genuinely silent thinking-only turn leaves it alone. The decision lives in one shared assistantTurn.resolveFallback across claude, codebuddy and qwen. Unreadable events are counted as unreadable_assistant_count so a CLI schema drift surfaces as a number rather than silently empty outputs.

Status stays completed: an empty output is a legitimate terminal result for a turn that ended on a tool call, and re-running it would repeat side effects that already happened.

Fixes #6462
2026-08-07 14:38:03 +08:00
75c11db048 MUL-5549: feat(agent): discover codebuddy models over ACP instead of scraping --help (#6203)
* feat(agent): discover codebuddy models over ACP instead of scraping --help (MUL-5549)

CodeBuddy speaks ACP, and `session/new` answers with a structured catalog under
models.availableModels plus a currentModelId — exactly the shape the shared
parseACPSessionNewModels already reads for Copilot / Kimi / Kiro / Qoder / Grok /
TRAE. Scraping the `--model` line out of `codebuddy --help` was never necessary.

The help text carried IDs and nothing else, which cost us three things:

- Labels were guessed from the ID and were simply wrong. `kimi-k3-1` rendered as
  "Kimi K3 1" where the CLI says Kimi-K3; `deepseek-v3-2-volc` as
  "Deepseek V3 2 Volc" where the CLI says DeepSeek-V3.2.
- The default model was a "first entry wins" guess rather than the advertised
  currentModelId.
- The effort catalog needed a second regex over the same output.

All three come from the handshake now. The effort catalog rides along in the
same session/new response as the `thought_level` config option, so it costs no
extra process — which also retires the "at most one --help per request"
constraint added in #6196, because --help is no longer run at all.

One trap worth naming: thought_level advertises `enabled` ("On (default)")
alongside the six real levels, but `--effort enabled` is not a valid command
line — the daemon passes the selected level straight to the flag. Advertised
levels are filtered against the flag's accepted set, and a currentValue outside
that set (the default `enabled`) becomes an empty DefaultLevel, which the UI
renders as a generic "Default" instead of a value we cannot pass through.

Two adjacent inaccuracies surfaced while confirming the real level set against
CodeBuddy 2.130.0, both fixed here: the static effort fallback omitted `minimal`
and `max`, and so did the server-side IsKnownThinkingValue gate — so the server
rejected two levels the CLI genuinely accepts.

Discovery keeps its fallback, still marked Fallback so it can never be cached as
authoritative (#6196). That covers the not-logged-in case, which is deliberately
NOT special-cased with an auth step: the catalog came back without calling
authenticate on a logged-in CLI, and inventing an auth branch we cannot exercise
would be speculation.

Removes codebuddyModelRe, parseCodebuddyModels, codebuddyModelLabel,
codebuddyModelProvider, codebuddyEffortRe, parseCodebuddyEffortHelp,
codebuddyEffortSuperset, codebuddyHelpOutput and its 60s help cache.

Co-authored-by: multica-agent <[email protected]>

* fix(agent): keep codebuddy's vendor grouping after the ACP migration (MUL-5549)

Review nit, and a real regression in the previous commit. Dropping
codebuddyModelProvider looked like removing dead code, but it was the only thing
populating Model.Provider for CodeBuddy — and the picker groups on that field.

acpModelEntry can only recover a vendor from a `vendor:model` id. CodeBuddy's
are bare (`glm-5.2`, `kimi-k3-1`), so every model came back with an empty
Provider, and model-dropdown renders the empty group with no header at all: all
16 models would have collapsed into one unlabelled list where main shows Zhipu /
Kimi / MiniMax / DeepSeek / Hunyuan sections.

Restores the prefix inference as a post-pass over the ACP catalog, exactly the
shape discoverCopilotModels already uses for the same reason.

Verified against the real CLI: all 16 models land in five vendor groups with none
ungrouped. Tests assert the vendor for every id CodeBuddy 2.130.0 advertises plus
the static fallback ids, and that the fallback entries' hardcoded providers agree
with the inference. Removing the post-pass fails them.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-07-30 21:40:23 +08:00
280fa28e0d fix(agent): deny CodeBuddy's interactive plan-mode tools in headless runs (MUL-5383) (#6104)
CodeBuddy exempts AskUserQuestion and ExitPlanMode from permission-mode
finalization, so `--permission-mode bypassPermissions` never auto-approves
them. Once the model entered plan mode the session mode is Plan, not
BypassPermissions, so ExitPlanMode also missed the daemon's bypass
fast-path and went to the SDK permission bridge — which waits with no
timeout for a confirmation the headless runtime cannot render. The task
sat in-flight until the 2h tool watchdog, and users killed it by hand.

Deny EnterPlanMode/ExitPlanMode alongside the AskUserQuestion we already
deny. Each tool is passed as its own argv value because CodeBuddy matches
disallowedTools entries exactly and does not split on commas.

Also send `allowed: true` on control_response: CodeBuddy's
SdkPermissionClient reads `allowed`, not Claude Code's `behavior`, so the
daemon's "auto-approve" was being read as a denial.

Fixes #6012

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-07-29 15:55:02 +08:00
cf1fc64671 MUL-4824: fail closed on incomplete Claude-style result streams (#5522)
* fix(agent): fail closed on incomplete stream results

Co-authored-by: multica-agent <[email protected]>

* fix(agent): tighten empty result fallback

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-07-16 15:22:46 +08:00
Liu Guanzhongandkrislliu 4594c776e1 feat(agent): add CodeBuddy as first-class CLI backend (#3186)
* feat(agent): add codebuddyBackend struct and buildCodebuddyArgs

Introduces the codebuddy agent backend skeleton with args builder
that mirrors claudeBackend's protocol flags (stream-json, bypass
permissions, blocked args filtering) for the codebuddy CLI fork.

* feat(agent): implement codebuddyBackend.Execute with stream-json parsing

* feat(agent): wire codebuddy into New() factory and launchHeaders

* feat(agent): add codebuddy dynamic model discovery from --help

* feat(agent): add codebuddy thinking/effort discovery and providerThinkingEnums

* feat(daemon): add codebuddy CLI probe, env vars, and args support

* fix(agent): use len(models)==0 for default model instead of loop index

* fix(agent): increase codebuddy --help timeout to 35s for slow CLI startup

* fix(agent): address codebuddy PR review feedback

- Wire codebuddy into execenv: reuse claude's CLAUDE.md, .claude/skills,
  and ~/.claude/skills paths since CodeBuddy is a Claude Code fork
- Replace hardcoded 20-min timeout with runContext for zero-timeout =
  no-deadline semantics matching all other backends
- Restore runContext regression tests lost in rebase merge
- Mirror claude.go execution model: concurrent stdin write to prevent
  pipe deadlock, sync.Once for stdin closure, keep stdin open for
  control_request auto-approval mid-run
- Add control_request handling with auto-approve behavior
- Add RequestID/Request fields to codebuddySDKMessage
- Add codebuddy to metrics knownRuntimeProviders
- Add codebuddy to provider-logo.tsx (reuses ClaudeLogo)
- Consolidate --help discovery: shared codebuddyHelpOutput cache
  eliminates duplicate cold-start invocations

---------

Co-authored-by: krislliu <[email protected]>
2026-06-12 15:22:16 +08:00