mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-04 17:18:35 +02:00
fix/codex-patch-apply-payload
450 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e4ee615f88 |
fix(daemon): redact nested tool input before it leaves the daemon
Addresses review on #6158. Recursive redaction ran only in the server's ingest handler. The daemon built the new nested edit payload and sent msg.Input verbatim, so a daemon that self-updated ahead of the server — or one talking to a server mid-rollout — would ship whole-file edit contents to a peer that does not scrub nested values yet. The legacy protocol reports a deletion as the whole outgoing file, so that window covered a deleted .env in cleartext. Ordering three commits inside one PR is not a deployment barrier, and daemon and server upgrade independently. Deployment order is not a control we have, so the sending side is now safe on its own; the server keeps redacting on ingest as the second line of defence. Scoped to Input, which is the field this PR newly fills with file contents. Content and Output are plain strings already redacted server-side, and changing their daemon-side handling would be unrelated to this fix. Verified: the new daemon test asserts the nested token is masked in the reported batch while the change metadata survives. It fails without this change, reporting the full GITHUB_TOKEN= line on the wire, and passes with it; ./internal/daemon green. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
423d86da04 |
fix(daemon): prevent runtime update starvation (#6123)
Co-authored-by: Mia <mia@firtal.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a0ef1a43f3 |
fix(daemon): diagnose silent OpenClaw npm shim failures on Windows (MUL-5422) (#6084)
* fix(daemon): diagnose silent OpenClaw npm shim failures on Windows (MUL-5422)
#6061 reported every OpenClaw task failing in execenv prep on Windows with
a bare `exit status 1` and no stderr, leaving the user nothing to act on.
An npm-installed `openclaw.cmd` is a batch shim that re-execs OpenClaw's
`openclaw.mjs` entrypoint through `node`, resolved from PATH. The daemon pins
`openclaw` to an absolute path, so the shim always looks correct — but that
interpreter lookup is a second, invisible resolution step that can fail on its
own. The reporter had to run their own subprocess experiments to find it.
Enrich the error instead of guessing at a fix: when a `.cmd`/`.bat` shim exits
non-zero with no stderr, report whether the interpreter resolves. Both
directions are useful — missing names the likely cause with a next step,
present clears PATH of blame and points at the remaining hypotheses (PATH
drift between the runtime `--version` gate and task prep, or a broken install).
Deliberately NOT included: rebuilding or freezing a Windows PATH. The
version-probe gate (probeBuiltinRuntime skips a provider whose `--version`
fails) and the prep helper both inherit the same daemon environment, so a
daemon that could not resolve `node` would never have registered OpenClaw at
all. That contradiction is unresolved, and a boot-time PATH snapshot would also
fight the MUL-4486 self-heal design, which re-resolves per attempt on purpose.
This change collects the evidence needed to settle it.
- Error text only; no control flow change, and real stderr still wins.
- PATH summarised as an entry count, never dumped, so daemon logs and pasted
bug reports carry no environment detail.
- Tests: shim detection (case, spaces, Unicode), both diagnostic directions,
out-of-scope no-ops (timeout, missing binary, native exe), and end-to-end
through execOpenclawCLI. A windows-tagged file reproduces a real npm shim
with and without node on PATH, and pins TEMP/TMP as not load-bearing — the
originally reported root cause, since retracted upstream.
- New scoped step in the existing ci.yml windows-execenv job.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): address review on OpenClaw shim diagnostics (MUL-5422)
Four must-fix items from PR review, each verified against the real behaviour
rather than assumed.
1. Timeout was misdiagnosed as a missing interpreter. openclawCLITimeout kills
the child via CommandContext, and a killed process surfaces as
*exec.ExitError ("signal: killed") — the same type a genuine exit 1
produces. The errors.As gate accepted it and appended "install Node.js",
sending users to fix something that was never broken. execOpenclawCLI now
attributes ctx.Err() before consulting the diagnostic. Confirmed locally:
`signal: killed`, errors.As(*exec.ExitError)=true, ctx.Err() set. The old
test passed context.DeadlineExceeded directly and so never saw the real
shape; replaced with a genuine CommandContext timeout regression on both
Linux and Windows.
2. The interpreter lookup did not match npm's. npm's cmd-shim template emits
`IF EXIST "%dp0%\node.exe" (...) ELSE ( SET "_prog=node" )`, so a co-located
node.exe wins over PATH entirely. Checking only LookPath reported "node is
not resolvable" for installs that actually run fine — confidently wrong,
which is worse than silence. Now resolves co-located `node.exe`/`node`
first, then PATH, and reports which. Wording is also conditional now
("if <name> is an npm-generated shim"): a batch extension does not prove npm
authorship, since MULTICA_OPENCLAW_PATH can point at any batch file.
3. The message leaked local paths off-box. On prep failure this text is not
log-local — it travels reportTerminalTask → Client.FailTask and is persisted
server-side as the task error, so an absolute Windows shim path uploads the
account name and install layout. Now reports only the shim's base name,
whether the interpreter resolved and from where, and a PATH entry count.
Never an absolute path, never PATH contents.
4. Windows CI was green without exercising the new code. The job log showed
`cmd.exe stderr DID reach Go's pipe` with `'node' is not recognized`, so the
missing-node case takes the existing stderr branch and the diagnostic never
ran — masked by an "either branch passes" assertion. That disjunction is
gone: the missing-node test now asserts the observed stderr behaviour
(disproving #6061's premise), and a new test drives a genuinely silent shim
to prove the diagnostic branch itself works on Windows. Also added Windows
coverage for the co-located interpreter and the timeout case.
The windows-tagged shim is now npm's real generated template rather than a
hand-simplified `node ...` one-liner, so the co-located branch is reproduced
faithfully instead of hidden.
Co-authored-by: multica-agent <github@multica.ai>
* test(daemon): make the OpenClaw timeout regression PATH-independent
The new timeout test stripped PATH (so a stray interpreter lookup would report
"missing") while its hanging shim invoked `sleep` through a PATH lookup. macOS
`sh` quietly falls back to a default PATH so this passed locally; dash on Linux
does not, so CI failed with `exit status 127 (stderr: sleep: not found)` — the
shim died instantly instead of hanging, and the assertion never saw a timeout.
Resolve `sleep` before PATH is stripped and embed it by absolute path, so the
shim needs no PATH of its own. Verified the failure mode and the fix directly:
`env -i /bin/sh -c 'PATH=/nonexistent; sleep 0.05'` reproduces
"sleep: command not found", while the absolute path runs fine with the same
empty PATH.
Windows is skipped here and covered by TestWindowsOpenclawShimTimeoutIsNotMisdiagnosed,
which has a real cmd.exe host and a System32 PATH that can resolve its own helper.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): bound execOpenclawCLI so its 5s timeout is actually enforceable
The new timeout regression exposed a real bug in the code it was testing, not
just a flaky test: openclawCLITimeout could not bound the call at all.
CommandContext kills only the direct child, and cmd.Output() blocks in Wait()
until the stdout pipe closes. Any grandchild that inherited stdout keeps the
call parked for its own lifetime. Verified on linux/dash: a shim whose child
slept 5s ran the FULL 5.01s against a 150ms deadline. With a WaitDelay backstop
the same case returns in ~2.17s.
This is not a hypothetical shape — it is precisely an npm shim on Windows
(cmd.exe → node), so a wedged node could stall task prep far past the 5s cap
that comment claims. detectCLIVersion already carries this exact backstop for
the `--version` probe for the same reason; execOpenclawCLI now matches it.
Also corrected the test comment: an earlier revision claimed a trailing
`exit 0` was needed to force the grandchild. Docker showed otherwise — dash
hangs either way and macOS reproduces neither, which is why CI caught this and
local runs did not. The comment now records the measured behaviour.
Verified in a linux/dash container (the CI platform, not just macOS): the full
execenv package passes with -race, and the timeout case takes 2.17s.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): drop the WaitDelay change and wrap the context error (MUL-5422)
Round-2 review: take option 1 — keep this PR to diagnostics and split the
timeout/process-tree work out.
The reviewer is right that WaitDelay traded a hang for a process leak, and I
had the mechanism wrong. Measured on linux/dash by recording the grandchild PID
and reading /proc/<pid>/stat at the moment Output returns:
no WaitDelay: elapsed 6.01s (6s sleep, 150ms deadline), grandchild state Z
with WaitDelay: elapsed 2.17s (60s sleep, 150ms deadline), grandchild state S
So without WaitDelay the call is hostage to the descendant's lifetime but no
live process is left behind — it returned precisely because the descendant had
exited. With WaitDelay the call is bounded but a live descendant survives. My
earlier claim that the orphan pre-existed was an artifact of a sleep duration
that happened to equal the return time.
Go's WaitDelay contract covers killing the direct child and closing our pipe
ends; it does not reap orphans. Closing this properly needs process-tree
ownership (Unix process group, Windows Job Object) so the deadline can terminate
the whole tree — and on Unix nothing else will, since
preparationProcessController.finish() is a no-op there (isolation_unix.go).
That is its own change with its own risk surface, so it is tracked separately
and openclawCLITimeout now documents the gap with the measurements rather than
shipping half a fix.
Also fixes the round-2 nit: the context branch %w-wrapped the process error
while printing ctxErr with %v, so errors.Is(err, context.DeadlineExceeded) was
false despite the text containing it. The context error is now the wrapped
cause and the process error is attached for diagnosis:
openclaw config file: context deadline exceeded (process: signal: killed)
Tests: the timeout cases no longer depend on WaitDelay and no longer leave a
live process — short sleeps keep them about attribution, which is what they are
for. Added an explicit errors.Is assertion for both DeadlineExceeded and
Canceled. Verified in a linux/dash container (the CI platform): full execenv
package passes with -race and `ps` shows no leftover sleep processes.
Co-authored-by: multica-agent <github@multica.ai>
* docs(daemon): correct two stale comments on the OpenClaw CLI timeout (MUL-5422)
Both nits from the third review. Comment-only; no code change.
1. openclawCLITimeout's doc contradicted itself — it opened with "caps ...
without letting a hung CLI stall task dispatch indefinitely" and then
explained that the deadline cannot actually bound the call. Reworded to say
what it is (a 5s context deadline) and to point at the gap rather than assert
a guarantee it does not provide. Also names MUL-5467 instead of the vague
"tracked separately".
2. The two timeout tests claimed a long wait would "leave a live process
behind". That described the reverted WaitDelay behaviour, not the current
code. Without WaitDelay, cmd.Output() returns only once the descendant has
closed stdout — its exit is what produces the EOF — so a long wait makes the
test slow, it does not leak. Re-verified on linux/dash after the fix: the
case takes 1.01s for a 1s sleep and `ps` shows no leftover process, and the
earlier PID probe recorded the grandchild in state Z at the return point.
Rebased onto
|
||
|
|
e45a8f6c12 |
fix(daemon): scan comment roots before bulk reads in agent catch-up (MUL-5372) (#6093)
* fix(daemon): scan comment roots before bulk reads in agent catch-up The mandatory step-3 catch-up in the issue runtime brief asked for `--recent 10`. `--recent N` caps THREADS, not comments: each returned thread carries its root plus every descendant with no depth bound, so on an issue with fewer than N root threads it returns the entire comment history. Because the step is mandatory and fires on every run, every reply turn re-read the whole issue -- and on comment-triggered turns it duplicated the bounded thread read the per-turn message had already pointed at, so the same bytes were fetched twice. Lead the step with `--roots-only --summary` instead: every top-level thread with reply_count and last_activity_at, contents clipped. That keeps the property the step exists for -- the agent still sees every thread that exists, so it cannot act on stale context -- and makes the drill-down into `--thread <id> --tail 30` explicit. `--recent 10` stays documented for when several complete threads really are needed, now with its saturation semantics spelled out. Measured on a live 2-thread issue: 21,249 -> 1,518 bytes for the mandatory read (-93%), and the duplicate 11,082-byte thread read is gone. The brief stays byte-identical across runs of a session (MUL-5377): the new text interpolates only the issue id, no per-run state. The three per-turn pointers that express the same rule move with it so the two layers cannot drift. MUL-5372 Co-authored-by: multica-agent <github@multica.ai> * refactor(daemon): keep comment-read flag semantics in one place The previous commit fixed the payload shape but restated the read surface in four places: the workflow step, both per-turn prompt fallbacks, and the cold-start hint each explained what `--recent 10` does. `## Available Commands` is already the brief's single discovery point for these flags, and `TestInjectRuntimeConfigStaticCatchUp` pins it as such -- so those restatements were duplicated reference text, and the per-turn ones were paid on every turn rather than once in the cached prefix. Move the `--recent N` saturation warning into the `comment list` line in Available Commands, next to the flags it qualifies, and add `--roots-only` and `--summary` to that signature so the bounding options are discoverable where an agent already looks. Workflow steps and per-turn hints now name only the reads they actually want run. Per-turn prompt sizes: assignment 1170 -> 749 bytes (-36%), cold-start comment turn 1550 -> 1355 (-13%). Step 3 is 1065 bytes and no longer carries a ready-to-paste bulk read. MUL-5372 Co-authored-by: multica-agent <github@multica.ai> * docs(daemon): address review nits on comment-catchup change Three cosmetic follow-ups from review: - `--recent N` saturation warning said it hands back "the entire history"; resolved threads are still folded by default on that read, so say so. - Rename two tests whose names still advertised `--recent` after their assertions stopped mentioning it, plus the one added in this branch whose name referenced a bulk read the step no longer contains: MentionsRecent -> ScansRootsFirst, ScansRootsBeforeBulkRead -> ScansRootsFirst. - Fix the stale doc comment that still described the mandatory read as bounded to "the recent active-thread window". No behavior change. MUL-5372 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c25a82eee0 |
perf(agents): fast model discovery on runtime switch (MUL-5444) (#6098)
* perf(agents): make runtime model discovery fast on runtime switch (MUL-5444) Switching runtime in the agent creation form left the model picker spinning for ~8-20s. Two costs stacked up: - the list-models request sat in the store until the daemon's next scheduled heartbeat (0-15s, avg 7.5s of pure dead wait), and - the daemon then enumerated the catalog locally (static for claude, but a CLI/ACP round trip up to ~15s for everyone else). Both are addressed with the two standard techniques for a slow, low-frequency, read-only operation: push instead of poll, and stale-while-revalidate. Push (removes the heartbeat wait): - new additive `daemon:pending_work` hint, runtime-scoped, delivered through the existing daemon WS hub and the Redis relay so the API node holding the socket does the delivery. - the daemon answers a hint with ONE immediate heartbeat and dispatches what it claimed. The hint deliberately carries no work, so nothing has to be un-claimed when delivery fails and a duplicate hint cannot duplicate work - PopPending stays the atomic claim. - per-runtime coalescing plus a 1s floor keeps a caller-triggered hint from becoming a heartbeat amplifier. Cache (removes the discovery wait on repeat opens): - server-side per-runtime catalog cache (in-memory single-node, Redis multi-node) written on every successful report. - a snapshot younger than 15min answers the POST immediately as an already-completed request; older than 60s it also enqueues a background refresh that only warms the cache. - only supported, non-empty catalogs are cached; a completed-but-empty report invalidates instead, while a failed report keeps serving the last known good list. Frontend: staleTime 60s -> 5min and gcTime 30min, so a runtime revisited in the same session renders from cache and revalidates in the background instead of showing the spinner again. Compatibility: every wire change is additive. Old daemons ignore the unknown hint type and keep using the scheduled heartbeat; new daemons against an old server simply never receive one. The cached response is shaped exactly like a completed live discovery apart from the optional `cached` / `cached_at` markers. Co-authored-by: multica-agent <github@multica.ai> * fix(agents): address review on model discovery SWR (MUL-5444) Sol-Boy's review on #6098 found the client cache could outlive the server's own staleness promise, and that the two changed endpoints were still cast rather than validated. Must-fix 1 — client freshness now derives from the served answer. `staleTime` was a flat 5min, so a 14-minute-old snapshot (which the server returns while queueing its own refresh) was held as fresh for another 5min: observable staleness became server window + client window, and the refreshed catalog never reached the tab that triggered the refresh. `staleTime` is now a function of the query data: a `cached` answer is stale on arrival (bound stays the server's window alone, and the next mount/focus picks up the refreshed snapshot), while a live discovery — which just measured the truth — is trusted for the full 5min so a cold runtime is never re-enumerated inside one form session. `gcTime` stays 30min, so a revisited runtime still renders from cache and revalidates in the background; the pickers gate their spinner on `isLoading`, which stays false throughout. Must-fix 2 — both model-discovery responses go through a zod schema. `POST /api/runtimes/{id}/models` and its poll companion were casting network JSON to `RuntimeModelListRequest`, which the root CLAUDE.md API-compatibility rules forbid. Added a lenient schema (`status` stays `z.string()`, `supported` defaults to true, `.loose()` keeps unknown fields) plus a fallback record whose `status` is `failed`: a malformed body now surfaces "discovery failed" with manual entry still usable instead of a fabricated empty catalog or an endless spinner. `resolveRuntimeModels` was tightened to match — only an explicit `completed` is a catalog, so an unrecognised status is an error rather than a silent empty list, and `supported` can no longer be `undefined`. Nit — the in-memory catalog cache now deep-copies each entry's `Thinking` (and its level slice) and `ServiceTiers`, so it delivers the independent value its comment promises and matches the Redis backend's JSON round-trip semantics. Tests: staleTime policy for cached/live/no-data; a QueryObserver test proving the refreshed catalog reaches the same client with no blank loading state; unknown-status and omitted-`supported` handling; schema tests for live, cached, old-backend and nine malformed shapes; client tests that both endpoints degrade to an explicit failure; nested-field mutation isolation for the cache. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
30318b79bc |
MUL-5426: fix(daemon): retire sessions whose history the provider refuses to replay (#6083)
* fix(daemon): retire sessions whose history the provider refuses to replay A run killed mid-reply (machine shutdown, force-quit, SIGKILL) can leave an empty assistant message in the agent CLI's transcript. Every later resume replays it, the provider rejects the request, and the (agent, issue) pair is bricked with no self-healing and no user-facing recovery. Multica already has the mechanism for this — poisoned-session classification — but its detector paired "400" with "invalid_request_error", which is the Anthropic wire shape. The same defect reported by any other provider carried neither token, so it classified as agent_error.unknown: resume-safe by omission. GetLastTaskSession kept handing back the dead session on every follow-up, manual Rerun resolved it through the same predicate, and the in-turn fresh-session retry never fired because ResumeRejected is false here (nothing rejected the resume — the transcript loaded and the provider refused to replay it). Add taskfailure.UnresumableHistory, which recognises the defect by what the provider says is wrong — some content is empty, and here is which message in the history — rather than by status code or provider name. Both signals are required, so a tool reporting "field must not be empty" does not match. Wire it into the four places that decide whether a session survives: - classifyPoisonedError, so the task is written as api_invalid_request - shouldRetryWithFreshSession, so the turn recovers on all 17 backends instead of the subset whose adapter learned to detect it; the tools == 0 gate is unchanged, so a run that already used a tool is never re-run - ResumeUnsafeFailure, covering the manual-Rerun path - both resume queries, as defense-in-depth for hosts whose daemon predates this (self-host daemons upgrade on their own cadence) Fixes #6066. Also covers the daemon half of #5760. Co-authored-by: multica-agent <github@multica.ai> * fix(session): close the Chat and fresh-retry paths that resurrect a poisoned session Review found the previous commit stopped short in two places, both of which put the dead transcript back in play. Chat never consulted the guarded query. The claim handler reads chat_session.session_id first and only falls back to GetLastChatTaskSession when it is empty, so a poisoned pointer there bypasses every filter that query applies. The fail path merely declined to OVERWRITE the pointer, leaving it in place. It now clears it in the same transaction, matched on session and runtime so a concurrent turn's newer pointer survives. The promote guard moves to ResumeUnsafeFailure as well — the reason-only check passed an un-upgraded daemon's agent_error.unknown row and re-pinned what the clear had just removed. GetLastChatTaskSession also kept the row-level filter the issue query dropped in GH #5975: it discarded the newest poisoned row and fell back to an older completed row carrying the same dead session. It now judges each session by its latest terminal state, matching GetLastTaskSession. A recovered turn could not retire anything. A terminal report carried one session_id, and an empty one meant both "nothing to report" and "forget the old session", so a fresh-session retry that SUCCEEDED left the id it retried away from selectable — through an older completed row on the issue, or through the chat pointer. agent_task_queue.retired_session_id records the abandonment itself, reported on every terminal path including completed, and both resume lookups exclude it. This is the contract gap the previous PR deferred; the fresh-retry path now runs on all backends, so deferring it is not safe. Also narrows what the cross-backend test claims: it pins the shared decision, not that all 17 adapters surface the error into Result.Error (#5760 is the counter-example), and says so. Co-authored-by: multica-agent <github@multica.ai> * test(session): require pgx.ErrNoRows in the resume-exclusion assertions The `if err == nil && prior.SessionID.Valid` form these tests shared is false-green: any real fault — undefined column, syntax error, dead connection — makes err non-nil, so the condition is false and the test passes. Run against a database missing this branch's new column, the exclusion tests reported PASS on a SQLSTATE 42703, meaning they could not have caught a broken query. requireSessionExcluded demands pgx.ErrNoRows specifically and fails loudly on anything else, so a green run now means the filter worked rather than the query never ran. Applied to all nine sites, not just the four this branch added: the other five guard the same GetLastTaskSession exclusion behaviour that this branch changes, so leaving them false-green would leave the change under-tested. All nine pass on a correctly migrated database. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
07a78d910d |
fix(daemon): discover agent CLIs installed after startup (MUL-5439) (#6086)
* fix(daemon): discover agent CLIs installed after startup (MUL-5439) The built-in agent availability set was built exactly once, in LoadConfig, and every later consumer read that static map. A CLI installed while the daemon was running was therefore invisible until the daemon process restarted. On Desktop that is worse than it sounds: the app defaults to autoStop=false and auto-start only compares CLI versions, so quitting and reopening the app does not restart the daemon. A user who installed a CLI, verified it in their shell, and relaunched the app was left with a runtime that never appeared — which is GH #6077, reported against Antigravity but not specific to it. - Extract discovery into probeAgentCLIs (pure availability, no version gate). - Add agentDiscoveryLoop: re-probe every 2 minutes and register providers that appeared, reusing applyRegisterResponseInPlace so nothing restarts and no in-flight task is interrupted. RecoverOrphans is deliberately not called here (MUL-3332): surviving runtimes may be executing tasks. - Additive only. A provider that stops resolving is kept, because a narrower PATH or a version manager mid-upgrade would otherwise tear down a working runtime. Removal stays with an explicit restart. - Hold the set in an atomic.Pointer copy-on-write: cfg.Agents was read unlocked from task-execution paths, so a mutable map would be a data race. - Cache the login-shell PATH fallback process-wide with a 30m TTL keyed on PATH/SHELL/HOME, so the 2-minute loop stays a pure LookPath sweep instead of forking the user's rc files every round. - Report skipped_agents on /health with the reason a discovered provider was dropped at registration, so "not installed" and "installed but rejected" stop looking identical (they were only distinguishable in the daemon log). - Fix the onboarding template in all four locales: it told users that restarting the desktop app was enough, which is exactly the false lead the reporter followed. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): retry discovery until registered, and never evict live runtimes Addresses both P1s from review. P1-1: a failed first attempt was never retried. refreshAgentAvailability published a newly discovered provider into the availability set and only acted on providers "gained" in that same round, so a version probe that timed out or a register call that failed left the provider permanently unregistered — the user was back to restarting the daemon, with skipped_agents able to explain the problem but not fix it. Registration is now driven by live state instead of by the round that discovered the provider. providersMissingRuntimes derives, from runtimeIndex, which discovered providers lack a built-in runtime in each tracked workspace; convergeRuntimeRegistrations registers only those, for only the workspaces that need them. Nothing records "already handled", so a version-probe failure, a register failure, or a partial failure across workspaces all retry on the next tick, and a provider rejected for being below the minimum version recovers on its own after an in-place upgrade. A permanently stuck provider is bounded by exponential backoff (one discovery interval up to 30m) on the expensive half only; discovery itself stays on its 2-minute cadence, and the steady state issues no version probes and no register calls at all. P1-2: the "additive only" refresh could evict existing custom runtimes. applyRegisterResponseInPlace treats the response as authoritative and drops prior runtime IDs it does not mention — correct for the convergence paths that re-derive a whole runtime set, wrong here. appendProfileRuntimes is best-effort, so one failed GetRuntimeProfiles call yields a builtins-only response, which would evict the workspace's custom profile runtimes from runtimeIndex and stop their heartbeats, possibly mid-task. Added mergeRegisterResponseInPlace: indexes and appends returned runtimes, never deletes an unmentioned one, and keeps profileSetSig when the fetch failed. Safe because the server's register endpoint is a pure per-entry upsert that prunes nothing, so omitted runtimes still exist server-side. ID rotation is still handled destructively for that one runtime, so a re-issued ID replaces its predecessor instead of leaving two heartbeat goroutines. New regression tests: first version probe fails then recovers; first register call fails then recovers; partial failure across two workspaces retries only the one that needs it and makes exactly one call; a failed profile fetch leaves the custom runtime indexed, watched, and its signature intact; below-minimum provider registers after an upgrade; steady state re-probes nothing; rotated runtime ID is swapped not duplicated; stuck provider is retried but bounded. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): keep CLI discovery out of custom-profile convergence (MUL-5439) Third-round review P1: discovery could mask a concurrent profile disable. The discovery path registered through registerRuntimesForWorkspaceBatch, which fetches the workspace's custom runtime profiles and returns their content signature, and the additive merge cached that signature. So if a user disabled a custom profile at the same moment a newly installed CLI was discovered, the merge correctly kept the disabled profile's runtime ID (it must not delete unmentioned runtimes) while recording the POST-disable signature. refreshWorkspaceRuntimeProfiles short-circuits on a matching signature, so the drift path then saw "already converged" and the disabled runtime stayed tracked and heartbeating forever. Discovery is now strictly built-ins only: - registerBuiltinRuntimesForWorkspace posts a builtins-only register request. It never calls appendProfileRuntimes, so discovery cannot observe the profile set at all and there is no signature to cache. - mergeRegisterResponseInPlace becomes mergeBuiltinRegisterResponse: it ignores any entry carrying a ProfileID (invariant guard — only the drift path may introduce a custom runtime), and neither reads nor writes profileSetSig. - Custom profile add/edit/disable remains owned exclusively by refreshWorkspaceRuntimeProfiles. Regression tests: a profile disabled concurrently with a new CLI install is still converged away by the drift path (this test reproduces the reviewer's exact failure — "disabled custom runtime rt-2 remained tracked" — when the old signature write is replayed); the merge ignores profile-bearing entries; the merge leaves profileSetSig untouched. The profile-fetch-failure test now also asserts the signature is unchanged rather than merely non-empty. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
0066ab259e |
refactor(agent): drop unreachable inline system-prompt branches (MUL-5392) (#6050)
* refactor(agent): drop unreachable inline system-prompt branches (MUL-5392) The daemon only populates ExecOptions.SystemPrompt for openclaw, kimi and traecli (providerNeedsInlineSystemPrompt); every other backend receives the runtime brief as a per-task context file in the workdir. The inline branches in claude, codex, opencode and pi were therefore dead, and read as if they were the live delivery path. Probed each backend over its real launch path with a canary in the context file and no inline delivery — claude 2.1.220 (CLAUDE.md), codex 0.144.6 via the app-server, opencode 1.17.7, pi 0.67.2, hermes 0.18.2 via ACP — and all of them picked the brief up from disk, so the branches were removable with no behaviour change. An empty workdir returned no canary, confirming the probe could fail. opencode's branch was worse than dead: `opencode run` has no --prompt flag, so enabling inline delivery there would have made every opencode task exit 1 with a usage dump. The DevEco backend, forked from opencode, already documents this constraint; opencode itself never got the fix. Regression tests pin all three arg builders against re-adding the flag, and providerNeedsInlineSystemPrompt now documents what was verified and what is still unprobed (grok, qoder, codebuddy). Hermes and kiro are untouched: their exclusion is deliberate and already tested. Co-authored-by: multica-agent <github@multica.ai> * test(agent): pin codex developerInstructions contract, drop stale pi flag doc Review follow-up on MUL-5392. buildPiArgs' doc comment still advertised --append-system-prompt after the branch that emitted it was removed — exactly the stale-signal this PR set out to delete. The two codex sites fixed to a literal nil had no regression test, so restoring nilIfEmpty(opts.SystemPrompt) would still have gone green. Both thread/start and thread/resume now run with a canary SystemPrompt and assert developerInstructions comes through as an explicit null. Mutation-checked: reverting either site fails its test. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1018c052e9 |
test(daemon): extend the byte-identity guarantee to chat and the other kinds (#6033)
The MUL-5377 regression guard only covered issue runs, but chat sessions resume too: handler/daemon.go:2172 hands the daemon a PriorSessionID from the chat_session row, with the same PriorWorkDir and PriorSessionResumeUnavailable plumbing as an issue task. A chat brief that varied per turn would lose the prompt cache the same way, and a long chat is exactly where that costs most. The three blocks that moved out of the brief (Task Initiator, Session Continuity Notice, Connected Apps) were removed for every kind, so chat is already stable — this locks that in rather than leaving it as an accident. The initiator variant matters most: in a Slack-backed session a different person can trigger each turn, which is precisely when the old brief's Task Initiator block changed. Autopilot and quick-create are single-shot today; the invariant is free to hold for them too and stops a future resume path from silently reintroducing the bug. Test-only change. Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4eb80c8b77 |
fix(daemon): keep the runtime brief byte-stable across triggers (MUL-5377) (#6021)
* fix(daemon): keep the runtime brief byte-stable across triggers (MUL-5377) Claude Code loads the runtime brief (CLAUDE.md / AGENTS.md) into messages[0], ahead of the entire conversation. A cache breakpoint is all-or-nothing, so a single differing byte there invalidates the prompt cache for the whole history on every `--resume`. InjectRuntimeConfig rewrites that file on every run and interpolated nine per-run values into it, so in practice the cache was thrown away on the first comment that landed on any issue. Measured on one issue over three runs: run 1 (cold) spent 89.9k cache-write tokens building 105k of context; runs 2 and 3 each spent ~425k re-creating a prefix they should have read. 842k of 946.5k cache-write tokens (89%) went into re-creation, with only tools[]+system[] surviving each resume (a constant 18,085 tokens both times). Fix: the brief now carries only what is stable for the lifetime of a resumed session, and per-run state travels in the per-turn user message, which is appended after the cached prefix. - Merge kindCommentTriggered + kindAssignmentTriggered into kindIssue, and stop reading TriggerCommentID in classifyTask. The brief can no longer diverge by trigger type structurally, rather than by convention. - Replace writeWorkflowComment/writeWorkflowAssignment with one writeWorkflowIssue that routes on the per-turn message. The mode-specific status rules live inside their own mode block, so "own the status arc" and "do not touch the status" can never be read as unarbitrated peers. - Move Task Initiator, Session Continuity Notice and Connected Apps out of the brief into BuildPrompt via BuildTaskInitiatorBlock / SessionContinuityNotice / BuildConnectedAppsBlock. - Drop TriggerCommentID, TriggerThreadID, NewCommentsSince, NewCommentCount, PriorSessionResumed and CommentReplyTargets from the brief; BuildPrompt already emitted all six from the same helpers, so this is de-duplication. - Set PriorSessionResumeUnavailable on `task` as well as `taskCtx` in both local resume gates, or the notice would silently vanish on exactly the failure path it exists to disclose. Tests: TestInjectRuntimeConfigByteIdenticalAcrossTriggers renders the brief across nine per-run variants (trigger type, differing comment/thread ids, resume delta, resume-unavailable, cross-thread fan-out, member/agent initiator, connected apps) for two providers and requires bytes.Equal, with a non-vacuity guard so it cannot pass on a function that ignores its input. Daemon-side tests assert the moved sections still reach the agent through the per-turn prompt. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): route the issue workflow on an explicit turn-mode marker Review follow-up on the mode router. The brief said Reply mode applies when the per-turn message "opens with a [NEW COMMENT] block", but buildCommentPrompt writes two paragraphs before that block and only emits it when TriggerCommentContent is non-empty. Two ways to get it wrong: - The message never literally opens with the block, so the router's own wording did not match the prompt it describes. - A comment-triggered run with an empty comment body — or an older server that does not send one — emitted no block at all. An agent following the brief would fall through to Ownership mode and change the issue status on a turn whose rule is "do NOT change the issue status". BuildPrompt now emits an unconditional `**Turn mode: Reply.**` / `**Turn mode: Ownership.**` line from the same branches it uses to pick a code path, and the brief routes on that marker. Brief and prompt can no longer disagree about the mode, because the value that selects the path also states it. The router also names a safe fallback (treat an unlabelled turn as Reply mode and leave the status alone). Tests: TestTurnModeMarkerAlwaysPresent covers comment-triggered with and without comment content, plus both assignment shapes; TestTurnModeMarkerAbsentOnIssuelessKinds keeps the marker off chat / quick-create / autopilot; TestBriefModeRouterMatchesPromptMarkers fails if the brief ever describes a marker the prompt does not emit. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ba108978ac |
fix(channel): deliver the final answer only to Slack and Lark (MUL-5378) (#6016)
* fix(channel): deliver the final answer only to Slack and Lark (MUL-5378) Channel replies could carry the agent's interim narration alongside its answer (GH #6006). Two independent causes, both fixed at the layer that owns the deliverable. Prompt regression. #4776 told every channel-backed chat to reply with the answer only and not narrate. The MUL-4899 split (#5557) moved that rule into the Slack branch along with the `chat history` / `chat thread` commands its wording happened to mention, so Feishu/Lark silently lost it on 2026-07-17. The rule is a third axis — it keys off "is there a channel at all", like the attachment-upload axis — so it now sits outside the Slack gate, generalized from "these history reads" to any progress note. The old two-layer matrix could not catch the regression because it only asserted the rule on the Slack case; the new test pins all three states. Runtime contract. Result.Output is documented as "final user-facing output selected by the backend" (agent.go), but Codex concatenated every agent_message and Copilot joined every assistant turn with "\n\n", so a tool-using run handed the daemon narration + answer as one string. Codex now takes the message its app-server labels phase="final_answer", falling back to the most recent agent message on the legacy protocol; Copilot keeps the latest complete turn, with the streaming deltas retained as the process-died-mid-turn fallback. This narrows delivery only — every message still streams as MessageText, so the Multica transcript is unchanged. Claude Code, CodeBuddy and qwen already selected a terminal result and are untouched; opencode/deveco/openclaw share the accumulating shape and want the same audit (pi was already fixed this way in #4894). Verified: go build ./..., go vet, full ./pkg/agent and ./internal/daemon/... suites pass locally. Co-authored-by: multica-agent <github@multica.ai> * fix(channel): scope the no-narration rule to process, not results Review follow-ups on the channel delivery rule and the Copilot turn boundary. The prompt said a reply "must not say what you are about to do or just did", which literally forbids the deliverable itself: asked to create an issue, the correct reply IS "created issue X". Rewritten to ban planned and in-progress narration while explicitly protecting the completion confirmation. The test now pins both halves — a future edit that drops the carve-out, or restores the blanket past-tense ban, fails. The example also referenced "check the code", which is not a thing an agent does inside a Slack or Lark conversation. Replaced with a generic "let me look into that first". Copilot cleared pendingDelta only when the authoritative assistant.message carried content. A tool-only turn reports content:"" with the requests as the whole turn, so its streamed deltas stayed buffered and were stitched onto the next turn's partial text if the process then died mid-stream — verified: the new test yields "Checking the logs now.The retry loop" before the fix. The reset now happens on every assistant.message, since that event is the turn boundary regardless of whether it carries text. Verified: go build ./..., go vet, full ./pkg/agent and ./internal/daemon/... suites pass locally. Co-authored-by: multica-agent <github@multica.ai> * fix(channel): tighten the no-narration rule to one sentence Same contract, fewer tokens: the four-line rationale comment collapses to one, and the delivery rule drops the restatement, the second example and the completion examples. What survives is exactly the semantic boundary the tests pin — no planned/in-progress narration, completed actions still count as the outcome — plus the one narration example actually observed in the report. Verified: go build ./..., go vet, ./internal/daemon/... suite pass locally. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c271f80999 |
MUL-5370 fix: label stalled skill-bundle downloads, align failure-reason copy with the backend taxonomy (#6001)
* fix(daemon): label stalled skill-bundle downloads and make them retryable A skill bundle that could not be downloaded during task preparation surfaced as the bare string "resolve skill bundles: context deadline exceeded". taskfailure.Classify has no rule for a Go context deadline, so it landed in agent_error.unknown — a bucket that is NOT on the server's retry allowlist. A transient stall therefore became a terminal chat failure carrying a label nobody could act on, and the failure was invisible on the Usage page's Errors breakdown. (MUL-5370) - Add the platform-side reason skill_bundle_unavailable and put it on retryableReasons. Retrying is cheap and safe: the agent process never started, and bundles that did arrive are already cached on disk, so successive attempts converge. - Carry a sentinel error from the resolve loop so the reason is derived structurally rather than by matching the wrapped transport error's text, and name the skill, its declared size and the elapsed wait in the wrap — enough to tell "this bundle is too big for the link" from "the link is dead" without reading daemon logs. - Normalise the wire shape an OLD daemon produces (a non-empty catchall plus the previous "resolve skill bundles:" wrapper) on the server side. Installed daemons upgrade on their own cadence, and FailTask only classifies when the caller supplied nothing, so without this the fix would reach only hosts that happened to update — while the un-upgraded hosts most likely to be hitting the bug kept failing terminally. - Teach Classify about "deadline exceeded" and net/http's "Client.Timeout exceeded while awaiting" so any other Go-side deadline that reaches it as text stops falling into the unknown bucket too. - Backfill historical rows in both agent_task_queue and chat_message. Scoped to agent_error.unknown alone — the old wrapper string postdates the in-flight classifier by three weeks, so no row carrying it can hold the legacy coarse value — which keeps the down migration an exact inverse. Co-authored-by: multica-agent <github@multica.ai> * fix(chat): give chat its own failure copy for the refined reasons #5991 rebuilt the operator-facing failure labels around an open wire string with a raw-value fallback, but the chat bubble kept its own exact-key lookup against the six coarse values from migration 055. So all 14 agent_error.* values still missed and rendered the generic "Something went wrong and the agent couldn't finish replying" — the classification the backend had already computed was discarded at the last step, and that is the message the MUL-5370 reporter saw. - Add resolveFailureReasonKey in packages/core: exact match, else degrade an `agent_error.*` value to its family, else undefined. A reason newer than the shipped client now lands on the family line instead of the fallback. - Rekey the chat copy map by wire value and route it through the helper. Chat deliberately degrades to friendly copy rather than adopting the operator surfaces' raw-value fallback: it is read by the person who just sent a message, and the raw error is one click away under the collapsible. - Add refined chat copy (en / zh-Hans / ja / ko) only where it can say something the family line can't — a different next step: network, auth, quota, rate limit, context overflow, missing/outdated CLI, skill download. - Give skill_bundle_unavailable a label on the web and mobile surfaces and a class on the Usage page's Errors breakdown (runtime — the operator response is "check the daemon's link to Multica", the provider is not involved). - Mobile's two label maps were still coarse-only for the same reason; rekey them by wire value and fill in the refined taxonomy. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
7d2d745d9d |
fix(daemon): probe built-in agent CLIs once per registration batch (MUL-5225) (#5842)
Built-in agent CLIs are a machine-level fact, but runtime registration is per-workspace, and every registration re-ran `<cli> --version` for every configured agent. A daemon serving N workspaces spawned N×M probe processes at startup instead of M (24 workspaces × 5 agents = 120 instead of 5). - Detect the built-in payload once per workspace-sync batch and reuse it for every workspace that sync registers. Nothing is cached on the Daemon, so standalone re-registrations still re-probe and an in-place CLI upgrade is still reported with its current version. The probe is lazy: a steady-state sync with nothing to register never shells out. - Retry a failed provider's probe once inside the round before dropping it, so one transient failure can't cost every workspace in the batch that runtime. Only failed providers retry, so the round stays O(M). - Time each attempt across resolve + detect, so a slow probe inside the MUL-4486 self-heal is not misread as a fast failure and retried. Fixes #5837 Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
2294f450e8 |
fix(kiro): recover from oversized-history-image session resume failures
Merges MUL-5338 / fixes GH #5975. |
||
|
|
85a14cde37 |
fix(daemon): gate codex session pointer writes on rollout presence (MUL-5305) (#5960)
* fix(daemon): gate codex session pointer writes on rollout presence (MUL-5305) Codex issue follow-ups on local_directory projects intermittently lost their session: the server sent a prior session whose rollout was not in the task CODEX_HOME, so the daemon dropped the resume and started a fresh thread (gateCodexResumeToRolloutPresence), losing the conversation. Root of the bad pointer: the daemon persists a Codex session id as the resumable pointer at two points -- the mid-flight pin and the terminal report -- before the rollout is guaranteed on disk. A task that exits early (crash / runtime offline / timeout) leaves a pinned/reported session id with no rollout; GetLastTaskSession (which accepts failed rows) then hands it to the next follow-up, which drops it. Enforce the invariant at write time: only record a Codex session as the resumable pointer once its rollout is present in the per-issue store, with a short bounded wait for flush. If it never lands, don't overwrite the last good pointer -- a blanked session_id becomes NULL server-side, so GetLastTaskSession falls back to the most recent session whose rollout is real. Non-Codex providers are unaffected; crash recovery is preserved because a present rollout still pins. - codexSessionResumable: shared write-time presence check (bounded wait) - runTask: gate the terminal session_id before reporting - executeAndDrain: gate the mid-flight pin (thread codexHome through) - tests: helper cases + behavioral pin test Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): address review — don't silently downgrade completed sessions (MUL-5305) Follow-up to review feedback on #5960: - Must-fix 1 (silent downgrade): limit the write-time session withholding to NON-completed terminal states. A missing rollout means no resumable conversation was persisted, so a withheld non-completed attempt loses nothing; a completed session is authoritative and, if its rollout is anomalously absent, is still recorded so the next run's resume gate discloses the loss (PriorSessionResumeUnavailable, MUL-4424) instead of silently falling back to an older session. Extracted resumableTerminalSessionID. - Non-blocking risk: pin the mid-flight resume pointer with a per-status presence check instead of one fixed 2s window, and set sessionPinned only once the rollout is confirmed, so a rollout that lands shortly after the first status is still pinned this run. - Must-fix 2 (regression coverage): pin skipped when rollout absent (no /session call); terminal helper (completed keeps / failed withholds); and a DB-backed GetLastTaskSession test proving the next claim falls back to the older recorded session when the latest was blanked. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): disclose Codex session continuity gaps end-to-end (MUL-5305) Addresses review feedback on #5960. Must-fix 1 — a completed turn whose rollout is missing is exactly the #5934 case (the reporter waits for each turn to finish), so it can no longer be excluded from withholding. Withhold the session for ANY terminal state, and pair the withhold with a persisted continuity-gap signal so the next claim still discloses the loss even while resuming an older good session: - new agent_task_queue.session_rollout_missing column (migration 224) - daemon sends session_rollout_missing on the terminal report; the handler clears the resume pointer (MarkTaskSessionRolloutMissing, overriding FailAgentTask's COALESCE) and flags the row - claim reads GetLatestTaskRolloutMissing and sets a new prior_session_resume_unavailable response field, which the daemon ORs into the brief's PriorSessionResumeUnavailable disclosure Must-fix 2 — Codex reveals the session id on a single task_started status, so a one-shot presence check missed a rollout that flushed later and lost in-flight crash recovery. Pin via a background waiter bounded by the run's context that pins the moment the rollout lands. Tests: - completed + rollout missing -> next claim withholds the bad session AND flags the continuity gap (cross-layer DB test) - session pinned once its rollout appears after the status (mid-run) - pin skipped while the rollout is absent Co-authored-by: multica-agent <github@multica.ai> * fix(server): make continuity-gap write atomic + disclose on all claim paths (MUL-5305) Addresses review round 3 of #5960. Must-fix 1 — the previous handler-level marker ran AFTER the terminal transaction committed, and FailTask creates + wakes the auto-retry inside that same transaction, so a retry could claim the rollout-missing session before the marker cleared it (and a marker failure was swallowed). Move session_rollout_missing INTO the terminal write: CompleteAgentTask and FailAgentTask now force session_id NULL (overriding Fail's COALESCE that would keep a stale mid-flight pin) and set the flag in the SAME UPDATE, so the withhold + gap flag commit atomically with the retry creation. The flag is threaded through TaskService.CompleteTask/FailTask; the swallowed best-effort MarkTaskSessionRolloutMissing query is removed. Must-fix 2 — the daemon withholds for all Codex tasks, but only the issue non-rerun claim consumed the disclosure. Now every fallback path sets prior_session_resume_unavailable: the manual-rerun branch reads the source task's session_rollout_missing, and the chat branch reads a new GetLatestChatTaskRolloutMissing. Tests (cross-layer DB): - completed + rollout missing via the real CompleteAgentTask terminal write -> session withheld AND gap flagged - failed + rollout missing forces session_id NULL over the COALESCE- preserved mid-flight pin in ONE statement Deploy order: migration + server first, daemon second (new fields are omitempty and ignored by an old peer). Co-authored-by: multica-agent <github@multica.ai> * fix(handler): return 5xx on FailTask error + cover claim-response gap paths (MUL-5305) Addresses review round 4 of #5960. Must-fix 1 — the FailTask handler returned 400 on a service/DB error, but the daemon's terminal callback treats 400 as permanent (postJSONWithRetry / isTransientError bails without retrying). Since the fail transaction is now the sole persistence point for the withheld session + continuity-gap flag + auto-retry, a rolled-back fail must be retried, so return 5xx (an invalid request body still returns 400), mirroring CompleteTask. Regression: client.FailTask retries on a transient 5xx and eventually succeeds. Must-fix 2 — add claim-response-level regressions that drive the two new disclosure branches through buildClaimedTaskResponse: - chat: the latest terminal task on the session withheld -> the next chat claim sets prior_session_resume_unavailable - manual rerun: the source task withheld -> the rerun claim discloses These handler DB tests run under CI's fully-migrated database (the local workspace DB cannot set up the handler fixture). Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
28a4203bc2 |
MUL-5261: revert domain-specific prompt additions (#5913)
Revert the built-in focused-testing skill (#5877) and the always-on Repository Setup Preflight brief section (#5886). Both delivered software-engineering domain content through platform-level prompt surfaces that every agent receives regardless of workspace type. - multica-focused-testing was the only built-in skill that did not describe a Multica platform contract, and the only one without `user-invocable: false` / `allowed-tools: Bash(multica *)`. Built-in skills are meta/system skills; a workspace with no repository bound still carried it in its skill index and slash-command menu. - Repository Setup Preflight was emitted for every non-quick-create task without consulting `ctx.Repos`, so non-code workspaces received build/dependency instructions in the always-on brief. writeRepositories already elides itself when no repo is bound; this section did not. Pure revert. No replacement behavior is introduced here. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3c6bebaff0 |
MUL-5274: allow explicit persistent service handoffs (#5895)
* fix(runtime): allow explicit persistent service handoffs Co-authored-by: multica-agent <github@multica.ai> * fix(runtime): resolve review ambiguities in persistent-service handoff wording Address MUL-5274 review findings on #5895: - Drop the "The rules above apply only to work owned by the current run" scoping sentence: with the persistent-service exception inserted above it, it would have swept in work that is precisely no longer run-owned after handoff. The external-systems bullet carries the boundary on its own, and both pin tests now reject any "The rules above" reintroduction. - Replace "detach it" (skill-level mechanism) with the lifecycle contract: hand off only once the service no longer depends on this run. - End the negative-boundary bullet with "the CI-specific rules below still apply" instead of "must be collected before exit", which misread as license to start CI polling and collect it. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
d84e5654df |
fix(runtime): add repository setup preflight (#5886)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
581d9527ba |
feat(vcs): self-hosted Git providers (Forgejo, Gitea, GitLab) alongside GitHub (MUL-3772) (#5006)
Adds self-hosted Git provider support (Forgejo, Gitea, GitLab) alongside GitHub: per-workspace token connection, a provider-dispatched webhook, PR/MR and CI mirroring, and the shared issue auto-link / auto-close machinery. Off until MULTICA_VCS_SECRET_KEY is set, so existing deployments are unaffected. Co-authored-by: Bohan <bohan@devv.ai> |
||
|
|
a3fe6d91dd |
MUL-5150: add project context to Chat (#5765)
* feat(chat): add project context Co-authored-by: multica-agent <github@multica.ai> * fix(chat): resolve MUL-5150 review blockers - Renumber project-context migrations to unique prefixes after current main: 206_chat_session_project -> 212 (column), 207_chat_session_project_index -> 213 (concurrent index). 206/207 collided with 206_agent_disabled_runtime_skills and main's 207-211 client_usage_daily set. - Add the 4 missing chat input.project_context keys to ja/ko locales so the locale parity test passes (en/zh-Hans already had them). - Lock the project-context control while a send is in flight (isSubmitting), not just while the agent is running. A brand-new chat creates its session lazily during send bound to the project at click time; switching project mid-send would create the session against the stale project and clear the editor as if the send landed on the new selection. Add a regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): complete project context handling * fix(chat): pin fresh chat to open session's agent on project switch Switching an existing session to a different project opens a fresh chat but only cleared the active session, dropping selection back to the stored `selectedAgentId`. When that preference was stale (open session belongs to agent B while the persisted pick is still agent A), the lazily-created session and its first send bound to the wrong agent (agent A). Extract the project-switch decision into a shared `planProjectContextChange` pure helper in use-chat-controller.ts and route both chat surfaces (the chat tab controller and the floating ChatWindow) through it, so the fresh chat is pinned to the open session's agent and the rule cannot drift between the two copies. Add a dual-entry regression test (pure-fn guard + controller integration) covering the stale selectedAgentId case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * chore(ci): re-trigger required checks on latest head The prior push updated the branch ref but GitHub did not emit a pull_request synchronize for it (PR head-sync lag), so CI/Mobile Verify never ran on the commit carrying the stale-agent project-switch fix. Empty commit to force a fresh synchronize on a head that includes it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): renumber project migrations to 213/214 after main added 212 Current main added 212_agent_service_tier; the PR's 212/213 chat migrations collided with it on the merge ref, failing TestMigrationNumericPrefixesStay UniqueAfterLegacySet. Merge current main and move the chat column migration to 213 and the concurrent index migration to 214 (column before index preserved). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): lock ProjectPicker clear control during send (keyboard path) The send-pending lock only put pointer-events-none on the wrapper, which blocks the mouse but leaves ProjectPicker's inline clear button in the tab order — a keyboard user could Tab to "Remove from project" and press Enter mid-send, detaching the project after the lazily-created session already went out with the old one (reopens the mid-send retarget path via keyboard). Add an explicit `disabled` capability to the shared ProjectPicker that locks the trigger, the menu (forced closed), and the inline clear button (disabled + out of the tab order). Defaults to false, so issue/create/autopilot callers keep their hover/keyboard clear. ChatInput passes disabled while the project selection is locked. Tests: real-ProjectPicker regression (keyboard activation of the clear control is inert when disabled; still works when enabled) + ChatInput wiring assertion that the picker is disabled mid-send. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Walt <walt@multica.ai> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Co-authored-by: NevilleQingNY <nevilleqing@gmail.com> |
||
|
|
ffa8e16369 |
MUL-5228 fix(usage): bill Grok at xAI's reported cost, fix $0 resumed sessions (#5841)
* fix(agent): attribute Grok usage from the turn's own model id A resumed Grok session with no configured model recorded its entire spend under the model id "unknown", which matches no pricing row — so the task reported $0 cost instead of its real spend. grok.go only learned the model from the session handshake, and ACP's `session/load` carries no model id (only `session/new` does). When neither the agent nor MULTICA_GROK_MODEL pins a model, `daemon.go` legitimately passes an empty model, leaving nothing to attribute the usage to. Every Grok turn stamps `result._meta.modelId` with what it actually billed against. Parse it in the shared ACP result parser and use it as the fallback in grok.go. Other ACP backends are untouched — they keep whatever the handshake gave them. Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * fix(metrics): price the Grok catalog in server-side cost metrics server/internal/metrics/pricing.go carried no Grok rows at all, so RecordLLMUsage took the unpriced branch for every Grok turn: llm_cost_usd reported zero Grok spend while the tokens accumulated in llm_unpriced_tokens. Internal cost monitoring simply could not see Grok. Add the six SKUs xAI publishes rates for, mirroring the frontend table in packages/views/runtimes/utils.ts. Aliases are anchored exact matches like the gpt-5.6 rows, so `grok-composer-*` (in the catalog, absent from the price sheet) stays unmapped instead of inheriting a guessed rate. Short-context tier on purpose: xAI bills a request at 2x once its prompt reaches 200K tokens, but a usage record aggregates every model call in a turn and cannot say which tier an individual request hit. A regression test re-derives the cost of a real grok 0.2.106 turn from the table and checks it against the costUsdTicks xAI returned for that turn. Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * docs(changelog): scope the Grok cost claim to what was actually fixed The v0.4.9 entry promised "accurate cost" in all four languages, but the fix corrected catalog pricing and cached-input double-counting — it did not implement xAI's 2x long-context tier, so a turn whose requests reach 200K prompt tokens still under-reports by up to 50%. Say what was fixed instead. Also correct two stale claims in the pricing comment: the daemon tags usage rows with the runtime provider `grok`, not `xai` (the bare `grok-*` keys are what make them resolve), and record why thresholding the long-context tier on an aggregated row would be worse than not pricing it at all. Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * feat(usage): carry the provider's own cost through to the usage record Cost has always been derived client-side as tokens x a static rate, which cannot express request-level pricing rules. xAI bills a Grok request at 2x once its prompt reaches 200K tokens, and a task_usage row aggregates every model call in a turn — so the stored token counts genuinely cannot say which tier any individual request hit. Thresholding on the aggregate would be worse than the status quo: it turns a bounded 50% under-estimate into an unbounded over-estimate for turns made of many short requests. Grok already reports what it charged, per turn, in `_meta.usage.costUsdTicks`. Parse it, carry it through agent -> daemon -> API, and store it on task_usage as a nullable BIGINT of 1e-10 USD ticks (integer, so sub-cent turns stay exact end to end). NULL means the provider reported no cost — every pre-existing row and every provider that doesn't return one. No backfill: there is no authoritative figure to recover for those, and inventing one is the guess this removes. A single hourly bucket can mix rows that carry a cost with rows that don't, so task_usage_hourly gains both halves: `cost_usd_ticks` sums the authoritative side, and `uncosted_*_tokens` carry exactly the tokens that still need a rate-table estimate. Consumers report authoritative + estimate(uncosted), which degrades to today's behaviour when nothing in the bucket is authoritative. The existing token columns keep covering every row, so token displays are untouched. The new columns are additive with defaults, so the unique key, the dirty-queue shape, and migration 102's triggers are unaffected. Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * feat(usage): prefer the provider's own cost over the rate table With the authoritative figure now stored, both cost consumers use it: the usage dashboard (estimateCost / estimateCostBreakdown) and the server-side llm_cost_usd metric. Each reports `authoritative + estimate(uncosted tokens)`, so a row or bucket that mixes priced and unpriced sources stays whole. The static rate tables remain, but for Grok they are now a fallback — they still price usage recorded by a daemon too old to report cost, and every provider that reports none. Custom pricing overrides likewise apply only to the estimated half: they are a user's guess at a rate, and the authoritative half is not a guess. A model with no rate-table row but a provider-reported cost now also drops out of the "unmapped models" banner, since asking the user to supply a rate for it would invite overriding a real bill. llm_cost_usd is labelled by token_type and the provider reports one number per turn, so the charge is distributed across the buckets in the rate table's own proportions. Only the total is authoritative; the split stays an estimate, which is why this scales the existing buckets rather than inventing a label. estimateCostBreakdown does the same, keeping the stacked chart summing to the headline figure instead of silently under-drawing every Grok row. Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * docs(changelog): say Grok cost now follows xAI's actual charge The earlier wording scoped the claim down to catalog pricing and cached input because the long-context tier was still unhandled. It is handled now — the cost comes from what xAI charged for the turn — so the entry can say so. Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * fix(usage): keep the provider's cost when the model has no rate row Both cost consumers bailed out before reading the authoritative figure when the rate table had no row for the model. A `grok-composer-*` turn — in the Grok Build catalog, absent from xAI's price sheet — was therefore reported as $0 spend even though xAI told us exactly what it charged. Worse on the client: estimateCost returned the real cost while estimateCostBreakdown returned zeros, so the headline and the stacked chart disagreed on precisely the rows whose cost is exact — and the unmapped-models banner was (correctly) hidden, so nothing explained the discrepancy. Handle the charge before the rate lookup in both places. Without rates there is nothing to split a total by, so it lands whole in the `input` bucket, the same fallback distributeAuthoritativeCost already uses when it has no shape to scale. Tokens with no rate keep going to llm_unpriced_tokens: "unpriced" describes the rate table, not the money. Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * perf(usage): drop the historical rewrite from the cost-split migration Migration 213 rewrote every existing task_usage_hourly row to seed the uncosted counters. That is a full-table UPDATE inside a schema migration — lock time, WAL and bloat all scaling with table size — for rows this issue explicitly does not care about. Deleting the UPDATE alone would have zeroed historical cost: with `NOT NULL DEFAULT 0`, an untouched row asserts "nothing here needs estimating", so every pre-split bucket would report $0 until the rollup happened to touch it. Make the uncosted columns nullable with no default instead. NULL means "never recomputed since the split existed", readers COALESCE it to the row's own token total ("estimate all of it"), and the pre-split behaviour is preserved exactly — with nothing to seed, so no rewrite. A bare ADD COLUMN is metadata-only, so this is now fast DDL. Rows heal into the split naturally as the rollup recomputes their buckets. Verified on a fresh database: a legacy-shaped row reads back as its full tokens to estimate, and a group mixing legacy and post-split buckets sums to the authoritative cost plus both rows' estimable tokens. Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e2ce3a2da8 |
MUL-5223 fix(runtime): forbid blocking on external CI in the runtime brief (#5840)
* fix(runtime): forbid blocking on external CI in the brief (MUL-5223) The external-work boundary added in #5803 did not stop agents from waiting on GitHub Actions. Two holes: the section's only concrete "how to wait" example was a blocking foreground call, which is exactly the shape of `gh pr checks --watch`; and the "unless acceptance criteria require it" escape was satisfied by the repo's own merge requirement that CI be green. Name the banned tool shapes, allow a single non-blocking status snapshot, deny branch protection as an acceptance criterion, and give the replacement hand-off phrasing (local test result + PR link). Co-authored-by: multica-agent <github@multica.ai> * fix(runtime): scope the CI-wait ban so the explicit exception stays executable (MUL-5223) Review feedback on #5840: - The ban read as absolute ("Blocking on external CI is never part of your deliverable") while the next bullet allowed waiting when the task explicitly asks for the CI result, leaving no way to satisfy both. The ban is now scoped to "unless the explicit exception below applies", and the exception names the one executable shape: a single foreground blocking watch inside the same turn. - `gh pr merge --auto` enables auto-merge and returns; it is not a wait. Only waiting for it to land is banned. Both hard-pin tests now also pin the exception so it cannot be dropped or re-absolutised. Co-authored-by: multica-agent <github@multica.ai> * polish(runtime): group Background Task Safety into run-owned and external-CI clusters (MUL-5223) Co-authored-by: multica-agent <github@multica.ai> * polish(runtime): cut redundant phrasing from the external-CI cluster (MUL-5223) The cluster said "report and finish" three different ways and carried two rhetorical tails. Fold the delivery template into the post-push playbook bullet, tighten the merge-gate denial, and drop filler. 5 bullets -> 4, -36 words, every behavioral fact and test pin intact. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6992c58de3 |
MUL-5185: add Codex Fast mode (#5821)
* feat(agents): add Codex fast mode (MUL-5185) Co-authored-by: multica-agent <github@multica.ai> * fix(agents): make Codex Fast override authoritative Co-authored-by: multica-agent <github@multica.ai> * fix(agents): remove Codex Fast config conflicts Co-authored-by: multica-agent <github@multica.ai> * chore: refresh checks after conflict resolution Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
2c749ddf9f | fix(runtime): clarify external background work (#5803) | ||
|
|
36533bbc2b | fix(test): prevent agent CLI execution in default tests (#5789) | ||
|
|
e8746c5030 |
fix(onboarding): eliminate Desktop runtime-step false-negative flash + parallelize daemon version detection (MUL-5119) (#5756)
* perf(daemon): parallelize runtime version detection during registration (MUL-5119) Registration probed each agent CLI's `--version` serially, so total latency was the sum of every probe. On an onboarding host with several coding tools installed that stacked into many seconds before runtimes registered — long enough that the desktop runtime step timed out into its empty 'no runtime found' state while the daemon was still working. Fan the probes out with a bounded errgroup so total latency tracks the slowest single probe instead of their sum. Each probe still self-heals a vanished pinned path and re-detects the live version (no cross-registration caching, so an in-place upgrade is still reported correctly); failures are logged and skipped as before. Results are sorted by provider for a deterministic payload. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): stop the runtime step flashing 'no runtime found' while the daemon probes (MUL-5119) The runtime step flipped from scanning to the empty 'no runtime found' state on a fixed 5s wall-clock, so a machine that does have coding tools installed saw a false-negative flash whenever registration outlasted the timeout (cold start, slow/wedged CLI, many CLIs). Gate the empty flip on a desktop-only `runtimesPending` signal derived from the local daemon's live status (booting, or running with agent CLIs detected on the host): while pending, keep the scanning skeleton past the soft timeout. An absolute hard-timeout ceiling still guarantees a fallback so a wedged probe can't pin the step on the skeleton forever. Web omits the signal and keeps the plain wall-clock timeout. Also drop the two dead/duplicated affordances on the step: the permanently disabled 'Start exploring' button now renders only in the found phase, and the empty state's duplicate footer 'Skip for now' is removed in favour of its own Skip card. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fcb370edfd |
fix(squads): align parent issue status with agent-managed model (MUL-5156) (#5758)
* fix(squads): align parent issue status ownership with agent-managed model Squad leaders now open assigned parents to in_progress on first dispatch, keep them there while members work, and only move to in_review when overall completion is confirmed—matching ordinary agent status semantics without server auto-flips. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(squads): scope leader parent-status ownership to squad-assigned issues Review follow-up on the parent-status alignment change. Two boundaries were left ambiguous, both of which the change's own premise ("don't make the model resolve a contradiction in the prompt") argues should be closed in-place. 1. Status ownership was granted too widely. The leader briefing is injected on every leader path, keyed off is_leader_task — including the MUL-3724 case where an issue is assigned to a plain agent and a squad was merely @mentioned for help. The unqualified "Own the parent issue status" responsibility therefore also reached guest leaders, who could push another assignee's in-flight issue to in_review. buildSquadLeaderBriefing now takes ownsIssueStatus and selects between two variants of responsibility 6: the grant only when the issue's assignee is this squad, otherwise an explicit "do NOT change this issue's status". Quick-create passes false — no issue exists on that turn. Everything else in the protocol (roster, delegation, evaluation) is unchanged for both. 2. The comment-triggered path still contradicted itself. The runtime brief says "do not change status unless the comment explicitly asks", and a member's delivery comment never asks. Squads that dispatch by @mention create no child issues, so no child-done system comment exists to carry the explicit ask either — that parent would sit in in_progress indefinitely. writeWorkflowComment now names the protocol responsibility as the one exception for squad leaders. It is safe to state unconditionally because the grant is only present in the instructions when the server decided this squad owns the issue; for a guest leader the sentence has nothing to activate. Tests: two composition tests assemble both halves (server-side briefing + daemon-side CLAUDE.md) for one real scenario each, since asserting each half alone is how the original contradiction shipped. Plus execenv coverage that the carve-out appears only for leaders and the ordinary-agent rule stays absolute. Docs and the multica-squads skill / source map record the narrower contract. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1157d3f139 |
fix(repocache): repair promisor config on isolated checkouts of a partial cache (#5772)
The isolated-checkout path used by Linux Codex builds a task-local repository with `git clone --local` from the workspace's bare cache. That command has two properties that combine badly when the cache is a partial clone: it does not carry the promisor remote configuration across, and it does not treat an incomplete source object store as an error. The result is a checkout that exits 0 with every tracked file reported as deleted, so an agent starts work in what looks like a repository someone emptied. Swap origin to the real remote and restore `remote.origin.promisor` / `remote.origin.partialclonefilter` before the first checkout, so git can lazily fetch the blobs it needs. Do the same on the reuse path, where a workdir created against a complete cache can later be resumed against a partial one. No cache is created as a partial clone today, so this changes nothing for existing installs; it is a prerequisite for the on-demand clone mode in MUL-4983 and hardens a path that fails silently rather than loudly. Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
216aee5629 |
[MUL-5125] Add daily Desktop/Web usage and runtime reporting (#5763)
* feat(analytics): add daily client usage reporting (MUL-5125) Co-authored-by: multica-agent <github@multica.ai> * fix(analytics): clarify daily usage semantics (MUL-5125) Co-authored-by: multica-agent <github@multica.ai> * fix(analytics): resolve usage review blockers (MUL-5125) Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5d9295ac65 |
feat(agents): add per-agent runtime skill controls (#5686)
* feat(agents): add per-agent runtime skill controls Co-authored-by: multica-agent <github@multica.ai> * fix(agents): renumber runtime-skill migration and broadcast agent:status on toggle Address the MUL-5101 review blockers on PR #5686: - Rebase onto main and renumber the runtime-skill-disable migration 202 -> 203. main added 202_runtime_profile_add_qwen, so the pair collided on prefix 202 and migrations_lint_test would reject the duplicate. 203 is the next free prefix. - Publish an "agent:status" event after persisting a disabled_runtime_skills override, mirroring the workspace-skill toggle in writeUpdatedAgentSkills. The realtime layer keys off this event to invalidate workspaceKeys.agents, so other open web/desktop/mobile clients now drop their stale toggle state instead of only the initiating tab refreshing. Reload junction-table skills before the broadcast so it doesn't signal cleared skills (#3459). - Add a handler regression test proving the broadcast fires on both disable and enable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Walt <walt@multica.ai> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
4233e82441 |
fix(skills): tree-first skill import to stop 504 on large repos (MUL-5136) (#5753)
Rework skills.sh and github.com skill imports around a single recursive git-tree fetch to stop the 504 on large mono-repos (e.g. api-gateway-skill): - One tree call replaces the per-directory contents crawl. - Import caps checked arithmetically from tree metadata before any download (fail fast with 413 instead of timing out). - Most-specific skill-dir resolution; repo root only as a last resort, which fixes the root SKILL.md name collision. - Concurrent downloads (errgroup, limit 8). - Overall 45s fetch deadline; cancellation is fatal on every supporting-file path (tree downloader, crawl listing/recursion/download, ClawHub) so a mid-download abort never persists a half-populated bundle. - A skills.sh tree-fetch failure returns a retryable 503 instead of an unsafe root-directory fallback. - Lenient conventional-path acceptance restored for both complete and truncated trees. - maxImportFileCount 128 -> 256 (aligned daemon cap); 8 MiB bundle cap remains the real guard. |
||
|
|
a5a42846e6 |
fix(daemon): retry with a fresh session only when the resume was actually rejected (MUL-4966) (#5715)
* fix(daemon): gate fresh-session retry on tools executed, not session id (MUL-4966) Switching provider accounts leaves the stored session id pointing at a conversation the new account does not own. The daemon still passes it to --resume, the provider rejects it, and the task dies before doing any work. The existing fresh-session fallback was supposed to catch this but was gated on `result.SessionID == ""`, which is not a lifecycle fact: - Too narrow: a backend that echoes the requested id back when it rejects a resume keeps SessionID non-empty, so the fallback never fired — the reported bug. - Too broad: a provider 401 before the first stream message also leaves SessionID empty, so an unrecoverable auth failure burned a second full run. Gate on `tools == 0` instead. That states the property that actually makes a retry safe — the agent executed no tool, so it mutated nothing, so re-running cannot double-post a comment (comment creation has no idempotency key and a duplicate re-fires its @mention triggers), reopen a PR, or re-plan on top of its own half-finished work in the reused workdir. Auth failures are excluded, mirroring retryableReasons in service/task.go. The predicate is extracted to shouldRetryWithFreshSession so the tests exercise production logic; both existing fallback tests re-implemented the condition inline and would not have caught a regression in it. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): gate fresh-session retry on a positive resume-rejected signal (MUL-4966) Review of the previous commit was right: `tools == 0` plus "not an auth error" answers whether re-running is *safe*, not whether a new session can *fix* the failure. Those are orthogonal, and answering the second by exclusion inverts the burden of proof — the failures a fresh session cures are a small enumerable set, while the ones it cannot are open-ended. Concretely, the previous predicate fresh-retried on provider_network, 429/529, quota, 5xx and unclassified startup failures. provider_network is the sharpest conflict: internal/service/task.go marks it resume-safe (MUL-4910) specifically so the platform retry inherits the session and continues the truncated conversation. Resetting the session first made that contract unsatisfiable, silently discarding conversation context on a transient blip — and rate limits got an immediate no-backoff re-run. Replace the inference with positive evidence: agent.Result gains an explicit ResumeRejected field, set only when a backend has proof the resume itself was refused. claude/codebuddy/qwen derive it from resumeWasRejected, which promotes the predicate resolveSessionID was already computing and encoding as the side effect of blanking SessionID — using an empty string to carry that meaning is what made the original bug possible. SessionID keeps being dropped for a rejected resume (a dead pointer must not be persisted), but it is no longer the signal the daemon reads to decide *why* a run failed. The six ACP backends that recover from "session not found" set the flag at the same points they already clear the id, so their existing recovery is not caught by the narrower gate. codex needs nothing: thread/resume already falls back to thread/start in-process, and deliberately does not on transport errors. Matching now includes the account-switch guardrail reported in #5704 (Claude Code 2.1.207, zh-CN): "400 此 session 已绑定另外的ai账号,请执行 /new 开启新 session". The en-US wording of the same guardrail has not been captured yet, so those variants are marked inferred in the source; a miss degrades to a terminal failure carrying the provider's raw text rather than a mis-routed run. Tests: backend-level fixtures drive ResumeRejected from real stream-json for both the account-binding 400 and a network drop, and the predicate now covers network/rate-limit/quota/5xx/auth/unclassified as explicit non-retries. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): restore fresh-session recovery for backends with no rejection signal (MUL-4966) Final review caught qwen regressing: its verified rejection string ("No saved session found with ID ...", already captured in testdata/qwen-code-0.20.0-resume-not-found.stderr.txt) was not in the phrase list, and qwen reports no session id on that path, so the new inclusion gate turned a working auto-recovery into a terminal failure. Auditing the other 17 resume-capable backends showed qwen was not alone. antigravity, copilot, cursor, deveco and opencode all recovered from a refused resume purely by reporting an empty SessionID, and none of them has any rejection detection to convert into ResumeRejected — copilot's own comment documents the hole (session.error before session.start), and antigravity's helper returns "" when "the CLI exited before dispatching". Making ResumeRejected the sole gate silently removed recovery from all five. Fixing that by guessing rejection phrases for five more CLIs is the wrong trade: no real output has been captured for any of them, and a false positive discards a recoverable session pointer. So the gate is now two tiers. Positive evidence (ResumeRejected) decides on its own where a backend can produce it. Where none is available, an empty SessionID still gates the retry — it proves no session was established, which is exactly what the pre-change behaviour relied on — minus the classes a fresh session provably cannot cure (network, rate limit, quota, provider 5xx, auth). That keeps the resume-safe contract in internal/service/task.go intact while restoring what these five backends had. Also renames claudeResumeRejectedPhrases to resumeRejectedPhrases: it is matched by claude, codebuddy and qwen, so a qwen-only string living under a claude-prefixed name would be actively misleading. Tests: qwen's existing missing-resume fixture now asserts ResumeRejected (verified failing without the phrase), and the predicate covers the no-signal tiers — retry when nothing was established, no retry once a session exists or the failure classifies as uncurable. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): scope the no-signal fallback to backends that cannot detect rejections (MUL-4966) Final review caught the compatibility path applying to every backend, not just the five it was justified for. shouldRetryWithFreshSession only saw (Result, priorSessionID, tools), so a false ResumeRejected could not be told apart from a backend that has no way to answer — and claude/codebuddy/qwen/ACP startup failures with no session id still fell through to the exclusion branch. That contradicted both the stated intent and the function's own doc comment ("where a backend can produce it, it is the whole answer"). Make the capability explicit. agent.ResumeRejectionUndetectable names the five backends that scrape SessionID out of stream output and have no rejection detection at all; the daemon takes provider and consults it, so a capable backend reporting false is now taken at its word. Membership is opt-in, so a new backend fails closed instead of silently inheriting a guess-based retry. Also completes the exclusion set: missing config, unavailable model, missing executable, unsupported runtime version and (defensively) agent timeout all have defined non-session remedies and were reaching `default: true`. What is left through stays narrow — unknown, process failure, unparseable output, context overflow — because a real rejection from these five most likely surfaces as a non-zero exit or unparseable output, none of them reporting one explicitly. Tests: one identical result asserted across all five undetectable backends (retries), twelve capable ones (no retry), and an unregistered provider (fails closed), plus table cases for each newly excluded reason. Classifier inputs were verified to map to the intended reasons rather than passing by accident. Also updates the ResumeRejected doc comment, which still said the daemon gates on it alone. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fbe00ca164 |
fix(daemon): run Codex unsandboxed on Windows to stop reject-by-policy (MUL-4957) (#5672)
* fix(daemon): run Codex unsandboxed on Windows to stop reject-by-policy (MUL-4957) Windows has no Landlock/Seatbelt-equivalent filesystem sandbox that the daemon configures, so the per-task `sandbox_mode = "workspace-write"` it wrote was unenforceable. Worse than having no sandbox, it pushed Codex into rejecting non-safe mutation commands "by policy": `multica issue create` fails with "was rejected by policy" because Codex can neither sandbox the command nor (under approval_policy = "never") escalate it to the daemon's auto-approver, so the request never reaches the approver. Mirror the existing macOS fallback and give Windows danger-full-access so those commands run. Also generalize the danger-full-access warn log so it no longer hardcodes "on macOS" and only surfaces the macOS-specific upgrade hint on macOS (new codexSandboxPolicy.Hint field). Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): correct Windows sandbox rationale and respect user windows.sandbox (MUL-4957) Addresses two review must-fixes on #5672: 1. Correct a false security fact. The comments and log Reason claimed Windows has no filesystem sandbox backend. Codex 0.144.5 does ship a native Windows sandbox (windows.sandbox = "unelevated"/"elevated"); it is experimental with open upstream reliability bugs, so the daemon defaults to danger-full-access as a deliberate compatibility choice. Enabling the native sandbox is tracked as separate follow-up work. 2. Stop silently downgrading users who opted into isolation. The fallback was unconditional. Add codexSandboxPolicyForConfig: on Windows an explicit windows.sandbox = unelevated|elevated keeps workspace-write so Codex enforces task isolation with the user's chosen backend; danger-full-access applies only when windows.sandbox is absent, disabled, or unparseable. This is also the branch point for a future native-sandbox rollout (flip the default; callers unchanged). Adds fixture tests locking the priority (user opt-in kept vs. unconfigured fallback) plus predicate coverage for codexSandboxPolicyForConfig. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): fail closed on undecidable Windows sandbox config, honor -c windows.sandbox (MUL-4957) Second review round on #5672. Two must-fixes. 1. Undecidable config no longer fails open. The old bool detector collapsed "unparseable / invalid value / failed copy" into "unconfigured" and then loosened to danger-full-access. Replaced with a tri-state (absent/native/undecidable): only exact-lowercase unelevated|elevated (the sole values Codex accepts — verified: any other value makes Codex refuse to load the config) counts as native; any other present value, unparseable TOML, a read error, or a missing per-task config when a shared ~/.codex/config.toml exists (i.e. the copy failed) is undecidable and fails closed to workspace-write — it never loosens — logged at error level. 2. windows.sandbox set via `-c`/`--config` custom args is now honored. Such args never land in config.toml, so config-only detection silently downgraded those users' isolation. The effective Codex args (daemon defaults + profile-fixed + per-agent custom_args) are threaded through PrepareParams/ReuseParams/CodexHomeOptions into the sandbox decision and scanned for a windows.sandbox override (inline, two-token, quoted, spaced; last-wins). Also drops issue-status-bound source comments (openai/codex#24098 has since closed). Adds unit coverage for config/args classification, the fold precedence (undecidable > native > absent), and the copy-failed fail-closed path. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): fail closed on config-sync errors and honor shell-quoted -c windows.sandbox (MUL-4957) Round-3 review must-fixes: 1. resolveWindowsSandboxState now takes the config.toml sync error and a tri-state shared-config presence instead of re-stat-ing inside. A failed sync (stale/absent per-task copy) or an un-stat-able shared source is undecidable and keeps workspace-write, closing the fail-open where a failed sync was read as "unconfigured". Splits IO from the decision so the paths are unit-testable without faulting the filesystem. 2. The Windows sandbox decision consumes agent.NormalizeCodexLaunchArgs (the shared helper buildCodexArgs now uses) so a shell-quoted -c windows.sandbox opt-in is normalized identically to launch, instead of being missed by a raw-token scan and silently downgraded. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): abort when the Codex sandbox block cannot be written (MUL-4957) Round-4 review must-fix: ensureCodexSandboxConfig failures were warn-and-continue, so a computed fail-closed workspace-write policy could stay only in memory while config.toml kept a stale danger-full-access from a prior run — the decision failed closed but the effective config failed open. prepareCodexHomeWithOpts now returns the error, which blocks startup on both paths: fresh Prepare fails the task, and Reuse leaves env.CodexHome unset, which configureCodexTaskShellEnvironment already refuses to start. Regression covers the full reuse scenario (stale danger-full-access + failed config sync + failed managed-block write); it fails with "got nil" without the fix. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: J <j@multica.ai> |
||
|
|
f8bf6cd8b9 |
feat(runtime): add Qwen Code runtime (MUL-5015)
Merge approved PR #5666. |
||
|
|
9e2cfe098c |
MUL-5038: fix project resource preparation decoding (#5688)
* fix(execenv): decode project resources in helper Co-authored-by: multica-agent <github@multica.ai> * fix(execenv): preserve OpenClaw gateway pin in helper Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c2df5d786d |
test(execenv): fix flaky Windows prepare-helper deadlock (MUL-4923) (#5676)
The windows-execenv job's TestPrepareIsolated_WindowsKillsDescendantBeforeRetry
flakes on the Windows runner: its helper subprocess ends in a bare select{},
which the Go runtime can reap with 'all goroutines are asleep - deadlock!'
(exit status 2) once every goroutine is parked with no wakeup source. That
races the parent's Job Object kill, so PrepareIsolated returns 'helper failed'
instead of the context.Canceled the test asserts.
Block on a timer-backed sleep loop instead: a pending timer is a wakeable
source, so the runtime never declares a deadlock, and the process still dies
the instant the Job Object tears the tree down.
Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
9961db15ed |
feat(issues): bump issue updated_at when a comment is added (MUL-5009) (#5667)
* feat(issues): bump issue updated_at when a comment is added (MUL-5009) A new comment now counts as activity on its issue and advances updated_at, so the "Updated date" Kanban/list sort surfaces recently-discussed cards — not only cards whose status changed. Applies to all three comment-creation paths (user/agent HTTP, agent task delivery, and the child-done system comment) via a best-effort TouchIssue query. The bump never fails an already- persisted comment; it self-heals on the next activity if it errors. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): make comment updated_at bump atomic (MUL-5009 review) Address Elon's review. Move the updated_at bump into CreateComment as a leading data-modifying CTE so the comment insert and the timestamp bump commit or roll back together — closing the non-atomic window where a comment could persist while updated_at stayed stale. That window also skewed the daemon GC TTL, which reads issue.updated_at to reclaim done/cancelled workdirs. Centralizing the bump in the query drops the three per-caller TouchIssue calls and guarantees any future comment entrypoint inherits it. Also refresh the now-stale gc.go / gc_test.go comments that asserted 'CreateComment does not bump issue.updated_at'. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): make comment/issue workspace match a query-level guarantee (MUL-5009 nit2) The touch CTE now RETURNING id, workspace_id and the INSERT SELECTs from it, so the comment insert depends on the issue actually existing in the passed workspace. A mismatched (issue, workspace) pair matches 0 rows in the CTE, the dependent INSERT selects nothing, and the :one query returns pgx.ErrNoRows — no mis-attributed comment is written and the issue is not touched. CreateComment is now the single carrier of the 'a comment belongs to an issue in the same workspace and always bumps it' invariant, so no future caller can break it by passing the wrong workspace. Signature unchanged; no migration or foreign key. Add TestCreateComment_WorkspaceMismatchPersistsNothing (error returned, no comment persisted, updated_at unchanged). Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
152d82e42e |
MUL-4999: reclaim managed Codex sandbox task caches
* fix(daemon): reclaim Codex sandbox task caches Co-authored-by: multica-agent <github@multica.ai> * refactor(daemon): tighten managed cache GC signals Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ed57707bb2 |
MUL-4923: bound daemon task preparation time (#5584)
* fix(daemon): bound pre-start task preparation Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): isolate pre-start env preparation Run execution-environment Prepare and Reuse in a killable helper process so a timed-out attempt cannot keep writing after retry. Add FIFO lifecycle and squad Stage retry regression coverage. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): terminate Windows prepare process trees Assign the pre-start helper to a kill-on-close Job Object before releasing its request, wait for all job members to exit on cancellation, and add a Windows runtime regression job. Co-authored-by: multica-agent <github@multica.ai> * ci: target Windows prepare tree regression Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
099e145611 |
MUL-4937: preserve daemon terminal callbacks during shutdown
Merge approved after review; CI checks passed. |
||
|
|
f738e8ca1f |
MUL-4952: pass agent custom env secrets to Codex shells
Merge approved after review; all CI checks passed. |
||
|
|
21613131c9 |
fix(kiro): stop repeating runtime brief every turn (#5605)
Co-authored-by: Xisheng-Zhao <zhaoxisheng@xiaomi.com> |
||
|
|
92676fbe51 |
fix(daemon): recover from uncertain ws claims
Squash merge approved after review and regression verification. |
||
|
|
90ee83e10a |
MUL-4925: fix Linux Codex Git metadata writes (#5575)
* fix(daemon): isolate Linux Codex git metadata (MUL-4925) Co-authored-by: multica-agent <github@multica.ai> * refactor(daemon): address isolated-checkout review nits (MUL-4925) - rename sameFilesystemPath -> sameResolvedPath (it compares resolved paths for equality, not same-device), with a clarifying doc comment - prune earlier tasks' agent/* branches when reusing an isolated checkout so a long-lived reused workdir stops accumulating one local branch per checkout; deleteLocalBranches now takes a keepBranch arg and the prune is non-fatal - cover the prune in TestCreateWorktreeReusesIsolatedGitMetadata * fix(repocache): preserve user branches on reuse (MUL-4925) Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
910671b185 |
Fix Codex task MULTICA_TOKEN passthrough (ZIC-82)
Merge approved PR after independent final review; all required CI checks passed. |
||
|
|
1507997272 |
fix(agent): stop agents shipping local-path links, make Desktop 404 recoverable (MUL-4899) (#5557)
Agents were writing runtime-local paths into deliverables as clickable links (`[screenshot](/Users/agent/work/shot.png)`). Two root causes, both fixed here. A. The brief never stated the delivery contract. Add an always-on delivery invariant (outside writeOutput's kind switch, so no task kind can inherit none) plus a per-surface file-delivery line for each of the five surfaces. Chat splits into two: `attachment upload` works only on web/mobile chat, never on an IM channel, so ChatChannelType is now threaded into TaskContextForEnv. The claim path only ever looked up Slack bindings, so a Feishu session reported as a web chat and got upload guidance for a channel that cannot carry attachments. Probe every channel type. The chat policy is two independent layers and stays that way: delivery keys off "is there a channel at all"; the `chat history` / `chat thread` commands stay Slack-only because both endpoints are hardwired to h.SlackHistory and there is no Feishu reader — ChatInThread only selects between those two commands, so it stays Slack-only too. Add a CLI hard-fail lint on `issue comment add` / `issue create` / `issue update` as the enforcement backstop. Scoped narrowly, since a false positive blocks a real deliverable: agent task context only (a human's PAT run is untouched), real CommonMark link/image/autolink destinations only via goldmark (a path in a code span or fence — how an agent quotes a path it is discussing — is structurally invisible), and three high-confidence signals only (`file://`, inside the workdir, or an existing local file). A bare `/foo` is a valid origin-relative URI and is deliberately allowed. `issue update` has no --attachment flag, so its hint redirects to `comment add` rather than naming an argument it rejects. B. Desktop presented the resulting router 404 as an unknown crash. 8 of 18 desktop_route_error reports were users clicking such a link and being told the app broke and to file a bug. Split the 404 into a first-class Not Found view: no crash framing, no Report error. Its recovery entry comes from the tab store's active workspace, never from the failed pathname — deriving a slug from `/Users/me/shot.png` yields "Users" and a button to `/Users/issues`, a second 404. Also add a will-navigate trusted-origin guard via the shared loadRenderer (main + issue windows). This is origin hardening only, NOT the mechanism for in-app links: client-side routing never fires will-navigate, so app paths never reach it. Issue windows need no 404 work — their router only accepts paths validated by parseIssueWindowPath and they do not listen for multica:navigate, so a bad path cannot reach them. Server-side completion observation is metric/log only and never blocks: it is lexical (`file://` + task work_dir prefix) because the server cannot stat the daemon's filesystem, and the metric label is a closed enum so no path or reply text reaches Prometheus. Verified: pnpm typecheck/lint/test (3582 tests), go vet, full Go suite including new claim-path integration tests. cmd/multica was verified outside the daemon workdir — inside one, 93 of its tests fail identically on origin/main because the suite walks up and finds the runtime's own task marker. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
07538e9928 |
fix(execenv): isolate Hermes SQLite state (#5560)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
18d41151eb |
feat(gc): batch issue reconciliation (#5534)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3547201bbe |
fix(daemon): reuse managed workdirs for squad leaders (#5429)
Squad-leader follow-ups on the same issue now reuse the prior daemon-managed workdir and provider session instead of starting fresh, while never binding or locking a user-provided local_directory. Reuse eligibility is keyed off a Prepare-time .managed_env.json provenance marker, so it does not race the completion→GC-metadata write. Closes #5535 Co-authored-by: Bohan <bohan@devv.ai> |
||
|
|
ed9adc2bbe |
feat: improve create issue field controls (#5532)
Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |