* fix(agent): fail codebuddy runs that emit a top-level error event
CodeBuddy's startup-failure shape is one top-level {"type":"error"}
line followed by a process that stays alive and silent: with an
inherited SERVER__PORT (a user-scope config shared by several tools),
a second concurrent run of the same agent fails to listen, prints
{"type":"error","error":"listen EADDRINUSE: ..."} and never emits
anything again. The parser had no branch for that event type, so the
run sat at "waiting for agent response" until the idle watchdog
stopped it - 19 of 48 runs hung this way on the reporter's machine.
- type=error is now fatal when no terminal result has been seen: the
CLI's own message becomes the run error and the hung process is torn
down immediately instead of idling. A result already seen keeps
governing the outcome, so a late informational error line cannot
flip a finished run.
- buildCodebuddyEnv now also forces SERVER__PORT=0 (OS-assigned port
per run, so concurrent runs never share one) and
CODEBUDDY_BASH_AUTO_BACKGROUND_DISABLED=1 (CodeBuddy's PowerShell
tool moves timed-out commands to the background and only honors that
variable). Both are appended last like the existing background-task
disable so a caller-provided value cannot outvote them.
Fixes#9200
Generated-by: GLM-5.3-Flash (ZCode)
* fix(agent): keep CodeBuddy running after recoverable errors
MUL-7983
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Yushen <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(daemon): don't count host sleep as agent silence in idle watchdog (#9024)
The idle watchdog stamped activity in wall-clock unix nanos, so a laptop
that slept past the idle window woke up with every in-flight run already
over budget and force-stopped them all on the first tick.
Detect host suspend from the wall-clock gap between watchdog ticks and
credit that time back to lastActivityAt before each check, clamped to now.
Works the same on macOS/Linux (monotonic clock pauses in sleep) and
Windows (monotonic clock keeps counting), and covers the tool-in-flight
budget too since both read the same stamp.
MUL-7851
Co-authored-by: multica-agent <[email protected]>
* fix(daemon): measure idle watchdog on an awake-only clock (#9024)
Review on #9201 showed the tick-gap heuristic still charges sleep to the
agent: a sleep shorter than one tick is never credited, and when the
overdue tick fires on wake (Windows) only gap−tick is credited.
Replace it with pkg/awakeclock, a clock that only advances while the host
is awake: Go's monotonic clock on Linux/macOS (already stops in suspend),
minus host sleep on Windows (GetTickCount64 − QueryUnbiasedInterruptTime).
lastActivityAt, the drain loop stamp, and Cursor's native tool activity
(Session.ToolActivity now returns an awakeclock reading) all share it.
Regression tests cover both review boundaries plus a long sleep, each with
a wall-clock control that proves the scenario reproduces the bug.
MUL-7851
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-agent <[email protected]>
* fix(agent): deliver MCP servers to Pi tasks
Pi skipped untrusted project config in JSON mode, and Multica never wrote .pi/mcp.json, so composed MCP servers never reached the runtime.
Co-authored-by: Cursor <[email protected]>
* fix(agent): hand Pi its MCP servers through a temporary extension
Writing .pi/mcp.json and passing --approve trusted the whole project directory and failed when that file already existed. An explicit --extension loads the servers without project trust.
Co-authored-by: Cursor <[email protected]>
* docs: mark Pi as reading agent MCP configuration
The provider comparison still said Pi skipped the MCP field. Pi now receives that configuration.
Co-authored-by: Cursor <[email protected]>
* fix(agent): harden Pi MCP registration
Co-authored-by: multica-agent <[email protected]>
* fix(agent): allow empty Pi MCP configuration
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Cursor <[email protected]>
Co-authored-by: Yushen <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(agent): fail kimi runs whose wire log records a failed turn
kimi's ACP adapter can answer session/prompt successfully while its own
turn failed: the session wire log carries a
{"type":"agent.turn.ended","outcome":"failed",...} record (e.g. a bare
"OAuthConnectionError: OAuth request to https://auth.kimi.com/api/oauth/
token failed" that matches none of the stderr sniffer's terminal
signatures). The run then kept status=completed and published the
intermediate narration as a successfully delivered result, so the daemon
sent no failure signal and retry/continuation policy never saw the
failure (#9054).
Fix: after the run, scan the session wire logs - the same source,
turn-boundary rules, and tolerant-line policy as the usage scan
(scanKimiSessionUsage) - for the main agent's failed agent.turn.ended
record, and promote completed->failed carrying kimi's sanitized
diagnostic. Only the structured outcome verdict counts, so conversation
text echoing failure words cannot flip a healthy run; a delegated
agent's failed turn stays that agent's own task path; timeout, abort,
and already-failed runs keep their own terminal semantics.
Complementary to #8573's zero-output guard: that incident shape had 26
tool calls and nonempty narration, which a zero-output guard
deliberately preserves.
Fixes#9054
Generated-by: GLM-5.3-Flash (ZCode)
* fix(kimi): sanitize the wire diagnostic and scan past oversized records
Review fixes for #9057 (CHANGES_REQUESTED, both repros offline with a
fake ACP; no live provider requests):
P1: kimiWireTurnFailure copied the record's errorMessage into the run's
diagnostic unsanitized. The failure path persists Result.Error, and the
shared redact.Text does not redact a quoted JSON credential shape
(request={"api_key":...}) — the sanitized diagnostic the PR described
was not actually provided. The diagnostic now goes through the same
sanitizeAgentDiagnostic pass as every child-process diagnostic before
it reaches Result.Error.
P2: the scan loop used newAgentStreamScanner, whose bufio.Scanner ends
its scan for good at a line beyond agentStreamMaxLineBytes, and the
loop never checked scanner.Err() — a valid but oversized context.append
record ahead of the terminal event hid the failed outcome and kept the
run completed. kimiWireTurnFailure now reads through
readAgentStreamLine, which consumes and discards an over-bound record
whole and keeps the later records readable, and ends the walk
explicitly on any other read failure (keeping what was read, the same
best-effort policy as a truncated tail).
Regressions, all red before the fix and green after:
- TestScanKimiMainTurnFailureSanitizesWireDiagnostic (P1)
- TestScanKimiMainTurnFailureReadsPastOversizedRecord (P2, scan level)
- TestKimiBackendWireTurnFailurePastOversizedRecord (P2, backend e2e
with the fake ACP: red as got "completed" (error=""), the review's
exact repro shape)
- TestReadAgentStreamLineSkipsOversizedAndContinues /
TestReadAgentStreamLineReturnsUnterminatedTail (helper contract; the
first caught a boundary miss where the cap was crossed only on the
final buffer chunk)
Generated-by: GLM-5.3-Flash (ZCode)
* feat(issues): add multi_text and multi_url custom property types
Workspaces could only hold list-shaped values as preset option lists
(multi_select) or member references (multi_actor). There was no way to
keep a list of free-form strings or links on an issue, so teams with
"related docs / spec links / aliases" needs had to either cram them into
one text field (no structure, nothing clickable) or burn one select
option per link.
This adds two property types:
- multi_text: an ordered list of free-form text entries
- multi_url: an ordered list of http(s) URLs
Each entry is validated exactly like a single text / url value
(length caps, http(s)-only for URLs), duplicates are dropped, insertion
order is preserved, and values are capped at 20 entries to respect the
16KB properties-bag budget. Values ride the existing jsonb bag, so the
jsonb_path_ops GIN index, the @> containment filter, and the client
value schema all keep working unchanged.
Filtering matches any single list element exactly (equality) plus "No
value" — the containment form the filter compiler already emits for
array elements, so no new server predicate is needed. List types
deliberately get no sort order and cannot group, like the other
multi-value types.
On the web, list values render as chips; multi_url chips are clickable
and open in a new tab, and the editor popover offers a per-entry open
button, so several related documents or links stay directly reachable
from the issue. The filter menu reuses the scalar input (equality-only
plus "No value"). CLI: comma-separated --value form for both types,
element-equality --property filtering, and display_values rows.
Server-side allowlist widened by migration 500 (check constraint
re-added NOT VALID then validated, same pattern as migration 341).
* fix(properties): address list property review nits
Co-authored-by: multica-agent <[email protected]>
* fix(properties): scope list drafts to the edited issue
Co-authored-by: multica-agent <[email protected]>
* test(agent): avoid fork races when writing zeroclaw fixtures
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
antigravityCompletedDespiteTrailingNetworkError exists to preserve a finished
answer when agy fails a follow-up round trip after producing it, but it
recognised exactly one error spelling. agy also reports an exhausted retry as
`API error (attempt N): request failed: Post "...": EOF`, which fell through and
discarded a response already sitting in result.response with its agent_response
step at DONE.
Match the transport cause rather than one sentence. The classifier stays narrow
on purpose: reportTaskResult documents failing closed so a provider 429 or
out-of-credit run can never be shown as Completed, and quota, capacity,
overload, policy and auth rejections all arrive as an HTTP response rather than
a failed round trip. agy's own `request failed:` prefix is excluded too — only
one spelling has been observed and nothing rules out agy reusing it for a
status error.
* fix(pi): fail a run whose agent_end reports an error
Pi 0.74.0 and earlier report a failure raised outside a model stream,
such as a provider API key command that fails, only on agent_end. The
stream carries no turn_end, error or auto_retry_end event, and print
mode exits 0, so the adapter recorded the run as completed with no
output. Pi resolves a command-backed API key again on every model
request, so the same path can also end a run part-way, after tool work.
Decode agent_end and record its last assistant message's stopReason
"error" through the existing turn-error guard, with errorMessage as the
reason. An automatic retry or a later successful turn still clears it,
as with turn_end.
* fix(pi): fail a run whose event stream overflows the scanner
The pi backend never checked scanner.Err(). A line over the shared 32 MiB bound ended the read loop, and the run then either recorded completed (when the rest of the line fit in the pipe buffer and pi exited) or hung until its timeout, with pi blocked writing to the unread pipe. agent_end repeats every message of the run, so on a long run it is the line most likely to overflow, and it can carry the run's terminal state.
Close stdout on a scanner error so pi cannot block on the full pipe, and fail the run with "pi stdout read error", keeping any provider error pi already reported. This matches the other stream backends.
* fix(agent): pass Hermes custom args before the acp subcommand
Hermes only accepts its global flags (--provider, --yolo, -m, ...) ahead
of a subcommand, so the daemon's `hermes acp <custom args>` exited with
an argparse usage error before the ACP handshake and every run of a
Hermes agent with custom_args failed. Assemble `<prefix> <custom args>
acp` instead, keep the profile resolver and overlay stripping on the
same argv, and drop a trailing custom arg left without its value so it
cannot capture `acp` and hang the task in interactive chat.
Fixes#8878 (MUL-7748)
Co-authored-by: multica-agent <[email protected]>
* fix(agent): keep acp's own flags after the Hermes subcommand
Hermes' acp subparser declares --accept-hooks, --version, --check,
--setup, --setup-browser and --yes/-y, and argparse only accepts them
after the subcommand: moving them ahead of `acp` turned an agent with
custom_args ["--yes"], which launched before, into a usage error.
Lay custom args out around `acp` instead — global flags (with their
values) before it, acp's own flags after it — and map profile
stripping back through that layout so custom args keep their
configured order.
Co-authored-by: multica-agent <[email protected]>
* fix(agent): resolve Hermes flag abbreviations like argparse
Hermes keeps argparse's allow_abbrev, so custom args must be classified
the way its parsers resolve them, not by string equality. Behind `acp`
a `--y` is `--yes`; moved in front, the root parser reads it as
`--yolo` and turns off dangerous-command approval, while `--ye` went
from valid to a usage error. Keep every token the acp subparser would
take as its own option (exact, abbreviated or ambiguous) behind the
subcommand, and treat abbreviated value flags (`--prov`) as value flags
when pairing and when guarding against a bare flag capturing `acp`.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-agent <[email protected]>
Since the fallback catalogs only feed the model picker (#8811), listing
current models there is cheap. Add the models the installed CLIs now
advertise but the static lists were missing:
- Claude: claude-opus-5-5 (Claude Code 2.1.280 list_models default)
- Codex: gpt-6-sol (low..ultra) and gpt-6-luna (low..max), both default
medium, from codex-cli 0.155.1 `debug models`; live discovery labels
them "GPT-6 Sol" / "GPT-6 Luna" like GPT-6 Astra
MUL-7691
Co-authored-by: multica-agent <[email protected]>
When model discovery fails, providers substitute a static catalog so the
picker stays usable. The daemon then validated saved thinking levels and
service tiers against that stand-in, silently dropping values the static
list did not know about — which forced hand-maintained model and per-model
effort tables to chase every new release.
Only a verified catalog (discovered, non-empty) may now reject, rewrite, or
drop a saved model, thinking level, or service tier. Fallback and empty
catalogs pass the saved values through to the CLI for every provider. The
checks that describe the binary rather than its models stay: a Claude CLI
without --effort (or without the saved value in its --effort list) still
drops the level, and Codex's explicit standard tier still follows the CLI
version.
- Catalog.Verified / Catalog.CLIThinkingLevels; missingFromFallbackCatalog removed
- claudeModelEffortAllow removed: the fallback picker offers the --help superset
- ModelKnownIncompatibleWithProvider no longer consults static lists (prefix-only, same result)
MUL-7691
Co-authored-by: multica-agent <[email protected]>
* feat(grok): support steering active Grok Build runs
* test(grok): cover steering before first agent output
* fix(grok): bound interject acknowledgement time
* fix(cursor): own Windows background shells by launch Job membership (MUL-7417)
captureCursorBackgroundProcess on Windows walked a machine-wide PID->PPID
snapshot to prove a reported shell descends from the launch and to find
its pre-existing children. Windows keeps a child's recorded PPID after
the parent exits and reuses the PID, so the walk could reach a process
owned by someone else; opening it failed with 'Access is denied' or
'not an owned descendant' and the whole capture aborted. The daemon then
fell back to the idle watchdog and a Cursor run that later went quiet
took the 10-minute watchdog path instead of completing. In CI this was
TestCursorBackgroundLifecycle/multiple failing about 2 runs in 43.
Take candidates from the launch's own Job Object instead
(QueryInformationJobObject / JobObjectBasicProcessIdList). Descendants
inherit the Job at creation and cannot leave it, so membership is the
ownership proof: the parent-chain walk goes away, the PPID snapshot is
used only to attach a member's pre-existing children, and a member that
exits or is reused between listing and open is skipped instead of
failing the capture. Creation ordering still tells a real child from
one that points at a reused parent PID.
Restore TestCaptureCursorBackgroundLateDescendant on Windows; it was
excluded for exactly this race.
ci.yml runs the Windows Cursor step with -count=20 on this PR only to
show the race is gone; it is reverted before merge.
Co-authored-by: multica-agent <[email protected]>
* ci: restore -count=1 on the Windows Cursor background step
The -count=20 stress run on this PR passed (run 35580440594, windows-execenv);
the per-merge step goes back to a single pass.
Co-authored-by: multica-agent <[email protected]>
* fix(cursor): complete the Job member list and refuse unopenable live members (MUL-7417)
Review follow-ups on #8638:
- cursorJobProcessIDs grew its buffer only on ERROR_MORE_DATA. The kernel
can also return success with NumberOfProcessIdsInList short of
NumberOfAssignedProcesses, which a Job over the initial capacity or one
growing during the query would hit; the short list then passed for the
membership and could reject the target or leave children out. Retry
until the counts agree, fail explicitly if they never do, and make the
initial capacity a package var so a three-process tree exercises the
growth path in a test.
- The child walk skipped every open failure as 'exited or reused'. A live
member whose DACL denies PROCESS_SET_QUOTA|PROCESS_TERMINATE also fails
to open, and skipping it left the capture reporting success with that
member outside the tool Job. Skip only PIDs proven to carry no member
(handle outside the launch Job, or no longer listed after a re-query,
confirmed by one more open); otherwise fail the capture so the caller
takes its existing watchdog fallback. OpenProcess goes through a
package var so a test can make a live member unopenable.
Windows-only tests cover both, plus the plain claim-and-terminate path
for a shell with a pre-existing child.
Co-authored-by: multica-agent <[email protected]>
* test(cursor): name the Job list growth test so the Windows CI filter runs it
The windows-execenv step selects 'TestCursorBackground|TestCaptureCursorBackground';
TestCursorJobProcessIDsGrowsPastInitialCapacity matched neither and never ran.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-agent <[email protected]>
OpenCode 2.0 changed four things the daemon depends on. Two fail loudly —
`--dir` and `--variant` both exit 1 on an unrecognized flag, which is why every
task on a 2.x runtime died in ~130ms with empty output (GH #8586). Two are
silent: MCP config had no working channel left, and `opencode run` became a thin
client in front of a resident service, so signalling the client's process group
no longer stopped the run.
- `--dir` is dropped on 2.x; the workdir comes from cmd.Dir + PWD.
- The thinking level folds into the model string as `provider/model#variant`.
- Cancellation calls POST /api/session/<id>/interrupt in front of the existing
SIGTERM→SIGKILL, which is the only thing that stops a 2.x run.
- Runs carrying Multica-managed MCP servers are refused with an error naming
every source, rather than started without them: 2.x accepts MCP only through a
file in the task workdir, where the agent's own commits would capture the
credentials. Restoring MCP is tracked separately.
The 1.x path is unchanged, pinned by argv and MCP regression tests. Verified
against @opencode/cli 2.0.10 and opencode-ai 1.18.31 on macOS/arm64; Windows
cancellation is not yet verified end to end.
* fix(runtimes): preserve custom profile base runtime identity
Co-authored-by: multica-agent <[email protected]>
* fix(runtimes): pass both url params to the profile identity test
TestUpdateRuntimeProfile_RejectsIdentityChange nested two withURLParam
calls. That helper installs a fresh chi route context each time and does
not copy what is already there, so the outer call for profileId dropped
id: the handler answered 400 "workspace_id is required" and never
reached the immutability check the test exists to cover.
Use withURLParams, which carries existing params forward and is what the
rest of this file already uses.
Co-authored-by: multica-agent <[email protected]>
* fix(runtimes): name a runtime target the same way on every surface
The base-runtime picker spelled Oh-My-Pi with an inline ternary, so it
was the only place that used the product name. The catalog row, both
detail headers, the base-runtime detail row and the locked chip in the
form kept rendering the stored id under `capitalize` — a user picked
"Oh-My-Pi" and the next screen, and the saved profile, said "Omp".
Move the name into runtimeTypeLabel next to the target whitelist and
read it from every surface. profileRuntimeType replaces the repeated
runtime_type ?? protocol_family fallback.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Bohan-J <[email protected]>
Owning the runtime process tree (#7522) made cmd.Cancel kill every descendant
in that tree, so the usual pipe holder dies with it and both readers reach EOF.
A descendant outside the tree does not: on POSIX because it called setsid and
left the process group, on Windows because startOwnedProcessTree failed open
and the child runs unowned, so the kill reaches the leader alone. The join
after the forced shutdown was unbounded, so the turn then hung forever: no
tool result, no reply, nothing until the user cancelled by hand.
Reap the process as part of that forced shutdown instead. cmd.Wait() returning
is what closes the parent ends of the pipes, so it frees a reader the kill
could not reach, and the 10s WaitDelay the claude, codearts and antigravity
backends already use bounds Wait itself once the context is cancelled. The
readers are still joined before the buffers are read, so providerErr.Finalize
keeps its documented precondition — a fully drained stderr pipe — instead of
racing the copier and splitting a provider error across its flush. The same
reap now runs in the deferred cleanup, where joining the readers before the
goroutine closes msgCh keeps a live reader from panicking in trySend.
What this buys is a bounded recovery, not a repaired turn: past the existing
2s drain grace the backend delivers from the buffers it has, so an exceptional
shutdown can report a turn without output the readers had not handed over yet.
It does not fix the Windows terminal tool — that belongs in hermes' own bash
startup probe (NousResearch/hermes-agent#73403, still open) — and it does not
reap the escaped process, which is out of the daemon's reach by definition.
Co-authored-by: multica-agent <[email protected]>
* fix(agent): require OpenCode >= 1.1.54 so old builds cannot fill the host disk
OpenCode <= 1.1.53 ignores TMPDIR, TMP and TEMP when its embedded Bun runtime
extracts a native module, writing into the shared system temp directory
whatever the daemon exports. Each successful run leaves one 4-8 MB module
behind under a fresh, non-content-addressed name, so nothing ever reuses or
overwrites it. Upstream fixed this in 1.1.54 (2026-02-10).
Reproduced on Linux against pinned builds, verifying each binary's
self-reported version: 1.1.49, 1.1.52 and 1.1.53 write into the shared /tmp
with the three variables pointed elsewhere; 1.1.54 and everything after honour
them. #8392 is what that costs a self-hosted daemon left on a pre-fix build —
~2,960 files, 11.16 GiB, root filesystem at 99%, and no error anywhere until
the disk runs out.
The per-task temp directory cannot contain a CLI that never reads the
variables, and deleting by filename in a shared /tmp is not safe — a module
another live process still has dlopen'ed looks identical. Refusing the CLI at
registration is the only place this can be stopped, and it turns a silent host
outage into "opencode version X is below minimum required 1.1.54 — please
upgrade".
Note this floor is unlike the others in the table: every existing entry names a
protocol the backend speaks through, so an older CLI cannot serve a task at
all. A pre-1.1.54 opencode serves tasks correctly; it damages the host while
doing it. The table comment now says which kind each entry is.
Refs #8392
Co-authored-by: multica-agent <[email protected]>
* docs: list the OpenCode minimum runtime version
The install-agent-runtime callout enumerates the per-runtime floors the daemon
enforces at registration; OpenCode now has one, so it belongs in the list.
Updated in all four translations.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(desktop): do not prepend PATH fallbacks over login-shell Node
Prepending /usr/local/bin after fix-path let a stale system Node 12
shadow nvm/fnm, breaking shebang CLIs (CodeBuddy/OpenClaw) on --version.
Append only missing fallback dirs so recovered shell PATH keeps precedence.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(agent): align CodeBuddy stream-json guards with Claude
Pass stderr into resume rejection, fail on prompt_too_long terminal_reason,
and force foreground when Bash requests run_in_background — the same
failure modes Claude already handles for Multica-managed headless runs.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(agent): align CodeBuddy guards with real CLI protocol
Address review on #8340: force CODEBUDDY_CODE_DISABLE_BACKGROUND_TASKS,
fail on system/task_* background events, and drop Claude-only
prompt_too_long / async_launched handling that CodeBuddy 2.150.0 never emits.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(agent): update CodeBuddy handleUser test after signature change
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(agent): force CodeBuddy background-disable env last for Windows
Append CODEBUDDY_CODE_DISABLE_BACKGROUND_TASKS=1 so os/exec's
case-insensitive last-wins dedup keeps it over lowercase custom_env.
Also extract Desktop PATH fallback helper with unit tests.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Agents on the built-in omp (Oh-My-Pi) runtime could not set a reasoning effort at
all. omp dispatches to the pi backend but was missing from providerThinkingEnums,
so the server rejected every value, and parseOmpModels discarded omp's per-model
effort catalog, leaving the picker hidden and the daemon's guard nothing to
validate against. `--thinking` is blocked in custom_args by design, so the only
workaround was folding the level into the model string (`devin/swe-2:max`).
What a catalog entry means is taken from can1357/oh-my-pi v18.2.0 rather than
inferred from pi, because two of pi's rules are inverted for omp:
- `models --json` emits `thinking` as an effort array or `null`, never an object.
- omp's Effort vocabulary excludes `off`, yet resolveThinkingLevelForModel returns
`off` before clamping runs, so it is honoured for every reasoning model.
- `reasoning: true` with `thinking: null` means "reasons, no controllable dial";
inferring levels there would offer efforts omp then clamps away.
So a reasoning model advertises `off` plus exactly the efforts the catalog listed,
a non-reasoning model gets no picker, and nothing is inferred. `auto` is
deliberately not exposed: omp keeps it as a session-level sentinel that is never
an Effort or ThinkingLevel.
An effort with no pinned model is refused rather than stored, because omp resolves
its own default role model at task time and clamps the level to what that model
supports — so a stored level would save cleanly, read back as set, and then run at
a different level. The refusal holds in all three layers: the inspector hides the
picker, CreateAgent/UpdateAgent return 400 (Update against the state the request
lands on, so clearing the model while an effort is carried over is caught too),
and ValidateThinkingLevelWith rejects before any catalog load, so a discovery
error cannot reach the daemon's fail-open path.
codex shares that execution constraint but stays out of the API-level refusal via
a separate narrower predicate: agents already hold an effort with an empty model,
and 400ing that pair would block unrelated edits to them. omp has no such history.
Closes#8458
* MUL-7415 ci: retire the standalone Cursor lifecycle job
cursor-background-lifecycle ran a macos-latest and a windows-latest runner
on the runtime scope, i.e. on nearly every backend change, to cover one
integration's background shell. Neither leg needed to be its own job.
Windows: windows-execenv is already a windows-latest job on the same
runtime scope testing ./pkg/agent process-tree ownership. The Cursor
patterns move there as a step, so a backend PR spins up one Windows
runner instead of two. The ./internal/daemon watchdog command does not
come along: every test it selected is untagged except
cursor_uncaptured_watchdog_test.go, which is `linux || darwin` and never
built on Windows, so the ubuntu backend suite already runs all of them
under -race.
macOS: nothing else in this workflow runs Go on macOS, and
cursor_background_process_darwin.go is 208 lines of XNU identity
signalling whose two tests are `//go:build darwin`. Deleting outright
would leave it with no coverage anywhere, so it becomes macos-runtime,
gated on `full` — the daily cron and workflow_dispatch that #8451 added
for exactly this trade. Its single-count finalization step is dropped as
dead: `full` is now always true here, so the -count=5 stress pass that
was already the full-run variant is the only one left.
The backend gate tracks both moves; ci-scope's production gate test
parses ci.yml, so it verifies the new needs/if/scope wiring directly.
Co-authored-by: multica-agent <[email protected]>
* MUL-7415 test(agent): stop building the Cursor late-descendant test on Windows
TestCaptureCursorBackgroundLateDescendant failed twice in the last 43 main
pushes on windows-latest and zero times in 40 on ubuntu. The two Windows
failures reported different errors -- `Access is denied.` and `not an owned
descendant` -- from the same captureCursorBackgroundProcess call, which is
one race wearing two faces: cursorWindowsProcessParents snapshots every
PID->PPID pair on the machine, Windows keeps a child's recorded PPID after
the parent exits, and that PID is then free to be reused, so the ancestry
walk can reach a PID someone else now owns.
Narrowing the tag to `linux || darwin` keeps the Linux and Darwin arms,
which have never flaked and still cover the whole contract: a descendant
forked after capture is owned and terminated, the leader's death does not
orphan it, and an unrelated process is never signalled. Windows keeps
TestCaptureCursorBackgroundProcessRejectsForeignJob for Job membership.
This is deliberately a mute, not a fix. MUL-7417 carries the race itself
and restores the tag.
Co-authored-by: multica-agent <[email protected]>
* MUL-7415 ci: run the Windows Cursor step last in windows-execenv
Folding the Cursor job into windows-execenv means Cursor's failures now
truncate windows-execenv's own steps, which the separate job could never
do. Eight Windows-only checks -- bounded output collection, the Pi session
lock, the OpenClaw npm shim, junction resolution -- sat after it and would
be skipped by a single Cursor failure.
Moving the step behind `Build Windows CLI helper entrypoint` keeps the
one-runner saving while leaving the pre-existing coverage first in line.
Also adds the -timeout=5m this job's other test steps carry.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Follow-up to the review notes on #8416. Comments only — token arithmetic, fields,
JSON tags and test assertions are unchanged.
State the actual rule on the shared TokenUsage contract: only a breakdown already
included in a total must not be added to that total again, rather than banning
provider breakdowns as a category. Keep the four-bucket mutual-exclusivity
sentence normative.
Explain why codexRawTokenUsage retains ReasoningOutputTokens — it mirrors the
provider payload and keeps resume-baseline subtraction symmetric — and record
that no downstream consumer reads that breakdown.
Restore the normative sentence on the usage contract test and scope its coverage
claim to the Codex and Claude cases it actually exercises.
The task goroutine published c.threadID after thread/start or
thread/resume returned, while the stdout goroutine was already reading
the same field in isNotificationFromOtherThread. That filter runs ahead
of the current-turn gate, so the gate's state could not serialize the
read, and the app-server routinely emits a notification immediately
after the thread setup response.
Guard the field with its own RWMutex and route the write plus all three
reads through setThreadID/getThreadID, mirroring how turnID is already
handled. Behaviour is unchanged: the filter still compares a single
snapshot and keeps the existing no-current-thread, missing-threadId,
subthread and resume-history cases.
Fixes#8422
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Codex reports reasoning tokens as a breakdown inside `output_tokens`, but both the
app-server event path and the rollout JSONL fallback added them on top of the
output total, inflating recorded output usage and the usage-based cost estimate.
Consolidate both readers onto one shared conversion that keeps the provider's
inclusive output total, preserving cache-bucket exclusivity, resume baselines and
duplicate-snapshot handling. Add cross-provider contract tests covering both Codex
sources and both Claude result formats.
Verified against a live codex app-server session (codex-cli 0.144.6): the provider's
own totalTokens equals input + output with reasoning not added, and the recorded
buckets now sum to that total exactly. Previously persisted rows are not rewritten.
Co-authored-by: importcpp <[email protected]>
CodeBuddy is a Claude Code fork on the same stream-json protocol, and its
adapter carried the same usage-accounting defect #8382 fixed for Claude: one
response can emit several assistant events sharing a message ID and usage, and
the adapter summed every one of them. A run that ends without usable final
usage therefore reported inflated input and cache tokens.
Count each identified main-loop response once within an execution. Keep
forwarding every text and tool block, and keep the existing precedence of the
result's modelUsage over result-level usage over the fallback.
The fallback also stops summing the assistant output_tokens placeholder and
excludes nested-agent events, matching claude.go and the documented crash
recovery procedure. It recovers main-loop input/cache usage, not a full bill.
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Count each main-loop response once when a Claude run ends without usable final usage. Claude can split one API response into several assistant events sharing a message ID and usage; summing them inflated input and cache tokens on the fallback path. Terminal modelUsage/usage precedence is unchanged, and all text and tool blocks are still forwarded.
The fallback no longer sums the assistant output_tokens placeholder and skips nested-agent events, following the documented per-step accounting and crash-recovery behavior. It recovers main-loop input/cache usage only, not a complete bill.
Co-authored-by: importcpp <[email protected]>
TestCodexExecuteRetriesAfterSignaledProcessIsReaped spends the handshake
budget twice, and the two spends pull in opposite directions: the first
attempt only reaches the signal/reap path by burning the whole budget, while
the retry has to answer initialize inside that same budget from a cold sh
spawn. At 50ms only an idle machine satisfied the second spend, so under CI
contention the retry timed out too and the test reported the retry as
missing — it failed that way on main twice today (361edd90a, 8908fcfbc).
Reproduced in a CPU-constrained Linux container: 25/25 failures with the
same message CI prints, and a measured 100-309ms cold-spawn handshake
latency under that load. With a 2s budget the same loop passes 15/15. The
cost is one deliberate 2s wait on a Linux-only test.
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* test(server): reduce slow unit test waits
Co-authored-by: multica-agent <[email protected]>
* test(server): cut slow Go unit test time further
Follow-up to the first pass on MUL-7271. Test-only changes plus a few
package-private seams whose production values are unchanged.
- Re-executed race-built test binaries no longer pay the race runtime's 1s
atexit sleep (GORACE=atexit_sleep_ms=0 for child processes only).
- pkg/agent: event-driven or shorter test-only bounds for handshake, drain
and idle graces; redundant timeout variants merged.
- daemon/execenv: git fixtures copied from a template repo built once; tests
that re-ran the same worktree lifecycle folded into their supersets;
ForkLock-guarded fake-CLI writes so parallel tests cannot hit ETXTBSY.
- handler/service: cap and paging tests shrink their caps instead of seeding
thousands of rows; lock waits probe pg_locks instead of sleeping; one
shared pgxpool per service test binary; cheaper cron and RSA fixtures.
- wecom: the phishing-link sweep reuses one parser and derives CRLF from LF
with a sampled cross-check; fake sockets acknowledge instead of waiting out
the ack timeout.
- migrate: the migration corpus is parsed once; isolated DB tests run in
parallel.
Co-authored-by: multica-agent <[email protected]>
* test(server): remove transport timeout unit tests
Co-authored-by: multica-agent <[email protected]>
* test(server): restore two non-timeout tests and make a bound load-bearing
Follow-up to the review of #8318. All test-only; no production code changes.
- daemon: restore the three read-liveness cases that the transport-timeout
sweep took with it. They assert that a ping, a pong and an application
frame each EXTEND the read deadline, so a healthy wakeup socket survives
without traffic of our own — the deadline never fires on the happy path,
which is why they are not transport-timeout tests. Verified they fail
(all three) when the extension calls are removed from wakeup.go, and pass
in 0.57s together. The pure "times out without peer traffic" case stays
deleted.
- pkg/agent: restore the parent-DEADLINE half of the opaque-env redaction
pair. codex.go treats a parent deadline and a parent cancellation as one
ending; only the Canceled arm was left covered. The fake now writes the
secret to stderr before reading the handshake, so the check no longer
races our own initialize write, and the budget is 2s against a measured
430-465ms launch. Verified it fails when the !contextEnded guard is
removed.
- pkg/agent: TestRunCollectQuietDoesNotWaitWhenTheAnswerIsIn had its bound
widened to a literal 5s, which the shrunk 750ms drain grace can never
reach — the test could no longer fail. It now raises the drain grace to
10s for its own duration and asserts under 3s: healthy runs take
0.19-0.44s, and a broken short-circuit takes 10.19s and fails.
- wecom: gofmt the import block in relay_settle_budget_test.go.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: J <[email protected]>