mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-10 18:36:45 +08:00
d276991c603e6041d20fb5c1e65df8756d35d2ae
317
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d276991c60 |
MUL-6743 refactor(issues): remove the assignment handoff note (#7626)
* refactor(issues): remove the assignment handoff note (MUL-6743) Usage data puts the handoff note at 0.2% of assignments, so the box occupied the confirm dialog's centre for the other 99.8% and bought them nothing. Removing it takes the whole feature with it: the note input and its runtime version gate, the `handoff_note` write field on PATCH/batch issue updates, the task column write, and the daemon's opening-prompt + issue_context.md handoff sections. The confirm dialog itself stays — it still names who is about to run and still offers "Don't start yet" (suppress_run). The `*WithHandoff` enqueue methods only ever varied by that note, so they become `EnqueueTaskForIssueByActor` / `EnqueueTaskForSquadLeaderByActor` — the actor-carrying variants autopilot already called with an empty note. `preview-trigger` drops `handoff_supported`, which nothing read after the gate moved client-side. `agent_task_queue.handoff_note` is deliberately left in place: it holds historical rows and this is the expand half of an expand-contract pair. Nothing reads or writes it now, so a follow-up migration can drop it once no deployed binary selects it. Co-authored-by: multica-agent <[email protected]> * docs(issues): reshoot the assignment confirm dialog (MUL-6743) The asset still showed the handoff note box the surrounding prose no longer describes. Recaptured from the app at the same 600x450 frame; the confirm button now carries the send-chord keycaps it has shipped with since MUL-5694. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
3df7c38ed1 |
fix(agent): normalise the provider prefix in the Hermes set_model skip-gate (MUL-6958) (#7908)
* fix(agent): normalise the provider prefix in the Hermes set_model skip-gate The gate added in #5690 (MUL-5029) compares the configured model against the model the session reports as current with a raw `==`: if opts.Model != "" && effectiveModel == sessionCurrentModel { Those two values come from different id namespaces, so for an agent whose `model` is stored bare the comparison can never be true. The gate is dead code for those agents and `session/set_model` is replayed on every turn — which is the exact situation #5690 was opened to prevent. Why the two sides cannot match: - Hermes always reports a provider-encoded id. `_encode_model_choice` in its ACP adapter returns `f"{provider}:{model}"` whenever a provider resolves, and the provider falls back to `detect_provider()` or `"openrouter"`, so it is effectively never absent. Confirmed against a live `hermes acp` session: session/new populates `models.currentModelId` from that encoder, so a bare configured `claude-sonnet-4-5` on a `custom` provider is reported back as `custom:claude-sonnet-4-5`. - `agent.model` is persisted verbatim. `handler/agent.go` writes `req.Model` straight into the column on create and update; nothing normalises or validates a `provider:` prefix on that path. - The bare spelling is a documented, first-class input. `cmd_agent.go`'s own flag help is `--model Model identifier (e.g. claude-sonnet-4-6, openai/gpt-4o)`. Only the ACP-catalog dropdown produces the prefixed form, because `acpModelEntry` uses the advertised id as `Model.ID`. So dropdown-configured agents match and skip; every agent configured through the CLI, the API, a seed, or a row predating the catalog does not, and never will. What the replay costs, per turn, in the Hermes runtime: `set_session_model` replaces the session's agent with `_make_agent`, which re-runs `load_config` and `resolve_runtime_provider`, joins background MCP discovery for up to `mcp_discovery_timeout` (~1.5s), and constructs a fresh agent whose system prompt is then re-derived from disk. It also re-runs `_resolve_model_selection`, i.e. the provider auto-detection this gate exists to avoid — so the `custom:deepseek-v4-pro` mis-route from #5690 is still live for precisely the agents that hit it. Running Hermes' own resolver over the ids this daemon sends reproduces it: `custom` + `deepseek-v4-pro` resolves to `openrouter`, and `anthropic` + a bare Claude id resolves away from `anthropic`, both of which make `_make_agent` drop base_url and api_mode. The fix replaces the raw comparison with `acpModelIDsEquivalent`, which normalises the optional prefix before comparing: - both sides carry an explicit provider → compare the whole id, so a genuine provider switch (openrouter:X → custom:X) still sends set_model; - exactly one side carries a provider → compare model names only; a bare configured id expresses no provider preference, so the session's own provider is authoritative; - either side empty → not equivalent, preserving the existing fall-through for runtimes that report no current model. Only the first colon separates provider from model, so slash-bearing OpenRouter-style names (`nous:moonshotai/kimi-k2.6`) split correctly, and the comparison is case-insensitive because Hermes lowercases the provider when it encodes the id. No migration and no operator action. Rows already written in the prefixed form keep working — both spellings now resolve to the same selection. Deliberately not argued on cost. A cache-hit-rate difference first pointed here, but that measurement was grouped by "does the agent have a configured model", which on the measured deployment was confounded with the transport wire, so this change does not claim it. It stands on the comparison being unsatisfiable and on the guard it restores. Tests: 12 `acpModelIDsEquivalent` cases, including the provider-switch and different-model negatives, the case-folding and whitespace cases, and the slash-bearing model id; plus 6 `splitACPModelID` cases covering the leading-colon and multi-colon shapes. * fix(agent): only skip set_model when an explicit provider is confirmed Review caught a regression introduced by the previous commit: the third equivalence rule was symmetric, but the risk is not. acpModelIDsEquivalent("openrouter:hermes-4", "hermes-4") // => true Comparing model names whenever *either* side lacked a provider meant a caller who named a provider explicitly got the switch swallowed, silently, and the session kept whatever provider it had defaulted to. `origin/main` sends set_model for that input, so the previous commit changed behaviour for it. The premise that justified the symmetry — "Hermes always reports a provider-encoded id" — is false, and this repository already contained the counterexamples: `acp_effort_test.go` captures `currentModelId: "hermes-4"` from `hermes acp` v0.20.0 and `currentModelId: "gpt-5.6-sol"` from jcode, both unprefixed, and jcode routes through this same backend (hermes.go states it covers both binaries). `_encode_model_choice` returning a prefix is what happens when a provider resolves; it is not a property of every input. The rule is now asymmetric: - bare configured id -> equivalent on the model name alone. It expresses no provider preference, so the session's provider is authoritative. This is the case the gate exists for, and it still skips. - explicit configured provider -> equivalent only when the session reports that same provider. A bare current id cannot confirm the match, so we fall through and send set_model, as before this change. Tests: - three `acpModelIDsEquivalent` table entries, including the missing `provider:model` vs bare-id row that let the regression through; - `TestHermesSkipsRedundantSetModelForBareConfiguredModel`, an end-to-end frame-recording test that asserts the gate actually fires for the bare spelling — the unit tests never checked that set_model stopped going out; - `TestHermesSendsSetModelWhenExplicitProviderAndBareCurrent`, which fails without the asymmetric rule (verified: the recorded frame is absent). Also addressed from the same review: - dropped the stale cache-hit-rate paragraph from the `acpModelIDsEquivalent` comment. The PR description claimed it had been removed; it had not. - dropped the paragraph arguing the benefit via MCP refresh and tool-cache prefix invalidation. It described Hermes' Python internals this repository cannot verify or track, and it read as arguing against the sentence right after it. - softened "the comparison can never be true": it holds when the runtime reports a prefixed id, which is not always. - noted that `splitACPModelID` cannot distinguish a provider prefix from a colon inside a bare model name (`llama3:8b`), and that the degradation is safe in the only direction that matters — it falls back to sending set_model, never to a false skip. * docs(agent): correct the bare-current-id evidence in the set_model gate The justification for the asymmetric equivalence rule cited acp_effort_test.go as capturing unprefixed model ids from both `hermes acp` and jcode. Only the jcode half holds. Verified against a live `hermes acp` v0.20.0 — the same version the fixture names — driving `initialize` + `session/new`, the two calls this backend makes: `models.currentModelId` came back provider-encoded in all three cases (no model param, a bare model param, and a provider-encoded one), and all nine availableModels ids were prefixed too. Sending a bare `gpt-5.6-terra` returned `openai-codex:gpt-5.6-terra`, which is the defect this gate fixes and is now first-hand rather than inferred. Hermes Agent's encoder does have a bare branch — acp_adapter/server.py `_encode_model_choice` returns the model alone when the provider is empty, confirmed by calling it directly — but `_build_model_state` defaults the provider (`state.agent.provider or detect_provider() or "openrouter"`) and `normalize_provider("")` falls back to `openrouter`, so a normal install never reaches it. The `hermes-4` in that fixture is better read as a simplification made while writing a test about configOptions, not a verbatim capture. The rule itself is unchanged and still correct: this backend serves Hermes Agent and jcode, jcode really does report unprefixed ids, and refusing to confirm an explicit provider against a bare current id costs nothing. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Garfield <[email protected]> Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
603519d158 |
MUL-6977: test: remove implementation-only coverage (#7932)
* test: remove implementation-only coverage Co-authored-by: multica-agent <[email protected]> * test: remove remaining implementation-only coverage Co-authored-by: multica-agent <[email protected]> * test: restore compatibility and fragment navigation coverage Co-authored-by: multica-agent <[email protected]> * test: preserve behavioral cleanup contracts Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
a075e58b80 |
MUL-6961 feat(agent): discover Claude Code's model catalog from the CLI (#7917)
* feat(agent): discover Claude Code's model catalog from the CLI
Claude was the last first-party runtime answering "what models do you
have?" from a hand-maintained list. A static catalog cannot know what the
CLI in front of it can run, so every new Anthropic model opened a window
where Multica offered a model that 400s:
API Error: 400 Claude Code 2.1.246 does not support this model;
version 2.1.251 or newer is required.
Claude Code does have a discovery hook, just not on the command line: the
stream-json control protocol answers a `list_models` control request with
the rows its own /model picker renders. Ask the installed binary and the
window closes at the source — a model it cannot run is either absent or
flagged disabled, computed against the real CLI version and the logged-in
account.
Verified against 2.1.223/228/246/258. The three older builds return Fable
5.1 as a disabled row carrying "Update to 2.1.255+ to use Fable 5.1";
2.1.258 returns it as a normal pick. Those rows are the reason no
per-model minimum-version table is needed here: upstream already computes
one, including the remedy copy, which we forward verbatim and grey out.
No version gate guards the request. An unsupported subtype is answered,
not ignored — every build tested replies "Unsupported control request
subtype" in ~2s and exits 0 — so an old CLI costs one cheap round trip and
falls back. Gating would mean hand-maintaining exactly the kind of number
this change removes.
Notes:
- The static catalog stays, now as the fallback and as the server-side
accept list. It is flagged Catalog.Fallback so a discovery failure is
not pinned in the server's day-long cache (MUL-5549).
- Discovery replaces rather than merges, as with every other dynamic
provider. The picker narrows to the CLI's own list; agents already
pinned to an older model keep working, and manual entry still reaches
them.
- Effort levels now come from each row instead of scraping `claude
--help` and narrowing through the hand-kept claudeModelEffortAllow.
- Catalog ids keep the context-window tag, because `claude-opus-5[1m]` is
what the CLI would really run for that row. That exposed a latent bug
in two capability lookups, which normalized the query side but not the
catalog side; both now normalize both, or a model would fail to find
its own effort picker.
MUL-6961
Co-authored-by: multica-agent <[email protected]>
* fix(agent): keep unrunnable Claude models out of the selectable catalog
Review found the first cut carried "this CLI cannot run it" as a flag on
the model itself. That was wrong in a way a flag cannot fix.
Only the create-agent dropdown learned the flag. The inspector picker and
the agent builder read the same list and kept offering the row, so
editing an existing agent still reached the 400 this change exists to
prevent. And the part no amount of teaching fixes: an already installed
desktop client has never heard of the field, so it renders the row as an
ordinary model and persists a model id the CLI rejects — exactly what the
repo's mixed-version API rule forbids.
Unavailable models now travel in their own list, end to end: daemon
report, store, catalog cache, and the client result. `models` contains
only selectable entries. The inspector and the builder needed no changes
to become correct — they read `models`, and the rows are no longer in it
— and an old client is correct for the same reason. Both pickers render
the advisory rows through one shared component that takes no click
handler, so nothing can turn them back into controls.
Two more review findings:
- The default row was dropped when no sibling row named the same model,
which an org-restricted install can produce. That is not one missing
picker entry: ValidateThinkingLevelWith resolves an empty model — the
common "follow the CLI default" configuration — through the entry
flagged Default, and fails closed without one, silently discarding the
user's effort on every such task. It is now materialised, keeping the
row's own capabilities. The test that claimed to cover this asserted
the opposite of its own comment; rewritten, plus a case proving an
unrunnable default is never materialised.
- "One cheap round trip" for a CLI without list_models was one round trip
PER TASK: every Claude task carrying a thinking_level loads the catalog
before it starts, and cachedDiscovery refuses to memoise a fallback.
The structured unsupported-subtype answer is now remembered per
(command, CLI version) — a property of the binary, not a bad moment —
so an old CLI is probed once. Transient failures stay uncached and
retryable, and the version in the key makes an upgrade take effect at
once rather than after a TTL. Detecting it costs ~0.01s against the
~1.7s it saves, and the fallback path already paid for it.
The discovery timeout keeps its 20s ceiling, now with the measurements
that justify it: 1.57-2.19s over five runs against 2.1.258, 1.64-1.79s
against 2.1.246. The ceiling is for a CLI that never answers, not a slow
one.
Verified against real binaries: 2.1.246 offers four selectable models and
one unavailable row carrying upstream's own upgrade hint, 2.1.258 offers
Fable 5.1 normally, a missing binary falls back to the flagged static
catalog.
MUL-6961
Co-authored-by: multica-agent <[email protected]>
* docs(agent): record the downgrade case as an accepted boundary
Dynamic discovery governs selection, and only selection. An agent already
configured with a model whose CLI is later downgraded still starts and
still gets the upstream 400 — discovery cannot reach a value already
persisted.
That is deliberate, not an oversight, so it is written down next to the
code that invites the wrong fix. Clearing the configured model turns a
legible error into a silent config change the user did not make;
substituting a runnable one is worse, because it succeeds using a model
nobody chose. The downgrade is rare and the upstream error is accurate,
so failing loudly at task start is a fine way to report it.
MUL-6961
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
2e424e06a1 |
MUL-5154: fix(agent): surface Kimi provider API errors (#5785)
Surface Kimi provider failures that ACP reports as successful empty turns, preserve terminal error details across retries and EOF, and connect poisoned-history failures to the existing guarded fresh-session recovery path. |
||
|
|
9d09902dfe |
MUL-6943 feat(agent): add Claude Fable 5.1 model and pricing (#7892)
Adds `claude-fable-5-1` to the static Claude Code model catalog and to the backend and frontend pricing tables, so the model is selectable with its effort picker enabled and its usage prices at the published rates ($10 input / $50 output / $12.50 5m cache write / $0.25 cache read — 5.1 prices cache hits at 0.025x input rather than the standard 0.1x). Maintainer follow-ups on top of the contribution tighten backend alias matching: both Fable rules now end at a shared `claudeVersionEnd` terminator anchored to the end of the id, and the tag-stripping retry in `PriceForModelAlias` peels exactly one context tag. Together these stop Fable 5 and 5.1 from inheriting each other's tier, keep an unknown variant (`claude-fable-5-2`, `-10`, `-1-preview`, `[1m][2m]`) unmapped instead of silently borrowing a rate, and keep the backend and the dashboard resolving the same suffix forms. Regression coverage added for the dated, `-latest`, context-tagged and doubly-tagged spellings. Sonnet 4.6 remains the sole default. Known follow-up: Claude Code rejects this model below 2.1.251 while the global Claude runtime floor is 2.0.0, so members on an older daemon see it in the picker and fail at run time; the fix is a per-model minimum-runtime-version signal, tracked separately. |
||
|
|
438a30ef00 |
MUL-6902 fix(agent): fix Pi v0.4.37 Windows session regression (#7843)
* fix(agent): avoid locking Pi transcript on Windows Co-authored-by: multica-agent <[email protected]> * test(pi): cover sidecar lock contention and release * test(ci): run the Windows Pi session-lock regression tests The windows-execenv job runs ./pkg/agent through an explicit -run allowlist, and the ubuntu backend job cannot compile a windows-tagged file. Without this step the two tests added for #7840 would never execute in CI, leaving the same coverage gap that let the v0.4.37 regression ship. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: worker-opencode <[email protected]> Co-authored-by: multica-agent <[email protected]> Co-authored-by: J <[email protected]> |
||
|
|
108407088c |
MUL-6836: fix(codex): allow explicit standard speed (#7756)
* fix(codex): allow explicit standard speed * fix(codex): negotiate explicit standard support --------- Co-authored-by: L <[email protected]> |
||
|
|
2ebe0ae327 |
MUL-6862: fix(codex): widen thread setup handshake budget (#7790)
* fix(codex): widen thread setup handshake budget * fix(codex): preserve handshake timeout overrides |
||
|
|
6231962e98 |
MUL-6840 fix(pi): preserve sessions across workdir changes (#7760)
* fix(pi): resume session across workdir changes * fix(pi): serialize session transcript writers * fix(pi): keep busy sessions resumable --------- Co-authored-by: L <[email protected]> |
||
|
|
88e9170dc1 |
refactor(redact): drop home-directory masking from Text (#7772)
redact.Text rewrote the local home directory to /Users/****/ on its way to the database and the WebSocket broadcast. Removing it. It was never an access-control boundary. Anyone who can read a task transcript already sees repository paths, file contents, commands, diffs and error output; masking one path segment out of all that protected nothing, while it did make paths non-copy-pasteable and harder to line up when debugging a run. It was also incoherent about whose home directory it hid. homeDir and username were resolved from the process running Text, and the main transcript fields (Content / Output) are only redacted in the server's ingest handler -- so a hosted deployment matched them against the *server's* home directory, never the machine actually running the agent. Only tool inputs and stderr diagnostics went through the daemon-side call where the resolved home was the right one. Transcript visibility is an authorization concern and belongs at that layer. Secret redaction (API keys, tokens, PEM blocks, connection strings) is a real boundary and is untouched. Two tests asserted the removed behaviour and now assert its absence, so nobody reintroduces host-path masking as a side effect of another fix: redact.TestTextLeavesHomePathIntact (replacing TestRedactHomeDirectory and TestInputMapRedactsNestedHomePath) and the home-path arm of agent.TestCursorExecuteReportsSanitizedStderrOnProcessFailure, which keeps its bearer-token assertion. Dropping the branch also resolves the Windows failure in TestRedactHomeDirectory reported in #7744: Windows can return a DOMAIN\user account name that is not a substring of the home path, so the strings.Replace never fired. CI only runs Go tests on ubuntu, which is why main stayed green. Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
fe59262188 |
feat(agent): add Huawei Cloud CodeArts support (#6985)
* feat(agent): add Huawei Cloud CodeArts support * fix(migrations): resolve CodeArts prefix collision * fix(agent): address CodeArts review feedback * fix(agent): tighten CodeArts integration scope * fix(agent): resolve CodeArts follow-up review blockers * fix(agent): redact CodeArts launch arguments * docs: sync supported runtime counts * fix(agent): own CodeArts model probe process tree |
||
|
|
dcc2d58251 |
MUL-6737: fix(daemon): raise the agent inactivity budget to 2h and derive the tool budget (#7685)
The idle watchdog was sized to notice a hang quickly (30m), but the two costs it trades between are not symmetric: force-stopping a healthy run throws away the work and flips the issue to blocked, while noticing a hang late only holds a concurrency slot. - DefaultAgentIdleWatchdog 30m -> 2h, sized by the longest legitimate silent step. - AgentToolWatchdog now derives from the idle budget in both directions; MULTICA_AGENT_TOOL_WATCHDOG still overrides and 0 keeps its 'never force-stop during a tool call' meaning. - Watchdog tick capped at 5m, so worst-case detection is budget + 5m instead of budget * 1.5. - Dropped the unreachable 30s tick floor (window >= 1m implies window/2 >= 30s) and asserted the property it was written for directly. - Removed the dead MULTICA_CODEX_TIMEOUT from .env.example; documented the generic watchdog and Codex timeout rows in CLI_AND_DAEMON.md; synced four doc locales. |
||
|
|
7d128b5a45 |
fix(daemon): own the openclaw CLI process tree so its deadline is enforceable (MUL-5467, MUL-5630, MUL-6736) (#6275)
* fix(daemon): own the openclaw CLI process tree so its deadline is enforceable (MUL-5467) openclawCLITimeout has carried a "Known gap (deliberately not fixed here)" note since #6084: the 5s deadline could not actually bound an `openclaw config ...` invocation. This closes it, and fixes a second OpenClaw misbehaviour found alongside it. ## Bug 1 — the deadline was decorative exec.CommandContext kills only the direct child. cmd.Output() blocks in Wait() until the output pipes os/exec manages reach EOF, and cancelling the context does not unblock that io.Copy. So when openclaw leaves a descendant holding stdout — its `openclaw-config` helper, or cmd.exe → node on Windows — the call runs for the *descendant's* lifetime. #6084 measured this on linux/dash (a shim whose backgrounded child slept 6s took 6.01s against a 150ms deadline), tried a cmd.WaitDelay backstop, and reverted it on review because it bounds the call by leaving the descendant running — trading a hang for a process leak that nothing on Unix reaps. The note left behind names the real fix: process-tree ownership. Three call sites had the shape: - execenv.execOpenclawCLI on the task's critical path (Prepare → prepareOpenclawConfig) - agent.detectCLIVersion runs for every provider inside the daemon's blocking registration preflight - agent.discoverOpenclawAgents four invocations per call ## Bug 2 — the answer was already on stdout Measured on an OpenClaw host (openclaw 2026.5.27): openclaw --version 258ms exits cleanly openclaw config file 60s correct path printed, then never exits openclaw agents list 60s correct list printed, then never exits Waiting for exit turns two working commands into a task-fatal error: agent_error.process_failure (prepare execution environment: execenv: prepare openclaw config: locate openclaw active config: openclaw config file: context deadline exceeded (process: signal: killed)) The contract of these commands is "print a value". Once the value has arrived and nothing more is coming, whether the process tidies itself up is not the caller's business, and it should not fail a chat task. ## The fix Two helpers in server/pkg/agent, both built on the process-group primitives codex/claude/opencode already use — no new platform-specific functions: - RunCollect: hands os/exec an *os.File instead of a buffer, so os/exec starts no copy goroutine and Wait returns the instant the direct child exits. Then signals the child's process group, which reaps the helper and releases the last write end, so our own readers see EOF. Unlike WaitDelay this reports the CLI's real exit status, which matters because a failed `--version` probe skips runtime registration entirely, and because openclawShimDiagnostic type-switches on *exec.ExitError. - RunCollectQuiet: returns when the process exits, or when stdout has produced output that then stayed idle for 400ms — whichever comes first. Reports quiet=true on the idle path so callers can log the CLI's failure to exit without failing on it. The idle shortcut deliberately cannot mask a silent CLI: with no output there is nothing to salvage, so the deadline still governs and the call still fails. A non-zero exit still fails too, with stderr intact. execOpenclawCLI keeps its shim diagnostics and its context-attribution order byte-for-byte; only the exec mechanism changes. discoverOpenclawAgents keeps its existing 30s function-level budget rather than gaining a per-attempt one. Windows: there is no process group to signal, so calls are bounded (by the drain grace) but a descendant can still outlive one. Owning descendants there needs a Job Object and is not attempted here. ## Verification - Both packages green with -race; 6 consecutive runs, no flakes. - All 51 existing Openclaw* tests in execenv pass, including TestExecOpenclawCLITimeoutIsNotMisdiagnosedAsMissingInterpreter and TestExecOpenclawCLICancellationIsWrapped — their shims produce no stdout, so the salvage path is not reachable and the errors.Is(err, context.DeadlineExceeded) contract is preserved. - 13 new tests. Both mechanisms are mutation-verified: * RunCollect reverted to buffers with no group reap → the guard test hangs and the dump is the production stack: cmd.Wait → awaitGoroutines blocked, writerDescriptor parked in io.Copy. * RunCollectQuiet's idle path disabled → both the unit test and the execenv end-to-end test fail after a full 60s. * fix(daemon): address review on the openclaw CLI collector (MUL-5467) Three blocking items from review of #6275. All three were reproduced before being fixed, and the first one was a genuine correctness hole rather than a style objection. ## 1. The deadline branch salvaged partial answers RunCollectQuiet returned success from the ctx.Done() branch whenever stdout was non-empty, without requiring the idle grace or any check that the answer was finished. A CLI still streaming when the deadline arrived therefore had its truncated output reported as a completed response. Reproduced with a stub emitting a JSON document forever against a 250ms deadline: 9 runs in 10 came back as success carrying an unparseable fragment. The early return now needs two independent conditions, neither sufficient alone: - an OutputComplete rule supplied by the caller accepts the buffer, and - the buffer has since been idle for idleGrace. Reaching the deadline is never success. The captured bytes are still returned alongside the error so a caller with its own rule can inspect them, but the runner no longer decides on the caller's behalf. A nil rule disables the early return entirely, which is the conservative default for a command whose output shape has no completeness rule. The rule cannot be inferred inside the runner, which is the point: only the caller knows whether `{"agents":[{"id":"a"},` is truncated or legitimate. - `--json` subcommands -> agent.JSONOutputComplete (whole buffer parses). - `openclaw config file` -> openclawConfigPathComplete (last non-empty line looks like a path). Deliberately stricter than openclawParseActiveConfigPath, which resolves a relative line through filepath.Abs and would therefore accept a Doctor warning border as an answer. That matters because OpenClaw prints Doctor UI *before* the path (MUL-3136), so an early return on idle output alone hands back the banner as the config path — and nothing downstream catches it. - `agents list` text fallback -> RunCollect (wait for exit). Decorated text has no completeness rule and a silently short agent catalog is worse than a slow one; this path still gains the reaping and the bounded return. ## 2. quietWriter published bytes before their timestamp The buffer write and the last-write timestamp were updated in separate critical sections, so a reader could observe new bytes carrying a stale timestamp and conclude the stream had gone quiet at the moment it was producing — truncating an answer mid-write. Both now move in one critical section (outputBuffer). ## 3. The drain-timeout path raced the readers, and could park a goroutine RunCollect read its buffers after the drain grace expired while io.Copy could still be appending to them; the deferred Close on the read ends ran only after the return values had been evaluated. Separately, RunCollectQuiet handed os/exec io.Writers, so its cmd.Wait goroutine could sit forever on a descendant holding the pipe — on Windows nothing released it, since there is no process group to signal. Both are now structural rather than patched. A single collector owns the pipes for both helpers (the *os.File ownership RunCollect already had), and finish() guarantees that when it returns, every goroutine startCollector spawned has been joined and nothing can still append to the buffers: it reaps the tree, waits bounded for Wait, and if the readers still have not seen EOF it closes the read ends outright — which makes the in-flight Read return — and only then joins them. ## Verification - Both packages pass with -race under CI's flags (-p 2 -parallel 2). - All pre-existing Openclaw* tests still pass, including the timeout attribution and shim diagnostic ones. - New regression tests, all three named in the review: continuous output until the deadline (must not be success), a prompt followed by a delayed body (must not return the prompt), and a late non-zero exit within the grace (must be reported as the failure it is). Plus the completeness rules per subcommand, the banner-then-path case end to end through execOpenclawCLI, goroutine collection on both helpers, and the two outputBuffer contracts. - Windows contract coverage added (run_collect_windows_test.go) with a scoped step in the existing windows-execenv CI job, since the close-the-read-ends path is unreachable on Unix where the group kill releases the pipe. - Mutation-verified: restoring the unconditional salvage fails both the unit and the execenv regression; dropping the completeness rule from execOpenclawCLI makes the Doctor banner come back as the config path. * test(daemon): make the openclaw config-path fixtures hermetic The two execenv fixtures printed the literal path the host had printed, `/root/.openclaw/openclaw.json`. openclawParseActiveConfigPath stats what it parses and tolerates only os.ErrNotExist, so that path resolves differently by environment: ENOENT on a developer machine, where /root does not exist, but EACCES on a Linux CI runner, where it exists and is unreadable. The second case is a hard error, so TestExecOpenclawCLIWaitsForThePathAfterDoctorBanner failed in CI for a reason unrelated to the boundary it covers. Both fixtures now print a real file inside t.TempDir(), which stats cleanly everywhere. The second test only inspects raw stdout today, but a hard-coded path there would be a trap for whoever next adds a parse to it. The literal path stays in the completeness-rule table, which calls the predicate directly and never stats, so it keeps documenting the shape the CLI actually emits. * fix(agent): own one-shot CLI process trees on Windows * fix(daemon): support current OpenClaw config discovery * Address review, and narrow this branch to the process-tree fix Blocking items from the review: 1. The deadline branch no longer salvages. It returns an error unconditionally, and callers supply a per-subcommand completeness rule (openclawOutputComplete) that decides whether the captured bytes are a finished answer. Regression coverage for the three shapes named in the review: output continuing right up to the deadline, a banner ahead of the real answer, and a late non-zero exit. finish() now reports, rather than being wired to a field that was never assigned. Two cases do not converge and both cost the caller something it would otherwise never learn: a process still outstanding in Wait after the tree kill, and a descendant still holding the pipe when the read ends had to be force-closed, which can truncate trailing output. collectorError joins that with the command's own error, so nothing is displaced. 2. outputBuffer publishes bytes and the write timestamp inside one critical section, so a poller cannot see new bytes with a stale timestamp and conclude the stream went quiet while it was in fact producing. 3. The drain-timeout path closes the read ends and waits for both absorb loops before any snapshot is taken, so no io.Copy can be appending to a buffer the caller is about to read. Windows contract coverage runs as its own CI job. The goroutine guarantee is stated at its real strength instead of "no goroutine outlives the call, on any platform". Convergence holds whenever the tree kill takes. When it does not, one goroutine can stay parked in cmd.Wait on a process nothing can terminate; that residue is not removable here, so it is reported instead of asserted away. On Windows the same case extends the call rather than leaking, because an anonymous pipe is not pollable and closing the read end waits for an in-flight Read to release it. Also from review: - A JSON error envelope is valid JSON, so JSONOutputComplete accepts one and a CLI that prints `{"error": ...}` and then lingers past the idle grace hands back err == nil with the envelope in stdout — where the exit-status path never sees it. All three JSON readers now check for that explicitly. It matters most for `config get agents.list --json`, where the envelope is how a 2026.6+ host says the key moved to the registry: decoding it as data turned a graceful fallback into a failed preparation. For the object-target read the failure is silent, since the envelope would be sanitized and written into the task's snapshot as if it were the user's config. - RunCollectCmd selects on ctx.Done(). It takes a command the caller built, so enforcement must not depend on that caller having used exec.CommandContext; the ordering matches RunCollectQuiet so the two entry points cannot report different error shapes for the same situation. startCollector drops its unused ctx parameter rather than implying it enforces one. - checkOpenclawVersion keeps stderr in the parse. It replaced CombinedOutput(), and a build that prints its version there would otherwise fail the gate on empty output, which skips runtime registration entirely. extractVersionLine is bypassed on purpose: parseOpenclawVersion already scans the whole text, while picking the first line containing a semver lets unrelated stderr noise answer for it. The probe is bounded because the caller's ctx is the task's, and ExplainExecError is applied here too so an ENOEXEC shim is still named (MUL-6164). - TestRunCollectQuietWithoutCompletenessRuleWaitsForExit synchronizes on a marker the CLI writes after its answer instead of a deadline. Its two assertions want opposite budgets, and the reviewer reproduced the resulting flake 3/3 under load. Waiting for the marker also proves more than a timeout could: that the runner was still blocked at a moment when the answer was already fully available. Scope: the MCP work that shared this branch is reverted to main's behaviour and will come back on its own, because it needs a discussion this one does not. It changed which command supplies the resolved MCP settings, restructured the include chain, and moved where the user's config bytes live — none of which the deadline fix depends on. Preparation for a managed-MCP agent behaves exactly as it does on main here. $OPENCLAW_HOME path expansion stays: openclawConfigPathComplete calls openclawHomeRest directly, so the completeness rule for `config file` cannot tell whether that shape is a finished answer without it. * fix(agent): converge the collector's tree kill and stop failing calls on cleanup (MUL-5467) Review follow-up on #6275. All three findings share one root cause: whether the caller got its answer depended on whether cleanup converged, and cleanup convergence depended on a single kill(-pgid) landing. - Retry the tree kill across collectReapWindow. One pass loses a descendant whose fork completes between the kill's enumeration of the process group and the signal's delivery — measured 3 misses in 10 runs of the forking stub, each leaving a `sleep 300` reparented to init and stalling the call until the drain budget expired. The window is deliberately short so it does not widen the pid-reuse window reapProcessTree already documents. - Replace the 5s EOF budget with a 400ms settle. Once the direct child has exited, at most one pipe buffer of its output can still be unread — a child that had written more would have blocked in write() and could not have exited — so a short cap cannot truncate a well-behaved CLI's answer. What it cuts short is a surviving descendant's trailing output, which is not the answer. - Stop reporting cleanup residue as a call error. finish() now logs and returns nothing: a probe whose output arrived and whose helper the kernel would not kill is exactly the case #6084's cmd.WaitDelay got wrong, and a failed version probe skips runtime registration entirely. This drops finishErr, collectorError and the drainForced/waitPending split, which the four call sites were interpreting two different ways by accident. - Fix the reaping tests. They asserted on `$$` captured inside `( )`, which POSIX keeps at the invoking shell's pid, so they named the direct child that cmd.Wait reaps regardless — all of them passed with reapProcessTree removed, while the run leaked 7 orphans. Record `$!` instead, and add TestRunCollectRetriesTheTreeKill, which injects a missed pass because the race lives inside one syscall and cannot be forced from a stub. Verified: collector group 0/10 failures (was 3/10) with 0 orphans, 0/5 under -race, full ./pkg/agent -race green, execenv Openclaw -race green, and both mutations — retry removed, reaping disabled — fail the tests that now cover them, on their own assertions rather than on a cleanup error. * refactor: move the `$OPENCLAW_HOME` expansion out of this branch It is not process-tree work. `openclaw config file` printing `$OPENCLAW_HOME\...` instead of the expanded path is the same class of bug as #6630 — a path the daemon cannot stat, reported as a fresh install, wrapper without the user's `$include` — and #6643 shipped the tilde half of it as its own change. This half belongs there too, not folded into a branch about owning the CLI's process tree. Removed here: openclawHomeRest, its branch in expandOpenclawPath, the unit test, and the completeness-rule row that depended on it. Neither PR now depends on the other. openclawConfigPathComplete therefore does not recognize the variable shape, which is correct in isolation: `main` cannot expand it either, so a rule that accepted the line would hand the caller a path the parser then fails on. Such a host keeps `main`'s behaviour — no rule match means RunCollectQuiet waits for exit — so it loses the hang tolerance and nothing else. The comment on that function says so and names the change that should close it, because a gap nobody wrote down is one a rebase re-opens silently. * fix(daemon): teach the config-path rule the `$OPENCLAW_HOME` shape (#7310 hand-off) #7310 landed the parser side of this, so the deferred half is now due — and it was recorded in the code precisely so a rebase could not drop it silently. While expandOpenclawPath could not resolve `$OPENCLAW_HOME\...`, this rule had to refuse the shape: accepting it would have reported a finished answer and then handed the caller a path the parser failed on. The cost was that a host with that variable set lost the early return entirely and went back to waiting for the CLI to exit — which is the hang this branch exists to survive. With the parser fixed, the refusal was the only thing left standing between those hosts and the tolerance. Resolved through openclawHomeFromEnv rather than read raw from the environment. The value may itself be a tilde path that upstream expands before printing, so `filepath.IsAbs` on the raw string answers false for `OPENCLAW_HOME=~/svc` and silently withholds the early return on exactly the configuration #7310's review was about. Mutation-checked: the raw-env version fails the new `tilde-valued OPENCLAW_HOME` row with `rule(...) = false, want true`. TestOpenclawOutputCompleteRules becomes subtests so a row can carry the environment it is about, and gains three: an absolute OPENCLAW_HOME, a tilde-valued one, and an unset one — the last staying false, so a line with nothing to resolve it against still waits for exit and lets the parser report the real error instead of this rule guessing. * docs(agent): site the collector against outputOwned, not cmd.Output() #7531 moved every one-shot probe in this package onto launch.go's runOwned / outputOwned / combinedOutputOwned, which makes three claims in this branch stale rather than wrong-in-detail: - RunCollect called itself "the safe replacement for cmd.Output()". There are no cmd.Output() call sites left in pkg/agent. The comparison a reader needs is against outputOwned, which owns the same process tree and differs only in who owns the pipes — so that is what the doc compares, including why the probeWaitDelay bound is the right default for the probes it serves and the wrong one here. - RunCollect's path-taking form advertised itself as the migration entry point for "the remaining cmd.Output() call sites (models.go's catalog probes, codex.go, deveco_models.go, thinking.go)". #7531 migrated all four to the owned helpers. It stays as the form for a caller holding only a path, and for a probe that turns out to need pipe ownership. - collector's doc now says outright which of the two properties launch.go cannot provide, since "os/exec owns the draining" now describes in-package code rather than a stdlib call nobody here makes. Also adds the `if: needs.changes.outputs.backend == 'true'` guard the new Windows CI step was missing. Every other step in that job has it; without it the step ran on frontend-only changes. * docs(agent): cite upstream for the hang tolerance, not just the timings The measured "prints its answer and then never exits" for `config file` and `agents list` came from one host on openclaw 2026.5.27, which invites the obvious objection: this is an upstream bug, so why compensate for it here rather than wait for a fix. Read at openclaw v2026.7.1, it is not a bug in a build — it is the exit model. `src/cli/one-shot-exit.ts` provides an explicit opt-in, `requestExitAfterOneShotOutput`, which `runCli` flushes after both streams drain. In the shipped 2026.7.1 tarball exactly three modules take it: `models list`, `models status`, and the hooks CLI. The commands this package invokes — `config file`, `config get`, `agents list` — do not: their success paths only print, and `runtime.exit` appears solely on their failure paths, so termination is whatever the event loop happens to do. A version that exits promptly is not promising the next one will. Also pins openclawHomeRest's citation forward. `runConfigFile` still prints shortenHomePath at v2026.7.1, and resolveHomeDisplayPrefix there still selects `$OPENCLAW_HOME` on a non-empty trimmed variable and `~` otherwise. Worth recording that the separator comes from the original path rather than being inserted by the prefix, since that is what the rest-slice in this function relies on. * fix(daemon): size the config-file idle grace to the real warning gap E2E on a real OpenClaw host (2026.7.1-2, plus 2026.6.8 and 2026.7.2-beta.7) falsified two claims this branch was making. Both are corrected here; one needed a code change. **The 400ms idle grace was sized against the wrong gap.** The claim was that a one-shot response arrives in back-to-back writes, so 400ms is "orders of magnitude longer than the gap between those writes". The first half is confirmed — 0.012-0.033ms between writes. The gap that matters is a different one: with a config that produces Doctor and plugin warnings, `openclaw config file` spread its stdout over 3289ms, of which a single 3285ms silence sat between the end of the warning block and the path. The default grace is eligible to fire inside that silence, on whatever the warnings left in the buffer. Why it lands on `config file` alone: upstream routes incidental console output to stderr when argv carries `--json` (withConsoleLogsRoutedToStderrForJson, src/cli/json-output-mode.ts at v2026.7.1), so every `--json` caller here gets a stdout that is the JSON document and nothing else, and a partial document cannot parse however long the writer pauses. `config file` has no `--json`, so the warnings share its stdout with the answer. openclawConfigPathComplete is still the guarantee — a box-drawing border is not an absolute path — and a grace can only be defence in depth, since the gap is a property of the host's plugin and Doctor work. But it should not be defence that loses by three seconds, so openclawQuietIdleGrace gives `config file` 5s and leaves `--json` on the default. The cost is paid only by a CLI that hangs: `config file` then returns in path + 5s rather than path + 400ms, against a 30s deadline. **RunCollect guarantee 2 overstated what a group kill can reach.** The helper was measured holding the same stdout pipe as its parent (both `pipe:[135202]`) while running with its own PGID and SID. `kill(-pgid)` never reaches that, and neither does #7531's ownership, which uses the same group. "Invoking a CLI on a timer cannot accumulate orphans" was therefore wrong on Unix, and the three comments that repeated it are corrected. What holds regardless is guarantee 1: it is pipe ownership, not the kill, that makes Wait return and the deadline enforceable. Windows is unaffected — a Job Object owns the tree irrespective of process groups. Tests: TestOpenclawQuietIdleGraceCoversTheWarningGap pins the per-command grace and asserts the `config file` value exceeds the measured gap; TestExecOpenclawCLISurvivesTheWarningGapBeforeThePath drives a stub with a warning block, a gap past the default grace, then the path, and requires the path back. * fix(agent): make pipe EOF the completion signal, and bound what is kept Second review round found three blockers on b9b39eaea. All three reproduce exactly as reported, and all three are cases where this branch was *worse* than the launch.go helpers it replaced — which is the bar a replacement has to clear, so the fix narrows the branch rather than patching around them. **1. Leader exit was treated as answer completion (worst of the three).** RunCollect*Cmd reaped the process group as soon as the direct child exited. A wrapper can exit 0 while the real CLI — its descendant, holding the inherited stdout — has not printed yet, which is the normal shape for an npm or PowerShell shim. Measured here: the wrapper exits 0, its child prints the version 500ms later, and this head returned an empty version with a *nil error* in 427ms where outputOwned returns the version in 517ms. Empty-and-nil is the worst available shape, and detectCLIVersion routes all 23 providers through it. Pipe EOF is the only signal that means "no more output is coming", so: - detectCLIVersion, checkOpenclawVersion and discoverOpenclawAgents go back to launch.go's outputOwned / combinedOutputOwned, i.e. back to os/exec's own EOF wait. What this branch still fixes there is the *reporting*: a lingering descendant makes Wait report exec.ErrWaitDelay over an answer that arrived, and a failed --version probe skips runtime registration entirely (#6084 reverted a WaitDelay backstop over exactly that). So salvageProbeAnswer drops the error when, and only when, the caller's own parse found the answer — never on a non-zero exit, never on a deadline. - The collector keeps its reason to exist (execenv's task-critical path) and gains awaitOutputAfterExit: after the direct child exits it waits for pipe EOF, bounded by collectDrainGrace (2s, matching probeWaitDelay), unless the caller's completeness rule says the answer is already in — so a CLI that prints and exits with a helper on the pipe still pays nothing. - RunCollect / RunCollectCmd are deleted. With the probes back on outputOwned they had no production caller, and an exported wait-for-exit collector is exactly the API that invited this bug. **2. A path-shaped warning could be returned as the config path.** openclawConfigPathComplete accepted any absolute-looking last line, and the previous commit's answer — a 5s grace — was the wrong dimension, as the review pointed out by quoting this branch's own comment that no fixed grace can bound the host's plugin work. Reproduced: a CLI printing an existing plugin-cache.json path, pausing 6s, then printing the real path returned the warning path after 5.51s. Fixed by not asking a question content cannot answer. `openclaw config validate --json` reports the path in a named `path` field, on every branch (valid / invalid / file-not-found / error), in every version from v2026.5.5 — which is minOpenclawVersion — through v2026.8.1-beta.3, and `--json` makes upstream route incidental output to stderr so stdout is the document alone. openclawActiveConfigPath asks that first and keeps `config file` as a fallback with no completeness rule at all: it waits for exit and fails closed on the deadline. Upstream runs the same readConfigFileSnapshot() for both commands, so preferring the JSON form costs no extra work; it costs one extra invocation only on a CLI too old for either, which is pinned in TestPrepareOpenclawConfigFallsBackWhenConfigFileUnsupported. openclawConfigPathComplete, openclawQuietIdleGrace and openclawConfigFileIdleGrace are gone. `--json` is now the only shape with a rule, and the doc says why that is the design and not a gap. **3. Retention was unbounded.** absorb appended without limit, where outputOwned keeps the last probeStderrSampleBytes. Measured: 13,107,400 bytes of stderr retained from a CLI writing continuously, ~400x the bound it replaced. stderr now keeps its tail at collectStderrTail (= 32 KiB, so moving a call site between mechanisms cannot change what a failed probe reports) and stdout is capped at collectStdoutLimit and reports overflow as an error — deliberately asymmetric, because a head-truncated answer is how a partial catalog becomes a confident empty one. Every new guard was mutation-verified by restoring the mechanism it covers: | Mutation | Test | Failure | |---|---|---| | collector + reap on leader exit | TestDetectCLIVersionWaitsForAWrapperDescendant | `version = ""` in 0.42s | | awaitOutputAfterExit returns immediately | TestRunCollectQuietWaitsForAWrapperDescendant | `stdout = ""` | | drop the rule short-circuit | TestRunCollectQuietDoesNotWaitWhenTheAnswerIsIn | pays the full 2s | | unbounded buffers | TestCollectedStderrKeepsOnlyItsTail | retained 13,107,417 bytes | | unbounded buffers | TestCollectedStdoutOverflowIsReportedNotTruncated | 524,296 bytes, nil error | | shape rule for `config file` | TestOpenclawActiveConfigPathIgnoresAPathShapedWarning | returns plugin-cache.json | The new fallback test also caught a bug in this commit before it shipped: openclawValidatedConfigPath accepted upstream's `CONFIG_PATH ?? "openclaw.json"` literal, because expandOpenclawPath turned that relative name into a confident path under the daemon's cwd. It now requires the reported value to be absolute before expansion, allowing only the `~` and `$OPENCLAW_HOME` forms. * fix(agent): repair the Windows-only collector test call sites The mechanical conversion of the deleted RunCollect call sites over-widened three tuples in run_collect_windows_test.go, which already used RunCollectQuiet's four results. Host `go build` and `go vet` never compiled them — the file is `//go:build windows` — so the break surfaced on the windows-execenv job as `assignment mismatch: 5 variables but RunCollectQuiet returns 4 values`. Verified this time the way it should have been the first time: `GOOS=windows go build ./...` and `GOOS=windows go vet ./...`, plus darwin and linux. That is the check to run whenever this package's exported signatures move, since a third of its process-lifecycle tests only compile on Windows. * test(execenv): pin the config-path payload shapes E2E measured E2E on OpenClaw 2026.7.1-2 confirmed `config validate --json` as the config-path source and corrected two things this branch had inferred from source. **The exit status is load-bearing in the wrong direction, and this was untested.** Two of the three real states exit *non-zero* while carrying the answer: a missing config file gives `{"valid":false,"path":"…","error":"file not found"}` and an invalid one `{"valid":false,"path":"…","issues":[…]}`, both exit 1 with stderr empty. A fresh install is the missing-file case, so reading a non-zero exit as "no answer" would break first run. openclawValidatedConfigPath already ignored the status, but nothing pinned it — writeOpenclawConfigStub could only exit 0, so the tests covered the rarest branch. It now takes an exit code, and TestOpenclawActiveConfigPathReadsThePathFromANonZeroExit drives all three measured payloads. **Two source-based claims corrected.** - "Preferring the JSON form costs nothing extra" understated it. Measured over three runs each on the same host and config: 1.65/1.66/1.65s for `config validate --json` against 6.36/4.16/4.06s for `config file`. The JSON form is ~2.5-4x faster, which tracks the output it does not render — 715 bytes of JSON against 2663 bytes and 28 lines of warning UI. - The reason a `--json` stdout is clean was attributed to upstream's console-log reroute (`withConsoleLogsRoutedToStderrForJson`). That function exists, but it is not what was observed doing the work: the Doctor and plugin warnings arrive as a `warnings` array *inside* the payload. The distinction matters for why JSONOutputComplete is a real signal — the stream is structurally one document, not merely quieter — so the comments now describe the measurement rather than the mechanism inferred from it. Unrelated and pre-existing: `./pkg/agent`'s ACP timing tests (1-6s wall-clock budgets) flake under load, and which ones fail varies per run. Control run of `origin/main`'s whole package on this machine, same conditions: fails TestCodexExecuteSemanticInactivityAllowsContinuousMessages and TestCodexExecuteLegacyFirstTurnMessageSatisfiesProgress. Not introduced here. * fix(agent): require a recognised version before salvaging a probe answer The ErrWaitDelay salvage in detectCLIVersion was gated on `version != ""`, and extractVersionLine falls back to the trimmed raw output when no line carries a semver token — so any non-empty text satisfied it, including whatever a wrapper printed before the real version existed. Review reproduced it with a stub that prints a banner, exits 0, and has a descendant print the version past the 2s WaitDelay: detectCLIVersion returned "initializing plugins" with a nil error, and logged "CLI answered but left its output pipes open" — the opposite of what happened. That value is then persisted as the runtime version, for every provider, since DetectVersion routes all 23 through here. extractVersionLine now reports whether the semver scan actually matched, and only that answers the salvage question. The trimmed-raw fallback still stands on the success path: a CLI that exits 0 with an unusual version format is reporting its version (#2516), and it is only unusable as evidence that a bounded, incomplete read already holds the answer. Mutation-checked: with the old gate, TestDetectCLIVersionDoesNotSalvageABannerAsTheVersion fails with exactly the reviewed shape — version="initializing plugins", nil error, misleading warn. * test(agent): derive the collected-stdout cap from the largest answer collectStdoutLimit argued for why a fail-closed cap is needed but not for its value, and where it falls is a product compatibility boundary rather than only a memory bound: a legal config past the cap stops a task from starting at all. The derivation is now recorded and executable. Measured answers on 2026.7.1-2 are 715 B (`config validate --json`) and 2663 B (`config file`); the largest answer any call site asks for is the resolved config from `config get --json`, which upstream puts no ceiling on. So the worst case is constructed — 1000 agents and 250 MCP servers with the fields OpenClaw carries, 547,291 bytes, already orders of magnitude past a real deployment — and 8 MiB leaves 15x on top of it while matching what the daemon already holds for a comparable per-task payload (maxLocalSkillBundleSize). Two tests pin it: the constructed high-water mark must round-trip through the collector byte-identically with the stated headroom, and the boundary itself is exact — at the limit the answer is returned whole, one byte past it fails closed with errCollectStdoutTooLarge. * fix(execenv): share one CLI deadline across both path-resolution attempts openclawActiveConfigPath asks `config validate --json` first and falls back to `config file`, and the fallback carried a fresh full deadline. That made the worst case five budgets against a ceiling derived from four: at the 60s override ceiling, 5m of CLI time alone, landing exactly on daemon.defaultTaskPrepareTimeout — so the specific, non-retryable ErrOpenclawCLITimeout collapses back into the generic retryable prepare timeout, which is the outcome openclawCLIMaxTimeout exists to prevent. Both invocations answer the same question, so they now share one context. The multiplier is renamed to openclawMaxCLIDeadlinesPerPreparation because that is what it always measured — the budget is per step, not per invocation. The consequence is deliberate and asserted: a validate that burns the whole budget leaves the fallback none, os/exec reports the expired context on Start, and the failure still carries ErrOpenclawCLITimeout. A CLI that cannot say where its config is within the entire deadline will not answer the same question on a fresh one. TestPrepareOpenclawConfigWorstCaseCLIBudgets now drives the real worst case (validate fails, config file answers, registry fallback, managed MCP) and counts distinct ctx deadlines rather than calls — the old test synthesized a validate success from the `config file` response, so the two never fired in the same run. Mutation-checked: with separate deadlines it reports 5 budgets against a constant of 4. --------- Co-authored-by: weiweiwei <[email protected]> |
||
|
|
620f99d8ce | fix(codex): wait for authoritative turn completion (#7554) | ||
|
|
46b5d9e6d1 |
MUL-6658 fix(agent): own the runtime process tree on every backend (#7522) (#7531)
* fix(agent): own the runtime process tree on every backend Cancelling a task on Windows left the agent running. The daemon marked the task cancelled, revoked its mat_ token, and killed what it thought was the CLI — but on Windows the direct child is usually a cmd.exe shim and the real CLI is already a grandchild, so it survived. One reporter's orphan kept working for 40 minutes, read the daemon owner's profile PAT after its own token started returning 401, and posted comments as the member (GH #7522). Whole-tree ownership already existed on both platforms. It was opt-in, and opt-in rotted: 8 of the 27 places this package starts a process asked for a Unix process group, and 3 of the 23 backends took Windows Job Object ownership. Adding claude to that list would have left 19 backends with the same hole, so the fix is structural instead. - newRuntimeCmd, at the single point where a runtime process is built, now puts every command in its own process group and replaces os/exec's leader-only Cancel with a group-wide kill. A backend that wants a graceful shutdown still assigns its own cmd.Cancel and wins. This also covers the --version and model-discovery probes, which cmd.Output() runs without ever reaching a start helper. - startOwnedProcessTree is now the only way this package starts a process, so Windows Job Object ownership is not something a backend can forget. Each start is paired with releaseProcessGroup after the reap, which drops the job handle and kills whatever outlived it. - Two structural tests enforce both halves, the way TestOnlyLaunchGoSpawnsRuntimeProcesses already enforces the argv boundary: a new backend calling cmd.Start fails, and one that takes ownership without releasing it fails. The Unix half is #5918 for the 20 backends the original fix did not reach: their CLI was never in its own group at all, so no group-wide signal could have reached it. Co-authored-by: multica-agent <[email protected]> * fix(agent): own the process tree for synchronous probes too Review found the ownership boundary had a hole the PR claimed to cover. os/exec's Run, Output and CombinedOutput call Start themselves, so a probe written with cmd.Output() never reaches startOwnedProcessTree and owns nothing on Windows — where the direct child of a `--version` probe is typically the npm shim and the real CLI is already a grandchild. That is the same escape #7522 was reported for, in 15 call sites nobody was looking at; detectCLIVersion's own comment already describes a broken CLI leaving grandchildren that hold the stdout pipe. - runOwned / outputOwned / combinedOutputOwned give a synchronous probe the same start, reap and release a task launch gets. They keep the stdlib contract: stdout returned, stderr attached to the ExitError, combined output through a single writer so os/exec gives it one pipe. The stderr sample is bounded, as os/exec bounds its own. - All 15 probe sites route through them, and the structural guard now rejects Run/Output/CombinedOutput alongside Start. The release guard did not hold its invariant either: it only asked whether a file contained any releaseProcessGroup, so a second unreleased start in a file that already had a pair would pass — and models.go is exactly that shape. It now works per function: one owned start each, paired with at least one release. Equal counts would be the wrong rule, since dsh's Execute legitimately releases on two exit paths. Both guards are now pure functions over an AST, with tests that feed them synthetic sources — including the second-unreleased-start case that the file-level version missed — so the guards fail closed without needing a real regression committed to find out. Adds the Windows probe regression (descendant must be in the job and die with a cancelled probe) plus unix equivalents, and names the consequence in the degraded-ownership warning, which is the only signal an operator gets that cancellation on that host is back to killing the leader alone. Co-authored-by: multica-agent <[email protected]> * fix(agent): bound a finished probe's wait on pipes its descendants hold Review caught that runOwned could wait for its own cleanup and never return, and it reproduces: a descendant that inherited the output pipes holds them open after the leader exits, cmd.Wait blocks until the copy goroutines see EOF, and what would close those pipes is the release that runs after Wait. Cancellation breaks the cycle, which is why every test in the last round missed this — they all cancelled first, and they all set WaitDelay by hand. Normal exit has nothing to break it, and checkOpenclawVersion runs on the caller's context before runContext establishes any task timeout, so a version wrapper that exits cleanly while leaving a pipe-holding descendant would hang the task until a human cancelled it. runOwned now fills in a bounded WaitDelay when the caller left it zero — Go defines WaitDelay for precisely "a child process that exits but leaves its I/O pipes unclosed" — and signals the group before dropping ownership, so the descendant is reaped on Unix too, where releasing a process group is a no-op. A caller with its own bound keeps it; detectCLIVersion has set one by hand since MUL-3812 for this same shape. Adds the normal-exit regression on both platforms: the leader exits holding nothing, the descendant keeps the pipes, and outputOwned has to return in bounded time with the descendant gone. The Windows descendant spawner gains a spawn-and-exit mode and now hands its stdio to the descendant, which is what makes it hold the pipes at all. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
750fc0cc5a |
MUL-6655 Fix MCode ACP session startup race (ZIC-199) (#7516)
* Fix MCode ACP session startup race Co-authored-by: multica-agent <[email protected]> * fix(agent): only trust transport signals for MCode startup exit mcodeSessionStartupExited also matched "process exited" / "broken pipe" anywhere in the error text. A still-running MCode reports session/new failures as a JSON-RPC *acpRPCError, and session/new is the call that launches mcpServers, so a real error such as `failed to start MCP server "filesystem": process exited with code 1` was rewritten into "MiniMax Code ACP exited before starting a session; ... upgrade MiniMax Code". That branch returns a fixed string with no %v, so the original error was dropped, not just relabelled, pointing the member at the wrong subsystem. Drop the text matching and keep the transport-level signals. Real provider exits already arrive as errMcodeProcessExited via closeAllPending, and a stdin write racing the exit arrives wrapped as syscall.EPIPE, so no covered case is lost. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]> Co-authored-by: J <[email protected]> |
||
|
|
bdcb231ba0 |
MUL-5466 fix(agent): deliver the qwen prompt on stdin, not argv (#7343)
* fix(agent): route qwen launch through the Windows invocation chooser On Windows npm installs qwen as qwen.cmd, a batch shim. Spawning a .cmd goes through cmd.exe, whose %* forwarding re-tokenises the raw command line, so the `-p <prompt>` value and the `--output-format stream-json` flags buildQwenArgs puts behind it are mangled whenever the daemon's structured chat prompt carries newlines, quotes or `&`. Qwen then runs without a usable prompt or without stream-json, exits 0 after emitting a single unparseable stdout line, and finalizeStreamResult reports "qwen stream ended without terminal result". A short manual `qwen -p "hallo"` never hits the mangling, which is why the CLI looked healthy in isolation. Mirror the existing cursor/pi/copilot fix instead of inventing a new mechanism: resolve qwen.ps1 next to the .cmd and invoke PowerShell with `-File <ps1>` directly so Go hands each argv token whole, staying on the official launch path. qwenBackend.Execute now builds its process through Command.execVia(chooseQwenInvocation, ...) rather than Command.exec; on macOS/Linux the chooser is a passthrough and behaviour is unchanged. Fixes #6082 * test(agent): cover qwen's stdin prompt delivery and update signatures buildQwenArgs dropped its prompt parameter in the prior commit (the prompt now goes on stdin, not argv), so the existing unit tests needed their call sites and assertions updated to match — including an explicit check that "-p" never appears in argv again. Adds TestQwenBackendDeliversPromptOnStdin: a round-trip regression guard proving a prompt containing embedded double quotes, an em dash, and shell metacharacters reaches the child process byte-for-byte via stdin, the way cursor-agent and Pi already do (#5649's fix). The fake qwen script now optionally captures stdin to a file the same way it already captures argv. * test(agent): cover qwen Windows PowerShell launch Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
1cff79d256 |
fix(agent): discover Grok reasoning effort catalog (#7252)
Co-authored-by: Bowser Bot <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
6e70b1dc4b |
fix(agent): preserve newlines in Antigravity streamed text (#6717)
The Antigravity adapter scanned agent stdout line by line and emitted each streamed MessageText as the bare line without its "\n", skipping blank lines. The daemon concatenates streamed text with no separator (pendingText.WriteString), so the persisted task_message type=text rows lost every newline. The chat renders those rows, so block markdown (headings, lists) collapsed onto one line and rendered as literal "###" / "-" text. Mirror the output builder in the streamed chunks — prefix the newline on every line after the first and stream blank lines too — so the reconstructed task_message text matches output exactly. output itself is unchanged, so chat_message.content stays identical. Other backends stream whole blocks that already carry their newlines, so only this adapter was affected; the daemon concatenation, the frontend join, and the markdown renderer are all correct. Fixes #6149 |
||
|
|
8a5a6adbf7 |
feat(agent): add ZeroClaw as a native ACP runtime (MUL-6511) (#7351)
* feat(agent): add ZeroClaw as a generic ACP agent runtime
Add ZeroClaw (a Rust-based, single-binary generic agent CLI) as a
first-party runtime, driven over the standard ACP JSON-RPC transport via
`zeroclaw acp`, mirroring the Dim/QwenPaw/Grok ACP backend shape. Only the
binary, launch args, and blocked-flags policy are ZeroClaw-specific; the
backend reuses the shared hermesClient ACP transport and deliverable
tracker.
No provider-specific quirks (permission-preset injection, minimum-version
gate, or a separate authenticate step) are assumed without evidence — those
were verified against real Dim/Grok binaries and would need the same
hands-on verification against a real ZeroClaw binary before being added
here.
Backend: SupportedTypes/New/launchHeaders wiring, model discovery via
session/new's advertised catalog, daemon probe with
MULTICA_ZEROCLAW_PATH/MODEL overrides, AGENTS.md context delivery,
default-command-name registration, metrics label, and a migration widening
the runtime_profile.protocol_family whitelist.
Frontend: provider union, MCP-config support flag, and a placeholder
provider-logo mark (no official ZeroClaw brand asset exists yet).
Tests mirror the existing ACP backend conventions: fresh-session,
resume-via-session/load, resume-not-found, transient-load-error, blocked
launch args, timeout, and the shared ACP deliverable-boundary regression
suite.
* fix(agent): renumber ZeroClaw's migration to 378 to avoid a same-day collision
Both this branch and a concurrently-implemented Junie runtime branch
independently picked migration 377 off the same origin/main tip.
Renumbering ZeroClaw to 378 up front avoids opening two PRs from the
same author that claim the same migration number.
* fix(agent): align the ZeroClaw ACP backend with the real 0.8.4 runtime
The backend was written against an assumed vanilla ACP handshake and never
checked against a ZeroClaw binary. Driving a real zeroclaw 0.8.4 over stdio
shows five of those assumptions are wrong, and the test fake encoded the same
assumptions, so the suite stayed green against a contract the runtime does not
have.
- Drop the session/set_model request. It is not in ZeroClaw's dispatch table
(-32601) and no ACP handler reads a model param at all, so it could never
succeed; because its failure was fatal, any agent with a model configured
failed before session/prompt was reached. The model comes from the ZeroClaw
agent profile, so opt zeroclaw out of ModelSelectionSupported and stop
reading MULTICA_ZEROCLAW_MODEL rather than leaving the UI offering an
override that is silently ignored.
- Label token usage from initialize's _meta.zeroclaw.defaultModel, the only
model identity the ACP surface exposes. session/new returns just
{sessionId, workspaceDir}, so the two extractACPCurrentModelID reads were
dead code and the model-catalog discovery they fed could only ever return an
empty fallback while paying an ACP subprocess per catalog read. Return an
empty catalog directly, as qwenpaw/mcode do.
- Resume through session/resume instead of session/load. Both restore the
transcript into the agent, but load also replays every retained message back
as session/update notifications, which fed the previous answer into this
turn's deliverable. Add the acceptNotification turn gate every other ACP
backend carries, so out-of-turn chunks are dropped even if a runtime flushes
them early (#1997).
- Send agentAlias on session/new when the operator names one. ZeroClaw
auto-selects an agent only when its config holds exactly one entry, so
multi-agent installs failed with -32602 and had no way out: `zeroclaw acp`
has no --agent flag, and passing one kills the process at argument parsing.
The alias is consumed from custom_args and travels as a param; it is omitted
when unset, because a hardcoded guess breaks the single-agent case that
works today. The refusal now names both remedies.
- Stop advertising MCP support. No ZeroClaw handler reads params.mcpServers;
acp_enable_mcp gates that agent's own mcp_bundles, not the client list, so
MCP is operator-side only. Hide the MCP tab and warn on a stale mcp_config
instead of failing the task.
Also teach isACPSessionNotFound about ZeroClaw's custom -32000
SESSION_NOT_FOUND, without which a stale session id was retried forever
instead of restarting fresh. The wording check still decides, so a transient
error under the same code stays non-rejecting.
Tests now encode the observed handshake — _meta default model, session/new
without a catalog, -32000 for an unknown session, -32601 for set_model — and
cover the replayed-history, alias and MCP paths.
* fix(agent): make ZeroClaw integration merge-safe
Co-authored-by: multica-agent <[email protected]>
* fix(agent): tighten ZeroClaw ACP fallbacks
Co-authored-by: multica-agent <[email protected]>
* fix(agent): preserve ZeroClaw retry semantics
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
f48f24c405 |
fix(agent): route all Windows Pi runs through powershell -Command to preserve stdin (#7392)
Task and chat runs pass --mode json, so isPiRPCInvocation sent every real task down the PowerShell -File route. In -File mode PowerShell 5.1 reads stdin through the console ANSI codepage, so the prompt that has travelled on stdin since #6457 lost every non-ASCII byte (#7355). Route every Pi invocation through rewriteCmdToPS1Command (-Command with @args), which forwards stdin untouched; delete the now-dead isPiRPCInvocation predicate. Extract the argv construction into an untagged piPowerShellCommandArgs helper so the shape is testable off Windows, and update choosePiInvocation's doc comment: the #3306 -File reasoning is stale for Pi now that the prompt is on stdin, and the residual exposure is a multi-line custom option value. |
||
|
|
acecb7dd60 |
fix(pi): keep turn error when process exits 1 [MUL-6585] (#7451)
* fix(pi): keep turn error when process exits 1 Pi (and pi-print-clean-exit) exits 1 after stopReason=error. The backend then overwrote lastTurnError with "pi exited with error: exit status 1", so Classify filed a LiteLLM Connection error / Request timed out as non-retryable process_failure. Preserve the provider message and classify those strings as provider_network so auto-retry can resume the session. * fix(taskfailure): scope Pi network error matching Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: buihongduc132 <[email protected]> Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
25afaaf153 |
fix(agent): MUL-6606 surface hermes model-discovery failures instead of a silent empty catalog (#7464)
* fix(agent): surface hermes model-discovery failures instead of an empty catalog
Hermes model discovery swallowed every failure into an empty list with no
error, so the daemon reported "completed with 0 models" and the picker rendered
an authoritative empty dropdown — no reason, no hint, no prompt to type a model
in by hand. Hermes' own message already explains the common case ("No LLM
provider configured. Run `hermes model` ..."), and it was being discarded on
the discovery path.
- discoverHermesModels sets strictErrors. Unlike grok/codebuddy/copilot, hermes
has no static catalog to degrade to, so the failure must reach the UI rather
than be replaced by a stand-in list.
- Add a discovery-path annotation for "no LLM provider configured". It points
at the daemon's own environment, which is the actual cause when a GUI-launched
daemon cannot see credentials the user's shell exports. Deliberately not the
task path's HERMES_HOME/custom_env advice: discovery is per-runtime and reads
neither, so that remedy cannot populate the picker.
- Give acpDiscoveryProvider a per-provider timeout and set hermes to 40s.
Measured against hermes 0.20.0: a healthy session/new answers in ~2s, but a
hermes that cannot resolve its provider takes ~25s to say so. Under the shared
15s cap that diagnosis was replaced by "context deadline exceeded" — the one
failure text that tells the user nothing.
- Raise the client poll budget to the server's own 60s modelListRunningTimeout.
Below that the client abandoned requests the server would still have answered.
Manual entry stays live throughout the poll, so waiting costs the user nothing.
- Render the reason in the picker, next to the manual-entry input.
Also reconciles three comments that contradicted each other about which
providers report supported=false. ModelSelectionSupported names qwenpaw and
mcode; runtime_models.go claimed hermes, which if "aligned" to would have cost
hermes its model picker entirely.
Verified against the local hermes 0.20.0 CLI. Before: models=0, err=nil at the
15s cap. After: the full "No LLM provider configured" message plus the hint.
Co-authored-by: multica-agent <[email protected]>
* fix(agent): address review — poll budget window and Windows test portability
Two blockers from review, both confirmed against the code:
1. The client poll budget did not cover the server's full request lifetime.
The pending and running windows are sequential, not overlapping: pending is
measured from CreatedAt (30s to claim), running from RunStartedAt, which
PopPending sets at claim time. Heartbeat pickup therefore does NOT consume
the 60s running window, as the previous comment claimed. A request claimed
at ~29s that then runs hermes' ~40s discovery reports at ~69s — past the 60s
client budget, while the server still holds the record open. The user got a
generic client-side "timed out" instead of the daemon's reason, which is the
exact failure mode this PR exists to remove.
POLL_TIMEOUT_MS is now derived from the two server constants plus slack, so
the invariant is visible rather than asserted in prose. Letting the server's
own timeout win is also strictly better copy ("daemon did not respond within
30 seconds" beats "model discovery timed out"). Added two fake-timer tests:
one for a late claim plus a full discovery, one proving the poll still
terminates once both server windows have closed.
2. The new backend regression tests broke `go test ./pkg/agent` on Windows.
They write and execute `#!/bin/sh` fixtures, but the file carried no build
tag, and the Windows writeTestExecutable can only drop bytes on disk. Split
along the same line as codebuddy_discovery_fallback_test.go: process-based
cases move to hermes_model_discovery_unix_test.go (//go:build !windows), and
the assertions needing no subprocess — hint wording, classification
inertness, the timeout invariant — stay cross-platform against a literal of
the ACP error string.
Also corrects the Go timeout invariant itself, which had encoded the same wrong
model of the running window, and refreshes two daemon.go comments that still
described discovery as capped at ~15s and as uniformly returning empty lists on
failure.
Verified: GOOS=windows go test -c builds, with the shell-fixture tests excluded
from the Windows binary and all four portable tests present. Both new core tests
fail against the previous 60s budget.
Co-authored-by: multica-agent <[email protected]>
* test(agent): make ACP timeout fixture deadline-safe
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
179761a325 |
fix(agent): advertise grok-4.6 thinking levels and published rates [MUL-6549] (#7406)
* fix(agent): advertise grok-4.6 thinking levels and published rates
Grok Build CLI 1.0.5 lists grok-4.6 as the default model, but
annotateGrokThinking only tagged grok-4.5. A grok-4.6 agent therefore
got Thinking == nil, so the inspector hid the effort picker as soon as
thinking_level was cleared ("Follow CLI config"), and
ValidateThinkingLevel dropped --effort before launch.
Attach the documented low/medium/high catalog to grok-4.6 as well as
grok-4.5, default the offline fallback to grok-4.6, and add grok-4.6 to
the pricing tables at the published $2 / $6 short-context tier.
xhigh stays off the grok enum: Multica injects grok agent --effort, which
is still the grok-4.5 vocabulary.
Related to #7184.
* fix(agent): price grok-4.6 cached input at $0.50 and advertise xhigh
Review on #7406:
Grok 4.6's published short-context cached-input rate is $0.50/1M, not
the $0.30 copied from grok-4.5. Update both pricing tables and the
frontend 1M+1M+1M estimate from $8.3 to $8.5.
Grok 4.6 documents and accepts --effort xhigh (docs.x.ai and grok
1.0.5). Attach xhigh only to grok-4.6, let the grok server enum accept
the token, and keep ValidateThinkingLevel failing it closed for grok-4.5.
|
||
|
|
668a4e06c3 |
MUL-6464 fix(agent): support ACP terminals for Kimi runtime (#7323)
Closes #7289 |
||
|
|
20ceaccc43 |
MUL-6471: fix(daemon): let pi and opencode reach custom providers (#7312)
* fix(daemon): let pi and opencode reach custom providers (MUL-6471) A self-host setup whose pi/opencode providers point at an OpenAI/Anthropic-compatible gateway could not run a Multica task on either runtime, while the same work succeeded when the CLI was invoked by hand in the same container (GH #7300). Gateway-style providers register model ids that themselves contain a slash — `claude/claude-opus-5` under provider `multica-anthropic` — and both failures came from treating that slash as a provider boundary, or from having no provider at all. pi: buildPiArgs split the model id on the first slash and passed the leading segment as --provider, so a slash-shaped id became `--provider claude --model claude-opus-5`. pi answers an unknown --provider with a hard error before it ever looks at --model. The selector now goes to --model whole and --provider is never synthesized: pi's own resolver accepts `provider/id`, a bare id, and an id containing a slash, so passing less is strictly more capable. Verified against pi 0.67.2 — the old argv reproduces `Unknown provider "claude"`, the new one resolves to provider=multica-anthropic, model=claude/claude-opus-5. opencode: --model is `provider/model`, and an id persisted without its provider is rejected outright (`UnknownError`, reproduced against opencode 1.18.14). The daemon cannot invent the missing segment, so it asks the runtime's own catalog: an id that exactly one provider claims is promoted to the catalog's canonical selector, and anything uncertain — a static fallback catalog, an already-canonical id, two providers exposing the same bare id — is passed through untouched for the CLI to judge. Two reported side effects fall out of the same root cause. `thinking_level` was dropped for both runtimes because the capability lookup is keyed on the model id and missed every catalog entry; it now matches. And `starting agent` logged entry.Model, the env-var tier alone, so every task whose model came from agent.model announced itself with an empty model — the resolution now happens before the log. The task row's `model: null` is not part of this: it is read from the run's usage rows, which a run that fails before its first provider call never produces. Co-authored-by: multica-agent <[email protected]> * fix(daemon): gate selector qualification on the runtimes that can use it Review follow-up on MUL-6471. The first pass ran catalog discovery for every task that pinned a model, on the argument that the path already takes one memoized lookup for thinking_level / service_tier. That argument does not hold: cachedDiscovery deliberately skips the cache for empty and fallback results (#3729, MUL-5549), precisely so a transient failure can retry. A logged-out or failing runtime would therefore pay its full discovery ceiling — 15s for opencode/pi/omp, 30s for openclaw — on every task, including runtimes whose bare model ids could never be rewritten anyway. Qualification is now an explicit runtime capability. ModelIDsAreProviderQualified is true only where Model.Provider is literally the ID's own prefix: pi builds `provider + "/" + id`, opencode reads the provider back off the id. Runtimes carrying a bare id next to a display-name Provider — claude's "anthropic", copilot's "openai", dsh's "DeepSeek" against a `deepseek-official/...` id — never reach discovery. Built-in runtime identities resolve through their protocol family, so a pi-family fork like omp inherits the capability from its descriptor rather than a name check. The daemon side moves into qualifyTaskModel so the call boundary is testable. TestQualifyTaskModelSelectorsAndDiscoveryBoundary pins both halves: pi, opencode and omp resolve to the selector their CLI accepts, and claude, codex and an empty model never reach listModels at all. TestModelIDsAreProviderQualifiedMatchesCatalogShape keeps the capability honest by deriving the expected answer from the real catalog producers instead of restating the list — it fails if the predicate and the emitted ID shape ever disagree. Co-authored-by: multica-agent <[email protected]> * fix(daemon): read a runtime's model catalog at most once per task Second review round on MUL-6471. Two findings, one root cause: the daemon treated catalog discovery as cheap when cachedDiscovery deliberately makes it anything but — an empty or fallback result is never memoized (#3729, MUL-5549), so every read costs a logged-out or timing-out runtime its full 15-30s ceiling again. A single task could pay that twice. qualifyTaskModel read the catalog to promote the model selector, then ValidateThinkingLevel read it again to check the level — back to back, on exactly the reporter's configuration (pi or opencode plus thinking_level=high). The two now share one lazily-loaded, task-scoped catalog: resolveTaskModelSelection owns it, and the validators take that loader through ValidateThinkingLevelWith / ValidateServiceTierWith rather than reaching for ListModels themselves. The global immediate-retry contract is untouched — the reuse lives and dies with the task. The second finding removes the read entirely for pi. Qualification is not a launch requirement there: buildPiArgs already proves pi's resolver takes a canonical selector, a bare id, and an id containing a slash, so a pi task with no capability override now starts without spawning a discovery subprocess at all. ModelIDsAreProviderQualified is replaced by ModelSelectorMustBeProviderQualified, which states the contract that actually justifies the cost — the CLI refuses an unprefixed id — and is therefore true only for opencode and its DevEco fork. That also retires the catalog-shape claim the old predicate made, which deveco was already a counterexample to. The resulting matrix, pinned by TestResolveTaskModelSelectionReadsTheCatalogAtMostOnce: pi/opencode with a thinking level qualify and validate on one read; pi, omp, claude and unpinned-model tasks read nothing; opencode without an override still reads, because it cannot launch otherwise; codex without a model still fails both checks closed without reading. A failed read is not retried inside the task. Verified the count assertion bites by removing the memoization and watching the pi and opencode cases report 2 reads. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
e1be1625cb |
MUL-5959: feat(agent): add Dim (DimCode) ACP runtime (#6675)
* feat(agent): add Dim (DimCode) ACP runtime Add Dim (dimcode, the `dim` CLI) as a first-party agent runtime, driven over the ACP (Agent Client Protocol) transport via `dim acp`. Key integration points: - New dim backend (pkg/agent/dim.go): spawns `dim acp`, performs the ACP initialize/session/new handshake, and raises the runtime's hardcoded read-only permission preset to full-access (plus agent mode) via session/set_config_option before the first prompt — without it every file write is denied by a capability rule. Model override uses session/set_model; the model catalog is read from session/new. - Session resume is intentionally skipped: Dim binds sessions to the creating process, so a later process's session/load is rejected with "held by another process" even after a clean session/close. The backend always starts a fresh session and reports ResumeRejected so the daemon classifies the run correctly. A best-effort session/close keeps Dim's own session list free of orphaned entries. - Registration: SupportedTypes whitelist, New() factory, launch header, daemon probe (MULTICA_DIM_PATH / MULTICA_DIM_MODEL), default agent command name, protocol_family CHECK migration (255), model discovery, MCP config support, metrics label, and runtime display. - Docs/UI: provider logo, landing i18n (en/zh/ja/ko), providers and install-agent-runtime docs, README, CLI_AND_DAEMON, SELF_HOSTING. - Tests: dim backend unit tests (fresh session + resume behavior), shared ACP deliverable case, logo and MCP-support tests. * feat(views): use official DimCode app icon for Dim provider logo Replace the placeholder inline SVG with the official DimCode desktop client app icon (from /opt/DimAgent/resources/build/icon.png, resized to 128px), imported as a static asset the same way the Qwen Code mark is handled. * test(agent): add real dim ACP smoke tests (agentintegration) Two gated tests behind MULTICA_RUN_REAL_AGENT_SMOKE=1: - TestDimRealACPSmoke: full end-to-end against real `dim acp` — initialize, session/new, set_config_option (permission/mode), prompt, and assert completed output. - TestDimRealResumeRejected: pass a fake ResumeSessionID, assert the backend starts a fresh session (no session/load), and reports ResumeRejected=true so the daemon classifies the run correctly. Verified against dimcode 0.3.2 and 0.3.8. * fix: renumber dim migration 265 → 271 (upstream added 265-270) * fix: renumber dim migration to 272 (upstream added 271) * fix(agent): dim cross-run resume via session/load + review fixes Address review feedback on the Dim ACP runtime: - #1 Deliver runtime brief to Dim: add dim to the AGENTS.md provider list (verified dim 0.3.8+ reads AGENTS.md from the session cwd). - #2 Cross-run session continuity: dim 0.3.10+ releases its per-process session lock ~5s after the owning process exits, so resume now goes through the standard ACP session/load (same as traecli/kiro/grok) instead of always starting fresh. A loaded session retains its permission/mode, so set_config_option runs only on fresh sessions. Add a two-run regression (TestDimRealCrossRunResume) proving run B recalls context established only in run A. - #3 Process lifecycle races: the deferred cleanup now cancels the run context before cmd.Wait (a child ignoring stdin EOF no longer hangs Result delivery) with a bounded force-kill fallback; the success path waits for the final prompt notification with a grace window instead of a non-blocking read that could miss the last message. - #4 Isolate model discovery by executable path (discoveryCacheKey). - #6 Real smoke test now writes a sentinel file to prove full-access is effective; a best-effort session/close is sent before teardown. - #7 gofmt. * test(agent): add dim process-lifecycle regressions (review #3) - TestDimCleanupKillsHangingChild: a child that ignores stdin EOF and SIGTERM after an early failure is force-killed within dimProcessWaitTimeout, so Result still closes. - TestDimPromptMissingNotificationStillCompletes: when session/prompt returns without stopReason (onPromptDone never fires), the bounded final-notification wait falls through and Result completes instead of hanging. * fix(migrations): renumber dim runtime_profile migration 272 → 273 (upstream added 272) * fix(agent): review round 2 — process tree, quiescence, fail-closed, version check Address all remaining blockers from the second review: - #1 Require dim >= 0.3.10 (version check at initialize); bounded retry on session/load when the lock is not yet released instead of silently starting fresh; integration test no longer sleeps before resume. - #2 Process-tree cleanup: configureProcessGroup + signalProcessGroup + waitProcessGroupGone; cmd.Cancel returns nil so we own all signalling; regression asserts the descendant PID is gone. - #3 Notification quiescence: onActivity + waitForACPNotificationQuiescence (same as hermes); Dim equivalent of the late-final-notification test. - #4 Permission fail-closed: set_config_option runs on BOTH fresh and resumed sessions; on config failure, session/close is sent before returning so a partially configured session is not resumed; regression. - #5 Migration renumbered 273 → 274 (upstream added 273). - #6 AGENTS.md mapping regression, two-executable cache isolation test, session/close unit assertion, smoke test now executes a command. - #7 README.md + CLI_AND_DAEMON.md updated with 22 CLIs including Dim. * chore: remove temporary review actions doc (not for PR) * fix(migrations): renumber dim migration 274 → 310 (upstream added 274-309) * review: fix retry break scope, dedup isACPSessionNotFound comment, update stale notes * fix(agent): pass *exec.Cmd to signalProcessGroup/waitProcessGroupGone (upstream signature change) * fix(agent): close session on set_model failure + resume regression test Audit-found gaps from self-review: - set_model failure now sends session/close before returning (same as set_config_option failure) — reviewer #4 said 'permission, mode, or model'. - Add TestDimConfigFailThenResumeReestablishes: run A config fails → session/close sent → run B resumes and re-applies set_config_option (fail-closed), completing successfully. - Fix two stale comments that contradicted the code (config block now runs on both fresh and resumed sessions). * fix(agent): add WaitDelay, use labeled break in retry loop Self-audit improvements: - Add cmd.WaitDelay=10s for consistency with claude.go (hard backstop if a process somehow survives SIGKILL). - Use labeled break (break loadRetry) so runCtx cancellation during the retry delay exits the for loop directly, not just the select. - Update PR description: migration 273→310, set_config_option now re-applied on both fresh and resumed sessions. * fix(migrations): renumber dim migration 310 → 313 (upstream added 310-312) * fix(migrations): remove stale 313 dim migration (replaced by 314 after upstream added dsh at 313) * fix: DSH compatibility + thinking levels + reviewer round 3 Systematic fix of all 39 items from the compatibility gap analysis: Rebase + migration: - Rebase to latest upstream/main (resolves all DSH conflicts) - Migration 314_runtime_profile_add_dim (whitelist includes both dsh + dim) - Remove stale 313 dim migration DSH + Dim coexistence (provider lists, counts, docs): - SupportedTypes, config.go, metrics labels: both dsh + dim - i18n (4 files): count 23, lists include DSH + Dim + Oh-My-Pi - README.md/README.zh.md: count 23 - CLI_AND_DAEMON.md: 23-row table - environment-variables.mdx (4 langs): MULTICA_DIM_PATH/MODEL callout - display.ts, mcp-support.ts, types/agent.ts, provider-logo.tsx: both dsh+dim Thinking levels (was MISSING — dim supports thought_level): - dim.go: call applyACPEffortOption after set_model - thinking.go: add dim to acpCatalogThinkingProviders - dim.go: retain sessionResult for effort option Reviewer round 3: - Version check fail-closed: empty/malformed → reject (not allow) - Cache test through ListModels call site with fake executables - set_model failure regression test - Test companions: sidecar_manifest_test, runtime_config_test add dim Code quality: - agent.go: fix unreachable duplicate return (rebase artifact) - dim.go: labeled break in retry loop, WaitDelay, session/close on set_model fail * fix: deliverable test fake version + provider-logo rebase artifact * fix(migrations): renumber dim migration 314 → 315 (upstream added 314_workspace_mcp_config) * fix(migrations): renumber dim migration 315 → 319 (upstream added 315-318) * fix: Windows process tree, MinVersions, retry tests, migration 327 Address all 5 remaining blockers from review round 4: 1. Windows process-tree ownership: use startOwnedProcessTree instead of cmd.Start(), releaseProcessGroup in cleanup defer. This attaches the child to a Job Object on Windows (no-op on Unix), ensuring descendants are captured and terminated. 2. Migration renumbered 319 → 327 (upstream added 319-326). 3. Add dim: 0.3.10 to MinVersions in version.go so the daemon registers old Dim binaries as offline and refuses triggers (defense in depth on top of the ACP agentInfo.version check in dim.go). 4. Session-lock retry contract tests: success after bounded retries, exhaustion without falling through to session/new. Fake script supports DIM_LOAD_HELD_N (held for N calls then succeed) and DIM_LOAD_HELD_ALWAYS (always held). 5. README.md:206 20→23 runtimes. PR description updated. * test: add missing reviewer-requested regressions (round 4) - Windows Job Object regression: dim_windows_test.go proves the Dim backend captures and terminates descendants via startOwnedProcessTree (Windows-only build tag; companion to TestStartOwnedProcessTree). - MinVersions registration: add dim test cases to TestCheckMinVersion (0.3.10 ok, 0.3.9 rejected, invalid rejected). - Retry cancellation: TestDimSessionLoadRetryCancelled cancels the context during the retry delay, asserts no session/new fallback. * fix: sync upstream Command signature changes + restore dim additions Upstream changed ListModels/discoverXxxModels/detectCLIVersion to accept Command (struct with Path+Prefix) instead of string. The previous rebase kept our old-signature versions, causing widespread compile failures. Restored all affected files from upstream, then re-applied dim-specific additions: - agent.go: dim in SupportedTypes, New(), launchHeaders - hermes.go: isACPHeldByProcess + -32002 in isACPSessionNotFound - thinking.go: dim in acpCatalogThinkingProviders - models.go: dim case in ListModels + discoverDimModels - version.go: dim in MinVersions - All dim test files preserved (dim_test.go, dim_integration_test.go, dim_windows_test.go, models_test.go dim cache test) * fix: rebase to latest main, migration 327→341, resolve DetectVersion conflict * fix: remove unused os/exec import in dim_windows_test.go * fix(migrations): renumber dim migration 341 → 342 (upstream added 341) * fix: use Command.exec instead of exec.CommandContext (upstream GH #7046) Upstream's TestOnlyLaunchGoSpawnsRuntimeProcesses requires all backends to build processes through Command.exec (launch.go), so a custom runtime's fixed_args are carried into the subprocess. Dim was the only backend still using exec.CommandContext directly. * fix: launch prefix policy + remove empty test files + deterministic retry tests Address all 4 findings from review round 5: 1. Add "dim": dimBlockedArgs to launchPrefixBlockedArgs so protocol-breaking flags (--help/--auth-setup/--remote) are filtered from fixed_args. 2. Remove 77 zero-byte *_test.go files accidentally added at repo root. 3. Make retry tests deterministic: TestDimSessionLoadRetryCancelled waits for the first session/load request before cancelling (no fixed sleep); TestDimSessionLoadRetryExhausted asserts exactly 4 load attempts. 4. PR description will be updated separately. * fix: rebase to latest main, migration 342→343 (upstream added mcode at 342) * chore: trigger CI * chore: refresh PR * fix: restore mcode that was lost during merge conflict resolution Merge took 'ours' side which predated mcode. Restore mcode in: - SupportedTypes, New() factory, launchHeaders - agent_supported_types_test.go want map - metrics/labels.go - config.go defaultAgentCommandNames - migration 343 up/down whitelists * fix: restore mcode probe in agents_probe.go + agent-cli-command-names.txt * fix: restore all remaining mcode references lost during merge Files fixed: - README.md: mcode row in runtimes table - mcp-support.test.ts: mcode assertion - config.go: mcode in Agents comment + error message - runtime_config.go: mcode in AGENTS.md case - version.go: mcode in MinVersions - version_test.go: mcode test cases Verified: no file has fewer mcode references than upstream. * fix: restore mcode in environment-variables docs + CLI_AND_DAEMON ACP list - environment-variables.{mdx,ja,ko,zh}: restored 'MiniMax Code' in the QwenPaw model-variable sentence - CLI_AND_DAEMON.md: added MiniMax Code to ACP-family list + loadSession fallback description Verified: every changed file now has >= mcode references vs upstream. * fix(migrations): renumber dim migration 343 → 344 (upstream added 343) * fix(migrations): renumber dim migration 344 → 348 (upstream added 344-347 plugin migrations) * fix(migrations): update down migration comment 344 → 348 * chore: retrigger CI (flaky TestOpenclawDiscoveryCacheConcurrentPreparations — upstream test, passes locally x5) * fix(migrations): renumber dim migration 348 → 352 (upstream added 348-351) * fix: Dim launch-prefix regression test + version.go comment repair (review #6) - Add TestDimLaunchPrefixFiltersBlockedFlags: proves allowed prefix reaches command before acp, and --help/--auth-setup/--remote/-h are stripped from Dim's launch prefix. - Add dim to TestLaunchPrefixReachesACPFamilies family list. - Repair version.go: dim's cross-run session/load comment was appended to mcode entry during a rebase conflict; restore two independent accurate comments. * fix(migrations): update down migration comment 348 → 352 * chore: retrigger CI (backend-tests stuck 50+ min) * fix(migrations): renumber dim migration 352 → 362 (upstream added 352-361) * fix: use logAgentCommand for dim argv logging (upstream redaction requirement) Upstream's TestOnlyLaunchGoLogsAgentCommandArgs enforces that all runtime argv logging goes through Config.logAgentCommand so sensitive args are redacted. dim.go was still using Logger.Info directly. * fix(migrations): renumber dim migration to 370 Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
3c3e77b00c |
MUL-6362: redact secrets from provider command logs (#7206)
* fix(agent): redact provider command values Co-authored-by: multica-agent <[email protected]> * fix(agent): preserve safe command diagnostics Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
4040345e09 |
perf(ci): path-filter the backend jobs to skip irrelevant PRs (MUL-6389) (#7198)
Adds a `backend` path filter to the `changes` job and gates backend-tests, go-vulnerability-scan and windows-execenv on it, so a PR that never touches server/ no longer pays for a `go test -race` run, govulncheck and a Windows runner. The filter is deliberately wider than `server/**`: four Go tests read three files outside the module (packages/core/runtimes/cli-version.ts, scripts/agent-cli-command-names.txt, scripts/go-test-with-agent-cli-guard.sh), and each must be listed or the PR that breaks such a contract is exactly the PR that skips the test. Steps are gated rather than jobs, matching the existing frontend/sqlc-check pattern: the aggregate check reports a genuine green instead of staying pending, and still fails when a job goes missing for a reason other than the path filter. |
||
|
|
5f4ba41d13 |
MUL-6323 fix(views): stop blaming the daemon when the backend is the stale half (#7126)
* fix(views): stop blaming the daemon when the backend is the stale half (MUL-6323) A self-hosted user on macOS Desktop could not pick worktree mode, and the dialog explained why: "the Multica on that machine is v0.4.28; parallel needs 0.4.24 or newer". v0.4.28 was the newest release at the time. There was no version left to upgrade to (#7113). Nothing has gated on a version since MUL-5707 moved all three gates onto the declared local-worktree-v1 capability. The floor survived only as the number this copy interpolated, so every capability miss — whatever caused it — was reported as a version shortfall the user had already cleared. Underneath was a second problem the copy could not express. The capability is recorded by the SERVER at registration, and servers before v0.4.25 write runtime metadata with no capabilities key at all. Desktop ships its own renderer AND its own daemon and updates both together, so the client routinely runs ahead of a backend the operator upgrades by hand: the daemon advertises the capability, the old backend drops it, and the UI blames the machine that was already newest. Upgrading it changes nothing. So the gate now answers with LocalWorktreeSupport instead of a boolean, and the discriminator is the PRESENCE of the capabilities key rather than its contents — a capability-aware server always writes it, null when the daemon sent no header, so a row without it can only have come from an older server. The two negatives get their own copy pointing at the half that can actually be fixed, and no message quotes a daemon version any more; the server's own 422 was rewritten the same way in #6904. Also records capabilities on runtime-profile registration failure rows. Those are written after the successful runtimes of the same request, so until the first heartbeat they are the newest row for the daemon — which is the row every worktree gate reads. A failed custom profile briefly made a capable daemon look incapable, and would now have made a current backend look stale. Removes localWorktreeSupported and MIN_LOCAL_WORKTREE_CLI_VERSION from the frontend: the first had no callers left, the second only fed the copy this change deletes. Co-authored-by: multica-agent <[email protected]> * fix(views): ask the live backend whether it records capabilities (MUL-6323) Review follow-up. The first pass read a missing `capabilities` key as "this backend predates the handshake", but the key's absence only proves the server was old WHEN THE ROW WAS WRITTEN. Upgrading the backend does not rewrite runtime metadata and heartbeats touch only last_seen_at, so an upgraded deployment would keep being told it was too old — and upgrading it again would change nothing. The daemon does not re-register on a timer, so that state can sit there indefinitely. The question is now asked of the server actually in front of us, via /api/config's server_version, and the stale-row case gets its own answer: both halves are fine, the machine just has to register again. An ABSENT version reads as capability-aware, which is the load-bearing default — /api/config omits the field on the managed cloud by design and leaves it empty for unstamped dev builds, while the field itself shipped in v0.4.2, 23 patches before the handshake. So every self-hosted release that could genuinely be blind still names itself, and cloud users are never told to upgrade a backend they do not run. Replaces the cross-side version-floor test rather than deleting the contract. It pinned MIN_LOCAL_WORKTREE_CLI_VERSION against a frontend twin that this branch removed, and the number it guarded is one the UI no longer shows. What the two sides must still agree on is the capability token: the daemon advertises that exact string, the server stores it, the UI looks for it, and a typo on either side disables worktree mode for everyone with no error anywhere. Also corrects a comment that claimed no version numbers appear in this copy — the backend floor does, deliberately, because it is what an operator upgrades past. Co-authored-by: multica-agent <[email protected]> * fix(views): stop assuming an unnamed backend records capabilities (MUL-6323) Review follow-up. Reading an absent server_version as "capability-aware" was right for two of its three sources and wrong for the third: a self-hosted server older than v0.4.2 — where the field itself shipped — reports no version AND predates the handshake, so it records no capabilities either. Such a deployment got told to restart Multica on the machine, which re-registered against the same blind server and wrote the same key-less row. A loop whose only real exit, upgrading the backend, the copy never mentioned. So awareness is three-valued now, and an unnamed server is resolved from evidence rather than assumption: if ANY runtime row in the workspace carries a capabilities key, some capability-aware server wrote it and servers move forward, so the deployment is aware whoever the row belongs to. That settles the managed-cloud shape — no version reported, several machines, one of them registered before the handshake — without a version to read. The converse is never inferred; every row missing the key is equally what an old self-hosted server and a one-machine workspace look like. When nothing identifies the backend the UI says so and names both remedies, still failing closed. Guessing wrong in either direction sends someone to upgrade a backend they do not run, or to restart a machine that was never the problem. Also rewrites the re-register copy, which explained itself in the vocabulary of the handshake rather than the user's. Co-authored-by: multica-agent <[email protected]> * refactor(views): let the server answer whether a machine can run worktree (MUL-6323) The client-side gate kept growing because it was trying to predict an answer the server already gives. Each round added a state: capability present or absent, then the backend's own version to interpret the absence, then evidence from other rows to interpret an unnamed backend. Three layers of inference over data the backend wrote, to avoid asking the backend. So it asks. The UI no longer blocks worktree mode on capability at all — only on `not_git`, which is local, certain and already checked by the picker. The mode stays selectable, the save is validated server-side, and the rejection lands in the dialog's existing errorMessage slot with the wording #6904 wrote for it. That deletes the awareness tri-state, the workspace-wide evidence scan, the server-version plumbing, three UI reasons and four copy strings in four languages. This also fixes the reported case outright rather than explaining it: a self-hosted backend older than the handshake runs the version gate it shipped with, which accepts a v0.4.28 daemon, so the user simply gets worktree mode instead of an instruction to upgrade something. The capability signal survives for one job — deciding whether to PRESELECT parallel mode — where being wrong costs a radio button rather than a blocked user. A machine that has not advertised is not preselected into a mode whose rejection would fail the whole project creation, but it stays selectable, because an old row is not proof the machine cannot. For any of that to be true the bundled create path had to actually ask: POST /api/projects attaches resources in one transaction and never ran the worktree gate that POST/PUT /resources have always run, so it would happily store a mode the machine cannot run and leave the claim gate to cancel the task later. It runs the same gate now, before the transaction opens. Co-authored-by: multica-agent <[email protected]> * fix: refuse worktree mode until the server says it will honour it (MUL-6323) Review caught a safety regression in the previous commit, and it is the worse half of this issue rather than a variant of it. Deferring to the server assumed the server would object. Releases before v0.4.25 do not: `validateLocalDirectoryRef` unmarshals the ref into a struct with no ExecutionMode field and re-marshals it, so `execution_mode: "worktree"` is silently DROPPED and answered 201. The daemon then reads a ref with no mode, treats that as in_place by design, and edits the working copy the user asked it to stay out of. No save gate, no claim gate, no error — on the exact new-Desktop-old-backend pairing this issue is about. My earlier claim that such a server would run its own version gate was wrong: worktree shipped in v0.4.25, so v0.4.24 has no concept of the mode at all. The client cannot tell that success from a real one, so it asks first. /api/config now carries local_worktree_supported, a property of the build: if this code is running, the save gate is running with it. Absent — which is what every too-old server sends — blocks the mode with copy naming the server, and the schema parses the missing key to false rather than undefined so a consumer cannot forget to coerce it. That keeps the simplification: one declared boolean from the live server, no inference over rows or version strings, and per-machine capability still left to the server's save-time gate. Also clears review nits: a comment still naming the removed localWorktreeSupport and stray blank runs in two test files. Co-authored-by: multica-agent <[email protected]> * fix(views): rename a local directory without resending its ref (MUL-6323) Review follow-up, and the same failure through a door the last commit left open. The client now refuses worktree mode when the server cannot promise to honour it — but renaming an EXISTING worktree resource sent the whole resource_ref back, capability check or not. The server replaces the ref with whatever it can parse. On a backend rolled back below v0.4.25 — documented as supported while the runtimes stay current — that means execution_mode is dropped and answered 200, and the next task edits the working copy the resource asked to isolate. The user changed a display name; nothing told them isolation went with it. So rename stops sending the ref at all. The update endpoint has always treated an omitted resource_ref as "keep the stored one", including in v0.4.24, so the mode survives on every server version and no capability check is needed for a rename. It also closes the general shape of this: any unrelated edit that resends a ref gets it re-normalised by whatever the server understands today. The label therefore moves to the top-level column, which is what the API already offered for exactly this, and display now reads that first and the ref's copy second — so rows created before this keep the name they were given until someone renames them. Also corrects wording review flagged as misleading: the config flag is absent on every server that predates the SIGNAL, not only on those that predate the fix. Releases between the two do gate the save but cannot say so, and are treated as unable — the client cannot tell them apart, and only one of the two guesses is safe. Co-authored-by: multica-agent <[email protected]> * fix(server): converge the two homes of a local directory's display name (MUL-6323) Review follow-up. Renaming stopped resending the ref so old servers cannot strip execution_mode — but that left the name with two writable homes: the top-level label column (what newer clients write) and the copy inside resource_ref (what desktop <= v0.4.28 both reads first and writes renames into). One row could show different names on different devices, permanently, and clearing a name resurrected the ref copy. The server is the only writer that sees both client generations, so it now converges the copies on every local_directory write: - a label-only rename patches the ref's copy too — as a raw-map edit around the other keys, so execution_mode and fields written by a NEWER server survive byte-for-byte (re-marshaling through this binary's struct is the exact silent-drop this PR exists to close); - a resent ref whose embedded label CHANGED is the legacy rename shape, and the column follows it; an unchanged embedded label (the mode dialog spreads the stored ref back) never overwrites a column rename — the write heals the row toward the column instead; - clearing removes both copies, while a column that was merely never set keeps its hands off the ref: for rows created by older clients the ref copy IS the name. The display-order helper moves to its own pure module shared by the resources section and the chat hint (review nit), with the read-order matrix pinned in a node-environment test beside it. Co-authored-by: multica-agent <[email protected]> * fix(server): tell a rename apart from a ref that went stale (MUL-6323) Review follow-up. Convergence read "the ref arrived with a different label" as "an old client renamed this", but that is also what a stale snapshot looks like. The execution-mode dialog captures the whole ref when it OPENS and sends it back on save, so: device A opens the dialog, device B renames the folder, device A saves a mode — and the server writes B's rename back to the old name, from a dialog that has nothing to do with naming. Only a ref whose SOLE difference is its label counts as a rename now. Anything else is an edit carrying a name it happens to have been holding, so the row keeps whatever it is called today — in both homes, which also heals the stale copy the request brought. Compared as decoded values, and unknown keys count: a difference this binary cannot interpret is still a difference, and reading such a request as a pure rename would be a guess. The same classification fixes a second complaint from the other direction. An old client's rename resends the ref, so the worktree capability gate refused it whenever the machine's runtime registration had drifted — failing an edit that changes nothing about what runs, which is exactly what that gate's comment already promised label-only updates would be spared. A rename skips it; the row already says worktree, and the claim gate is what keeps it from running somewhere that cannot. TestProjectResourceUpdateLifecycle pinned the old reading for a request that changes path, daemon and label at once. No client sends that shape — renames change only the label, the mode dialog changes only the mode — and honouring an embedded label there is indistinguishable from honouring a stale one, so it now pins the conservative answer. Callers that mean to rename have the top-level label field. Also softens a claim review flagged as overstated: the label patch preserves every unknown key and value, not the raw bytes, which JSONB does not promise either. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
c175530284 |
MUL-5879: fix(agent): surface every in-flight built-in Hermes tool call (#7127)
* fix(agent): emit ACP tool use when the start frame carries the command (MUL-5879) Hermes' tool_call start frame carries the whole invocation in content but no rawInput, which fell through to the kimi deferral path: nothing was emitted until tool_call_update completed. An in-flight Hermes tool was therefore invisible in run-messages, so a stalled tool rendered as a blank run with no visible tool call at all (GH#6583). It also kept the daemon's in-flight tool counter at zero, since that only advances on MessageToolUse, so Hermes tool calls were never charged to AgentToolWatchdog and were judged by the much shorter AgentIdleWatchdog. Emit at the start frame when its content already holds complete args. The discriminator is the shape of the text rather than the backend, because nine ACP backends share this client: only an unterminated JSON object or array can be a mid-stream kimi fragment. Rendering is unchanged - the same text produces the same Input map the deferred path built at completion - only the timing moves. Co-authored-by: multica-agent <[email protected]> * fix(agent): narrow early ACP tool-use emit to the terminal invocation Review of the first commit was right: content-shape was the wrong discriminator. In ACP, `content` is display output produced by the tool call and `rawInput` is the input, and a later update may still supply the real rawInput - so treating any complete non-JSON content as the invocation mis-records it and freezes the wrong value. Hermes makes this concrete: it suppresses rawInput for its whole polished tool set and renders each one differently. `terminal` renders `$ <command>` (the invocation), but `write_file` and `patch` render prose like "Preparing write to <path>", and the browser/web/media tools render their own summaries. Only the terminal rendering is the command. Recognise exactly that shape and defer everything else, so the nine backends sharing this client keep their previous behaviour. Also let a rawInput arriving on the update win over buffered start-frame text, which is what makes the deferred path record the real input rather than the rendering. Co-authored-by: multica-agent <[email protected]> * fix(agent): surface every in-flight Hermes tool, not just terminal Second review was right that the previous commit only fixed the terminal subset. Hermes omits rawInput for its whole polished tool set (~40 tools: write_file, patch, read_file, execute_code, web_*, browser_*, ...), so those calls still stayed invisible while running and still never advanced the daemon's in-flight tool counter - leaving long browser/code runs on the 30-minute idle budget instead of the 2-hour tool budget. Gate the early emit on a dialect flag instead of the frame's content. Hermes sets it because its start frame is the only place input ever appears: build_tool_call attaches rawInput for ordinary tools and omits it for polished ones, and build_tool_complete never sends rawInput at all, so waiting cannot yield more input. The other eight backends sharing this client leave the flag false and keep deferring - Kimi genuinely does stream its args across updates. Content is still not passed off as input: only the terminal command rendering becomes Input, and every other polished tool reports none. Co-authored-by: multica-agent <[email protected]> * fix(agent): scope the Hermes tool-start dialect to the built-in binary The hermes backend also serves custom runtime profiles declaring protocol_family: hermes (jcode is the documented example), so switching toolStartCarriesFinalInput on unconditionally applied a Hermes-Agent-only observation to unrelated implementations. A custom runtime whose start frame has no rawInput and whose later update supplies the real one would have had Input=nil recorded at start with emitted=true, and emitDeferredToolUse would then drop the real input on completion. Scope it to Config.BuiltinRuntime, the same signal acpToleratesOmittedMcpCapabilities already uses to fence a vendor-verified exception, so an unset caller fails closed onto the deferred path. Cover it at the construction layer rather than with a hand-built client, since the leak was in how the client is built: one turn driven through hermesBackend.Execute against an ACP stub whose start frame carries display text only and whose completion frame carries the real rawInput, asserted both ways round. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
0b9dcc6799 |
fix(agent): preserve Pi RPC stdin on Windows (#7111)
Windows Pi model discovery reused the task-execution launcher rewrite to `powershell -File pi.ps1`. That path suits normal JSON/task execution, where the prompt is written to stdin and stdin is then closed. Pi RPC model discovery instead needs a bidirectional session and sends requests while keeping stdin open, so npm PowerShell wrappers that wait for stdin EOF made discovery time out and fall back to the legacy model table without thinking metadata. - Route Windows Pi RPC invocations through `powershell -Command "& '<pi.ps1>' @args"` so the wrapper receives forwarded args while stdin stays live. - Keep ordinary Pi task execution on the existing `-File` path. - Add a Windows launcher argv regression test plus an end-to-end test that drives a real PowerShell host and asserts the RPC responses arrive before stdin EOF, verified on both Windows PowerShell 5.1 and pwsh in `windows-execenv`. Fixes #7108 |
||
|
|
de902a4aab |
MUL-6273: feat(agent): add MiniMax Code ACP runtime (#7035)
* feat(agent): add MiniMax Code ACP runtime * fix(agent): terminate mcode process trees * fix(agent): address mcode review blockers * test(agent): keep mcode model contract API-stable * fix(migrations): resolve mcode prefix collision * fix(agent): launch mcode through runtime command Assisted-by: MiniMax Code * fix(migrations): renumber mcode migration Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
4383497898 |
fix(agent): fail a Pi task when Pi ends the turn on an error without retrying (#7059)
Pi reports a provider call it could not complete by ending the turn with stopReason=error and the provider text in errorMessage. When it also decides the error is not worth retrying — 401, 429 for an exhausted quota — it emits neither a standalone error event nor auto_retry_end, and exits 0. Those two events were the only failure signals the adapter recognized, and piMessage discarded stopReason at decode time, so the run was reported completed with empty output and no error. The task was never retried and nothing alerted; a production task closed in 6s while the gateway was returning 429. Decode the two fields and remember the error of the turn that just ended. Pi emits the same stopReason before an automatic retry, so this is cleared on turn_start and only a trailing failed turn survives to fail the run — a retry that recovers still reports completed. Co-authored-by: damon.zhou <[email protected]> |
||
|
|
a17857e638 |
fix(agent): parse tab-separated Antigravity model catalog (#6654)
Fixes #6562. Related Multica issue: MUL-5856. |
||
|
|
3b08423684 |
MUL-6260 fix(daemon): treat custom runtime fixed_args as the argv prefix (#7052)
* fix(daemon): treat custom runtime fixed_args as the argv prefix (MUL-6260) A custom runtime profile configured as `ccms start q36` failed every task with `unknown option '-p'`. The profile's fixed_args were merged into ExecOptions.ExtraArgs, which each backend appends *after* the protocol flags it hardcodes, so the wrapper saw `-p` before its own `start` subcommand had been selected and rejected it (GH #7046). Ordering was only half the defect. ExtraArgs is opt-in — six of the twenty-one backends read it — so on the other fifteen families, including every ACP one, fixed_args were silently discarded rather than misordered. Model discovery and `--version` probing carried the profile's path but not its arguments, so even a fixed launch would have left the model picker and the reported version describing the wrapper instead of the CLI behind it. fixed_args now travel as agent.Config.LaunchPrefix and are spliced in directly after the executable, ahead of everything a backend builds. The prefix is applied by a new Command boundary in launch.go through which every runtime process in the package is constructed — launches, version probes and discovery alike — so a backend cannot forget it. A guard test fails the build if any file outside launch.go calls os/exec directly; distributed opt-in is what let ExtraArgs rot, and repeating it would rot the same way. Flag-style commands (`agent --model composer-2.5`) are unaffected: a CLI takes its own flags in any position, which is why prefix-first can be the single order rather than a per-runtime setting. One deliberate behaviour change. The prefix used to sit last and win every last-wins parse, so a profile pinning `--model` silently overrode the model chosen on the agent — the UI showed one model and ran another. Prefix-first inverts that: the more specific per-agent choice is the later token and wins. Overlaps are logged. Protocol-critical flags are filtered out of the prefix once in New(), which knows the family; positional tokens always pass through. Codex additionally strips `-c` overrides aimed at daemon-managed config namespaces, since those beat config.toml from any argv position. The order was never documented, and every shipped fixed_args example is flag-style, so this fills in a contract rather than breaking one. Co-authored-by: multica-agent <[email protected]> * fix(daemon): close the launch-prefix gaps found in review (MUL-6260) Four defects from the first round of review on the fixed_args-as-argv-prefix change. Hermes could escape its own task overlay. The profile selection was resolved from custom_args alone and stripped from custom_args alone, so a profile configured as `hermes -p research` kept that flag in the launch prefix and re-pointed HERMES_HOME past the overlay the daemon had just built. Resolution now parses the argv hermes actually sees — launch prefix then custom args, in process order — and the strip covers both regions. It also removes every selection rather than the first: taking out the first promotes the second, and two separately-configured argv regions make "two selections" an ordinary configuration rather than a user mistake. Positional tokens were not passing through as documented. filterLaunchPrefix delegated to filterCustomArgs, whose blocklists deliberately carry positional tokens — hermes and qwenpaw block `acp`, traecli blocks `acp`/`serve`, grok blocks `agent`/`stdio`/`serve` — because in custom_args those are someone re-issuing the backend's own subcommand. In a launch prefix the same token is the command's identity, so `hermes-wrapper acp tenant` was being rewritten to `tenant`. Only a leading `-` now makes a token eligible for the blocklist; a blocked flag still consumes its separate value so no stray value survives to be read as a subcommand. Codex fast mode lost to the prefix. `--disable fast_mode` beats `--enable fast_mode` regardless of argv order and `-c features.fast_mode=false` beats config.toml from any position, so prefix-first does not protect an explicitly selected priority tier. The removal half of enforceCodexFastMode is now a separate helper applied to the prefix too; the managed enable is still appended once, on the final args. Windows CI could not compile the daemon tests: the Command migration left an unused `path := runtimeCmd.Path` in a hook whose body never referenced it. Verified with GOOS=windows and GOOS=linux vet, which is what should have caught it the first time. Also softens the docs and the Command doc comment: flag-style prefixes usually parse the same in either position, but a CLI that separates global from subcommand flags can care, so the runtime docs now name the position change and tell operators with existing fixed_args to re-run one task after upgrading instead of promising the move is invisible. Co-authored-by: multica-agent <[email protected]> * fix(daemon): resolve and strip the Hermes profile from the real launch argv (MUL-6260) Second-round review found the Hermes profile chain still reasoning about a hand-assembled approximation of the command line instead of the command line itself. The backend launches `<prefix> acp <filtered custom args>`, but the daemon assembled `<prefix> <custom args>` — no `acp`, no filtering — and that token participates in Hermes' own argv scan. With fixed_args `--model` and custom_args `-p research`, the approximation reads `-p` as `--model`'s value and finds no selection, so the overlay is seeded from the default home; the real `--model acp -p research` skips `--model acp` and selects `research`. The selector was then stripped, so the task stayed inside the overlay — but an overlay built from the wrong profile, silently. Stripping had the matching defect, and this one was still an escape rather than a mismatch. A launch prefix ending in a bare `-p` captures the backend's `acp` as its profile value, so neither region holds a complete selection and per-region stripping removes nothing: `-p acp research --yolo` reaches Hermes intact and re-points HERMES_HOME straight out of the overlay. Both halves now go through one assembly. agent.HermesLaunchArgv builds exactly what hermesBackend.Execute builds — the backend calls the same helper, so they cannot drift — and the daemon resolves from it. StripHermesProfileSelectors strips against that same argv and hands surviving tokens back to the region they came from, iterating to a fixed point because removing one selection promotes the next. The `acp` token is never removed; the backend re-adds it, and dropping the flag that captured it is what breaks the selection. StripAllHermesProfileArgs, hermesLaunchArgs and hermesEffectiveArgs are gone: each was only ever correct on a complete argv, and keeping them would leave the same footgun for the next caller. Tests cover the case the review named (prefix ending in a value-taking flag, custom args opening with a selector), the straddling-selection escape, multiple occurrences across both regions, and an invalid value left untouched because Hermes discards it anyway. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
4d495056e8 |
[MUL-6139] Expand Plugin skills and hosted MCP connections (#6920)
* feat(plugins): add remote MCP runtime Co-authored-by: multica-agent <[email protected]> * chore(plugins): add private Exa example Co-authored-by: multica-agent <[email protected]> * docs(plugins): remove private plugin guide Co-authored-by: multica-agent <[email protected]> * fix(plugins): stabilize remote MCP tool digests Co-authored-by: multica-agent <[email protected]> * fix(plugins): preserve remote MCP notification status Co-authored-by: multica-agent <[email protected]> * fix(plugins): make Exa example runnable end to end Co-authored-by: multica-agent <[email protected]> * fix(codex): record MCP tool calls in agent log Co-authored-by: multica-agent <[email protected]> * chore(plugins): add community skills example Co-authored-by: multica-agent <[email protected]> * feat(plugins): support multi-file skill bundles Co-authored-by: multica-agent <[email protected]> * feat(plugins): add Mobbin MCP example Co-authored-by: multica-agent <[email protected]> * chore(plugins): bundle full Matt Pocock skill set Co-authored-by: multica-agent <[email protected]> * feat(plugins): add remote MCP OAuth connections Co-authored-by: multica-agent <[email protected]> * fix(plugins): gate enable on remote MCP readiness Co-authored-by: multica-agent <[email protected]> * fix(plugin): authenticate remote mcp credential broker Co-authored-by: multica-agent <[email protected]> * fix(plugins): harden remote mcp oauth flow Co-authored-by: multica-agent <[email protected]> * refactor(plugins): use a single feature flag Co-authored-by: multica-agent <[email protected]> * fix(plugins): clean up remote MCP OAuth state Co-authored-by: multica-agent <[email protected]> * chore(plugins): remove Matt Pocock example Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
b30a3ae55c |
MUL-6164 feat(runtime): take an unusable agent CLI offline and refuse its triggers with the fix (#6966)
* fix(daemon): take a runtime offline when its CLI cannot be executed (MUL-6164) A CLI that cannot exec was classified as a transient probe failure, so the runtime stayed online and every task it claimed died at fork/exec. The user in MUL-6164 hit the same error once per task with no signal that the runtime was the problem — an npm placeholder stub passes exec.LookPath, so nothing else the daemon checks can tell it apart from a working install. ENOEXEC (and its Windows spelling, ERROR_BAD_EXE_FORMAT) is a fact about the bytes on disk, not a hiccup: it will reproduce until someone reinstalls. Give it its own probe verdict and route it down the track below-minimum already uses — claim barrier, seq-stamped hold, deregistration — so the provider goes offline with its reason instead of collecting failed tasks. Recovery needs no new machinery: a demoted provider has no runtime, which puts it back in providersMissingRuntimes, and the first healthy probe clears the hold and lets converge register it again, with no restart. One thing the below-minimum track did not need: a confirmation window. The repair we tell users to run overwrites the bin entry in place, so a probe landing mid-copy sees a truncated file. Demotion therefore requires two sightings a window apart; a genuinely broken install pays one extra probe round, and a spurious verdict would in any case recover on its own. detectBuiltinRuntimes' second return value is now "providers condemned this round, with the evidence against them" rather than "below-minimum versions" — same shape, both kinds, and the value is the reason /health already shows. Co-authored-by: multica-agent <[email protected]> * feat(runtime): refuse triggers for an unusable agent CLI instead of queueing them silently (MUL-6164) Taking the runtime offline stopped the per-task failures, but it replaced them with silence: the next @mention or assignment queued as if the machine were merely asleep, and nothing ran for two hours until queued_expired. Waiting only makes sense when waiting is a plan — a sleeping laptop comes back on its own; a CLI that cannot be executed does not. dispatch/reason.go already draws exactly that line between runtime_offline ("bring the machine back, queued work waits") and agent_runtime_required ("nothing will ever claim this"), and warns that collapsing the two sends users to fix the wrong thing. This adds the third member of that family, runtime_unusable, and routes the new verdict to it end to end: - The daemon reports WHY on deregistration — a stable code plus the repair command read from the package's own manifest, since only that side knows the path. Stored on agent_runtime.metadata, which registration overwrites wholesale, so a repaired runtime drops the explanation structurally. - AgentReadiness now returns a verdict (available / waitable / blocked) instead of a bool plus prose, and the direct-agent trigger paths start consulting it — blocking ONLY on the blocked verdict, so an offline machine still queues. agentReadinessReasonCode, which re-derived the same code from agent state, is gone: one source, no drift. - Every path that refuses without a human watching the response leaves the reason on the issue: an assignment, or another agent's @mention. The interactive surfaces already carry the code back to the composer, which now localizes it in all four languages. Below-minimum keeps today's behaviour deliberately. It is equally futile to wait on, but changing when THAT refuses a trigger is a separate product decision. Co-authored-by: multica-agent <[email protected]> * fix(runtime): keep the unusable verdict through a late register, and repair Windows (MUL-6164 review) Two ways the refusal path failed to reach the user. The verdict could be erased by an older answer. A register whose request was still in flight across the demotion upserts the runtime row afterwards, and that upsert overwrites metadata wholesale — including the reason just stored. The local hold refuses the response correctly, but the cleanup that took the revived row offline again sent no reason, so the server ended up "offline, cause unknown". Every admission path reads that as "the machine will come back" and queues, which is exactly the behaviour this change exists to remove. The demotion record now keeps the structured cause and the revived-row cleanup re-attaches it per row. The regression test drives the real interleave — a register held inside the server handler across the verdict — and fails without the fix. The repair command was POSIX-only. Windows had just gained detection (ERROR_BAD_EXE_FORMAT), so a Windows user was correctly refused and then handed `cd 'C:\...' && node install.cjs`: cmd.exe treats the quotes literally and PowerShell 5.1 has no `&&`. The command is now rendered for the shell of the machine that reported it — Set-Location on two lines for PowerShell, with PowerShell's own quote escaping — and carries that shell so the notice fences it truthfully instead of labelling everything bash. Also corrected the notice's own docs: it is written for every author, not only unattended ones. The chip and toast carry the reason code but not the repair command, so a human needs the durable copy too. The behaviour was already this; the comment claimed otherwise. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
f51ee62b53 |
MUL-6164 fix(agent): diagnose an unrunnable agent CLI instead of leaking "exec format error" (#6963)
* fix(agent): diagnose an unrunnable agent CLI instead of leaking "exec format error" (MUL-6164) npm-distributed agent CLIs ship a placeholder text stub at their bin entry point and rely on a postinstall script to overwrite it with the platform-native binary. npm 12 blocks install scripts unless allowlisted, pnpm 10 needs `pnpm approve-builds`, and --ignore-scripts / --omit=optional do the same — all of them leave the stub in place with its executable bit set. exec.LookPath accepts it, execve rejects it, so the CLI reads as installed everywhere the daemon looks and only fails at launch with a bare "start claude: fork/exec <path>: exec format error". Add ExplainExecError: on ENOEXEC it names the path, the owning npm package, and the exact repair command read out of that package's own scripts.postinstall. The executable path is recovered from the error itself, so this is one provider-agnostic call at each boundary — the shared version probe and the daemon's single backend.Execute site — rather than a wrap in every backend. claude and opencode ship this layout today; a CLI added later is covered without a code change. Also classify "exec format error" as runtime_missing_executable: a stub that cannot exec means this host has no usable CLI, and that reason already tells the daemon a fresh-session retry cannot help. Co-authored-by: multica-agent <[email protected]> * fix(agent): keep the ENOEXEC diagnosis to one copy (MUL-6164 review) The openclaw preflight explained the error, then returned it through openclawBackend.Execute into the daemon's launch boundary, which explained it again — %w keeps ENOEXEC reachable, so the second pass fired and the user got the same paragraph twice. Drop the openclaw wrap: its error already travels through the daemon boundary, so the extra site bought nothing. Make ExplainExecError idempotent as well, so the next nested boundary cannot reintroduce the same regression silently. Also quote the package root properly in the repair command — it is meant to be pasted into a shell, and a path holding a quote or a space has to survive that. The chat copy for runtime_missing_executable said the CLI "couldn't be found / check it's on PATH", which is the opposite of this failure: the file is right there on PATH and cannot run. Reworded to cover both across all four locales; the compact labels ("Runner CLI not installed") are left for the follow-up that reworks the reason's presentation. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
3025c38027 |
MUL-6019: fix(codex): add MULTICA_CODEX_FIRST_TURN_TIMEOUT to raise the first-turn ceiling (#6753)
* fix(codex): add MULTICA_CODEX_FIRST_TURN_TIMEOUT to raise the first-turn ceiling The first-turn no-progress watchdog caps at the 60s default that the configured semantic-inactivity timeout can only shrink, never raise. An app-server that is legitimately slow to its first event (heavy MCP boot, cold model catalog) is killed at 60s with no way to extend it. Add a dedicated MULTICA_CODEX_FIRST_TURN_TIMEOUT knob (default 0 = unset) threaded through ExecOptions to codexFirstTurnNoProgressTimeout, which now honors an explicit value upward and is otherwise byte-identical. Default behaviour and the clamp contract are preserved. Supersedes #5959. Refs #3262. * fix(codex): pin and document the first-turn/semantic timer interaction The MULTICA_CODEX_FIRST_TURN_TIMEOUT override only raises the first-turn watchdog, not the semantic-inactivity timer that the same first status:running arms concurrently. An override above the semantic timeout is therefore truncated to it, and the resulting failure is reclassified as semantic inactivity — which also skips the #3291 model-catalog startup retry. The prior docs, comment, and test wording implied the override "wins outright / unbounded above", hiding this real upper bound and the configuration-dependent loss of automatic recovery. - Warn at config load when MULTICA_CODEX_FIRST_TURN_TIMEOUT exceeds the semantic timeout, naming both the truncation and the disabled startup retry. - State the effective bound (semantic timeout + overall execution timeout) in all four environment-variables*.mdx pages, and that both must be raised together when the desired first-turn wait exceeds the semantic limit. - Narrow the "wins outright / unbounded above" resolver comment and test name to describe only the first-turn watchdog ceiling. - Add a runtime regression test that drives the real run loop with an override above the semantic timeout and asserts the semantic timer wins the race and the startup retry is skipped; add a LoadConfig empty/explicit/zero test (this knob is environment-only, no Overrides parity). Addresses review on #6753. Refs #3262 / #5959 / #3291. * fix(codex): warn at equal first-turn/semantic timeouts, require strictly greater multica-eve's re-review on #6753: the warning fired only when the first-turn timeout was strictly greater than the semantic timeout, and told operators to raise the semantic timeout "to match". But the semantic timer is armed first (codex.go resets it before creating the first-turn timer), so at equal durations the semantic deadline is reached first and Go's select does not deterministically favour the first-turn branch — the failure can still be classified as semantic inactivity and skip the #3291 startup retry. An operator following the old guidance to equal values got no warning and silently lost the retry. - Warn when first-turn >= semantic (was >), and tell operators to set the semantic timeout strictly above the first-turn timeout (with margin), not "to match". - Update all four environment-variables*.mdx locales accordingly. - Add TestLoadConfig_CodexFirstTurnTimeoutEqualToSemanticWarns pinning the equal-duration warning and the strictly-greater no-warning boundary. Addresses review on #6753. Refs #3262 / #3291. |
||
|
|
e3ec3f8b57 |
MUL-6162 docs: sync landing copy and docs for the DeepSeek Harness runtime (#6940)
Completes the user-facing surface for the DeepSeek Harness runtime merged in #6923: landing copy (21 -> 22 tools), README, CLI_AND_DAEMON, SELF_HOSTING, provider comparison and install docs across en/zh/ja/ko, plus the provider logo. Review follow-ups folded in: the mark is inlined so currentColor tracks the app theme (an <img>-loaded SVG pinned it to black in dark mode), the landing enumerations and environment-variable docs are complete in all four locales, and the new rows sit in their tables' existing order. |
||
|
|
4f10a944b1 |
feat(runtime): add DeepSeek Harness support (#6923)
* feat(runtime): add DeepSeek Harness support * docs(runtime): reflect public DSH release |
||
|
|
063698f571 |
fix: gate worktree mode on a declared capability, not a version string (MUL-5707) (#6904)
* fix: gate worktree mode on a declared capability, not a version string (MUL-5707) Two tasks ran in the user's own directory despite the resource being set to worktree mode. The daemon was v0.4.23 — built before the worktree code — so it json-skipped execution_mode and took the in_place path. The claim gate existed to stop exactly that and did not fire: the runtime reports `v0.4.21-24-gcd3c0bb89`, a git-describe dev build, and the version floor deliberately exempts those so `make daemon` stays unblocked. The exemption is fine for a gate whose failure mode is a missing field; it is wrong for one whose failure mode is writing to somebody's working copy. Version strings cannot answer "does this binary implement worktree mode". The daemon now answers it directly: `local-worktree-v1` joins the existing X-Client-Capabilities negotiation, and all three gates read that instead. - Claim time (authoritative): decided from the capability on that very request. A daemon without it gets the task cancelled with the reason on the row — never a silent fallback to editing the directory, which is the outcome this mode exists to prevent. - Save time and the UI cannot see a live request, so registration now records the advertised capabilities on the runtime row and both read that. One signal everywhere instead of three different guesses. Absent capabilities means an older daemon, so every path fails closed. The error names the machine's directory and says to update the Multica app there, rather than quoting version numbers the user did not choose. MIN_LOCAL_WORKTREE_CLI_VERSION stays only as the number shown in the existing 422 payload; nothing gates on it any more. Co-authored-by: multica-agent <[email protected]> * fix(handler,views): judge daemon capability by the newest runtime row (MUL-5707) Review follow-up. Both gates asked "did ANY runtime row for this daemon advertise the capability", which never stops being true: deregistering a runtime only flips the row to offline, its metadata survives, and ListAgentRuntimes returns every row. A machine that once ran a capable daemon and then downgraded keeps an old capable row next to the fresh incapable one, so the save gate and the UI would go on offering worktree mode. The claim gate still refused the task — no directory was ever at risk — but the user only found out after the run instead of at the moment they could act on it. Both sides now read the daemon's most recently seen runtime row, and a row missing the capability is never skipped: being newest is what makes it authoritative, not whether its answer is convenient. A row that never reported sorts oldest so a live one always wins. The selection lives in one place per side (daemonAdvertisesWorktree / daemonSupportsLocalWorktree) with matching semantics, because a UI that disagrees with the API offers a mode the API refuses. Also pins the bridge the whole gate rests on, which nothing covered: DaemonRegister round-trips the advertised header onto the runtime row, and re-registering WITHOUT the header clears it — the exact downgrade the save gate has to notice. The daemon's own identity-header test now asserts local-worktree-v1, so dropping it from the advertised list can never be a silent change. Corrects three places that still described the version floor as the gate: version.go, the projects-and-resources source map, and the floor test, which now pins the two surfaces that merely DISPLAY the version to quote the same release. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
740ebdb8ef |
MUL-5991 feat(agent): enable ACP thinking_level for the hermes provider (jcode) (#6814)
* feat(agent): enable ACP thinking_level for the hermes provider (jcode) The reporter confirmed from jcode source (v0.71.1 and v0.73.0) that its advertised effort is genuinely applied, not just echoed: session/set_config_option waits for an `effort_changed` ack, and the provider request carries `reasoning.effort` upstream. That was the one open question blocking this half of GitHub #6720. The wrinkle is that `hermes` is one provider type over two unrelated binaries with opposite answers here: - jcode advertises option id `reasoning_effort` under category `thought_level` and applies it. The shared parser already matches on category, so the id being unrecognised costs nothing and the id we send back is jcode's own. - Hermes Agent has no effort surface on ACP at all. Re-verified against v0.20.0 today by driving `hermes acp` directly: session/new still returns only `_meta`, `models`, `modes`, `sessionId`, unchanged from the v0.18.2 finding in MUL-5770. A version gate cannot separate those — they are different projects, not different releases. The catalog can, and does: an empty catalog means no picker and no set_config_option call, so admitting `hermes` to acpCatalogThinkingProviders is safe for both. This is the case the catalog-driven design was built for. Also corrects the docs that stated hermes has no reasoning control. That is now binary-specific rather than universally true, and Copilot takes over as the example of a runtime with no effort dial at all. Co-authored-by: multica-agent <[email protected]> * test(handler): retarget the no-reasoning-control fixture from hermes to copilot These tests used hermes as the canonical runtime with no effort dial, so enabling it broke them in CI — they passed locally only because the handler suite skips without a database. Copilot takes over the role: it discovers over ACP but executes through its own CLI, so there is genuinely no live ACP session to carry an effort. The behavior being pinned is unchanged — a capability gap must not be reported as an unrecognised token. Adds the inverse assertion in the same test: hermes now accepts a level at the provider gate, since jcode applies one. Whether a picker appears is settled per session by the discovered catalog, not here. Verified against a real Postgres this time rather than the skip path. Co-authored-by: multica-agent <[email protected]> * fix(agent): keep the capability 400 for hermes runtimes with no effort dial Opening the thinking_level gate for the hermes provider regressed the majority of that provider's users. Hermes Agent, not jcode, is what most people run there, and it advertises no effort option — so a CLI `--thinking-level high` went from an immediate, accurate 400 to a 201 followed by the daemon silently dropping the level at task time. The provider name cannot distinguish the two binaries, but the model catalog the daemon already reports can: a hermes runtime whose catalog advertises no thinking levels is a Hermes Agent install. Consult it in the create and update paths and restore the capability message for that case, leaving jcode unaffected. Fails open on purpose. A cold cache, an offline daemon, or a never-discovered runtime all skip the check, so a valid jcode agent is never blocked before its first discovery; the daemon's pre-execution guard remains the backstop. Verified against a real Postgres, including the fail-open paths. Co-authored-by: multica-agent <[email protected]> * fix(agent): refuse a thinking level on an undiscovered hermes runtime Review caught that the fail-open branch was not the transient cold start I assumed. The model catalog is written only by ReportModelListResult, i.e. only after a client explicitly asks for a model list, so a caller who works through the CLI and never opens a model picker stays undiscovered indefinitely. Reading "unknown" as "supported" therefore left the majority Hermes Agent case exactly where it started: `--thinking-level high` accepted and persisted, then silently dropped by the daemon at task time. Replace the boolean with three states — present / absent / unknown — and refuse on unknown, but only for providers whose name does not determine the binary. `hermes` is the sole member; reasonix names one binary that does support an effort, so an undiscovered reasonix runtime stays allowed rather than being blocked before its first discovery. Unknown gets its own message rather than reusing "does not support a per-agent reasoning effort". That sentence would be actively false for a jcode runtime, sending its owner to look for a limitation that isn't there; naming the missing evidence points at what actually resolves it. Also adds the execution-chain coverage the review asked for: jcode-shaped ACP stubs prove set_config_option(reasoning_effort) goes out before session/prompt on both session/new and session/resume, and a Hermes Agent-shaped payload produces no config call at all. That is the substitute evidence for having no real jcode to test against. Skill and source map updated so the documented guarantee matches the code rather than being stronger than it. Co-authored-by: multica-agent <[email protected]> * fix(agent): word the undiscovered-runtime 400 for create as well as update The message pointed at "the agent's model picker", which does not exist on the create path — the agent is not created yet. "the model picker for this runtime" is accurate on both paths (review nit, PR #6814). Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
e7e01638ad |
MUL-6118: fix(agent): own Codex process trees with a Job Object on Windows (#6897)
* fix(agent): own Codex process trees with a Job Object on Windows Windows has no process groups, so configureProcessGroup was a no-op, waitProcessGroupGone always reported false, and signalProcessGroup killed only the direct child. That child is usually a cmd.exe wrapping a .cmd shim, so the real tree — the Node wrapper, the native app-server, and the sandbox helpers under them — was left running as orphans. Assign the started process to a Job Object with KILL_ON_JOB_CLOSE, which every process it later creates inherits. Termination now targets the job, emptiness is read from job accounting, and ownership is dropped only once the tree has been reaped. The process handle is held for the lifetime of the entry so Windows cannot recycle the pid out from under the key. Assignment failure is not fatal: cleanup degrades to the previous kill-the-child behaviour, and the cleanup_confirmed gate keeps suppressing initialize retries on any host where ownership could not be taken. Refs #6883 Co-authored-by: multica-agent <[email protected]> * fix(agent): close the pre-attach window and stop keying trees by pid Two defects in the Job Object ownership added by the previous commit. Windows grants job membership only to processes created after the assignment, never retroactively. Assigning after cmd.Start left a window in which the direct child — usually a cmd.exe wrapping a .cmd shim — had already spawned the real agent outside the job. That is worse than owning nothing: the job then holds a wrapper that exits immediately, so accounting reports the tree empty while the escaped app-server is still running and cleanup is reported as confirmed. startOwnedProcessTree now creates the child suspended, assigns it, and only then resumes it, so it cannot create anything before it belongs to the job. A child is never left suspended: ownership that cannot be taken is logged and the child runs unowned, and a child that cannot be resumed is killed and reported. Ownership was also keyed by pid. A pid is unique only while its process is alive, so a launch that outlived its process could look up — and terminate — an unrelated task that inherited the number. Key by the *exec.Cmd of one launch instead; signalProcessGroup and waitProcessGroupGone take the command on both platforms. The tests assert job membership through the job's own accounting rather than through timing, so an escaped descendant fails deterministically. Refs #6883 Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
c9727e6b2b |
feat(daemon): worktree mode for local_directory resources (MUL-5707) (#6759)
* feat(daemon): worktree mode for local_directory resources (MUL-5707) Local directory resources serialise every task on a per-path mutex, so two tasks on one folder queue behind each other. This adds an opt-in worktree mode: each task gets its own git worktree of the user's repo inside the daemon's env root, so tasks run concurrently and deliver their work as a branch instead of editing the user's working copy. Opt in per resource with execution_mode=worktree (default stays in_place, so existing resources are unchanged). Requires a git repo with at least one commit; a non-git directory fails with an actionable message rather than silently falling back to serial execution. Design points that make the mode safe to use: - The agent starts from what the user sees, not HEAD. Tracked edits are captured with `git stash create` (which writes a commit object without touching the index, working tree, or stash list) and replayed into the worktree; untracked non-ignored files are copied, bounded at 2000 files / 200MB with the skipped count logged at warn level. - That replayed state is committed as a baseline, so "did this task change anything?" has an exact answer and `git diff <baseline>..<branch>` is precisely the agent's work. - Nothing is silently discarded: leftovers are committed to the branch before the worktree is removed, including when the task fails partway. - A task that changes nothing leaves no branch behind. - Sidecars stay out of the branch by running the existing CleanupRuntimeConfig + CleanupSidecars pass over the worktree before finalizing. The .git/info/exclude approach repocache uses is not usable here: a linked worktree resolves info/exclude to the user's own common git dir, so it would change what `git status` hides in their checkout. - Worktree-mode env roots do not get the local_directory GC exemption, so they age out normally instead of accumulating one env root per task. - `git worktree prune` on create self-heals registrations orphaned by a crashed daemon. Adds --execution-mode to `project resource add/update`, and updates the docs and the projects-and-resources built-in skill. Co-authored-by: multica-agent <[email protected]> * fix(daemon): close data-loss and isolation gaps in worktree mode (MUL-5707) Review follow-up on #6759. Six issues, all on failure or skew paths. 1. Preserve work when the final commit fails. Finalize logged a warning and then ran `git worktree remove --force`, destroying the agent's changes. commit.gpgSign with no key available to the daemon triggers this, and --no-verify does not disable signing. Finalize now returns an error and deliberately keeps the worktree — it stays registered, so `git worktree list` points at the work — and the daemon fails the task instead of reporting success with a branch that is missing the changes. 2. Fail closed on an unsupported execution_mode. Treating an unknown mode as in_place was wrong: execution_mode is how a user asks for ISOLATION, so the "safe" fallback would edit the working copy they asked us to stay out of. An unrecognised mode now fails the task naming the version skew. 3. Keep Codex conversations alive across turns. The per-issue session store was keyed on LocalWorkDir != "", so worktree tasks — which also never reuse an env root — got a fresh empty CODEX_HOME per turn and silently lost the thread. Covered by a two-turn regression test that fails without the fix. 4. Fail closed on an incomplete snapshot. A failed stash create/apply, a failed untracked copy, or hitting the copy bounds all degraded to "agent sees HEAD" with only a daemon log line. A half-replayed tree looks like a working checkout, so nothing downstream questions it while the agent reads different code than the user has. All of these now abort the prepare and tear the partial worktree back down. 5. Do not snapshot a directory mid-mutation. The same real path can be attached to another project as an in_place resource; snapshotting under a running in_place task captured a half-written tree plus that task's in-flight .agent_context/.multica sidecars, which then reached the branch and could feed the agent another issue's brief. Worktree prepare now takes the per-path mutex for the snapshot alone (released before the run, so tasks still overlap), and the untracked replay skips Multica's own sidecars. 6. Stop the test helper from masking regressions. gitRun skipped on any git failure, including post-finalize assertions, so a lost branch could report as a skip. Git availability is now checked once at repo setup; every later git failure is fatal. Also rebased onto main, keeping both MulticaConfigRoot and LocalWorktree in the Environment initialiser. Co-authored-by: multica-agent <[email protected]> * fix(daemon): close sidecar-commit and wait-visibility gaps in worktree mode; gate worktree saves on daemon version (MUL-5707) - runTask: register the sidecar-cleanup defer immediately after the Finalize defer. Previously two early returns (temp-dir setup, StartTask failure) sat between them, and on those paths Finalize auto-committed the sidecars Prepare had just written — delivering a branch whose only content is Multica's own runtime files (in_place: leaving them in the user's tree). - runTask: give the worktree snapshot wait the same plumbing as the in-place acquire — waiting_local_directory status, resourceWaitTasks accounting, and a cancellation poller. The holder can be an hours-long in_place task; the wait was invisible and a user cancel went unnoticed until the 5-minute prepare timeout. - isMulticaSidecarPath: match sidecar dirs as whole path segments at any depth, not only at the repo root. An in_place resource pointed at a SUBDIRECTORY leaves its sidecars below that subdirectory; replaying them committed another issue's brief into the delivered branch. - handler: refuse saving execution_mode=worktree while the owning daemon's registered cli_version is below MinLocalWorktreeCLIVersion (422, daemon_version_unsupported). An old daemon json-skips the field and runs tasks IN PLACE — the daemon-side unknown-mode refusal cannot protect a daemon that predates the field, so the server has to. Residual (documented): a daemon downgraded after the save needs a claim-time gate. - copyUntrackedFiles: read the -z listing raw instead of TrimSpace-ing it, which corrupted filenames beginning/ending with whitespace into paths that silently vanished from the replay. - docs + skill: describe the save-time version gate; drop the stale claim that old runtimes fail the task themselves. Co-authored-by: multica-agent <[email protected]> * feat(views): UI switch for local_directory execution mode (MUL-5707) Worktree mode shipped CLI/API-only, which for its target user — someone running several tasks at once from Desktop — means it does not exist. This adds the switch to the resources panel. The choice is framed by what the user gets back, not by the mode identifiers: "Edit this folder directly" (one task at a time, changes land in your working copy) vs "Run in parallel, isolated" (tasks run at the same time and each hands back an agent/… branch). `in_place` / `worktree` stay visible as a secondary hint for anyone cross-referencing the CLI or docs. Two entry points, matching where the decision is actually made: - The add-a-folder flow now asks for the mode after the folder is picked and validated, instead of creating the resource and leaving the mode to be discovered later. - Every local_directory row has a mode control. It is deliberately NOT gated on being the owning machine — switching the mode only rewrites a field, so it works from the web app too, unlike rename or the folder picker. Both blocked states are handled where the user chooses, not after they commit: - Non-git folder: the desktop validator now reports whether the path is inside a git working tree (walking up for `.git`, so a subdirectory of a repo counts), and the option is disabled with that reason. Unknown — an older desktop build, or an existing row — stays permissive; the daemon re-checks authoritatively. - Daemon too old: the version floor is mirrored from the server as MIN_LOCAL_WORKTREE_CLI_VERSION and checked against the runtime for the resource's OWN daemon, not the machine the browser happens to be on. A server rejection is rendered inline in the dialog rather than as a toast, so the dialog stays actionable. A "Parallel" badge on the row is always visible (not hover-only): without it there is no way to tell whether tasks on a folder edit it directly or hand back a branch, which is the first thing anyone asks when a task queues. Also adds `execution_mode` to LocalDirectoryResourceRef — the field existed server-side but had no TypeScript counterpart — and brings the zh/ja/ko project-resources docs in line with the English ones, which were the only locale updated when the mode landed. Co-authored-by: multica-agent <[email protected]> * fix(daemon,handler): close worktree isolation and delivery gaps (MUL-5707) Second review round. Five must-fix items, all on paths where the mode's promises could still break. 1. A failed sidecar cleanup no longer ships Multica's own files as the user's branch. The cleanup errors were logged and Finalize then ran `git add -A`, so a permission/disk/lock failure could deliver a branch whose entire content was the runtime brief and issue context. Worktree mode now escalates a cleanup failure to an abort: nothing is committed, the worktree is kept for inspection, and the task fails with the preserved path. In place it stays a warning — no commit, no branch, and the leftovers are visible in the user's own tree. 2. The untracked replay no longer drops files silently. `Lstat` failures and symlinks were `continue`d without counting, so the caller read a partial snapshot as complete. Both now count as skipped, which the existing fail-closed check turns into a refused prepare. Non-content entries (sockets, FIFOs, devices) stay uncounted deliberately. 3. Prepare rolls back a worktree it created. Any later failure — context files, provider home, MCP config — returned without an Environment, so nothing downstream knew a worktree existed, leaving a registration and a branch no task ever ran in. A deferred Discard now runs unless Prepare reached its successful return. 4. The daemon-version gate moved to where it can actually hold. Save-time validation cannot see a machine downgraded afterwards, and an old daemon json-skips execution_mode entirely — it would run the task in place, editing the working copy the user asked to isolate. Claims are now checked against the runtime that is actually claiming, and a blocked task is cancelled with a reason rather than left to be redelivered to the same too-old runtime forever. 5. branch_name is delivered end to end. The daemon sent it, but TaskCompleteRequest had no field so it vanished at decode, and the failure path never sent it at all. It now has a column, travels on both terminal callbacks, and renders as a copyable row in Run details. The failure path matters most: worktree mode commits the agent's leftovers before tearing the worktree down, so a run that died partway still has work worth finding. Docs (all four locales) and the projects-and-resources skill are updated for the claim-time gate and the branch surface. Co-authored-by: multica-agent <[email protected]> * fix(handler,daemon): migration renumber, cancel reasons, branch on every terminal path (MUL-5707) Third review round. 1. Rebased onto main and renumbered the migration to 285 — main had taken 273 and is now at 284. sqlc regenerated on top of the rebased queries; the conflicts were confined to generated files. 2. The claim gate now persists why it cancelled. It called CancelTask, which writes only status='cancelled', and returned the reason as a 4xx that reaches the daemon's log and nothing else — batch claim drops the task from its response entirely. A run refused for a durable reason the user has to act on cannot present as a bare "cancelled", so this adds CancelAgentTaskWithReason and records error + failure_reason on the row. 3. branch_name now survives every terminal path, not just the two direct callbacks. Three were dropping it: - the context-exhausted complete→fail reroute did not copy it; - the daemon's complete→fail fallback (server rejected the complete, agent had succeeded) did not set it; - both cancellation paths discarded the result wholesale, even though the worktree is finalized — and its work committed — before the daemon learns of the cancel. The branch now rides the cancel-ack and is recorded by SetAgentTaskBranchName, which never overwrites a name a terminal callback already wrote. 4. Rollout/rollback compatibility. Isolation is enforced by two server-side gates, so a daemon downgraded at any point after the resource was saved is caught. The gap they cannot close is rolling the SERVER back past this change while sub-floor daemons are still connected: an old server has no gate and an old daemon ignores execution_mode. That constraint is now documented where an operator will meet it (the migration and the docs), and the version floor is pinned across server and frontend by test so the two gates cannot silently drift apart. Co-authored-by: multica-agent <[email protected]> * fix(server,views): route claim-gate cancels through TaskService, make cancel reasons visible, harden cancel-ack branch delivery (MUL-5707) Closes the fourth-round review findings on worktree mode's cancellation chain: - Claim gate: cancel through a new TaskService.CancelTaskWithReason instead of a raw CancelAgentTaskWithReason query, so a gate-cancelled task gets the full cancellation side effects — audit capture, chat settle, agent status reconcile, task:cancelled broadcast, NotifyTaskFinished. Added REAL claim integration tests: singular claim (422 + row cancelled with reason), batch claim (task omitted + row cancelled with reason), and a current-runtime control. - Cancel reasons are now user-visible: execution log rows, agent activity rows, and the transcript dialog label a SERVER-cancelled task (persisted error/failure_reason) like a failed one, with the actionable text on hover and a Reason row in Run details. User-initiated cancels stay a plain "Cancelled". New cancelReasonLabel helper + local_directory_error label; details_reason locale key in en/zh-Hans/ja/ko; widened AgentTask.failure_reason to the open string the wire actually carries. - Cancel-ack is now a terminal delivery: the daemon retries it on transient errors (same schedule as complete/fail), the server fails LOUD (5xx) when persisting the branch/error fails instead of warn-and-200, and re-broadcasts task:cancelled after a successful write so clients that already refetched a branchless row pick up the delivery. Replays are idempotent (never overwrite). - Cancel + Finalize-abort no longer swallows the preserved-worktree pointer: the finalize error is wrapped in worktreePreservedError (joined onto an existing run error, never displacing it), extracted on the cancel path, and delivered via the ack into agent_task_queue.error through the new SetAgentTaskErrorIfEmpty query — never overwriting a reason recorded by a fail callback or the claim gate. Other run errors on a cancelled run stay discarded as before: they are expected noise. - Docs: the self-hosted rollback constraint callout now exists in zh/ja/ko, not just English. Co-authored-by: multica-agent <[email protected]> * fix(handler): guard cancel-ack writes to cancelled rows; requeue when the claim gate's cancel fails (MUL-5707) Fifth-round review fixes: - SetAgentTaskBranchName / SetAgentTaskErrorIfEmpty now carry a status='cancelled' CAS. The daemon acks on EVERY terminal status it observes, so a late or replayed ack from a stale run could previously backfill its branch or preserved-path error into a completed/failed row whose own callback — the authoritative channel — recorded nothing, without any event invalidating the change. A CAS-refused write is a deliberate no-op (still 200: nothing to retry), and the rebroadcast stays guarded by the same status check. Regression tests: a late ack against a completed row with empty fields and against a failed row leaves both untouched. - When the worktree claim gate's CancelTaskWithReason fails, the claimed row no longer strands in dispatched (daemon refused, nothing to release it but stale reclaim). The gate requeues via RequeueTaskAfterClaimFailure — the next claim re-runs the gate and retries the cancel — and reports 5xx so the singular daemon reads a transient server problem, not a claim refusal; the batch path omits the task as before, now with the row requeued. Fault injection tests (row-scoped trigger raising on the cancel UPDATE while letting the requeue through) cover singular and batch. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
28a7176328 |
MUL-6053: fix(daemon): give Hermes conversations a session that survives the task (#6845)
* fix(daemon): give Hermes conversations a session that survives the task Hermes keeps every ACP session in `<HERMES_HOME>/state.db`. Because the daemon takes over HERMES_HOME to inject bound skills, that database lived inside the per-task overlay, so a conversation lasted exactly one task. The failure was silent, which is what made it expensive. `session/resume` against a database that does not hold the session does not fail: Hermes creates a fresh session and answers with an ordinary success frame, and ACP's ResumeSessionResponse has no `sessionId` field to contradict it. The daemon therefore believed it had resumed, stored the same dead id again, and every turn restarted from zero with nothing in the logs (GH #6806). Three changes, in the order they matter: - The backend now reads `_meta.hermes.sessionProvenance.acpSessionId`, the only place Hermes reports the session it actually served. A turn that landed on a different session continues under the new id and carries the existing ResumeContinuityNotice, so the agent can read the transcript back instead of pretending it remembers. - `state.db` is linked to a per-conversation store at `<profile dir>/hermes-sessions/<agent>/<hermes profile>/<conversation>/`. Sharding per conversation is what makes this safe where #6693 left it out of scope: tasks of one conversation are serial, so a shard has a single writer, while two issues never share a database. SQLite resolves the link when deriving the WAL paths, so the journal sidecars live in the store and a task teardown cannot take the write-ahead log with it. Windows without symlink privileges keeps the database task-local rather than copying it. - `gateResumeToReusedWorkdir` no longer treats a matching workdir as proof the session is reachable. The two keys came apart in the local_directory flow, where workdir reuse is disabled but envWorkDir still equals PriorWorkDir, so the gate forwarded an id that could never resolve. Stores are GC'd on their own retention (`MULTICA_GC_HERMES_SESSION_TTL`, default 14d, matching the Codex session store) and held against the GC for the duration of a task. Co-authored-by: multica-agent <[email protected]> * fix(daemon): never destroy a Hermes session database on a failed mount Review of #6845 found three ways the new session store could lose or misreport a conversation. All three are on failure and reclaim paths that the original tests did not reach. - **The degradation path was destructive.** The mount migrated the task-local state.db into the store and deleted it, and only then tried to create the symlink. On a host that cannot symlink, that turned a conversation which previously survived via workdir reuse into an empty one — a regression on exactly the hosts the fallback exists to protect. The link is now created under a staging name first, and nothing destructive runs until it exists; a host that cannot link is left byte for byte as it was found. - **The migration was not atomic.** It copied state.db and its journal sidecars straight into the store, so a failure after the main database landed left a store that looked migrated. The next attempt skipped the migration, deleted the source, and took the un-checkpointed tail of the conversation with it. It now stages the whole family and publishes with one rename, reusing the memory store's staging helpers (renamed to drop their `memory` prefix now that both stores share them). - **"Mounted" was read as "resumable".** A store the GC reclaimed between turns, a switched Hermes profile, an operator's `rm`, or a dangling link from an older overlay all mount cleanly onto nothing, and the resume gate forwarded a dead session id into them — the same silent restart this change set out to fix. The mount now reports two separate facts, and the gate reads whether the store actually holds a transcript, not whether the plumbing succeeded. A zero-length database counts as no history. Tests cover each path with injected failures rather than host-dependent skips: a link failure preserves the database and its WAL and still mounts once linking works again, a migration that dies between the database and its WAL publishes nothing and loses nothing on retry, and a mounted-but- empty store (fresh, reclaimed, dangling, zero-length) reports no history. Verified non-vacuous — reverting each fix reproduces the reviewed symptom. Co-authored-by: multica-agent <[email protected]> * fix(daemon): make the Hermes session store agree with itself on "empty" Review of the previous commit found a certain data-loss branch left between the migration and its publish step. The two used different definitions of an occupied store: - the migration asks hermesStoreHasSessionDB — a zero-length state.db, or journal sidecars with no database, carry no transcript, so it proceeds; - the shared promote asked hermesStorePopulated — any directory entry at all — so the same remnants read as "a competitor published first". With a non-empty source and a zero-length state.db in the store, the staging copy completed, os.Remove(storeDir) failed on the non-empty directory, promote reported a lost race, the migration returned success, and the caller deleted the source database and its un-checkpointed WAL. The link then pointed at a database with nothing in it. Occupancy is now the caller's predicate: the memory store keeps "any entry", the session store passes its own "holds a real database". The session publish clears the remnants it has already classified as nothing before the atomic directory rename, and a store it cannot clear fails closed — the migration returns the error and the source stays put — rather than passing for a lost race. The regression test drives the whole matrix through prepareHermesHome: a zero-length database, orphan sidecars, a real database that must never be overwritten, and an unclearable store that must fail closed with the source intact. It fails on the previous commit with the reviewed symptom (HistoryPresent=false after the source was deleted). Co-authored-by: multica-agent <[email protected]> * fix(daemon): never delete a competitor's freshly published Hermes transcript publishHermesSessionStaging decided the store held no history, then deleted the state.db family by path name. Between those two steps a competing task could publish its real transcript, and the stale decision would remove the winner's database — after the winner had already reported success and discarded its source. The check, the remnant cleanup and the promote now hold one process-wide mutex. In-process exclusion is the real thing here, not an approximation: the daemon is the store tree's only writer (even the GC guard is in-memory refcounts on the Daemon), so two publishers can only ever be two goroutines of this process. A deterministic barrier test parks a publisher inside the critical section and proves a competitor cannot enter the window. The family cleanup also stops using RemoveAll: regular files and symlinks are unlinked (never followed), while a directory or any other irregular type under a state.db name is content this code cannot identify, so it fails closed and the caller keeps its source. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> Co-authored-by: Steve Jobs (Multica Agent) <[email protected]> |
||
|
|
0c69f1f95f |
fix(daemon): recover in-turn when a resumed session cannot resolve provider auth (#6798)
A resumed Hermes session whose persisted provider identity can no longer resolve its credentials fails with the SDK's "Could not resolve authentication method" message. The server-side resume guards already blacklist that session, so the NEXT turn starts fresh and succeeds — which is exactly why Chat alternated success / failure instead of staying broken (GH #6777). Curing the failing turn needs the daemon's in-turn fresh-session retry, which never fired: nothing identified this error as fatal-to-resume. Add it as a third form of positive evidence in shouldRetryWithFreshSession, alongside ResumeRejected and UnresumableHistory. Deciding in the shared gate rather than in each ACP adapter is what makes it correct and narrow: - priorSessionID != "" already proves the run was a resume, which is the whole distinction between "the session's provider copy is broken" (curable) and "the credentials are wrong" (not curable). An adapter cannot tell those apart from the error text alone, which is why Result.ResumeRejected still documents auth errors as out of scope. - The failure surfaces at session/resume, session/set_model (MUL-5029 provider re-routing, the likelier path on a resumed session) and session/prompt; only two carry any resume-failure signal today. The final error text carries the phrase on all three. - tools == 0 is untouched, so a turn that already acted is never replayed. The phrase now has a single home in taskfailure.AuthMethodUnresolved, shared with ResumeUnsafeFailure. The SQL guards stay as-is: they are the only protection for rows written by a daemon too old to have this retry. Co-authored-by: Eve <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
7eeaaaa05e |
MUL-6015: WS-2: update Codex gpt-5.6 model labels (#6749)
* WS-2: update Codex gpt-5.6 model labels Co-authored-by: multica-agent <[email protected]> * WS-2: normalize Codex dynamic model labels Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: zhuchangmin <[email protected]> Co-authored-by: multica-agent <[email protected]> |