mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-13 03:15:34 +02:00
v0.4.21
1615 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
d598e7a3e6 | fix(channels): deliver terminal task failure details (#6534) | ||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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/. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
24d209794e | fix(channel): attach /issue media to created issues (#6388) | ||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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>
|
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |