1615 Commits

Author SHA1 Message Date
YYClaw
318d51377f MUL-5870: fix(chat): hide channel issue commands from Chat
Hide shared channel /issue commands from public Chat projections while preserving issue creation, ordinary conversations, and cleanup paths.\n\nCloses #6571.
2026-08-07 17:22:45 +08:00
Bohan Jiang
734ad51c93 MUL-5774 fix(daemon): isolate Git metadata for Windows Codex checkouts (#6565)
* fix(daemon): isolate Git metadata for Windows Codex checkouts

Codex's sandbox resolves a linked worktree's gitdir to the shared .repos
cache and keeps it read-only even when the task workdir is an explicit
writable root, so `git add` / `git commit` fail from inside a
`multica repo checkout`. Linux was fixed by moving those tasks to a
task-local clone (#2925); Windows native sandbox still took the linked
worktree path and hits the same wall (#6449).

- repoCheckoutModeFor now returns isolated for Windows Codex as well as
  Linux Codex. The MULTICA_REPO_CHECKOUT_MODE -> CLI -> daemon ->
  WorktreeParams.IsolatedGitMetadata chain already carries the mode, so
  no new API surface is needed.
- The isolated clone passes --no-hardlinks on Windows. NTFS hard links
  only exist within one volume and every link shares one underlying file
  and security descriptor, so linking would both break a cross-drive
  cache/workdir pair and let the sandbox re-permission the daemon-owned
  cache's objects. Objects are copied there instead.

The shared cache stays read-only; this does not widen ACLs, add a
writable root, or lean on safe.directory.

Co-authored-by: multica-agent <github@multica.ai>

* fix(daemon): correct Codex sandbox facts and prove Windows checkout in CI

Review follow-up on the Windows isolated-checkout fix.

The source map claimed both Linux and Windows sandboxes keep a linked
worktree's external gitdir read-only. That is not what the daemon does:
execenv's codexSandboxPolicyFor defaults BOTH platforms to
danger-full-access, so only a user who opted into windows.sandbox
actually trips the read-only gitdir. Linux keeping the isolated checkout
is a retained layout, not a live workaround. Agents read this file to
decide whether their checkout can be committed to, so it must not
contradict the sandbox policy. Corrected the source map, the stale
"(Linux Codex)" comment in partial_clone_test.go, and the
repoCheckoutModeFor doc — which now also records why the layout is a
per-platform choice rather than a per-policy one.

builtin_skills_test.go gains "Linux and Windows Codex" and "task-local
Git metadata" anchors; the previous assertions covered only generic repo
wording and would not have caught this platform contract regressing.
Verified the anchor fails when the SKILL.md sentence is reverted.

Windows evidence: cache_windows_test.go asserts on a real Windows host
that the gitdir resolves inside the task checkout, that branch/add/commit
succeed, that the shared cache base ref is unchanged, and that objects
are private copies rather than NTFS hard links — plus a cross-volume
checkout, which is what a hard link cannot express at all. Wired into the
existing windows-execenv CI job, which exists for exactly this class of
"only a real Windows host proves it" regression. The shared no-hardlink
assertion is now one helper used by both the cross-platform and Windows
tests.

This does not replace a manual smoke under a real Codex elevated sandbox
identity; CI cannot assume that identity.

Co-authored-by: multica-agent <github@multica.ai>

* ci: scope the Windows repo-checkout step to the windows-tagged tests

The step used a prefix match, which also selected the package's
cross-platform isolated-checkout test. That test cannot run on Windows:
repocache derives a cache directory name from the full source repo path,
so a t.TempDir() path already embedding a long test name doubles and
exceeds MAX_PATH, and git fails with "Filename too long". It belongs to
the ubuntu backend job, which runs the whole package.

Both windows-tagged tests already passed in the failing run, including
the cross-volume one, which used the runner's real C:/D: pair rather
than skipping. Anchoring the selector to their exact names turns the job
green without weakening any assertion.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-07 16:53:40 +08:00
YYClaw
d09019cd95 MUL-5839: preserve media in /issue descriptions (#6536)
* fix(channel): preserve images in issue descriptions

* fix(views): hide channel-media provenance in board card previews

descriptionPreview strips image Markdown but not HTML comments, so a
`/issue` description whose media was materialized rendered its provenance
marker as the card's visible preview text. cardProperties.description
defaults to true, so this showed on the default board view for every issue
created from a channel message carrying an image.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-07 15:35:59 +08:00
Bohan Jiang
9914d51040 MUL-5846 fix(agent): stop launching CodeBuddy with MCP disabled (#6556)
* fix(agent): stop launching CodeBuddy with MCP disabled (MUL-5846)

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

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

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

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

Co-authored-by: multica-agent <github@multica.ai>

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

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

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

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

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

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

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

Co-authored-by: multica-agent <github@multica.ai>

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

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

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

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

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

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

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

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

Co-authored-by: multica-agent <github@multica.ai>

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

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

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

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

Co-authored-by: multica-agent <github@multica.ai>

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

Review nits.

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

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

Co-authored-by: multica-agent <github@multica.ai>

---------

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

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

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

Fixes #6462
2026-08-07 14:38:03 +08:00
Bohan Jiang
ad2d2eb8b4 MUL-5843 fix(agent): stop silently dropping MCP servers on ACP runtimes (#6555)
Built-in Hermes declares no mcpCapabilities in its ACP initialize response yet accepts both http and sse McpServer entries on session/new, so applying the ACP v1 default there discarded every remote MCP server users configured — visible only in the daemon log. Carve a narrow exception for genuine silence on the provider's own verified binary, keeping fail-closed for malformed responses, custom runtime profiles, and the seven unverified ACP backends.

Also warn when mcp_config carries servers under a runtime-native top-level key instead of mcpServers, which previously forwarded nothing with no log at all, and document how mcp_config reaches ACP runtimes.

Closes #6540
2026-08-07 13:14:54 +08:00
Bohan Jiang
545d890dae feat(cli): add --compact to issue comment list, adopt it in every agent read command (MUL-5442) (#6546)
* feat(cli): add --compact to issue comment list, adopt it in every agent read command (MUL-5442)

The #5999 follow-up its closing note asked for, now that the workspace
measurements exist: the two bounded reads the agent workflow prescribes
are dominated by reader-noise metadata, and every byte of it enters the
conversation history at full price once and is replayed at cache-read
price by every later call of the session.

--compact (JSON output only, opt-in) drops per comment: the issue_id
echoed from the request path, source_task_id, updated_at when identical
to created_at, null-valued fields, and empty arrays. Content, identity,
and thread-summary fields pass through untouched; a real edit timestamp
or non-empty array is never dropped.

Measured on MUL-5442 (production data): --roots-only --summary scan
6,117B -> 4,738B (-23%); --thread --tail 30 deep read 47,359B ->
39,349B (-17%).

Adoption: all nine runnable comment-list command templates in the
per-turn channel (prompt.go, reply_instructions.go) now carry --compact,
and the brief's Available Commands line and workflow step 3 document it
(+~40B one-time, byte-stable). Command pins updated accordingly; new
unit test pins the drop/keep contract and the help-contract test pins
the flag's rendered usage.

Known local-env failure: TestResolveWorkspaceID_AgentContextSkipsConfig
fails identically on clean origin/main in this agent runtime (agent
context env interference) — unrelated.

Co-authored-by: multica-agent <github@multica.ai>

* test(cli): wire-level --compact coverage and zero-value keep pins (MUL-5442)

Review nits on #6546: TestRunIssueCommentListCompactWiring drives
runIssueCommentList against an httptest server both ways — default JSON
keeps every field, --compact drops the reader-noise set while zero-value
scalars (reply_count: 0, content_truncated: false, folded_count: 0)
survive, pinning that null pruning never generalizes to zero pruning.
Same zero-value pins added to the pure-function test.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-07 12:41:21 +08:00
Multica Eve
4900a4a85b MUL-5832: separate directory waits from working signals (#6553)
* fix(daemon): separate directory waits from working status

Co-authored-by: multica-agent <github@multica.ai>

* fix(status): address directory wait review feedback

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-07 12:40:12 +08:00
Jiayuan Zhang
45027c1db6 MUL-5824 fix(search): demote cancelled issues and projects below live work (#6515)
* MUL-5824 fix(search): demote cancelled issues and projects below live work

Cancelled work was outranking live work in both search and the @mention
picker. Three separate causes:

- Issue search ordered by relevance first and status second, so statusRank
  was inert across tiers: a cancelled issue matching a title exactly (tier 1)
  beat an in_progress issue that merely contained the phrase (tier 3). With
  LIMIT 20 applied after ordering, a workspace with many cancelled issues
  could push live work off the page entirely.
- Project search had no status ranking at all, and the command palette
  renders projects above issues — so a cancelled project could be the first
  row of the whole result list.
- The mention picker merged its local issue cache ahead of the server
  results and only then truncated to MAX_ITEMS, so a cached cancelled row
  both outranked and crowded out server-ranked live matches.

Demote cancelled ahead of the relevance tiers in both query builders, and
apply the same partition in the picker before its truncation. 'done' is
untouched: finished work is still worth referencing, only cancelled work was
thrown away.

Exempt direct hits — an exact identifier or exact title means the searcher is
targeting that one record, so it stays on top. Curated mention groups
(current / recent / search) are exempt too: "Current" has to survive the
truncation even when the issue being viewed is itself cancelled.

Co-authored-by: multica-agent <github@multica.ai>

* MUL-5824 fix(search): demote cancelled across issue+project result lists

Follow-up to review on #6515. The per-type ranking landed server-side, but
every client that shows issues and projects together aggregates two
independently ranked responses — so a cancelled row from one still outranked a
live row from the other:

- The command palette and the mobile search screen render the whole Projects
  list before the whole Issues list, so one cancelled project was the first row
  of the result list next to a live issue.
- The context @mention picker concatenates issues then projects and tags both
  'search', while the client-side demotion only touched *ungrouped* cancelled
  issues. Cancelled search results were exempt and cancelled projects never
  participated at all.

Move the partition to the aggregation point and make it cross-type. New shared
helper @multica/core/search/cancelled-rank does one stable partition over both
types, preserving the server's relative order on each side and mirroring the
server's direct-hit exemption (exact identifier, bare number, exact title).

All three consumers now render live projects, then live issues, then one
trailing Cancelled section. A single trailing section is the only arrangement in
which no cancelled row of either type can precede a live row of the other, and
because it runs before truncation the cancelled tail is what gets dropped rather
than a live row. The mention picker keeps its Current / Recent exemptions: those
are explicit context, and "Current" has to survive truncation even when the
issue on screen is itself cancelled.

Mobile's row builder moved to apps/mobile/lib/search-rows.ts so the ordering is
testable without mounting the screen.

Also corrects the include_closed note in search_cancelled_rank_test.go: the
mobile mention picker sends include_closed=false, so it never sees cancelled
issues at all.

Tests: mixed issue/project regressions for the palette (3), the context mention
picker (4), the mobile row builder (6), and the shared helper (9). Each was
confirmed to fail with the partition neutralised.

Co-authored-by: multica-agent <github@multica.ai>

* MUL-5824 fix(mention): pin cancelled direct hits above the truncation

Second review round on #6515. The mention picker only exempted the curated
Current / Recent groups, so a cancelled issue or project the user had typed out
in full was still demoted — contradicting the direct-hit rule the backend and
the other two search surfaces already share.

The picker now applies that same query-aware rule via isIssueDirectHit /
isProjectDirectHit: exact identifier, bare number, or full title for issues, and
full title for projects. Both the partition and groupItems() take the query so
they cannot disagree about which rows are exempt.

Exemption alone was not enough. slice(0, MAX_ITEMS) runs on the merged list, so a
direct hit behind a full window of cached candidates was still cut — a stale
"row is still listed" claim in the old comment. The partition is now three
tiers, pinned → live → cancelled, where pinned holds curated context plus direct
hits. This mirrors the server, which already ranks direct hits first, and makes
"Current survives the truncation" structural rather than incidental.

isIssueDirectHit now takes all fields optional and derives the number from the
identifier when `number` is absent, which is what lets the picker share the rule:
its rows carry the identifier in `label` and the title in `description`, with no
number field. An explicit `number` still wins if both are present.

Tests: 5 mention regressions (exact identifier, bare number, project full title,
direct hit visible behind 20 live candidates, and a partial match that must still
be demoted) plus 3 helper cases for the identifier-derived number. Verified the
four ordering tests fail with the exemption reverted, and that three of them
still fail with exemption but no pinning — so the pinning tier is load-bearing,
not belt-and-braces.

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: Eve <eve@multica-ai.local>
2026-08-07 12:33:47 +08:00
Bohan Jiang
3c7221492b fix(agent): deliver the OpenCode prompt on stdin, not argv (MUL-5841) (#6552)
* fix(agent): deliver the OpenCode prompt on stdin, not argv (#6538)

The daemon appended the whole task prompt as an argv element, so on Windows
every OpenCode task whose command line cleared CreateProcess's 32,767
character lpCommandLine limit failed to start at all. Go surfaces the
resulting ERROR_FILENAME_EXCED_RANGE (206) as "The filename or extension is
too long", which reads as an exe-path problem and is not one. A workspace
with a realistic set of models and skills clears that ceiling on the prompt
alone (39,629 bytes in the report), leaving no usable in-app workaround.

`opencode run` merges its variadic [message..] positional with whatever is
piped in, so passing no positional makes the piped text the entire run
message. That path is present in the reported v1.18.14 and back to v1.4.10.

Write the prompt from its own goroutine, as the Pi and Cursor backends
already do: a prompt larger than the pipe buffer blocks mid-write until
OpenCode drains it, and OpenCode cannot drain while nobody consumes its
stdout. OpenCode reads stdin to EOF, so stdin is closed exactly once as the
end-of-prompt signal — including on the cancellation path, where closing it
releases a writer still blocked on a full pipe. The existing process-group
SIGTERM -> SIGKILL ordering from #4533 is unchanged.

Keeping the prompt off argv also stops it being echoed into the "agent
command" log line; prompt_bytes replaces it there.

Scoped to OpenCode. DevEco, OpenClaw (#6032), Qwen, Copilot and Antigravity
inline their prompts the same way, but each CLI's input contract needs
verifying on its own before it moves.

Co-authored-by: multica-agent <github@multica.ai>

* fix(agent): do not fail a completed run on a benign prompt-write EPIPE

The backend CI run for this PR failed in TestPiExecuteRetainsOnlyLastTurnOutput
with "pi prompt write failed: write |1: broken pipe". The cause is a latent
race shared by pi and the OpenCode code added in the previous commit, which
copied pi's error handling.

A child that exits without draining stdin closes the read end while the parent
is still writing the prompt, so the parent's write returns EPIPE. Both backends
converted that into a failed result even when the agent had already emitted a
complete, successful stream — making the outcome depend on whether the write
landed before or after the child exited, i.e. on machine load.

OpenCode reads stdin to EOF before it does any work, so a run that produced a
complete stream necessarily received the whole prompt; an EPIPE recorded after
that only means the pipe closed on the way out. Report the write error only for
a run that did NOT complete, where it can actually explain the failure, and
append rather than overwrite so the stream's own diagnosis survives. cursor.go
already guards this correctly by ignoring writeErr once a result was seen.

Test fakes now drain stdin, matching what the real CLIs do. A fake that exits
without reading is what manufactures the spurious EPIPE, and that is what broke
CI — pi's piEventStreamScript is the fixture that failed.

Adds a deterministic regression test: a child that emits a full successful
stream and never reads a 1.2 MB prompt. Against the previous commit it
reproduces the CI signature exactly (status "failed", "broken pipe"); with this
change it passes.

pi.go's production handling has the same defect and still needs the equivalent
fix, but that belongs to whoever owns #6485 — this change keeps to OpenCode
plus the test fixture that was breaking the build.

Co-authored-by: multica-agent <github@multica.ai>

* fix(agent): gate prompt-write suppression on a positive terminal signal

Review caught a false-success path opened by the previous commit. It
suppressed a failed prompt write whenever status was "completed", on the
reasoning that a completed run must have received the whole prompt. That
reasoning does not hold: processEvents starts finalStatus at "completed" and
only fails closed on structural evidence — an open step, or a step awaiting a
continuation. A child that emits NOTHING and exits 0 sets neither, so the
default survives.

The full path: the parent writes a large prompt, the child exits 0 without
reading a byte, the write returns EPIPE, stdout hits EOF with no events, and
the run is reported as completed with empty output and no error. The prompt
never reached the agent and nothing said so. Reproduced deterministically
before the fix: status="completed" output="" error="".

Absence of a failure signal is not proof of success, so record the positive
one instead. eventResult.sawTerminalSignal is set only when a step_finish
closed the last step with no continuation pending, and the write error is
suppressed only on that evidence. It is deliberately not the negation of
noTerminalSignal: an empty stream sets neither, because there is nothing to
fail closed on and nothing proving completion either. When the signal is
absent the write error now also flips the default "completed" to "failed".

Adds the empty-stream regression alongside the existing complete-stream one,
so both directions of the branch are pinned.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-07 12:31:55 +08:00
Bohan Jiang
37e2b07094 MUL-5831 fix(agent): fail opencode runs that end on an empty step (#6545)
* fix(agent): fail opencode runs that end on an empty step (MUL-5831)

`opencodeBackend.processEvents` required a positive terminal signal but only
failed closed on two shapes: EOF while a step was still open, and a step that
finished with reason "tool-calls" whose continuation never started.

A final step that opens, emits only a reasoning part, and closes with reason
"unknown" and an all-zero token block matches neither, so `opencode run` exited
0 and the daemon reported `completed` with an empty `agent_error` — a run with
no deliverable that leaves the issue stalled until something re-wakes the agent.
Zero input tokens means the provider round-trip never happened; that is a dead
stream wearing a clean finish.

Track whether each step produced anything at all — text, a tool call, or any
non-zero token counter — and fail closed when the run ends on a step that
produced none of them. The criterion is deliberately "this step produced
nothing", not "the run produced no text", so a task whose only deliverable is a
tool side effect stays green. The finish reason is not consulted: a missing or
unrecognised reason must stay terminal for protocol compatibility, and voidness
is orthogonal to it.

The run now enters the existing failure path with a diagnosable error, so the
task's retry rules apply instead of a silent false-green completion.

Refs #6522 — this fixes the first of its three symptoms; see the PR body.

Co-authored-by: multica-agent <github@multica.ai>

* fix(agent): count every opencode usage field as round-trip evidence (MUL-5831)

The void-step guard claimed "any non-zero token counter keeps the step
productive", but only checked input, output and cache. OpenCode's protocol
keeps reasoning and the aggregate total in fields of their own and reports cost
as a sibling of the token block, and none of the three were parsed at all — so
a real step that landed with reasoning or cost positive while input and output
happened to be zero would be called void and fail a healthy run.

Parse reasoning, total and cost, and move the check into stepReportedUsage so
the criterion is stated once. Reasoning and total are read as evidence only:
total is derived, and TokenUsage has no reasoning bucket, so folding either
into the usage accumulator would change billing figures rather than fix a bug.

Also renames the guard's error text to "no reported usage", which is what it
now means.

Co-authored-by: multica-agent <github@multica.ai>

* fix(taskfailure): make opencode stream-ended failures retryable (MUL-5831)

Turning a false-green completion red only helps if the failure then retries.
It did not: none of the three errors the OpenCode terminal-signal guard raises
was on the retry allowlist. The two "terminal signal" variants matched rule
13's "signal" substring by accident and landed in agent_error.process_failure;
the new empty-step one fell through to agent_error.unknown. Either way the task
died on the first attempt and max_attempts never applied.

All three mean one thing — the provider stream died and `opencode run` still
exited 0 — which is agent_error.provider_network by definition. Route them
there on the shared "opencode stream ended" prefix. That bucket is retryable
and resume-safe, so the retry child inherits the session and continues the
truncated conversation instead of redoing the work already paid for.

Covered end to end: the classifier cases, and a service-level test that walks
each guard error through Classify into retryEligible — retrying with an attempt
left, and still terminating at the ceiling so a deterministically broken
provider cannot loop.

Co-authored-by: multica-agent <github@multica.ai>

* fix(taskfailure): upgrade legacy opencode stream-ended reasons (MUL-5831)

Rule 7 only decides where these failures land when THIS server classifies
them, and FailTask classifies only when the daemon sent no reason at all. An
installed daemon predating that entry reports a non-empty
agent_error.process_failure instead — its rule 13 matching the word "signal" in
"terminal signal" — so the empty-reason branch skips it and the run stays off
the retry allowlist. The fix reached only hosts that happened to update, which
is the installed-daemon cadence problem NormalizeDaemonReason exists for.

Upgrade process_failure / unknown / coarse agent_error to provider_network when
the error opens with the guard's own prefix. Wider than the context-overflow
rule above it on purpose: that one leaves refined reasons alone because a
phrase somewhere inside an error blob says less than the bucket an earlier rule
picked, whereas this witness is the guard's message from its first character,
so the old bucket cannot be describing a better-identified cause.

The retry test becomes a matrix over both wire shapes — current daemon (server
classifies) and each legacy reason — since the previous version called Classify
directly and never crossed the normalize boundary where the gap was. A
negative case pins the prefix: an unrelated crash that merely mentions the
phrase keeps the daemon's label.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-07 11:46:44 +08:00
Bohan Jiang
49feb190d7 docs(wecom): document WeCom as a channel and name its code owners (MUL-5226) (#6549)
WeCom shipped in #5833 with no user-facing documentation: it was absent from
the channels page in all four locales and from the README, so nobody could
learn how to create the bot, where bot_id/secret come from, or that the first
version is text-only and single-replica.

- channels.{mdx,zh,ja,ko}: add WeCom to the intro, the platform comparison
  table, session isolation and the self-hosting keys; state the text-only and
  single-replica limits; note that DingTalk and WeCom are community-maintained
  with no support SLA.
- README: list WeCom alongside the other channels.
- wecom/types.go: add the package maintenance header naming @leroy-chen and
  @seacen as code owners, mirroring dingtalk/config.go — including the
  deprecation rule and the reminder to loop them in on shared-engine changes
  that alter WeCom-visible behavior.

The step-by-step /wecom-bot-integration guide is deliberately not linked yet;
it is tracked separately for the code owners to write, since they use the
WeCom admin console and we do not.

Co-authored-by: multica-agent <github@multica.ai>
2026-08-07 11:45:49 +08:00
YYClaw
d598e7a3e6 fix(channels): deliver terminal task failure details (#6534) 2026-08-07 11:20:28 +08:00
YYClaw
7a809ddf80 feat(channel): materialize inline media references (MUL-5835) (#6529)
Replace adapter-generated inline media placeholders in channel message bodies with authenticated attachment references, preserving original image positions.

Closes #6528
2026-08-07 11:12:45 +08:00
leroy-chen
040bb97b0f feat(wecom): WeCom (企业微信) smart-bot integration via the Channel engine (MUL-5226) (#5833)
Adds the WeCom (企业微信) smart-bot integration built on the unified Channel engine — inbound chat, /issue, inbox notifications, install/revoke UI and an explicit user-binding flow.

Co-authored-by: seacen <10457833+seacen@users.noreply.github.com>

Follow-up: #6547
2026-08-07 11:02:13 +08:00
YYClaw
0453cdb903 MUL-5844 fix(agent): tolerate coarse Kimi wire log mtimes (#6542)
* test(agent): stabilize Kimi resume usage test

* fix(agent): tolerate coarse Kimi wire log mtimes
2026-08-07 10:54:59 +08:00
Bohan Jiang
882666d478 fix(runtimes): document QwenPaw and close its frontend drifts (MUL-5828) (#6521)
* docs(runtimes): document QwenPaw and fix its hidden MCP tab (MUL-5828)

QwenPaw shipped in SupportedTypes but reached the docs only through the
two READMEs. Add it everywhere the other 19 runtimes are enumerated:

- providers / install-agent-runtime (en, zh, ja, ko)
- SELF_HOSTING.md and CLI_AND_DAEMON.md, including
  MULTICA_QWENPAW_PATH / _MODEL / _ARGS and the launch contract
- environment-variables (4 locales): _ARGS covers five tools now that
  MULTICA_QWENPAW_ARGS exists, not four
- landing i18n (4 locales): the "15 supported tools" list was missing
  DevEco Code, Grok, Qoder CN, Qwen Code, and QwenPaw

QwenPaw is the only runtime where ModelSelectionSupported returns false,
so document that its model list is empty by design rather than a symptom
of an offline runtime.

Also add qwenpaw to MCP_SUPPORTED_PROVIDERS. qwenpaw.go forwards
opts.McpConfig, but the omission hid the MCP tab in the agent inspector,
which would have made the new "Multica-managed MCP: yes" row false.

While in these tables, move the Reasonix row back into runtime-id order.

Co-authored-by: multica-agent <github@multica.ai>

* fix(runtimes): close the remaining QwenPaw drifts and stale runtime lists (MUL-5828)

Frontend-side follow-ups to the QwenPaw docs pass:

- RUNTIME_PROFILE_PROTOCOL_FAMILIES was missing qwenpaw, so the custom
  runtime profile picker could not select it even though migrations
  253/254 and agent.SupportedTypes both allow it.
- PROVIDER_DISPLAY_NAMES was missing it too. The daemon's
  runtimeDisplayNameOverrides maps qwenpaw to "QwenPaw"; without the
  mirror an aliased runtime rendered "Qwenpaw", which is exactly the
  drift the comment on that map warns about.
- provider-logo had no qwenpaw case and fell through to the generic
  Monitor icon. Adds the standalone mark from the official wordmark
  (agentscope-ai/QwenPaw, Apache-2.0), drawn with currentColor so one
  path covers both themes.

Also corrects two stale runtime lists in the root docs:

- Drop the Gemini row and MULTICA_GEMINI_PATH / _MODEL from
  CLI_AND_DAEMON.md. Migration 126 dropped the gemini protocol family
  and no gemini probe exists, so the runtime has been gone for a while.
- Add Antigravity, CodeBuddy, and DevEco Code to CLI_AND_DAEMON.md and
  SELF_HOSTING.md, plus their PATH / MODEL / ARGS variables.

Co-authored-by: multica-agent <github@multica.ai>

* fix(agent): make MULTICA_QWENPAW_ARGS work and stop advertising _MODEL (MUL-5828)

Review caught two env vars the docs promised but the runtime did not
honour.

MULTICA_QWENPAW_ARGS was read by config.go and forwarded by daemon.go as
ExecOptions.ExtraArgs, but the backend consumed CustomArgs only, so the
value was plumbed all the way down and then dropped. Consume ExtraArgs
before CustomArgs, matching the precedence CLI_AND_DAEMON.md documents
and the five other backends that accept both. The new test fails on the
old backend with "acp --per-agent".

MULTICA_QWENPAW_MODEL cannot work by design: the backend never calls
session/set_model because that rewrites QwenPaw's shared agent config,
so ExecOptions.Model is ignored (pinned by TestQwenpawUsageModelIgnored).
The probe read it into AgentEntry.Model, which only ever feeds that
ignored field. Drop the read and stop documenting the variable, and note
the exception on the four environment-variables pages, which otherwise
claim every tool takes a _MODEL override.

Co-authored-by: multica-agent <github@multica.ai>

* docs(agent): stop enumerating ExtraArgs consumers in the field comment (MUL-5828)

The list said "claude and codex backends only", which was already wrong
for codebuddy, antigravity, and qwen. An enumeration here has to be
updated from a different file every time a backend opts in, and going
stale is exactly what let MULTICA_QWENPAW_ARGS ship plumbed but dropped.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-07 10:44:20 +08:00
Bohan Jiang
2f5efa34ca test(daemon): add fan-out guard anchors for --content-file and inline --content (MUL-5825) (#6544)
Elon's #6517 review nit: the block-level banned list omitted the old
cookbook's two central mechanism anchors, so a prose-only restatement of
the flag mechanics ("use --content-file / don't use inline --content")
could regrow in the fan-out block under green tests. Add both anchors,
and replace the phrasing-fragile "Do NOT write literal" anchor with the
semantic backtick-\n-escape anchor that survives rewording.

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-07 10:40:18 +08:00
Bohan Jiang
73ab498d1d MUL-5811: fix(squad) derive leader role from wire fields, not instructions text (#6519)
* fix(squad): derive leader role from wire fields, not instructions text (MUL-5811)

The daemon decided whether a task ran in the squad-leader role by grepping
the agent's Instructions for the "## Squad Operating Protocol" heading that
the server-injected briefing starts with. Role detection therefore depended
on user-writable Markdown: any ordinary agent whose own instructions happened
to contain that heading was promoted to leader and handed the leader rules —
mandatory `multica squad activity` calls, a licence to exit silently on
no_action, and the squad-maintenance CLI section in its brief.

The correct signals were already on the wire the whole time. The claim
handler decides injection from explicit fields (`task.is_leader_task` +
`squad_id` for issue-bound runs, the quick-create `squad_id` for the picker
path) and forwards both to the daemon. Read those instead:

- `taskIsSquadLeader` returns `IsLeaderTask || SquadID != ""`; brief assembly
  now shares that helper instead of repeating the substring check.
- The claim handler clears `is_leader_task` on the response whenever its
  defensive gate withholds the briefing (NULL squad_id, squad hard-deleted,
  leader swapped after enqueue). The flag now means "briefing injected", so a
  leader task with no roster and no protocol degrades to an ordinary agent
  turn instead of booting into a leader role it cannot perform.

Every legal path is byte-identical: the server sets these fields on exactly
the claims it injects into, so real leader runs and ordinary runs produce the
same brief and prompt as before. The only behavior change is the misdetection
case, which is the bug.

Also corrects two comments claiming IsSquadLeader is agent configuration
rather than a per-task role, and notes the flag contract in the squad
source map.

Co-authored-by: multica-agent <github@multica.ai>

* fix(squad): gate leader-role field reads behind a server capability (MUL-5811)

Review catch (#6519): reading is_leader_task / squad_id unconditionally
breaks the new-daemon → old-server direction the repo deliberately supports
(claimTasksLegacy, MUL-4257). is_leader_task only reached the claim response
in #4951, while briefing injection predates it — so against an older server a
real issue-bound leader task arrives with the full briefing and both fields at
zero, and field-only detection would silently demote that leader to a plain
worker, dropping the entire operating protocol.

Add `leader_role_resolved`, a claim-only wire capability the server sets on
every claim response (leader or not). Its absence is what identifies a server
too old to have answered the role question:

- capability present → the explicit fields decide, and an ordinary agent whose
  instructions contain the briefing heading is no longer promoted to leader;
- capability absent → keep the legacy briefing-marker inference, which is
  exactly today's behavior against those servers. That branch goes away once a
  minimum server version is enforced.

The field is claim-only and never rendered into a prompt, so it costs no
prompt tokens and changes no brief or prompt bytes on any path.

Tests: the role table now covers both server generations, including legacy
real-leader (heading, no capability), current ordinary-agent-with-heading, and
current withheld-briefing; an end-to-end legacy prompt test pins that an old
server's leader keeps its rules; and every claim test now asserts the server
actually advertises the capability — losing it would send all daemons back to
text inference.

Also rewords two comments per review: the brief byte-stability tradeoff is
owner-accepted and recorded in MUL-5811, not an open tracking item.

Co-authored-by: multica-agent <github@multica.ai>

* docs(squad): state the pre-capability server shapes precisely (MUL-5811)

Review nit: the comments said every server without leader_role_resolved
"never sent is_leader_task", which is only true before #4951. Servers between
#4951 and this change do send the flag — they just never promised it implies
an injected briefing, which is the actual reason the fallback covers them too.

Names both shapes at each site (daemon types, handler response, helper doc,
role-table doc, squad source map) so a future cleanup of the fallback reads
the version window correctly. Comments only; no behavior change.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-06 21:05:21 +08:00
Bohan Jiang
2d993a6a80 MUL-5823 fix(dashboard): count cancelled runs in usage run time (#6518)
* fix(dashboard): count cancelled runs in usage run time (MUL-5823)

CancelAgentTask accepts a task in 'running', so a cancelled row can carry
both started_at and completed_at — real agent occupancy. The run-time
rollups filtered on `status IN ('completed','failed')`, so every run the
user stopped mid-flight contributed 0 seconds and 0 to the task count.

The cost side has no status filter at all (UpsertTaskUsage and the hourly
rollup ignore status), so Cost/Tokens counted those runs while Time/Tasks
did not — two different task populations on the same dashboard, diverging
further the more runs get stopped.

Widen both run-time queries to include 'cancelled' and report it as a
third outcome alongside failed. The existing `started_at IS NOT NULL`
guard keeps a run cancelled while still queued out: it never occupied an
agent. The failure rollups keep the two-status filter on purpose — a
manual stop is not a failure and must not dilute the error rate.

Migrations 261/262 swap the supporting partial index to a predicate that
covers the third status; without that the widened filter can no longer
use it and the rollups fall back to a full table scan.

Co-authored-by: multica-agent <github@multica.ai>

* fix(migrate): guard the 261/262 index swap against an interrupted build

An interrupted CREATE INDEX CONCURRENTLY leaves an INVALID index behind.
`IF NOT EXISTS` then reports success on retry without rebuilding it, the
runner records 261 as applied, and 262 drops the still-valid v1 — leaving
every dashboard rollup on a full table scan.

Register cleanupInvalidConcurrentIndexHook for 261, the same guard
migration 257 already uses for the same hazard.

The down path needs different handling: hooks only run in the `up`
direction, so 262.down drops IF NOT EXISTS and fails closed instead —
matching what 258.down does for the 257/258 pair.

Adds a regression test covering both. Unlike 257's unique index this one
cannot be failed with a duplicate row, so the build is interrupted the way
a real one is: a concurrent open transaction blocks the wait phase until
statement_timeout cancels it. The test asserts the bare retry is a silent
no-op, that the hook repairs it, and that 262 only drops v1 once v2 is
valid.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-06 20:59:32 +08:00
Bohan Jiang
6ca6b27ce3 refactor(daemon): converge multi-thread fan-out cookbook into Comment Formatting pointer (MUL-5825) (#6517)
The cross-thread reply fan-out block embedded a full file-operations
cookbook (body file -> --content-file -> cleanup, the inline/stdin bans,
the \n-escape rule, plus two example command pairs) that triple-wrote
the mechanism already carried by the brief's ## Comment Formatting and
the single-thread cookbook. The block now keeps only what is
multi-thread-specific — fan-out announcement + override, posting order,
per-thread --parent targets, the DISTINCT-body-file-per-thread delta,
and the squad leader's whole-block no_action scope sentence — and points
at ## Comment Formatting for the mechanism.

Dropping the embedded commands removes the only OS-dependent text, so
the windows/unix cookbook variants collapse and the block no longer
branches on runtimeGOOS; the OS split lives solely in the brief.

Measured (2-thread, 36-char-UUID fixture): ordinary 1,657B (linux) /
1,675B (windows) -> 919B both; leader 1,870B / 1,888B -> 1,132B both.
~-740-760B per coalesced multi-thread turn.

Pins updated per discipline: ordinary unconditional form and the leader
scope-ordering assertions are retained (obligation strings track the new
text, ledger comments record the mapping); retired cookbook strings get
negative guards in the block test and the assembled-prompt test; a new
non-parallel test pins OS-invariance.

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-06 19:35:37 +08:00
Bohan Jiang
b24634e6eb MUL-5810: fix(agent): reconcile ACP terminal usage (#6503)
* fix(agent): reconcile ACP terminal usage

Co-authored-by: multica-agent <github@multica.ai>

* fix(agent): make ACP usage reconciliation deterministic

Co-authored-by: multica-agent <github@multica.ai>

* fix(agent): normalize merged ACP usage fields

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-06 17:02:02 +08:00
Jiayuan Zhang
3c9b861de3 MUL-5765 feat(chat): starter cards under Mika's onboarding opening (#6505)
* MUL-5765 feat(chat): starter cards under Mika's onboarding opening

Replace the LLM quick-action chips on Mika's onboarding opening with three
product-fixed starter cards (board / delegate / digest), so a new member
never starts from a blank canvas. Clicking a card sends its fixed prompt as
a visible member message through the existing quick-action path; cards
follow the chips' disabled rule while a task runs and stay clickable in
history. Older clients keep the LLM chips untouched.

The multica-onboarding skill gains matching starter plays (one-question
budget each) and a digest-only exception to the no-autopilot rule.

Co-authored-by: multica-agent <github@multica.ai>
(cherry picked from commit 32364cea89)

* MUL-5765 refactor(chat): drop pre-release compatibility around starter cards

The onboarding flow has not shipped, so no old client exists to protect
(per review): the server now skips the quick-actions pass for the opening
turn (new TaskHasOnboardingKickoffInput gate) instead of generating chips
the new client would hide, and the multica-onboarding opening points at the
cards directly — beat 4 is the mock's bridge line again.

Co-authored-by: multica-agent <github@multica.ai>
(cherry picked from commit 26e5242d0b)

* MUL-5765 fix(chat): stamp the onboarding opening so starter cards actually render

The cards never showed in the field: the client detected the opening by
finding the hidden kickoff row, but visibleChatMessages strips that row from
list responses — and the client zod schema coerced unknown message kinds to
'message' anyway, so the signal died twice before reaching the detector.

The opening now self-describes: writeChatCompletionOutcome stamps the reply
to a kickoff-input task with message_kind 'onboarding_opening', the schema
and type unions carry the new kind, and the client keys the cards off it
directly — pagination-proof, and the hidden kickoff stays hidden. The
explicit quick-actions eligibility gate is gone: a non-'message' kind
already skips the pass at both call sites. The kickoff prompt also forbids
pre-reply narration ('I'll load the onboarding skill first.' leaked into the
field opening).

Co-authored-by: multica-agent <github@multica.ai>
(cherry picked from commit ffacd3d02c)

* MUL-5765 fix(chat): stamp the opening on the input owner, not the retry clone

The kickoff check asked TaskHasOnboardingKickoffInput about task.ID. An
auto-retry clone gets a fresh id while inheriting the root's
chat_input_task_id (MUL-4351), and the kickoff user row stays bound to the
root -- so the child answered false.

An opening that only succeeded after a retriable failure (provider_network,
runtime recovery) therefore persisted as a plain 'message': no starter cards,
and a chips pass generated for a turn whose copy invites the member to pick
one of the cards that never rendered.

Keyed on chatInputOwnerID now, which exists for exactly this provenance
question. The query doc states that its argument is the input-owning id, so
the next caller does not reintroduce task.ID.

The regression test drives the real completion write with
child.ID != child.ChatInputTaskID; it fails on the old call site with
kind "message".

Co-authored-by: multica-agent <github@multica.ai>

* MUL-5765 fix(onboarding): give the digest schedule a timezone to name

The digest starter play proposed "every morning at 09:00" while the kickoff
profile carried only workspace name, role and use case. `autopilot trigger-add`
defaults an absent --timezone to UTC (DefaultAutopilotTriggerTimezone, and the
flag's own help), so a member outside UTC could confirm a morning summary and
receive an afternoon one -- a wrong assumption that recurs daily.

The member's IANA timezone now travels in the profile block, stated as a value
either way and emitted before the skipped-questionnaire early return: the
digest card is clickable whether or not the questionnaire was answered, and
"unknown" is the case the skill has to handle rather than paper over. What to
do with it lives in the skill, since that block declares itself data and never
a command.

The digest play must now quote the whole time ("09:00 Asia/Shanghai") and pass
--timezone; when the zone is unknown it spends the one allowed question there
instead of falling back to UTC silently.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-06 16:54:45 +08:00
Multica Eve
25d26c2bf7 fix(agent): send Pi prompts over stdin (MUL-5779) (#6485)
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-06 16:26:23 +08:00
Multica Eve
7bc1e3c36a test(agent): make OpenClaw EOF test deterministic (MUL-5802) (#6501)
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-06 16:14:02 +08:00
Bohan Jiang
d6d1996210 refactor(daemon): retire the generic no-reply rule — ordinary agents always post one comment (MUL-5442) (#6493)
* refactor(daemon): retire the generic no-reply rule — ordinary agents always post one comment (MUL-5442)

Owner decision (MUL-5442, 2026-08-06): the no-reply / silent-exit
behavior is squad-leader-only by design. Mechanically it always was —
computeCommentAgentTriggers routes agent-authored comments only via
explicit @mentions (the sole implicit wake is the squad-leader path), so
loop prevention lives in the Mentions discipline, and the generic rule
only suppressed one terminal noise comment on an already-enqueued run
while giving every ordinary agent a legal silent-exit path that risks
silently dropped results.

- prompt.go: delete the per-turn agent-trigger warning block entirely
  (−728B per agent-triggered turn; supersedes #6484's compression).
- runtime_config_sections.go: delete the Reply-mode reply-warranted
  bullet and step 5's conditional pointer; the reply bullets become
  unconditional (−472B one-time in the brief). Squad-leader no_action
  bullets and the Mentions section are untouched.
- reply_instructions.go: 'If you decide to reply, post it' → 'Post your
  reply' (−16B per Reply turn); the leader's no_action block states its
  own exception.
- Tests: retired pins are converted to negative guards with ledger
  comments (brief and per-turn both must NOT carry the retired
  apparatus); attribution, member-leak, and no-MUST-respond guards stay.

Co-authored-by: multica-agent <github@multica.ai>

* fix(daemon): scope the reply imperative with the leader's no_action carve-out, close the pin-ledger gaps (MUL-5442)

Review catches by Elon on #6493, both accepted:

1. The leader's only silent path was contradicted later in the same
   prompt: the no_action rule said EXIT without commenting, then the
   cookbook and the brief's mandatory bullet said post unconditionally.
   Both now branch on squad leadership — agent configuration, never
   per-run state, so each agent's brief stays byte-identical across
   turns and issues (cache-safe): leaders get 'Unless your outcome is
   no_action, post your reply…' in the cookbook and 'Unless your outcome
   is no_action (Squad leader rule above), posting your reply as a
   comment is mandatory' in the brief; ordinary agents keep the
   unconditional wording byte-for-byte. New tests render the COMPLETE
   leader prompt and leader brief and assert the carve-out is present
   AND the unconditional capital-P imperative is absent — the scope
   relation, not two isolated Contains.

2. The retirement ledger named five brief pins but guarded only two,
   and the per-turn 'If you decide to reply' had no guard. Added the
   four missing negative guards in ordinary-agent scope (the leader's
   'DO NOT post any comment' matches none of them) plus a leader-leak
   guard on the ordinary brief.

Co-authored-by: multica-agent <github@multica.ai>

* fix(daemon): scope the multi-thread fan-out under the leader's no_action carve-out (MUL-5442)

Ports the per-turn half of #6493 review rounds 2-3 into the slimmed PR
(the brief role-independence refactor moves to its own sub-issue/PR per
owner decision): BuildMultiThreadCommentReplyInstructions takes the
leader flag, and the leader lead sentence scopes the ENTIRE fan-out
block — every later obligation sits under the 'Otherwise' — with
index-order test guards. Ordinary fan-out output is byte-unchanged.

Also repairs a truncated TestFreshSessionMayHelp from the earlier
rebase conflict resolution (missing closing braces).

Co-authored-by: multica-agent <github@multica.ai>

* docs: correct the taskIsSquadLeader comment — leadership is a per-task role (MUL-5442)

Review nit on #6493: the helper's comment still claimed leadership is
'agent configuration, never per-run state' — the disproven premise that
drove the earlier review rounds. State the per-task-role fact and point
at MUL-5811 for the role-independence work.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-06 16:12:22 +08:00
Bohan Jiang
eb9302db41 docs(dingtalk): note community-maintained support status (#6499)
We told @yyclaw the DingTalk integration would be documented as
community-maintained. This makes good on that without turning the docs
page into an ownership notice.

- Docs (en/zh/ja/ko): one line under the intro — community-maintained,
  no official support SLA, where to report problems. Docs readers care
  about the support commitment, not who wrote it, so the maintainer's
  name is deliberately not here.
- Package comment in server/internal/integrations/dingtalk: the full
  contract, including the code owner and the deprecation rule, aimed at
  the people who actually need it — anyone refactoring the shared
  channel engine.

No behavior change.

Co-authored-by: multica-agent <github@multica.ai>
2026-08-06 15:44:37 +08:00
Bohan Jiang
7a7de4fa56 MUL-5773 fix(agent): report kimi token usage from its session wire log (#6495)
kimi-code 0.33.0 exports no token counters over ACP, so every kimi task landed on the usage dashboard with no row at all. Read the counters from kimi's per-session wire log instead, the same fallback codex.go uses for Codex rollouts.

The scan is bound to the ACP session id, buckets usage by the model each record names, covers subagent logs, filters records by timestamp so a resumed session is not re-billed, and sums only usage.record (the sibling step.end event repeats the same numbers).

Verified end-to-end against the real CLI: the task reports input:5075 output:26 cacheRead:17664; with the fallback disabled the same run reports no usage at all.

Fixes #6448
2026-08-06 15:44:25 +08:00
YYClaw
c3577cb04b feat(dingtalk): add DingTalk bot integration (MUL-3958) (#4829)
Adds a DingTalk (钉钉) bot integration on the bring-your-own-app model: a
workspace admin creates their own Stream-mode robot and pastes its AppKey /
AppSecret, so no public webhook or OAuth redirect is required. Each agent gets
its own bot identity, so several agents can be distinct, separately
@-mentionable contacts in one DingTalk organization.

Supports DMs, @-mentions in groups, inbound images, /issue quick-create, and
/new. Built on the shared channel engine (ForceFresh/BareFresh, MediaResolver /
MediaRef and the intent ledger) rather than a private implementation.

Off unless MULTICA_DINGTALK_SECRET_KEY is set. Docs in en/zh/ja/ko.

Closes #4791.

Community-maintained: @yyclaw is the code owner for
server/internal/integrations/dingtalk/.
2026-08-06 15:36:02 +08:00
Bohan Jiang
96d567d517 fix(agent): pair failed tool results for OpenCode (MUL-5802) (#6497)
Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-06 15:22:32 +08:00
qiushihao279-cloud
5a618e0925 fix(agents): stop resuming a session that can't resolve its provider auth (MUL-5803) (#6482)
* fix(agents): stop resuming a session that can't resolve its provider auth

A Hermes agent on a self-hosted install can get permanently stuck on a single
issue while every other issue on the same agent keeps working. The task
terminates with:

  hermes provider error: "Could not resolve authentication method. Expected
  either api_key or auth_token to be set. Or for one of the X-Api-Key or
  Authorization headers to be explicitly omitted"

and no amount of retry / Rerun recovers it.

Root cause (resume-pointer poisoning): every daemon version classifies this
text as agent_error.unknown, which is resume-safe. So GetLastTaskSession /
GetLastChatTaskSession keep returning the same failed session to every retry
and Rerun on that (issue, agent) pair, deterministically reproducing the auth
error forever. Other issues use fresh sessions, so they're unaffected — the
codebase's repeated "(agent, issue) permanently stuck" pattern (GH #6066 /
#5760 / #6360, MUL-5722), with this error falling through every prior defense.

The fix rests entirely on text guards; the classifier is deliberately left
untouched. Reclassifying this under missing_config would flip freshSessionMayHelp
to false and silently disable the in-turn fresh-session retry on the five
ResumeRejectionUndetectable backends — contradicting the (correct) diagnosis
that a fresh session cures it:

- service/task.go ResumeUnsafeFailure: text guard so manual Rerun and the
  fallback claim path start fresh rather than replaying the dead session.
- GetLastTaskSession / GetLastChatTaskSession SQL: ILIKE exclusion so
  already-wedged issues recover on their next trigger without a daemon upgrade.

Tests pin both halves: a freshSessionMayHelp regression (must stay true for
this error), the Go ResumeUnsafeFailure cases, and SQL exclusion + narrowness
regressions for both query families.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: address PR review nits

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: qiushihao279-cloud <301943329+qiushihao279-cloud@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-06 14:39:24 +08:00
Bohan Jiang
941aa41898 fix(daemon): discover models from the custom runtime profile's own binary (#6488)
handleModelList resolved the executable from d.agents()[rt.Provider], which
holds built-in CLIs only, while runTask launches the custom runtime profile's
own command (MUL-3284). A host with only the custom command installed answered
the model list with `no agent configured for provider "hermes"`, so the picker
stayed empty even though the same runtime ran tasks fine; a host with both
installed enumerated one binary and executed the other, so the picker could
advertise models the launched CLI rejects.

Discovery now mirrors runTask's resolution order: the profile owns the path when
the runtime is profile-backed, the built-in entry (with its MUL-4486 self-heal)
otherwise, and the original failure is kept for the case it was written for.

The 60s discovery memo carried the same defect — only codex / opencode / deveco
keyed on the executable path, so a built-in runtime and a same-family profile
runtime on one host shared an entry. Every dynamic-discovery branch now uses
discoveryCacheKey; an empty path still maps to the bare provider key.

Also stops pi's parser coining CLI usage text into model IDs: a pi-family
profile pointing at a fork without --list-models printed a usage hint that the
field splitter turned into a `Run/`omp` model. The filter is narrow enough that
a real catalog row alongside a usage hint still parses, so reading a catalog off
a non-zero exit (#3729) is unchanged.

fixed_args still does not participate in discovery — tracked in MUL-5807.

Fixes #6466. Fixes #4482. Fixes MUL-5789. Fixes MUL-5471.
2026-08-06 14:21:46 +08:00
Jiayuan Zhang
b99b04bb86 feat(onboarding): Mika issue-first onboarding (#6378)
* feat(onboarding): Mika issue-first onboarding

Replaces the starter-agent welcome with one conversation: onboarding creates
Mika, the workspace's built-in Chief of Staff, and opens a real chat whose
first turn is a product-authored kickoff hidden from the transcript. Every
workspace — first and subsequent — is created through this flow.

Mika is a system agent, not an agent-template instance. Her product prompt is
//go:embed-ed and composed at claim time, so a release updates it without
touching any workspace's row; the row holds only the workspace's own notes.
Creation is server-owned and idempotent under a per-workspace advisory lock,
and archiving a system agent is rejected.

This is the pre-merge half of the branch, squashed while rebasing onto main.
Replaying its fifteen commits individually meant re-deriving each one against
a main they were never written for; the net change reconciles against today's
main in four files, so it is reconciled once, here.

Three of those four are main moving under the branch: MUL-5573 took
quick-actions generation server-side and dropped QuickActionsDisabled /
RegenerateQuickActionsFor from the task payload and the SendDirectChatMessage
signature, so this takes main's shape and keeps only the onboarding entry
point. The fourth keeps main's OnboardingLogoutButton wrapper around the
flow's new mode/onCancel props.

Co-authored-by: multica-agent <github@multica.ai>

* feat(onboarding): let quick-action chips carry the opening's examples

Chat now renders agent-suggested follow-up actions as buttons under a reply
(MUL-5149), and the onboarding kickoff qualifies for that suggestion pass
with no extra wiring — it is a direct chat turn on a web session with a
non-empty reply.

That made the opening's fourth beat redundant: Mika wrote a three-to-five
line menu of example tasks, then three chips appeared underneath offering
the same thing. The prose menu is the worse half — a member has to retype a
line they read, but can send a button — so the beat is gone and the opening
budget drops with it. Measured on a real local run: the first reply went
from 307 to 208 characters, and the chips still arrived 21s after the
kickoff.

The questionnaire profile stays in the kickoff. The suggestion pass resumes
the same provider session, so the profile now steers the chips as well as
the reply; only the sentence naming its purpose changes.

Co-authored-by: multica-agent <github@multica.ai>

* refactor(onboarding): cut the step rails down to what the screen can't already show

The right rail carried more than twice the words of the column members were
actually working in — 42 vs 36 on the workspace step, 91 vs 34 on the runtime
step — and it got heavier the further in the flow went, which is backwards.

Step 3's rail was the clearest case. Its "Good to know" section promised the
runtime was swappable and that more could be added later; the step's own lede
already said both in nine words rather than forty-two, on the same screen. Its
60-word definition of "agent runtime" sat beside a list of named, online
runtimes under a headline reading "This computer is connected" — by then it
answers a question the member has stopped asking. What survives is the one
thing the screen does not show: what that background process is.

Step 2's rail keeps the workspace preview card, which does show something not
otherwise visible, and drops the bullet lists — promises the product is about
to keep on its own.

The freed words did not move to the rail; one moved into the main column. On
the create path "Mika" was never introduced before Step 3 used the name twice,
once on the primary button, so the lede now names the role in an appositive
right above that button. Mika stays ungendered, as everywhere else in the
product.

Net across the three regions: 167 words to 89.

Co-authored-by: multica-agent <github@multica.ai>

* refactor(onboarding): drop the right rail from the workspace and runtime steps

Every remaining rail item was either something the screen already showed or
something the product was about to do anyway, so the column was costing a
member's attention without answering a question they had. Removing it leaves
each step a single full-width column — the shape the questionnaire step has
always had, and the only step nobody has complained reads as sparse.

Gone with it: RuntimeAsidePanel, the workspace preview card and its entity
rows, and 27 copy keys per locale. The two runtime paths (desktop
runtime-connect, web platform-fork) shared that panel, so both lose it in one
move and stay identical.

The welcome step keeps its column — it holds an illustration, not prose.

Co-authored-by: multica-agent <github@multica.ai>

* fix(onboarding): put every step's header and content on one measured axis

Removing the right rail left the four steps geometrically inconsistent in
four ways, all of which the member sees as things moving while they advance.

The header carried the horizontal padding itself, so it ran flush to the
window edge while the content column stayed centred. A 480px rail had been
absorbing that difference; without it the two were 267px apart on a 1283px
window, and since StepHeader is justify-between the step indicator floated
off at the far right, ~270px from anything it labelled.

The rest compounded it: the measure changed between steps (920px on the
questionnaire, 620px after), the header bar and content block used different
vertical padding per step, and padding living inside a max-w box made the
reading width jump from 508px to 620px at the lg breakpoint.

All four now come from step-shell.tsx. Padding belongs to the gutter, never
to a measured box, so the reading width is constant from ~700px up. The
header measures on STEP_FRAME on every step, so the one element that survives
each transition never moves; content picks STEP_FRAME or STEP_COLUMN by what
it holds, and both centre, so a step that needs the width still sits on the
header's centreline. Vertical rhythm is one value.

The header was near-identical in four files, which is how it drifted in the
first place, so it is now one component. Its test pins the invariant that
broke — padding out of the measured box, header measured on the frame — and
fails if either is put back.

Co-authored-by: multica-agent <github@multica.ai>

* fix(onboarding): give the runtime step the frame measure and cut its copy

Nine runtimes in a 620px column meant truncated names, five rows, and
scrolling to reach the last four, with ~330px of dead space on each side.
This is the case step-shell's wider measure exists for, so the step takes
STEP_FRAME — which also puts it on exactly the header's measure — and the
card grid gains a third column at lg. Nine runtimes now land in three rows.

Copy went with it. The headline was two sentences over two lines; the first
one, "This computer is connected", is already said louder by the "9 agent
runtimes · all online" row directly beneath it, so only the instruction
remains. The lede was 44 words and five lines — I had grown it myself adding
Mika's introduction — and is 20 now, still naming the role. The
remote-computer note drops from 25 words to 15.

Prose stays capped at 620px inside the wider frame; a 920px measure is for
the card grid, not for reading.

The found-phase test keyed on the headline copy, so a copy edit read as a
behaviour regression. It now asserts the runtime count row, which is the
signal the test is actually about.

Co-authored-by: multica-agent <github@multica.ai>

* refactor(onboarding): put the workspace step on the shared frame

It was the only step still measuring at STEP_COLUMN, so its eyebrow, headline
and footer CTA started and ended ~150px inside where every other step's did —
the page margins visibly moved when you advanced from step 1 to step 2.

The step now sits on STEP_FRAME like the other two, with a new STEP_MEASURE
capping the prose and the form inside it. Matching the frame is not the same
as widening the field: a workspace name does not want an 800px input, so the
form keeps its reading measure and left-aligns to the frame instead. The
footer row spans the frame, which is what puts the CTA in the same place on
all three screens.

STEP_MEASURE deliberately does not centre — centring would pull the content
off the frame's left edge, undoing the alignment. Its test pins that.

Co-authored-by: multica-agent <github@multica.ai>

* fix(onboarding): close the gap above the step CTA and put Log out back on one row

Two defects that read as one thing on screen: a large empty block sitting
directly above the primary button.

The card list was capped at STEP_MEASURE along with the form. That cap exists
so a workspace name does not get an 800px input — a good reason for a text
field and a bad one for selection cards, which are a list like the runtime
grid. Capped, they stopped 299px short of the CTA, leaving a void above the
button. The cards take the frame now; only the form keeps the reading measure.

Log out came in from main as `fixed right-8 top-8`, pinned to the window
corner. Its own comment says the fixed position exists to survive the flow's
full-bleed layouts — which is what the measured frame replaced, so it landed
outside the measure and above Back / Step N of N as a second header row. It
now rides the header row on the frame.

StepShellHeader takes it as a `trailing` slot rather than rendering it:
calling useLogout inside the shared header forced a QueryClient into five
step test files just to render a header bar. The flow injects it, matching
how runtimeInstructions is already threaded, and the header stays
presentational.

Co-authored-by: multica-agent <github@multica.ai>

* feat(onboarding): make step 3 about Mika, with the runtime as a sub-decision

The step was titled after its dependency — "Pick an agent runtime" — while
the thing actually being created was named only in the grey lede. A member
reached a button reading "Start with Mika" without having been told who that
is, and said so: being confused there is not a failure to read carefully, it
is the page putting the lead role in a footnote.

So the subject changes rather than the step count. The headline names the
outcome, a card carries the introduction with a mark on it, and the runtime
list drops to a labelled sub-section under "Where should Mika run?". Reading
order becomes: who you are getting, she needs a machine, pick one, start.

MikaIntro sits above the phase switch so the subject holds still while the
runtime block below cycles through scanning / found / empty, and each phase's
own heading drops from h1 to h2 now that the page has a real h1.

Mika does not exist yet at this point — she is created on commit — so the
card cannot render her stored avatar. It reuses the mark the Runtimes page
already uses for "Start with Mika", so the two entry points read as the same
thing.

No fourth screen: the introduction and the only decision on this screen are
one beat, and splitting them would add a step between finishing setup and the
payoff. The defect was never a missing screen, it was an invisible
introduction.

Co-authored-by: multica-agent <github@multica.ai>

* feat(onboarding): pick the runtime and the model from dropdowns

Nine runtimes as cards took three rows and left nowhere to put a second
decision. Two dropdowns fit both on one screen, and the model is a real
choice the card grid had no room for.

Both controls already existed on the agents surface — RuntimePicker and
ModelDropdown, the same pair create-agent-dialog uses — so this reuses them
rather than growing a parallel picker. ModelDropdown owns its own discovery,
grouping and unsupported-runtime states, and selecting a different runtime
clears the model because models are per-runtime.

The model now reaches the agent: POST /api/agents/mika takes an optional
model, CreateSystemUserAgent writes it to the column the agent table already
had, and empty still means "whatever the runtime defaults to" — which is what
every deployment without per-agent model support gets anyway.

Also drops the "No local runtime, or prefer a remote computer?" note. It said
in twenty-five words what the Skip button next to it says by existing, and it
appeared on all three phases.

currentUserId comes in as a prop rather than from the auth store: reading the
store inside the step broke six tests that render it without one, the same
coupling the header slot avoided.

Co-authored-by: multica-agent <github@multica.ai>

* fix(onboarding): stop gendering Mika in the intro card

Mika is ungendered everywhere else in the product — the agent instructions,
the onboarding skill and every other locale string avoid a pronoun. The intro
card I added last round reintroduced one in two languages ("she turns it into
an issue" / "她会把它变成一个 issue"), which the Chinese screenshot made
obvious. All four locales now read around it.

Co-authored-by: multica-agent <github@multica.ai>

* feat(ui): port the ReUI stepper primitive

Groundwork for rebuilding onboarding on the @reui/onboarding-3 interaction
model: a persistent vertical stepper with named steps and click-to-return
navigation, which our horizontal "Step N of 3" dots cannot express.

Routed to components/ui rather than the vendor's components/reui namespace —
it is our code now — and rewritten to the role-named type scale (text-xs ->
text-caption, text-sm -> text-label / text-caption, dropping the leading-none
the token already supplies). No other convention fixes were needed: shadcn had
already rewritten the imports, "use client" survived because main's
components.json now sets rsc: true, and it pulls no npm dependency we do not
already have (@base-ui/react is declared).

The block's other twelve registry dependencies are primitives we already own,
so only this one was installed.

Co-authored-by: multica-agent <github@multica.ai>

* feat(onboarding): rebuild the step shell as a named progress rail

Onboarding's chrome was a row of dots and a "Step 2 of 3" counter. It told
a member how much was left but never what was coming, so every step arrived
unannounced -- the same reason the runtime step read as a surprise even after
it was retitled "Meet Mika". Naming all three steps up front makes onboarding
legible as a whole before the first field is filled in.

StepShellHeader becomes StepShell, and it owns the window rather than a strip
at the top: rail on the left, the step's own content scrolling on the right.
That lets the four steps drop an identical wrapper/DragStrip/header/<main>
preamble, including the scroll-fade wiring that was duplicated verbatim in
all four and is now set up once.

The rail is built on the ported ReUI stepper, but deliberately not as a
tablist: that component defaults to role=tablist with each trigger owning
aria-controls on a panel id, which is right for a stepper that renders its
own panels and wrong here, where the panel is a routed step and those ids
would dangle. It uses the presentational slots and marks position with
aria-current instead.

Only completed steps are clickable -- moving forward has to run the current
step's validation and submit, so the rail would skip it. New-workspace mode
gets no rail navigation at all: it enters at the workspace step and, once
that workspace exists, every step behind it is gone. Same invariant
runtimeStepBack already enforced for the Back button.

step_header.step_of is replaced by step_nav across all four locales; Mika
stays ungendered in each.

Co-authored-by: multica-agent <github@multica.ai>

* feat(onboarding): standardise the steps on the shared UI primitives

Follow-up to the rail, fixing what the rail exposed.

The workspace form was three hand-rolled `flex flex-col gap-1.5` stacks with
their own label sizing and a bare <p> for the slug error -- exactly what
Field/FieldLabel/FieldError/FieldDescription standardise. The manual version
had already drifted: its labels were caption-sized and muted while every
other form in the product labels at body weight.

The platform fork wrapped on STEP_COLUMN while the steps before it wrapped
on STEP_FRAME. Both measures centre, so 620px and 920px put their left edges
~150px apart and the headline visibly jumped right on arrival. It now sits on
the frame and caps its own content, which is the pattern step-shell already
documents.

The About you eyebrow read "About you" -- now the rail's label for that very
step, so the page said its own name twice, once in grey caps and once in the
headline under it. Dropped, along with the locale key. The other steps keep
theirs because they say something the rail doesn't ("Connect a computer",
"Workspace creation is disabled").

Log out is `inline` on every step, and inline now means "on the rail", which
is an inverted surface -- the muted/destructive pair it used unqualified is
mixed from the light palette, so it was rendering as near-invisible grey on
black.

e2e: the smoke spec asserted "Step 1 of 3", text the rail replaced. It now
asserts the rail's named steps and which one is aria-current, and carries on
into the runtime step so all three get captured. Two pre-existing bugs in
that spec surfaced while fixing it: the zh-Hans case pinned its locale cookie
to a hardcoded port, and it advanced with getByRole("button").first(), which
is the pinned Log out button -- so that case had been signing the user out
and asserting against a login redirect.

Co-authored-by: multica-agent <github@multica.ai>

* feat(onboarding): rebuild the rail as the ReUI inset panel

The first pass kept the block's idea -- a named vertical rail -- and dropped
most of its structure. Side by side with onboarding-3 the gap was obvious:
no brand lockup, no inset panel, no texture, steps jammed against the top
edge, numbered chips instead of the ring/check/dot progression, stub
separators instead of one continuous track, and a bare Log out where the
block has a footer row.

This ports the structure properly:

- Inset panel -- the aside carries the padding and the panel is rounded with
  a hairline ring, instead of a dark rectangle bleeding to the window edge.
- Brand lockup top-left (the existing MulticaIcon plus a wordmark), Back
  demoted to an icon button top-right, which is where the block puts it.
- Step list centred in the remaining height rather than stacked under Back.
- Indicators follow the block: filled + check when done, ring + dot when
  current, faint ring when upcoming. Numbers move to sr-only text, since the
  ring already encodes position and the digit was redundant next to a label.
- One continuous hairline behind each row instead of a stub between rows.
- DotSphere ported to packages/ui as the panel's texture. It is decorative,
  reads no product state, and already honours prefers-reduced-motion.

Two fixes fell out of doing it properly:

`.dark` is a plain class selector in our token sheet, so scoping it to the
panel redefines the custom properties for that subtree and `bg-background` /
`text-muted-foreground` mean the right thing inside it. That replaces the
hand-mixed `text-background/60` shades of the first pass -- which is what had
made Log out near-invisible -- and it is why the button is back on ordinary
tokens here.

StepperSeparator hardcodes a 3rem height for vertical navs, so an absolutely
positioned track overshot its row and drew the line straight through the next
indicator. Overridden with the same variant rather than !important, so
tailwind-merge drops theirs.

DotSphere cycles three constant arrays by `index % length`. Under
noUncheckedIndexedAccess that is `T | undefined`, so they are typed as
non-empty tuples and index 0 is the fallback -- no non-null assertions.

Co-authored-by: multica-agent <github@multica.ai>

* feat(onboarding): put the content pane on the block's type and layout

The rail matched onboarding-3 and the pane it sat next to did not, so the
two halves read as different products: serif display headlines and a grey
uppercase eyebrow on the right, the block's sans hierarchy on the left.

Typography now maps onto the block exactly, and it lands on our scale
without rounding -- ReUI's `text-xl/7` heading is our `text-title-lg`
(20/28) and its `text-sm/5` supporting line is our `text-body` (14/20).
StepHeading owns both, so no step hand-rolls a headline again.

The eyebrows are gone. The block has no such slot, and with the rail naming
every step, a grey uppercase label above a headline saying the same thing
was the third name for one screen. The workspace step's disabled-state
wording was the only eyebrow carrying information the headline lacked, and
that variant already exists as its own headline copy.

Geometry collapses from three competing measures -- a 920px frame, a 620px
column, an in-frame cap -- to one 28rem column. Three measures is what let
the platform fork sit ~150px right of every other step. STEP_MEASURE stays
for capping a single control inside the column.

Actions move into StepFooter: full-width, stacked, pinned to the bottom of
the column. In a 28rem column the old right-aligned inline bar left the
primary action floating mid-screen instead of where the eye finishes the
form. The column is `min-h-full` rather than centred by the pane, because
`items-center` on a scroll container clips the top of anything taller than
the viewport and these steps do overflow on short windows.

Questionnaire options become the block's wrapping chips. They were
full-width cards in a 4-column grid; inside a 28rem column that grid had
nowhere to go, and stacking all 18 as rows turned one screen into a long
scroll. Chips are the block's own answer for a many-option question. This
also reaches the workspace source-backfill prompt, which shares the
component -- intentionally, it is the same question in the same style.

MikaIntro moves onto StepHeading + Item for the same reason.

Co-authored-by: multica-agent <github@multica.ai>

* fix(onboarding): drop the rail clear of the macOS traffic lights

The rail is a dark inset panel and it started at the window's top-left
corner, which is exactly where macOS draws the traffic lights -- so the
close/minimise/zoom buttons sat on top of the dark surface.

The underlying mistake was structural. The desktop shell rule asks for a
single DragStrip as the first flex child of a full-window view; this had two
hand-rolled strips instead, one inside each pane, and neither was first. The
sidebar's was 28px of internal padding trying to duck under the traffic
lights from inside a panel that had already begun above them, which cannot
work -- the panel's own background was the thing being overlapped.

One DragStrip now spans the window above both panes. The panel starts below
it (48px strip + the aside's inset, measured at 64px on a wide window against
traffic lights that end around 32px), the whole band stays draggable, and the
two ad-hoc strips are gone.

Co-authored-by: multica-agent <github@multica.ai>

* fix(onboarding): make every block in the column share one width

Reported on the workspace step: the name field ended short of the
description above it. Measured rather than eyeballed -- the heading,
description and footer all render at 448px, the form at 384px, because the
FieldGroup still carried STEP_MEASURE.

STEP_MEASURE made sense against the old 920px frame, where a full-width
input would have been absurd. Against a 28rem column it does nothing but
misalign, so both remaining uses are gone -- the workspace form and the
runtime picker -- and the constant with them. A single measure that no step
can locally narrow is the whole point of the column; leaving the knob
exported invites the same drift back.

Two stale measures went with it. The runtime phase views still capped their
ledes at max-w-[620px], inert inside a 448px column, and sized them
text-body-lg against StepHeading's text-body; their h2 was text-title-lg,
the same size as the h1 above it. Both now match the shared scale.

Guarded in e2e rather than a unit test. The shell test renders a stub child,
so asserting "no narrow cap inside the column" there would pass whatever the
real steps do. The new spec walks all three steps and compares rendered
geometry -- it fails on the reported bug and passes after the fix, which is
what makes it worth keeping.

Co-authored-by: multica-agent <github@multica.ai>

* fix(onboarding): stop the whole window re-fading on every step change

Every step rendered its own <StepShell>. Because each step is a different
component type, React tore the shell down and built a new one on each
transition: the "persistent" rail remounted, its canvas restarted, and the
shell replayed `animate-onboarding-enter` -- a 0.4s fade from opacity 0
across the entire window. That full-window re-fade is the flash.

Measured before changing anything: tagging the live <aside> and switching
steps showed the attribute gone, and the shell root still reporting
animation-name `onboarding-enter` after the switch.

The upstream block has no step-change motion at all -- no animate-*, no
transitions, no framer-motion, no AnimatePresence. Its sidebar and section
live in one component and only the step body swaps. This does the same: the
flow owns a single StepShell and the steps render content. The shell's
entrance fade now runs once, on entering onboarding, which is what it was
for.

`backDisabled` was the one thing blocking the hoist -- the shell needs it but
only the workspace step knows the create request is in flight. It reports
upward through `onBusyChange`, with an unmount cleanup: a successful create
advances immediately, so without clearing the flag the next step would open
with Back and the rail dead.

Guarded in e2e by tagging the rail and content nodes and asserting they
survive a step change, plus a single DotSphere canvas rather than one per
visited step.

Co-authored-by: multica-agent <github@multica.ai>

* chore: drop a stray commit-message file

Co-authored-by: multica-agent <github@multica.ai>

* i18n(zh): retranslate onboarding and fix cross-file inconsistencies

Reported as "生硬" on the onboarding Chinese. Reviewed all 217 onboarding
strings against the Chinese voice guide in conventions.mdx, then swept the
other 24 locale files for the same classes of problem.

Three were mistranslations, not stiffness. The worst: an agent runtime was
described as the AI coding tool "我们接管的" — take over — where the English
says "we connect to". Also a "推荐" that exists in no source string, and
"12 calls" (user interviews) rendered as "12 通电话".

Punctuation, decided from the repo's own zh docs rather than guessed:
「」 is forbidden by the guide and appeared 16 times; 破折号 spacing was
split 26 spaced vs the rest unspaced, and the docs run 152 unspaced to 35,
so unspaced wins. Four strings had a stray space inside Chinese text
("正在跳转到 工作区").

67 English strings had two or more Chinese translations. Most are
legitimate — weekday pickers use single characters where labels spell them
out, 飞书/Lark are deliberate regional variants, and "Name" is 姓名 for a
person and 名称 for an object. 42 were arbitrary and are now unified.

ja/ko got the same consistency pass on the clear-cut cases only, kept
parallel with the zh choices. Their punctuation was deliberately left alone:
「」 is standard Japanese quoting, so the zh rule does not transfer.

Edits are applied to the raw file text rather than through a JSON
round-trip, which reformatted compact one-line objects and turned ~20 real
edits into a 97-line diff.

Co-authored-by: multica-agent <github@multica.ai>

* chore: drop a stray commit-message file

Co-authored-by: multica-agent <github@multica.ai>

* feat(mika): use the unicorn emoji as Mika's placeholder avatar

Mika shipped with a hand-rolled data-URI SVG — a sparkle glyph on a dark
rounded square — which only that one constant knew how to produce. Agents
already have an emoji avatar convention (`emoji:` marker in avatar_url,
owned by agentEmojiAvatarPrefix on the server and parseAvatarEmoji on the
client), so this reuses it instead: ActorAvatar renders the emoji as text and
no surface needs to special-case her.

The two cards that stand in for Mika before the agent row exists move with
it — the onboarding intro card and the Runtimes "Start with Mika" card. Both
were drawing the same sparkle mark, and leaving them would mean a member sees
one face during onboarding and a different one the moment Mika is created.
They now share MIKA_PLACEHOLDER_EMOJI, which carries a pointer to the server
constant so the two cannot drift apart silently.

The dark square went with the sparkle: it was built to frame a white line-art
glyph, and an emoji on it reads badly. These use bg-muted, matching how
ActorAvatar already frames an emoji avatar.

Placeholder until Mika has real artwork.

Co-authored-by: multica-agent <github@multica.ai>

* i18n: stop the skip path promising things it does not do

Two claims in the runtime-skip copy that the code does not back, both found
while walking the flow end to end.

"Enter your workspace in read-only mode" — there is no workspace read-only
concept on the server. The only READ ONLY in handlers is Postgres transaction
isolation on the issue-table and search queries. What actually exists is
admission.go's ReasonAgentRuntimeRequired, which blocks dispatch when no
runtime is connected. So the sentence's second half was already true and
enforced; the first half promised a restriction that does not exist — a
member who skips can create issues, comment, edit fields and invite people
exactly as normal. Same over-claim in cloud_waitlist.intro_warning.

"We've added one task" — the skip path creates an *issue* (it lands on the
Issues board as NOR-1), and task/issue are distinct entities in this product.
Now says issue.

All four locales.

Co-authored-by: multica-agent <github@multica.ai>

* i18n: reframe Mika as your first agent teammate

Requested copy change on the step 3 headline.

The rail's description for that step moves with it. It is the step's own
subtitle and sits on the same screen as the headline, so leaving it saying
"Your Chief of Staff" would have put two different framings of Mika side by
side on one page.

Three "Chief of Staff" references are deliberately untouched, because
dropping the title everywhere is a positioning call rather than a copy fix:
the role chip on the intro card (name + title reads fine under the new
headline), and the web fork's lede, which is a different screen.

Chinese follows the glossary (Agent -> 智能体) rather than the mixed-language
phrasing in the request.

Co-authored-by: multica-agent <github@multica.ai>

* fix(runtimes): let the member pick the runtime before Mika is created

"Start with Mika" provisioned immediately on
`runtimes.find(online) ?? runtimes[0]`. One machine commonly registers every
agent CLI installed on it — nine on the box this was reported from — so "the
first online one" is an arbitrary pick, and Mika could end up bound to a CLI
the member never intended to run their Chief of Staff on. Rebinding after the
fact is more work than choosing up front.

The action now opens a dialog with the same two controls onboarding already
uses for this decision, RuntimePicker and ModelDropdown, so the same choice
reached from a different entry point is asked the same way. The old heuristic
survives only as the dialog's initial selection, which makes it visible and
changeable instead of silent.

Model resets when the runtime changes, because models are per-runtime and a
value picked for the previous one may not exist on the next.

Co-authored-by: multica-agent <github@multica.ai>

* refactor(runtimes): one component for "which runtime should Mika use"

Three surfaces asked this same question and had drifted apart: desktop
onboarding and the Runtimes page offered a runtime plus a model, while the
web CLI dialog offered only a runtime. `step-platform-fork` called
`onNext(picker.selected)` with no second argument, so connecting through the
web terminal path silently created Mika on whatever model the runtime
defaulted to, while the desktop step let you choose. Two of the three also
re-implemented "changing the runtime clears the model"; the third simply
lacked it.

MikaRuntimeChoice now owns the pair and that reset rule, so no caller can
forget it and the three entry points cannot drift again. The web CLI path
gains model selection, which is the behaviour change here.

`layout` is a prop rather than a single unified presentation because the
difference is real: the CLI dialog lists machines because that is the moment
they appear one at a time after `multica setup`, and a collapsed dropdown
hides exactly the feedback that dialog exists to give. Everything below the
list is identical.

compact-runtime-row moves from onboarding/ to runtimes/ so imports only flow
onboarding -> runtimes rather than both ways.

Creation is deliberately left alone. A and B still funnel through
`handleRuntimeNext`, which also runs saveQuestionnaire, completeOnboarding
and onComplete; the Runtimes page must not do any of that, since that member
is already onboarded. Merging those would leak onboarding completion into a
non-onboarding surface.

The platform-fork test now needs a QueryClientProvider, because the dialog
renders a model dropdown that queries the runtime's model list, and its
onNext assertion moves to the two-argument signature.

Co-authored-by: multica-agent <github@multica.ai>

* fix(onboarding): stop the download card claiming a result it cannot know

Clicking "Use this computer" set `downloaded` unconditionally, which flipped
the card to "Opening the download page..." / "Opened in a new tab." Neither
is knowable: `window.open` is called with `noopener`, and per spec that
returns null whether the tab opened or a popup blocker ate it, so a blocked
click still produced a card asserting a tab had opened.

The same flag was also write-once, so the transient "Opening..." became a
terminal state — come back to the tab later and it still says the page is
opening. And because the swapped title wraps to two lines, the card grew
10px and pushed the two cards under it down, which is what made the click
read as a page refresh in the first report.

The card now states its intent up front — "Opens in a new tab — pick your
platform there" — which is true before the click, after it, and when the
popup never appears. The state, both `_after` strings and `hint_downloaded`
are gone from all four locales.

Its test asserted the flip, so it now asserts the opposite: the mocked
window.open returns null (the blocked case) and the card must be unchanged.
Measured after the change: primary card 330px before and after, and the
cards below it do not move.

Co-authored-by: multica-agent <github@multica.ai>

* fix(desktop): stop one worktree in a thousand booting into a blank window (#6436)

Worktree renderer ports are `5174 + cksum(path) % 1000`, a 5174-6173 window
that contains exactly one port Chromium refuses to navigate to: 6000, the X11
port on its restricted list. A worktree whose path hashes to offset 826 gets a
healthy Vite server on 6000 and an Electron window that fails the load with
ERR_UNSAFE_PORT -- so it reads as a renderer bug, not a port one, and the only
way out was setting DESKTOP_RENDERER_PORT by hand.

Restricted ports in the window are now remapped into the block immediately
above it (6000 -> 6174). Sending them past the end rather than shifting them by
one keeps the offset -> port mapping injective, so two worktrees still cannot
land on the same port and race for it.

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>

* fix(onboarding): close the Mika multi-member and failure-recovery gaps

From Emacs's review of #6378. All seven findings reproduced.

The two blocking server bugs were the same shape: Mika is one agent per
workspace but sessions and ownership are per member.

- StartMikaOnboarding required the agent owner, so the member who lost
  the CreateMikaAgent race — the race that handler's advisory lock exists
  to survive — got a valid Mika, opened a valid session, and then a 403.
  Mika is created workspace-visible and workspace-invocable; the session
  gate and canInvokeAgent were already the checks that matter.
- The onboarding session was resolved client-side by listing sessions and
  creating one on a miss, matched on the localized title.
  LockWorkspaceForChatSessionCreate is FOR KEY SHARE precisely so
  concurrent creators do not block, so two tabs each opened their own
  conversation with its own kickoff, and switching language between a
  failed attempt and its retry opened another. It is now get-or-create
  server-side under a per-(workspace, member) advisory lock, keyed on
  (workspace, creator, agent), returned alongside the agent.

Also:

- The skipped-runtime welcome dismissed itself silently when provisioning
  the guide issue failed. The signal is not persisted and onboarding is
  already complete, so a blip was terminal. It now offers a retry.
- The Runtimes recovery card gated on `agents.length === 0`, so creating
  any ordinary agent hid the only surface that can mint a Mika — the
  generic endpoint accepts no system_key. Gated on Mika's absence.
- CompactRuntimeRow ignored `disabled`; the CLI dialog was already
  passing it, so the runtime could change mid-submit. It is a real
  <button> now, which also gets focus and Enter/Space for free.
- The rail never went below 15rem while the content pane kept its gutter,
  leaving ~87px of form at 375px. It is hidden under md, where a compact
  bar carries the step name and the Back button instead.
- Dropped a stray __pycache__ artifact I had committed by accident.

Each new test was checked against the bug it covers: reinstating the
owner gate, the title-keyed lookup, or the dropped disabled prop makes
the corresponding test fail.

Co-authored-by: multica-agent <github@multica.ai>

* fix(onboarding): make the Mika entrypoint survive a partial bootstrap

From Emacs's second review of #6378.

Bootstrapping Mika is three server steps — provision the agent, open the
member's session, enqueue the opening turn — and the last two can fail
after the agent commits. Two things then conspired: the server reported
success with the session omitted, and the Runtimes card gated on the
agent existing. The agent's own `agent:created` broadcast invalidates the
agent list, so the card (and its open dialog) was torn down the instant
step one succeeded, and never came back on reload — the agent is durable,
the rest was not. The member was left holding a Mika they could not start.

- The endpoint now fails when the session cannot be resolved. Every step
  is idempotent, so a retry converges; handing back a half-built flow the
  caller cannot distinguish from a finished one does not.
- The entrypoint is gated on the member's own state — does this member
  have a Mika conversation that was actually kicked off — rather than on
  the workspace having an agent. That is the question the card answers,
  and it is true again for every partial state above.

Also restores the Log out escape hatch below `md`. Hiding the rail last
round took its footer with it, which stranded every step but Welcome with
no way out on a narrow screen; the compact bar now renders the same slot,
so `sidebarFooter` is `chromeFooter`.

Each new test was checked against its bug: the old agent-only gate fails
three of the memberNeedsMikaSetup cases, and dropping the footer prop
fails the chrome test.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-06 14:21:02 +08:00
Bohan Jiang
128d1d2bdd feat(notifications): split mentions out of the comments mute group (#6486)
`new_comment` and `mentioned` shared the `comments` preference group, so
muting comment volume also silenced @-mentions — and because a muted item
is never created, those mentions were lost rather than deferred. In
agent-heavy workspaces the only lever that reduces inbox noise also
removed the signal the inbox exists for.

`mentioned` now maps to its own `mentions` group. Preferences are stored
sparse (a missing key means "all"), so existing `{"comments":"muted"}`
rows default the new group to "all" with no migration: muting comments is
a volume decision, not a decision to become unreachable by name.

The split also aligns the global setting with behavior the platform
already had — per-issue unsubscribe keeps delivering direct mentions, and
mention delivery bypasses the subscriber table entirely.

Closes #6468

Co-authored-by: J <agent@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-06 14:17:19 +08:00
Bohan Jiang
1e60cece12 chore(daemon): halve the local version-probe frequency to 10 minutes (#6483)
Both local probes ran every 5 minutes. Neither has caused a measured
problem, but background work a user never asked for should justify its
cadence, and 5 minutes was tighter than either needs.

10 minutes for both, and the remote GitHub poll stays at 6 hours. That
split is the point: only the remote poll asks whether a new release
exists, so only it should track release cadence. The two local probes ask
whether the binary on this machine has already changed, which is bounded
by how long a user is willing to wait after acting, not by how often we
ship.

Not longer than 10 for either. selfReloadCheckInterval compounds, because
a tick landing on a busy daemon defers rather than interrupting a task, so
the real wait is the first tick that is both due and idle.
agentVersionRefreshInterval also gates the below-minimum verdict, so its
interval is the window an unsupported CLI keeps claiming work — cost
alone should not push it out.

Tests override both vars, so none needed updating.

Co-authored-by: multica-agent <github@multica.ai>
2026-08-06 13:23:50 +08:00
Bohan Jiang
62a2d70afd refactor(daemon): stage-2 — Mentions and Comment Formatting judgment rewrite (MUL-5442) (#6453)
* refactor(daemon): rewrite Mentions and Comment Formatting to their judgment form (MUL-5442)

Stage 2, final two sections, bundled per the small-sections rule.

Mentions: the four-link side-effect table stays verbatim (platform
facts); the two H3 subsections merge into one policy paragraph keeping
every anti-loop anchor — the no-mention default with its cost mechanism,
the no-sign-off-mention ban, the end-with-no-mention rule, the three
mention-warranted cases, and the silence closer. The retired headings'
pins re-anchor to the policy phrases.

Comment Formatting: both variants keep the full operational contract
(file-first sentence verbatim, both bans with incident ids, workdir
scope, --parent continuity, cleanup, newline rule); what goes is the
mechanism narration (what the shell rewrites, how flags get swallowed,
PowerShell version/encoding detail — the consequence stays).

Co-authored-by: multica-agent <github@multica.ai>

* fix(daemon): correct the PowerShell version claim, pin the mention scope qualifiers, section-scope the formatting assertions (MUL-5442)

Review catches by Elon on #6453, all three accepted:

1. The compressed Windows rationale over-generalized a version-specific
   fact: '$OutputEncoding drops non-ASCII' is true of Windows PowerShell
   5.1 (ASCII default), false of PowerShell 6+ (utf8NoBOM). Now reads
   'Windows PowerShell 5.1 ... may replace non-ASCII characters with ?';
   the Go comment documents the version split and why file-first stays
   version-agnostic (agents cannot rely on which shell services the pipe).
2. The merged Mentions paragraph was pinned only at the list head — the
   scope qualifiers ARE the anti-repeat-notify boundary: 'not yet
   involved', 'for the first time', 'explicitly asks to loop someone in',
   and the loop-cost mechanism are each pinned individually now.
3. The Comment Formatting assertions ran against the whole file, where
   '#4182' also appears in Available Commands — the HEREDOC ban could
   vanish with green tests. The assertions now slice the section (matched
   at the line-start heading, since Available Commands references the
   heading inline) and cover all seven contract elements within 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>
2026-08-06 12:44:09 +08:00
YYClaw
24d209794e fix(channel): attach /issue media to created issues (#6388) 2026-08-06 12:39:46 +08:00
Bohan Jiang
f44df04f96 refactor(daemon): rewrite the Output section to its judgment form (MUL-5442) (#6425)
Stage 2 section 2. The delivery contract keeps every platform fact and
boundary: comment-add as the only delivery channel, the invisible
terminal, exactly one comment per run before turn exit, --attachment as
the only file path, the runtime-local-path ban with the code-location
form and the say-so-in-words fallback. What goes is derivation: the
invisible-task consequence restatement, the plans-in-your-reasoning
elaboration, the good/bad style examples, the exists-right-now clause,
and two quick-create explanation tails.

One pin re-anchor: 'Do not assume any workspace issue prefix' follows the
rewrite to 'never assume a workspace issue prefix'. Every other Output
and delivery-invariant pin passes unchanged, including the MUL-4899 trio
and the anti-dangling pointer target the Attachments section names.

Issue-kind Output: 1,493 -> 978. Brief (real-UUID fixture):
13,382 -> 12,907 (-475); quick-create variant sheds ~160 more on its own
kind.

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-05 21:15:13 +08:00
Bohan Jiang
ca2541a2ae fix(agents): explain why a runtime refuses thinking_level (MUL-5770) (#6447)
Setting a reasoning effort on a Hermes agent failed with "thinking_level
\"high\" is not a recognised value for runtime \"hermes\"", which reads
like a typo. It is not: Hermes has no reasoning control on the surface
Multica drives it over, so no spelling of the value can ever work.

Add ThinkingControlSupported as the capability predicate behind the
existing token gate, and use it so the API answers with the capability
gap instead of blaming the value. The Hermes evidence (ACP session/new
advertises no configOptions, set_config_option is inert, _make_agent
never sets reasoning_config) is recorded next to the predicate so the
next reader does not re-derive it, with a pointer from hermes.go.

No behaviour change to which values are accepted.

Co-authored-by: J <agent@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-05 21:13:52 +08:00
beast
31cd51ae2f feat(daemon): converge on out-of-band multica and agent CLI version changes (MUL-3269)
A daemon whose `multica` binary or agent CLI was replaced out of band kept
running the old version until someone restarted it by hand, and most people
never knew they had to.

Two separate behaviors, deliberately not one:

- The `multica` binary being replaced (brew upgrade, a manual download, a
  downgrade) is followed by a restart, once the daemon is idle. A running task
  is never interrupted; a busy daemon defers to the next check and reports the
  reason through `daemon status` / `reload_pending_reason`. This is independent
  of the GitHub auto-update poller and has its own switch
  (`--no-auto-reload` / `MULTICA_DAEMON_AUTO_RELOAD` / `disable_auto_reload`),
  because "don't pull new versions" and "follow the binary I replaced myself"
  are different intents. Desktop-managed daemons stay excluded.
- An agent CLI upgrading in place is a hot refresh: re-probe, refresh the
  cached version and the server-side registration, and let subsequent tasks run
  under the new version's policy. Multica's availability does not track a third
  party's release cadence.

Failure semantics are explicit. An unreadable, blank, or unparseable version is
"no evidence", not a version change: the runtime and last trusted version are
kept and the next round retries. A version confirmed below the minimum takes
that provider's runtimes offline once the daemon is idle, and recovers
automatically on upgrade. A late register response, a newly synced workspace,
or an older cleanup request can neither revive a runtime already judged too old
nor knock out one that has legitimately recovered.

No migrations, no server endpoints, no frontend changes.
2026-08-05 19:54:22 +08:00
Jiayuan Zhang
ba129b1963 feat(issues): show per-run token usage on the execution log (MUL-5762) (#6440)
* feat(issues): show per-run token usage on the execution log (MUL-5762)

The execution log already lists every agent run on an issue; it just
never said what any of them cost. task_usage has held per-task token
counts since migration 032 — nothing surfaced them per run.

Three placements, one data source:

- Execution-log header carries the issue total ("2.1M · $4.92") and
  opens the breakdown.
- Each row carries its own token figure. This takes the slot the
  relative timestamp held: the sidebar is 288px and a third column
  would come out of the trigger text, which is what people scan. The
  list is sorted newest-first, so ordinal recency is already free; the
  timestamp moves into the row tooltip alongside duration and model,
  neither of which was surfaced there before.
- The transcript dialog gets the same figure in its header, with the
  input/output/cache split in the run-info popover.

Backend: ListIssueTaskUsage returns per-(task, provider, model) rows in
one query, joined onto the existing task-runs response. The model
dimension stays on the wire because cost is priced client-side per
model — a row that collapsed two models cannot be priced at all.

Cost reuses estimateCost from the runtime usage page, so the issue and
the workspace never disagree; the new summarizeTaskUsage helpers live
next to it rather than starting a second cost formula.

No usage recorded stays distinguishable from zero end to end — omitted
on the wire, undefined in the schema, null from the summarizer, an em
dash in the UI. A run from before usage reporting was not free.

Removes the standalone "Token usage" sidebar section: it showed the
same issue totals minus the cost and minus any way to attribute them,
and every field it had is in the dialog. The /api/issues/:id/usage
endpoint it read stays — the CLI's `issue usage` still uses it.

Co-authored-by: multica-agent <github@multica.ai>

* fix(issues): address review on per-run token usage (MUL-5762)

Three findings from @Emacs, all confirmed against the code:

1. Drop the token figure from active rows. The daemon reports usage
   once, after `runner.run` returns (internal/daemon/daemon.go), and
   ReportTaskUsage publishes no realtime event — so a running task has
   no usage to show and would not learn of it mid-run if it did. The
   branch was only ever exercised by a hand-written fixture, which is a
   test asserting a scenario production cannot produce. The row keeps
   its timer; restore the figure in the same change that adds
   incremental reporting + cache invalidation.

2. Subscribe the usage surfaces to the custom-pricing store. estimateCost
   reads custom rates imperatively via getCustomPricing(), so nothing
   re-rendered these after a saved rate change — the header total, the
   dialog's totals and per-run costs, the cost-by-agent split, and the
   "unmapped model" notice all kept quoting the old price until the task
   list happened to refetch. Same subscription the runtime usage page
   already carries, plus the snapshot in every memo that prices usage.
   Regression test pinned: it fails without the subscription.

3. Give the dialog's status glyph an sr-only label. TaskStatusIcon is
   aria-hidden, so a screen reader could not tell a failed run from a
   completed one — the execution log rows already pair the two.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-05 19:14:33 +08:00
Bohan Jiang
c896f677d5 fix(agent): recover from a codex resume that overflows the reader (MUL-5722) (#6420)
Layer 1 (#6383) raised the shared scanner cap to 32 MiB, which moved the cliff
without removing it — a codex rollout is append-only, so a thread that outgrows
any fixed cap still fails its resume forever. This adds the recovery path.

Layer 2: an oversized thread/resume response is reported as
Result.ResumeRejected rather than a crash, which is the positive evidence
shouldRetryWithFreshSession needs and reconnects the recovery #5715 had
unintentionally cut off. Reaching it required one real fix — the reader wrapped
the scanner error with %v, so bufio.ErrTooLong never survived to a caller.

Layer 3: a new codex_resume_oversized reason marks the session resume-unsafe,
and both resume lookups block by TIME rather than by matching the failed row.
That shape is required, not stylistic: the failure happens before the turn
starts, so the row lands with session_id NULL and is dropped by
latest_per_session before any error-text filter runs. The block expires once a
thread terminates after the overflow, so an issue recovers instead of starting
cold forever.

Also splits the MUL-4424 continuity notice by what each surface can still read
— issue comments and Slack channel history can be re-read, web chat and Feishu
cannot — so only the last group tells the user a loss happened. The backend no
longer holds any of that wording; it receives ExecOptions.ResumeContinuityNotice
from the caller and stays silent when the prompt already carries it, which makes
the duplicate injection on the retry path structurally impossible.

Known gap, tracked not claimed: for chat, the claim handler reads
chat_session.session_id before the fallback query, so a daemon predating this
PR leaves that pointer naming the oversized thread. Clearing it needs the
attempted session recorded at claim time, which is a schema change.
2026-08-05 16:55:29 +08:00
Naiyuan Qing
aaedb864a0 refactor(diagnostics): drop the dead hang telemetry (MUL-5345) (#6433)
Two hang fields cost something and told us nothing, so both go.

HANG STACK CAPTURE. Shipped in v0.4.13, flag published since v0.4.14,
on in production — and every stack it produced was a single entry frame
with an empty url. `Debugger.pause` lands on the next JS statement
boundary, so when the block is in native code (layout, paint, GC, sync
IPC) or the pause dispatches just after the block clears, the frame we
get is the next function to run, not the one that blocked. It also only
fires for hard hangs, so it sampled a fraction of a signal that was
already useless. The price was a CDP debugger channel held open on every
renderer for the whole session.

The server key goes with it, and that is the part that matters for
already-installed clients: v0.4.13–v0.4.18 are fail-closed on this flag,
so no longer publishing `desktop_hang_stack_capture` is what makes them
stop attaching a debugger. keys_test now pins the key as unpublished —
re-adding it would put a flag flip back within reach of a fleet that
still can't produce a usable stack.

`recovered`. Hardcoded `false` at the only site that sets it. A
recovered hang has its breadcrumb cleared and is reported by the
in-thread watchdog instead, so the field could never be anything else.

A machine upgrading from v0.4.18 can still have a breadcrumb on disk
whose context holds a captured stack. `buildFreezeEventProps` builds
props by whitelist, so it drops on its own — pinned by a test, since
nothing sanitizes frames anymore.

What stays: the longtask watchdog, the main-unresponsive breadcrumb with
its bucketed route, client_crash, and $exception. Those are the signals
carrying the analysis in MUL-5345.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-05 16:36:51 +08:00
Multica Eve
521a63671c fix(chat): preserve visible queue-head ordering (MUL-5751) (#6431)
* fix(chat): keep visible queue heads in transcript order

Co-authored-by: multica-agent <github@multica.ai>

* test(chat): cover cancelled follow-up ordering

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-05 16:26:07 +08:00
Multica Eve
86f255658f MUL-5730 feat(server): expose chat audience without bloating prompts (#6428)
Tell a chat agent whether it is answering in a shared room or a 1:1, without
splitting the cached brief prefix: the room shape rides the existing channel
binding row to the claim response, and is stated once per turn in the chat
prompt rather than in the static runtime brief.

Originally contributed as #6390; re-rolled to move the audience fact to the
per-turn prompt and to minimize the copy it renders.

Co-authored-by: Seacen Zhao <xichangzhao@outlook.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-05 16:12:17 +08:00
Bohan Jiang
c1bd4f995c fix(agent): fail a run whose provider session ran out of context (MUL-5739) (#6422)
Detect provider context exhaustion from Claude Code's structured
terminal_reason rather than from its error prose, so a saturated session is
retired and the next task cold-starts instead of resuming a conversation
that can no longer answer.

Refs #6402 — does not close it: the captured 2.1.220/2.1.221 frames report
is_error alongside terminal_reason, so the specific `completed` escape the
issue reports is not reproduced. The structured check is the load-bearing
fix and is proven present on the reported CLI version; the composite text
predicate is a bounded backstop for uncaptured shapes.
2026-08-05 15:41:15 +08:00
Multica Eve
b3fa17f0e0 fix(chat): preserve follow-up transcript order (#6419)
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-05 15:39:55 +08:00
Multica Eve
73283556d7 MUL-5750: fix(chat): keep idle sends out of follow-up queue (#6418)
* fix(chat): keep idle sends out of follow-up queue

Co-authored-by: multica-agent <github@multica.ai>

* fix(chat): preserve the positional queue head

Co-authored-by: multica-agent <github@multica.ai>

* fix(chat): polish deferred queue states

Co-authored-by: multica-agent <github@multica.ai>

* fix(migrations): resolve pending index prefix collision

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-05 15:06:42 +08:00
rongchenzheng
c6c3be5935 fix(autopilot): invalidate empty cache for scheduled tasks (MUL-5747) (#6410)
* fix(autopilot): invalidate empty cache for scheduled tasks

* test(autopilot): pin the wiring the empty-claim fix depends on

TestBackgroundServicesReuseRouterServices asserted that backgroundServices
returns h.TaskService / h.AutopilotService — which is the helper's entire
implementation — so it stayed green even when main() ignored the helper and
constructed its own services again. That is the exact regression it was named
for: reintroducing the duplicate-service wiring left the test passing.

Replace it with an AST guard over main.go that fails when main() calls
service.NewTaskService / service.NewAutopilotService or stops calling
backgroundServices(h), plus an anti-vacuity check that the schedule-job
registration still lives in main() so the guard cannot pass on a walk that
matched nothing.

Add a service-level test for the underlying hazard: EmptyClaimCache is
nil-safe, so a TaskService that never had EmptyClaim assigned fails silently —
the daemon wakeup still fires while the claim path's cached empty verdict
survives until the TTL expires.

Verified by mutation: reintroducing the original duplicate-service wiring
fails the guard, and refactoring the schedule-job registration out of main()
fails it too.

Co-authored-by: multica-agent <github@multica.ai>

* test(autopilot): match the router handler argument, not just the callee name

The previous guard only checked that main() called backgroundServices, so
backgroundServices(nil) satisfied it — a call that compiles, reuses none of
the router's wiring, and nil-derefs at startup.

Resolve the variable holding NewRouterWithOptions' *handler.Handler result and
require that exact ident as the argument. Reading the name off the assignment
instead of hardcoding "h" keeps a rename of that variable from silently
weakening the check, and an unrecognizable router assignment now fails loudly
rather than leaving the argument check with nothing to compare against.

Mutation-verified: backgroundServices(nil) fails, the original duplicate-service
wiring fails, moving the schedule-job registration out of main() fails, an
unresolvable router assignment fails, and renaming h to routerHandler still
passes.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-05 14:34:23 +08:00
YYClaw
c1be369f6d [MUL-5736] fix(channel): make issue commands terminal outcomes (#6398)
* fix(channel): make issue commands terminal outcomes

* fix(channel): stop handled command media from gating later chat tasks

A `/issue` turn is now answered synchronously and excluded from every later
input batch, but it still persisted a media deadline, and both session-scoped
media gates counted it. The next, unrelated chat message was therefore deferred
until the command's attachments bound — or for the full fallback budget on the
create-failure path, where no binder ever runs to clear the marker.

Give GetChannelMediaPendingUntil and PromoteChannelChatTasksIfMediaReady the
same population as the batch seal, so only a turn that can join a batch can gate
one. DeferChatTaskForSealedPendingMedia already scopes by task id and needs no
change.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-05 14:33:30 +08:00