mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
agent/lambda/fbcdb565
3629 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c9c269675c |
fix: align MCP support docs and UI gate (#3553)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
2cf8107fc8 |
feat(email): support implicit TLS (SMTPS/465) for SMTP relay (MUL-2768) (#3340)
* feat(email): support implicit TLS (SMTPS/465) for SMTP relay The SMTP relay previously only did opportunistic STARTTLS: it dialed plaintext and upgraded if the server advertised STARTTLS. Providers that only offer implicit TLS on port 465 and do not advertise STARTTLS (e.g. Aliyun enterprise mail) could not be used as a relay at all. Add an SMTP_TLS env var: - unset / starttls (default): unchanged STARTTLS-upgrade behavior. - implicit / smtps / ssl: dial with tls.DialWithDialer (SMTPS). Implicit TLS is auto-enabled when SMTP_PORT=465 and SMTP_TLS is unset, so the common case works with no extra config. The startup log line now reports the negotiated mode (starttls / implicit-tls). Co-authored-by: multica-agent <github@multica.ai> * feat(email): plumb SMTP_TLS through selfhost compose, warn on unknown values The backend reads SMTP_TLS but docker-compose.selfhost.yml never forwarded it, so SMTP_TLS=implicit on a non-standard port (or an explicit starttls override on 465) silently did nothing inside the container. Add it to the backend.environment block. Also log a one-line warning when SMTP_TLS is set to an unrecognized value (e.g. "tls"/"true"/"on"), which would otherwise fall through to STARTTLS and fail to dial a 465 SMTPS port with no startup hint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * test(email): cover SMTP_TLS precedence and alias resolution Table-driven test over NewEmailService asserting the implicit-TLS decision: 465 auto-enables implicit; explicit starttls on 465 overrides auto-detect; implicit/smtps/ssl aliases (case-insensitive, whitespace-trimmed) force SMTPS on any port; unknown values fall back to starttls. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * docs: document SMTPS / SMTP_TLS support, drop "465 unsupported" Port 465 implicit TLS is now supported, so the five places that said it was unsupported are wrong. Replace those sentences, add an SMTP_TLS row to the environment-variables tables (EN + ZH), and add a copy-pasteable SMTPS env block to the auth-setup pages. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: guofengchang <guofengchang@cumulon.com> Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9aa8ba0191 |
fix(runtimes): self-host daemon setup URLs (MUL-2804) (#3474)
Expose self-host daemon setup URLs from /api/config at runtime so the Add computer dialog renders the operator's own server/app domains, while Multica Cloud defaults stay unchanged. Fixes #3013. |
||
|
|
382cdd6a0b |
feat(agent): consume OpenCode mcp_config via OPENCODE_CONFIG_CONTENT (#3098)
Closes the runtime-side gap of #2106: previously `agent.mcp_config` was honored only by Claude Code (via `--mcp-config <file>`); for OpenCode the field was accepted by the API but silently ignored at execution time. ## Approach OpenCode has no `--mcp-config` flag. Project the agent's `mcp_config` into OpenCode via OPENCODE_CONFIG_CONTENT — OpenCode's general inline-config injection environment variable, which accepts any subset of OpenCode's config schema (model / agent / mode / plugin / mcp / …) and merges at "local" scope after the project-config loop. MCP is the only field this PR projects through that channel; if a future Multica field needs the same channel it would assemble a combined config slice before the env append. The env-var route was deliberate. An earlier draft of this PR wrote the translated MCP servers into <workdir>/opencode.json and removed the file on cleanup; review (#3098) flagged that the task workdir is reused across turns for the same (agent, issue), and any agent- or user-written model / tools / permission settings in opencode.json must survive across runs. OPENCODE_CONFIG_CONTENT avoids the workdir entirely — nothing is written to disk, no cleanup is needed, and the env entry dies with the spawned process. OPENCODE_CONFIG_CONTENT was added to OpenCode in v1.4.10 (2025-09); the official @opencode-ai/sdk uses the same env var to inject runtime config, so the surface is stable. Verified empirically against OpenCode 1.15.6 in our K8s runtime: `opencode debug config` returns the injected mcp slice deep-merged with the user's global config, and <workdir>/opencode.json is observably untouched. ## Translation surface `agent.mcp_config` accepts two shapes for portability: - Claude-style `{"mcpServers": {name: {url|command, ...}}}` is translated into OpenCode's native form: `type: "local"|"remote"`, `command` coerced to a string array, `env` renamed to `environment`. - Native OpenCode `{"mcp": {name: ...}}` accepts the three shapes OpenCode's schema permits and is strict-decoded against each: - McpLocalConfig: `{type:"local", command:[…], environment?, enabled?, timeout?}` - McpRemoteConfig: `{type:"remote", url:"…", headers?, oauth?, enabled?, timeout?}` - bare override: `{enabled: bool}` (toggle a server inherited from global / project config without redefining it) Decoding uses `json.DisallowUnknownFields` so any field outside the matching schema is rejected — matching OpenCode's `additionalProperties: false`. Without this, a malformed payload (e.g. `command: "node"` instead of `command: ["node"]`) would reach OpenCode verbatim and either silently disable the server or crash the CLI at startup. Field-level checks the strict decoder doesn't catch: - `timeout` must be a positive integer (rejects 0, negative, fractional) - `oauth` must be either an object (validated against McpOAuthConfig) or the literal `false`; primitives and `true` are rejected as ambiguous - `oauth.callbackPort` must be in 1..65535 when set ## Precedence Go's os/exec dedups `cmd.Env` by key keeping the LAST occurrence (Go 1.9+). Appending OPENCODE_CONFIG_CONTENT after `buildEnv(b.cfg.Env)` guarantees the daemon's value wins over any value the user happened to put in `agent.custom_env` — which matches the intended semantics (`mcp_config` is the authoritative daemon-managed field; `custom_env` is the escape hatch). When that override happens we surface a warning log so accidental clobbers are debuggable. ## Limitation (out of scope, accepted in review) OpenCode also deep-merges its **global** config (`~/.config/opencode/opencode.json`) into every session and exposes no flag to disable that. Operators who want strict per-agent isolation from the global layer can set: ```jsonc // agent.custom_env on the platform { "XDG_CONFIG_HOME": "/tmp/opencode-isolated" } ``` …pointing at any directory without an `opencode/` subdir. OpenCode then reads no global config and only honors what the daemon injects via OPENCODE_CONFIG_CONTENT. Verified with `opencode debug config`. ## Changes server/pkg/agent/opencode_mcp.go (new): - buildOpenCodeMCPConfigContent — translates raw mcp_config into the JSON string OpenCode accepts via OPENCODE_CONFIG_CONTENT, returns "" when there's nothing to inject so the caller can skip the env entry (avoids clobbering anything the user put in agent.custom_env.OPENCODE_CONFIG_CONTENT) - translateMCPConfigForOpenCode + helpers — Claude-style → OpenCode native shape - validateOpenCodeNativeMCPEntry + opencodeMCPLocal / opencodeMCPRemote / opencodeMCPEnabledOnly / opencodeMCPOAuth typed structs — strict-decode native-shape entries against the schema (DisallowUnknownFields), plus targeted post-decode assertions for timeout / oauth / callbackPort server/pkg/agent/opencode.go: - 12 lines of env injection in Execute(), placed AFTER buildEnv so the daemon's value wins via os/exec dedup - warning log when agent.custom_env duplicates the same key - no on-disk state, no rollback closure, no post-run cleanup — OPENCODE_CONFIG_CONTENT lives only in the spawned process env server/pkg/agent/opencode_mcp_test.go (new): - TestBuildOpenCodeMCPConfigContent_{Empty,Remote,Local,Native} - TestBuildOpenCodeMCPConfigContent_NativeAcceptsAllSchemaFields — covers each native variant round-tripping every optional field (local with env+timeout+enabled; remote with headers+oauth-object+ timeout+enabled; remote with oauth: false; bare {enabled} override) - TestBuildOpenCodeMCPConfigContent_RejectsMalformedNative — 31-case table covering every constraint on Bohan-J's review: command must be a string array, environment / headers values must be strings, oauth must be an object or false, timeout must be a positive integer, additionalProperties: false (per-shape allow-list checked via DisallowUnknownFields) - TestOpencodeBackendInjectsMCPConfigViaEnv — E2E happy path; fake opencode binary captures $OPENCODE_CONFIG_CONTENT, asserts the translated mcp slice is present AND <workdir>/opencode.json was NOT written - TestOpencodeBackendOmitsMCPEnvWhenEmpty — empty mcp_config does NOT inject the env, preserving any value the user set in agent.custom_env - TestOpencodeBackendOverridesUserOpenCodeConfigContent — daemon value wins via os/exec dedup keep-last apps/docs/content/docs/providers.{en,zh}.mdx: - flip OpenCode's MCP cell from ❌ to ✅ - reword the "MCP configuration: only Claude Code actually reads it" section so OpenCode is included; describe each tool's mechanism (Claude → `--mcp-config`, OpenCode → OPENCODE_CONFIG_CONTENT) apps/docs/content/docs/install-agent-runtime.{en,zh}.mdx: - update the Claude Code blurb (no longer "the only one") - expand the OpenCode blurb to mention mcp_config support - fix the now-broken /providers anchor Refs #2106 (TS types and per-agent UI for mcp_config are separate follow-ups, not in this PR). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
973a43923f |
fix(comments): revert since-delta to issue-wide, steer to parent thread first (#3535)
#3509/#3523 scoped the comment-trigger since-delta count to the triggering
thread, so an agent resuming a busy issue only saw "+N in this thread" and
lost visibility of new comments in other threads. Revert the count to
issue-wide (every thread), keeping the trigger-comment + agent-own
exclusions, and reshape the warm-path hint to:
- report the issue-wide new-comment volume,
- steer the agent to read the triggering (parent) thread FIRST
(`--thread <trigger> --since`, or `--tail 30` for full context),
- demote the issue-wide `--since` catch-up to an only-if-needed fallback
("don't read them all blindly").
Also fixes the now-stale "scoped to the triggering thread" wording in the
resumed-session no-delta hint (it's issue-wide zero now).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
v0.3.12
|
||
|
|
eda2150a97 |
fix(agents): show MCP tab for ACP runtimes (#3534)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
8b37edbbb1 |
fix(issues): move agent live bar back into the activity section (#3533)
PR #3517 moved the AgentLiveCard out of the activity section to a sticky bar above the editable title. This restores its original placement (after LocalDirectoryHint, above the timeline) and reverts the container margin from mb-4 back to mt-4 that suited the top slot. Everything else from #3517 is kept: the single-container multi-agent accordion, the lifted cancel-confirmation dialog, and the dropped parked/running background variant. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c7fdc77908 |
docs: add session resume and Korean support changelog (#3524)
* docs: add 2026-05-29 changelog Co-authored-by: multica-agent <github@multica.ai> * docs: clarify session resume changelog Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
cf4a93ac20 |
chore(deps): drop stray mobile deps from monorepo root (#3525)
PR #2337 (Multica for iOS) accidentally added expo / react / react-native to the monorepo ROOT package.json dependencies — almost certainly a `pnpm add` run from the repo root instead of `--filter @multica/mobile`. The root isn't an app and imports none of them; apps/mobile already declares all three itself (react pinned to 19.2.0 for Expo SDK 55, per apps/mobile/CLAUDE.md). The stray root react@19.2.0 hoisted to top-level node_modules/react, producing a React version skew against the catalog's 19.2.3 that the rest of the tree (web / desktop / ui / views) uses. After removal the hoisted top-level react resolves to 19.2.3 and mobile keeps its own 19.2.0 untouched. Lockfile shrinks ~411 lines as the root importer no longer pulls the Expo/RN transitive tree. Pure dependency hygiene — no source changes, no behavior change for any app. Mobile build unaffected (verified its package.json still declares expo/react/react-native/react-dom). |
||
|
|
d1c7d478e1 |
MUL-2785: clarify thread-scoped comment delta (#3523)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
9fe7c935a9 |
MUL-2817: docs(i18n): add Korean (ko) documentation translation (#3521)
* docs(i18n): translate documentation corpus to Korean Add Korean (.ko.mdx) translations for all 32 navigable docs pages plus meta.ko.json navigation, mirroring the English source. Product terms (Issue→이슈, Agent→에이전트, Squad→스쿼드, Runtime→런타임, Skill→스킬, Workspace→워크스페이스, etc.) follow the in-app Korean locale at packages/views/locales/ko/. Roles (owner/admin/member) and issue status enums stay lowercase English per the conventions glossary. MUL-2817 Co-authored-by: multica-agent <github@multica.ai> * feat(docs): serve Korean docs content, remove English-fallback stopgap Now that the *.ko.mdx corpus exists, drop the temporary docsContentLang ko→en shim and the static-params fallback-synthesis loop so /docs/ko/* renders real Korean content. Korean is now a first-class locale whose params come straight from source.generateParams(). Also align the docs home hero copy (agent→에이전트) with the app and the translated body. MUL-2817 Co-authored-by: multica-agent <github@multica.ai> * docs(i18n): align residual Korean UI/product terms with the app Address review: sweep the .ko.mdx corpus for product/UI terms left in English and match the in-app Korean locale. - skills page title Skills → 스킬 - UI nav paths localized: Settings → 설정, Runtimes → 런타임, Agents → 에이전트, Projects → 프로젝트, Squads/New squad → 스쿼드/새 스쿼드, Usage → 사용량, Personal Access Tokens → API 토큰, Provider → 제공자, and the agent-create form labels (Name/Provider/Model/Instructions → 이름/제공자/모델/지침) - see-also links Issues/Workspaces/Environment variables and 'Providers Matrix' → Korean - kept as literals (verified): code blocks, the conventions i18n glossary data, 'Anthropic Agent Skills' (standard name), the Squad Operating Protocol/Roster/Instructions prompt-block names, the literal 'Project Context' prompt section, and Xcode's Settings path - add a docsAlternates test asserting ko hreflang is emitted when a real *.ko.mdx page exists MUL-2817 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e024348c1f |
fix(cli/login): accept mcn_ Cloud Node PATs alongside mul_ (MUL-2815) (#3518)
* fix(cli/login): accept mcn_ Cloud Node PATs alongside mul_ (MUL-2815)
multica login --token rejected anything not starting with mul_, so
users with a Multica Cloud Node PAT (mcn_ prefix) hit
"invalid token format: must start with mul_" even though the server
middleware verifies both kinds.
Replace the inline literal check with validateLoginTokenPrefix(), backed
by a small loginTokenPrefixes list ({mul_, auth.CloudPATPrefix}) so the
accepted set has one source of truth. Add unit-test coverage so adding
a new prefix in future is an obvious one-line edit.
Co-authored-by: multica-agent <github@multica.ai>
* fix(cli/login): mention mcn_ Cloud Node PATs in --token help and comments
Follow-up to
|
||
|
|
fc9cec8e87 |
feat(issues): sticky agent live bar above title with multi-agent accordion (#3517)
Move the per-issue "agent is working" bar out of the activity section to a sticky bar at the top of the main content, above the editable title, and fix the multi-agent layout. - Single active task fills one bordered container as a directly-actionable row (Logs + Stop inline). - Multiple active tasks collapse into the same container: a summary header (avatar stack + count) that expands the rows inline as a divided list, instead of stacking N detached banners. - Lift cancel confirmation to AgentLiveCard so one dialog serves both the lone row and the expanded list (avoids a confirm dialog being torn down by an enclosing popup). - Drop the redundant parked/running background variant; running vs queued is signalled by icon + label only. - Reuse the existing agent_activity.hover_header plural for the summary; no new i18n keys. - Update tests for single-inline vs multi-accordion behavior. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5aa4fb7487 |
MUL-2760: feat(i18n): add Korean locale support (#3369)
* feat: add korean locale support * feat(i18n): localize Korean landing page * fix(i18n): refine Korean landing copy * fix(i18n): refine Korean translations * fix(i18n): translate Korean landing subpages * fix(i18n): route Korean landing docs links * fix(i18n): add Korean use case content * fix(i18n): polish Korean locale copy * fix(i18n): improve Korean landing copy * fix(onboarding): persist Korean helper artifacts Co-authored-by: multica-agent <github@multica.ai> * fix(web): add use case locale fallback Co-authored-by: multica-agent <github@multica.ai> * Align Korean pull requests wording Co-authored-by: multica-agent <github@multica.ai> * fix(i18n): dedupe docs href helper Co-authored-by: multica-agent <github@multica.ai> * fix(i18n): localize changelog dates Co-authored-by: multica-agent <github@multica.ai> * fix(docs): prerender Korean fallback pages Co-authored-by: multica-agent <github@multica.ai> * fix(docs): align fallback hreflang metadata Co-authored-by: multica-agent <github@multica.ai> * fix(i18n): preserve Chinese CJK font fallback order Co-authored-by: multica-agent <github@multica.ai> * chore(onboarding): update localized comment wording Co-authored-by: multica-agent <github@multica.ai> * test(i18n): harden CJK font fallback assertions Co-authored-by: multica-agent <github@multica.ai> * fix(docs): keep Chinese font fallbacks first Co-authored-by: multica-agent <github@multica.ai> * test(i18n): harden locale fallback coverage Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
9616d78e47 |
MUL-2785: optimize resumed comment reads (#3509)
* feat(comments): skip default thread read on resumed comment sessions Co-authored-by: multica-agent <github@multica.ai> * fix(comments): scope since delta to trigger thread Co-authored-by: multica-agent <github@multica.ai> * chore(comments): address thread delta review nits Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
645af40ed9 |
refactor(views): unify detail/list headers into shared BreadcrumbHeader (#3510)
* refactor(views): unify detail/list headers into shared BreadcrumbHeader
Replace four hand-rolled, divergent header styles (workspace-name root,
"/" separator, back-arrow, raw div) with one shared BreadcrumbHeader
component. The mental model is now identical everywhere: leading crumbs
are the thing's real containers and clicking one navigates up.
- New packages/views/layout/breadcrumb-header.tsx (segments/leaf/actions)
- Detail pages (issue, project, runtime, skill, autopilot, agent, squad)
now render `{Section} › name`; org name removed as a breadcrumb root
- Issue breadcrumb shows the single most-direct container only (parent
wins over project; they are orthogonal columns), never a fabricated
chain; bare issue shows just its title
- Issue leaf (identifier + title) is now a clickable link to the issue
detail page with a subtle hover:opacity-80
- Issues / My Issues list headers drop the workspace prefix, matching the
icon + title style of the other list pages
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(views): update breadcrumb tests for unified header behavior
The header unification changed three observable behaviors the tests
asserted against:
- issue detail no longer renders the workspace name as a breadcrumb root
- bare issue shows only its (now clickable) title leaf, no ancestor crumbs
- the project "Unknown project" error placeholder was removed
Rewrite the two affected issue-detail tests to assert the new leaf-link
and no-project-crumb behavior, drop the obsolete Unknown-project test, and
update the issues-page header test to assert the workspace prefix is gone.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
75b5be3f8e |
feat(comments): roots-only thread stats + summary projection for comment list (MUL-2809) (#3505)
* feat(comments): roots-only thread stats + summary projection for comment list Enrich the roots_only read so each root carries reply_count (recursive descendant count) and last_activity_at (MAX created_at over the subtree), letting an agent triage which thread to open without fetching any replies. Add an orthogonal summary=true projection (--summary) that clips each returned comment's content to a fixed budget and sets content_truncated, so an agent can scan a list cheaply before pulling a full body. It composes with every read mode (default, since, thread, recent, roots_only). New response fields are optional (omitempty) and only populated for the agent-facing query params, so the default response shape is unchanged for the desktop/web and existing CLI callers. Co-authored-by: multica-agent <github@multica.ai> * test(comments): cover roots_only + summary composition end-to-end The summary projection composing with roots_only is the spec's headline "table of contents" read, but it was only exercised at the CLI param- forwarding level — no handler test asserted that a roots_only response both clips content AND keeps reply_count / last_activity_at. A refactor moving the clip into a per-mode branch would silently break that composition with no failing test. Add TestListComments_RootsOnlySummaryComposes: a long root + a reply, read via roots_only=true&summary=true, asserting the root is clipped (content_truncated=true) while its subtree stats still surface. Co-authored-by: multica-agent <github@multica.ai> * refactor(comments): address review nits on roots stats + summary - ListRootComments[Since]ForIssue: scope the recursive membership walk to a selected_roots CTE (the @row_limit page, with the @since cut applied up front) so stats are only computed over the subtrees of the roots actually returned, instead of every thread in the issue. - summarizeContent: scan by rune and stop at the budget+1th rune instead of allocating a full []rune for the whole body, so a pathologically long comment costs only the budget under summary mode. Add a multi-byte (CJK) test to lock rune-boundary clipping. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ca1ea5716e |
fix(server/child-done): trigger agent parent assignee on child done (MUL-2808) (#3507)
Remove the agent-path self-trigger guard in triggerChildDoneAgent so a child going done wakes its parent agent even when the same agent owns both — a serial sub-task handoff across two different issues, not a loop. Runaway re-triggering stays bounded by HasPendingTaskForIssueAndAgent. Squad path unchanged. Closes #3374. |
||
|
|
c730e906b9 | feat(cli): add roots-only issue comment listing (MUL-2805) (#3288) | ||
|
|
d63d2328ab |
fix(runtimes): consolidate out-of-band (WSL2) local daemon under LOCAL (MUL-2799) (#3497)
* fix(runtimes): consolidate out-of-band local daemon by host name The desktop derived `localMachineName`/`localDaemonId` only from the daemon it manages itself. When the real daemon runs out-of-band (e.g. in WSL2 on a Windows host), the app never gets a device name, so the #3336 device-name consolidation short-circuits on `!!localMachineName`: the local-mode runtime falls into REMOTE and an empty "This machine" placeholder is synthesized. Fix in two parts: - Desktop exposes the host OS hostname (`os.hostname()`) via a new `daemon:get-host-name` IPC and uses it as the final fallback for `localMachineName`, independent of daemon state. - Scope device-name consolidation to the current user's own local runtimes (`owner_id === currentUserId`). The runtime list is workspace-wide, so a host-name match alone could otherwise claim another member's identically named machine as "this machine". Once the real runtime classifies as current it moves to LOCAL and the empty placeholder is no longer synthesized. MUL-2799 Co-authored-by: multica-agent <github@multica.ai> * fix(runtimes): use OS-neutral "This device" label for the current machine The current-machine badge was hard-coded to "This Mac" (en), so it rendered incorrectly on Windows/Linux hosts. zh-Hans already used the neutral "本机". MUL-2799 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3187bbf90c |
feat(comments): re-add since-delta + cold-start thread read + parent-root write normalization (#3494)
* feat(comments): since-delta new-comment hint + default-on comment session resume (#3432) * feat(db): add unresolved comment count + list filter queries Add CountUnresolvedComments (excludes the agent's own comments) and ListUnresolvedCommentsForIssue. Both are additive — existing callers stay on the unfiltered queries — so old clients are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(handler): support unresolved-only comment listing Wire an additive `unresolved` query param into ListComments. Defaults off so an old CLI that never sends it gets unchanged behavior; only true/1 enable it. Rejects combining unresolved with thread/recent (whole-issue filter vs navigation models). Includes filter + count query tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(handler): plumb unresolved count + thread root into claim, gate comment resume Populate trigger_parent_id (thread root of the trigger comment) and unresolved_count (excludes the agent's own comments) on comment-triggered claim responses. Both fields are omitempty so old daemons ignore them. Gate comment-triggered session resume behind MULTICA_RESUME_COMMENT_SESSION (default off): resumed comment turns can inherit the prior turn's "Done." final message, so this stays an explicit rollout switch. The runtime-match and poisoned-session guards still apply regardless of the flag. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(daemon): inject unresolved-comments hint + resolve step into agent brief Add a shared BuildUnresolvedCommentsHint helper rendered on both the per-turn prompt and the CLAUDE.md workflow (kept in sync per PR #2816). It ships only the count and the relevant CLI call — never comment bodies — so the server stays cheap. Thread case points at --thread <root>; issue case points at --unresolved. Suppressed when the count is 0. Also add a workflow step telling the agent to `multica comment resolve <thread-root>` once a thread is fully handled, so the unresolved set converges. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): add comment list --unresolved and comment resolve command Add an --unresolved filter to `issue comment list` (wired to the server's unresolved param, rejected when combined with --thread/--recent) and a top-level `comment resolve <id>` command that POSTs to the existing /api/comments/{id}/resolve endpoint, letting an agent close threads it has fully handled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(comments): since-delta new-comment hint + default-on comment resume Simplifies the comment-triggered agent flow down to what's actually needed: - New-comment awareness is now a pure time delta: the claim response carries new_comment_count + new_comments_since (anchored on the prior run's started_at, never completed_at so a long run can't miss comments). The per-turn prompt and CLAUDE.md workflow render one line — "N new comment(s) since your last run, --since <ts>" — via a shared BuildNewCommentsHint so the two surfaces can't drift. Cold start (no prior run) falls back to a plain read. - Comment-triggered tasks resume the prior session by default (same runtime), dropping the MULTICA_RESUME_COMMENT_SESSION rollout gate. The "Focus on THIS comment" prompt guard defends against inheriting the prior turn's "Done." marker; GetLastTaskSession still excludes poisoned sessions. - Drops the resolved-based machinery from the first draft: CountUnresolvedComments / ListUnresolvedCommentsForIssue queries, the `comment list --unresolved` flag, the `multica comment resolve` command, and the resolve workflow step. - Removes the verbose cursor-pagination paragraph from the comment prompt; the --thread/--recent/--since flags stay in the CLI/API, just no longer explained inline every turn. Compatibility: new claim fields are omitempty (old daemons ignore them). Comment resume is default-on and affects even old daemons, which already consume prior_session_id. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(comments): collapse reply parent_id to thread root on write Comment threads are a 2-level model (root + flat replies, like Linear/Slack), enforced today only by the UI and the agent path — the CreateComment handler stored whatever parent_id it was handed, and the agent-side flatten walked just one level, so a reply-to-a-reply could land at depth 3+. Add GetThreadRoot (a recursive walk to the parent_id=NULL root) and run both write paths (handler.CreateComment, service.createAgentComment) through it, so every stored reply's parent_id IS its thread root. Readers can now treat parent_id as the thread root without re-walking. The agent-drift guard still compares the raw parent_id to the trigger comment before normalization. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(comments): cold-start reads triggering thread, warm keeps --thread pointer The since-delta rework dropped the thread-first read on the COLD path: a first-time agent fell back to the flat `comment list` dump (oldest-first, cap 2000), burying the trigger's context in ancient chatter. Point cold start at the triggering conversation instead via a shared BuildColdCommentsHint (`--thread <trigger> --tail 30` + a --recent pointer for cross-thread background). On the WARM path, --since is a pure time delta and can miss the triggering thread's pre-anchor history, so BuildNewCommentsHint now also emits a --thread pointer. Both surfaces (per-turn prompt + CLAUDE.md workflow) render via the shared helpers so they cannot drift (PR #2816 rule). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e1745d09ea |
MUL-2797 feat(agent): add Claude Opus 4.8 to model catalog & pricing (#3492)
Claude Code now ships Opus 4.8 (claude-opus-4-8). Add it to the three places that enumerate Claude models so the picker, thinking-level catalog, and usage cost estimates all recognize it: - claudeStaticModels(): list Claude Opus 4.8 (Sonnet 4.6 stays default) - claudeModelEffortAllow: Opus supports the full low..max set incl. xhigh - MODEL_PRICING: $5/$25 in, $0.50 cache read, $6.25 5m cache write — same current-gen Opus tier as 4.5/4.6/4.7, confirmed against platform.claude.com/docs/en/about-claude/pricing Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
270fb6aa73 |
MUL-2792 fix(agent): preserve skills in update/archive/restore response (#3464)
* MUL-2792 fix(agent): preserve skills in update/archive/restore response (#3459) agentToResponse always initialises Skills as []; the mutation handlers relied on the caller to refresh it, but only GetAgent and ListAgents actually did. UpdateAgent / ArchiveAgent / RestoreAgent therefore returned "skills": [] regardless of what the agent_skill junction table contained. The DB write path was never wrong — skills weren't actually deleted — but the misleading response (and its matching agent:status / archived / restored WS broadcast) scared users into manually re-running `agent skills set` and risked scripted clients writing the empty set back as truth. Extract the existing GetAgent skill-reload block into attachAgentSkills and call it from the three buggy handlers. Add regression tests that attach skills, hit each mutation endpoint, and assert both the response and the junction table. Co-authored-by: multica-agent <github@multica.ai> * fix(agent): attach skills before env/template broadcasts (#3459) Two follow-up sites flagged in PR #3464 review that shared the same "agentToResponse zeroes Skills, callers forget to reload" pattern as the mutation handlers: - agent_env.go: the agent:status broadcast after UpdateAgentEnv used a bare agentToResponse, so subscribers saw skills wiped on every env rotation. HTTP body is AgentEnvResponse so the response itself is unaffected, but the WS event still misleads any cache that ingests it. - agent_template.go: CreateAgentFromTemplate attaches imported and extra skills inside the tx, then builds the response/agent:created broadcast without reloading them — so callers (and any client tracking the create event) see the freshly created agent as skill-less despite the template having just imported them. Both call sites now reuse attachAgentSkills introduced for UpdateAgent. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
27b473151b |
refactor(ui): move CODE_LIGATURE_CLASS to zero-dep code-style module (#3463)
Extracts CODE_LIGATURE_CLASS and CODE_LIGATURE_DESCENDANT_CLASS into packages/ui/lib/code-style.ts. Non-markdown CLI command surfaces (onboarding/cli-install-instructions, runtimes/connect-remote-dialog) can now import the class strings without pulling in the shiki + react-markdown + katex dependency graph via the markdown barrel. CodeBlock and Markdown continue to consume the constants from the new module; the markdown barrel no longer re-exports CODE_LIGATURE_CLASS. MUL-2793 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
2662291cf2 | fix(runtimes): disable ligatures in CLI command snippets (#3357) | ||
|
|
fa076d38f2 |
MUL-2778 feat(agent): wire mcp_config through OpenClaw runtime (#3450)
* MUL-2778 feat(agent): wire mcp_config through OpenClaw runtime The MCP config tab (#3419) lets admins save mcp_config on an agent, and recent work (#3439) plumbed it through the three ACP runtimes. OpenClaw still ignored the field, leaving the Tab silently inert for any OpenClaw-backed agent. Translate the agent's Claude-style `{"mcpServers": {...}}` into the per-task OpenClaw wrapper's `mcp.servers` block — OpenClaw resolves MCP via its own config schema rather than ExecOptions, so the existing OPENCLAW_CONFIG_PATH preparer is the right seam. Fail closed on malformed JSON / entries missing `command` or `url`, matching the fail-closed posture the preparer already uses for the agents.list step. Null / absent mcp_config leaves the wrapper free of an `mcp` key so the user's global mcp.servers flows through untouched; an explicit empty managed set (`{}` / `{"mcpServers":{}}`) is honoured as "admin saved no servers" mirroring `hasManagedCodexMcpConfig`. Strict-mode replacement (drop user-only servers entirely) would require OpenClaw to do a per-key replace rather than a deep merge at `mcp.servers`; the comment documents that caveat rather than relying on undocumented behaviour. Also adds `openclaw` to `MCP_SUPPORTED_PROVIDERS` so the MCP Tab actually surfaces in the agent overview pane, and pins the new visibility case with a renderPane test. Co-authored-by: multica-agent <github@multica.ai> * MUL-2778 fix(agent): make openclaw mcp_config strict-replace via sanitized snapshot Elon flagged on #3450 that the previous wiring let user-only mcp.servers leak through the wrapper's `$include` of the live user config: deep-merge at `mcp.servers` keeps user-only names, and the strict-empty case (`{ "mcpServers": {} }`) silently inherited user globals. Switch the strict-replace path to write a sanitized snapshot of the user's fully resolved config (via `openclaw config get --json`) with the `mcp` block stripped, then have the wrapper `$include` the snapshot instead of the live user file. With the user's `mcp` gone from the $include resolution, the wrapper's `mcp.servers` is the only definition the embedded OpenClaw sees — managed only, including the explicit empty set. The snapshot lives in envRoot at 0o600 alongside the wrapper so the GC reaper sweeps it with the rest of the task scratch, and no extra OPENCLAW_INCLUDE_ROOTS entry is needed (same-dir $include). Fail-closed on `config get --json` errors so the daemon never silently falls back to the leaky $include path. The inherit branch (null mcp_config) still uses the live user file directly — no extra CLI roundtrip and no snapshot is written. New tests pin the contract Elon's review required: - TestPrepareOpenclawConfigStrictReplacesUserMcpServers: user has global_one + shared, managed has shared + managed_only → wrapper has exactly {shared (managed value), managed_only}; global_one does NOT leak; snapshot file has the user's `mcp` stripped while preserving gateway / providers / API keys. - TestPrepareOpenclawConfigStrictEmptyManagedSetDropsUserMcp: empty managed set drops user's global_one (both `{}` and `{"mcpServers":{}}` cases). - TestPrepareOpenclawConfigNullMcpConfigKeepsUserInclude: null path inherits the live user config, writes no snapshot, makes no extra CLI call. - TestPrepareOpenclawConfigFailsClosedOnResolvedConfigError: errors during `config get --json` surface; no stale wrapper or snapshot. - TestPrepareOpenclawConfigManagedSetFreshInstall: fresh install with managed mcp_config skips the snapshot dance entirely. Also tightens en + zh-Hans MCP Tab copy to mention OpenClaw goes via the per-task wrapper, and to use OpenClaw's own `transport` field rather than Claude's `type` for HTTP/SSE entries. Co-authored-by: multica-agent <github@multica.ai> * MUL-2778 fix(agent): narrow openclaw snapshot strip to mcp.servers only Elon's third-round must-fix: the previous strict-replace snapshot deleted the entire `mcp` block, which wiped out non-server settings under `mcp` like `sessionIdleTtlMs`. Those are documented OpenClaw config keys (https://docs.openclaw.ai/gateway/configuration-reference#mcp) outside the MCP Tab's scope — the agent's saved mcp_config only manages server definitions, so other `mcp.*` tuning the user set must survive. Replace the blanket `delete(resolved, "mcp")` with a stripUserMcpServers helper that: - deletes only `mcp.servers` when `mcp` is an object - drops the parent `mcp` key only when the object is empty after the strip (so we don't emit `mcp: {}` placeholders) - leaves non-object `mcp` values untouched (we only know how to strip servers from the documented shape) Pinned with TestPrepareOpenclawConfigStrictPreservesNonServerMcpKeys: user resolved has both `mcp.sessionIdleTtlMs: 300000` and `mcp.servers.global_one`; after the strict path runs the snapshot keeps the TTL and drops the servers map, and the wrapper's `mcp.servers` is exactly the managed set with no leak. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
16336ad33c | docs: ITT-236 clarify issue mention Markdown (#3365) | ||
|
|
063e9711df |
docs(changelog): add v0.3.11 release notes (#3449)
* docs(changelog): add v0.3.11 release notes Co-authored-by: multica-agent <github@multica.ai> * docs(changelog): refine v0.3.11 release notes Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai>v0.3.11 |
||
|
|
d90732750f |
Revert "feat(comments): since-delta new-comment hint + default-on comment ses…" (#3455)
This reverts commit
|
||
|
|
372a330707 |
fix(billing): wire test page through useT instead of silencing i18n rule (#3451)
The previous fix (#3446) for the i18next/no-literal-string CI failure took the lazy route — added a file-level eslint-disable comment with the rationale that the test page is slated for deletion when the real billing UI ships, so investing in a translation namespace was wasted churn. That argument is weak in practice: * The test page has been around since #3442 and there is no concrete date for when the real billing UI lands. "Throwaway" surfaces tend to outlive their stated half-life. * File-level disables hide future violations of the same rule from review — anyone touching this file later inherits an opt-out they didn't ask for. * The translation namespace is genuinely cheap. The page has ~50 distinct strings, all of which now have a single en.json/zh.json home that the locale-parity test guards. This commit replaces the silencer with a real `billing` namespace: * packages/views/locales/en/billing.json + zh-Hans/billing.json — every literal English label from the page, plus interpolated sentences with i18next `{{var}}` placeholders. The zh-Hans bundle is a basic translation; not perfect prose, but the parity test passes and a Chinese reviewer testing the page won't see raw English mixed with native UI chrome. * packages/views/i18n/resources-types.ts — adds the namespace to `I18nResources` so `t($ => $.billing.x.y)` is type-checked. * packages/views/locales/index.ts — registers both bundles in `RESOURCES` so the parity test sees them. * packages/views/billing/billing-test-page.tsx — every JSX literal rewritten as `t(($) => $.path)`, including aria-label, interpolated sentences (balance meta line, transactions row meta, paging footer), and conditional branches (with-bonus vs without-bonus rendering goes through two distinct keys rather than two literals). The file-level eslint-disable is removed along with its multi-paragraph rationale comment. * formatDate now takes `t` as an argument so the en/zh "—" dash placeholder is itself translated; calling useT inside the util would have violated the rule of hooks (the function is called from conditional render branches). Verification: * pnpm --filter @multica/views lint — 0 errors (15 unrelated warnings, all pre-existing on main) * pnpm --filter @multica/views typecheck — clean * pnpm --filter @multica/views test — 883/883 passing, including the locale-parity test (which fails the build if en and zh-Hans diverge) When the real billing UI ships and this file is deleted, the `billing` namespace JSONs and the `resources-types.ts` / `locales/index.ts` entries get deleted with it — same blast radius as the eslint-disable would have had, but with proper i18n along the way. |
||
|
|
ee4ec3b76d |
MUL-2784 fix(daemon): cleanup sidecar tree (.agent_context / .multica / provider skills) after local_directory tasks (#3444)
* fix(daemon): cleanup .agent_context / .multica / provider skill sidecars after local_directory tasks (MUL-2784) PR #3438 (MUL-2753) only restored CLAUDE.md / AGENTS.md / GEMINI.md to their pre-task bytes; the sidecar tree writeContextFiles seeds (.agent_context/, .multica/, .claude/skills/, .github/skills/, .opencode/skills/, skills/, .pi/skills/, .cursor/skills/, .kimi/skills/, .kiro/skills/, .agents/skills/, fallback .agent_context/skills/) was explicitly deferred to this follow-up. In local_directory mode the agent's workdir is the user's repo, so each task accumulates one more layer of those directories in the user's tree. Plan A: track every file/dir Prepare creates inside workDir in a sidecarManifest written to envRoot/.multica_sidecar_manifest.json (daemon scratch — never in the user's workdir). On local_directory teardown CleanupSidecars walks the manifest, removes the recorded files, then rmdir-iterates the recorded directories in reverse. Pre-existing files and directories are deliberately NOT recorded, so a user-installed .claude/skills/my-own-skill/ sibling — or any unrelated file the user keeps under .claude/, .github/, etc. — is preserved bit-for-bit. Non-empty rmdir fails ENOTEMPTY and is silently skipped, which is the signal that the user owns the directory. Daemon wiring lives next to the existing CleanupRuntimeConfig defer in runTask: runtime brief first, sidecars second. Cloud-mode runs still write a manifest for symmetry but never trigger the cleanup (the GC loop wipes envRoot wholesale). Tests (sidecar_manifest_test.go) cover the round-trip invariant per the issue's acceptance criteria: - empty workdir → Prepare → Cleanup → empty workdir, byte-exact, for every file-based provider (claude, codex, copilot, opencode, openclaw, hermes, pi, cursor, kimi, kiro, antigravity, gemini), - user's .claude/skills/my-own-skill/ (and equivalents per provider) survives Cleanup intact, - unrelated user files under .claude/, .github/, etc. survive, - three repeated cycles do not accumulate any orphan state, - project_resources branch (.multica/project/resources.json) is also reversible, - recordWriteFile refuses to record pre-existing files, - recordMkdirAll refuses to record pre-existing dirs, - Cleanup is a no-op when the manifest file is missing. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): refuse to overwrite pre-existing sidecar paths; pick collision-free skill slugs (MUL-2784 review) Addresses PR #3444 review (Elon): **Must-fix #1**: recordWriteFile used to overwrite pre-existing target files unconditionally and only skip the manifest record. That destroys user bytes at write time AND leaves the corrupted contents in place at cleanup time — the byte-exact contract the issue requires is violated on both halves. Fixed by making recordWriteFile detect any pre-existing entry (regular file, symlink, directory) via Lstat and return a sentinel errPathPreExists without touching the path. The user's bytes are preserved verbatim. For per-skill collisions (user's .claude/skills/issue-review/ vs Multica's "Issue Review"), writeSkillFiles now allocates a collision-free sibling slug via allocateCollisionFreeSkillDir: first attempt is the natural slug, then `<base>-multica`, `<base>-multica-2`, …, bounded at 64 attempts. Provider-native discovery still picks the skill up (every subdir under skillsParent is a distinct skill) and the user's path stays bit-for-bit intact. For Multica-only namespace files (.agent_context/issue_context.md, .multica/project/resources.json), the writer swallows errPathPreExists and continues — the runtime brief already carries every fact those files would, so a collision degrades to brief-only mode rather than destroying user content. **Must-fix #2**: Added byte-exact collision matrix tests covering every file-based provider (claude / codex / copilot / opencode / openclaw / hermes / pi / cursor / kimi / kiro / antigravity / gemini): - TestPrepareThenCleanupSidecarsSameSlugCollisionPerProvider: seeds user's `<provider>/skills/issue-review/SKILL.md` plus a private notes.md sibling, runs Prepare → Inject → Cleanup, asserts workdir snapshot is byte-identical to seed. - TestPrepareThenCleanupSidecarsIssueContextCollisionPerProvider: seeds user's `.agent_context/issue_context.md`, asserts round-trip preserves it. - TestPrepareThenCleanupSidecarsProjectResourcesCollisionPerProvider: same for `.multica/project/resources.json`. - TestPrepareThenCleanupSidecarsMultiSkillCollisionFreeAllocation: end-to-end check that the Multica skill lands at the collision-free sibling and Cleanup removes only the Multica side. - TestAllocateCollisionFreeSkillDir: directed unit test pinning the slug-bumping sequence. - TestRecordWriteFileRefusesToOverwritePreExistingFile (was TestRecordWriteFileSkipsPreExistingFile): flipped to assert the user's bytes survive and errPathPreExists is returned. - TestRecordWriteFileRefusesToOverwriteSymlinkOrDir: covers the Lstat path for non-file entries. **Should-fix**: CleanupSidecars used to swallow ANY non-ENOENT rmdir error as "user content present," silently dropping real I/O failures (EACCES, EPERM, EBUSY). Now it re-reads the directory after a failed rmdir via the new dirHasEntries helper — non-empty → silently skip (ENOTEMPTY, the intended branch); empty → genuine error, captured into firstErr and surfaced. Plus directed tests: - TestCleanupSidecarsSurfacesRealRmdirErrors - TestDirHasEntries Local verification: - go test ./internal/daemon/execenv/... — all green - go test ./internal/daemon/... — all green - go vet ./... — clean Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): surface original rmdir error when post-rmdir ReadDir also fails (MUL-2784 review) Addresses remaining PR #3444 review blocker (Elon): dirHasEntries used to return true when ReadDir failed with anything other than ENOENT, which made CleanupSidecars treat every locked / faulted directory as ENOTEMPTY and silently drop the original rmdir error. The v1 fix from the previous round closed the EACCES-on-empty-dir branch but missed the case where the chmod also blocks ReadDir — exactly the failure mode the review called out. Helper change: dirHasEntries now returns (hasEntries, ok bool): - (false, true) — dir exists and is empty (or missing, race-safe) - (true, true) — dir has user content (the ENOTEMPTY branch) - (_, false) — ReadDir failed (EACCES, ENOTDIR, EIO, …); the caller cannot tell ENOTEMPTY from a real error and MUST surface the original rmdir error CleanupSidecars switches on (ok, hasEntries): - !ok → surface the ORIGINAL rmdir error (not the ReadDir failure — that's diagnostic plumbing and would distract from the root cause) - ok && hasEntries → swallow silently (intended ENOTEMPTY branch; preserve user content) - ok && !hasEntries → surface the rmdir error (empty dir + EACCES / EPERM / EBUSY → genuine cleanup failure) Tests: - TestDirHasEntries: extended with a regular-file sub-case (ReadDir returns ENOTDIR) asserting (false, false). The v1 helper returned (true) here, hiding the bug. - TestCleanupSidecarsSwallowsMissingAndNonEmptyDirs: renamed from TestCleanupSidecarsSurfacesRealRmdirErrors. The old name claimed to test the surfacing path but never actually exercised it. - TestCleanupSidecarsSurfacesEACCESOnEmptyRecordedDir: chmod parent to 0o555 so rmdir(recorded) fails EACCES while ReadDir(recorded) still succeeds (empty). Asserts firstErr is non-nil and references both the recorded path and the rmdir branch. Skipped when running as root (chmod is bypassed for uid 0). - TestCleanupSidecarsSurfacesEACCESWhenReadDirFailsToo: the must-fix case — chmod parent 0o555 AND chmod recorded 0o000 so BOTH rmdir and ReadDir fail. The surfaced error must be the ORIGINAL rmdir failure, not the ReadDir one. Skipped on uid 0. Local verification: - go test ./internal/daemon/execenv/... — all green - go test ./internal/daemon/... — all green - go vet ./... — clean Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
9b280bc631 |
chore(billing): silence i18next/no-literal-string in dev-only test page (#3446)
billing-test-page.tsx (introduced in #3442) ships dozens of raw English JSX labels and is explicitly documented as "not a finished UI — slated for deletion when the real billing UI ships". Routing every label through useT() and inventing throwaway translation keys would be wasted churn that gets deleted in the same week the real UI lands. Add a file-level eslint-disable so CI passes on every PR that touches unrelated files. When the real billing UI replaces this page, this file (and the disable) gets deleted together. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
90a737fc7e |
fix(daemon): retry terminal task callbacks on transient errors (MUL-2780) (#3443)
CompleteTask / FailTask used to be fire-once. A 1-second upstream 502 burst would drop the call, then the immediate fail-fallback also 502'd, leaving the task stuck in `running` forever and showing the agent as "still working" in the UI. Add a bounded retry around the two terminal callbacks: 4s, 8s, 16s, 32s, 64s backoff schedule (5 retries, ~124s ceiling), retrying only on transient errors (5xx, 408, 429, transport-level) and bailing immediately on permanent 4xx. Also fix a latent bug where a transient complete failure would silently downgrade a successful run to a fail: the fallback now triggers only on permanent errors. Server-side CompleteTask / FailTask are already idempotent on "already terminal", so replays from a retry are safe even if the prior 502'd response was actually persisted. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
7bbca54e92 |
feat(billing): test page consuming /api/cloud-billing/* (#3442)
* feat(billing): test page consuming /api/cloud-billing/*
Stuffs every cloud-billing endpoint onto a single dev page so we can
verify the proxy + Stripe end-to-end flow without a designed UI.
Reachable at /<workspaceSlug>/billing — the page is account-level
data but lives under the workspace dashboard layout because that's
where the authenticated shell sits. No sidebar entry on purpose;
this is test-quality and meant to be deleted when the real billing
UI ships.
What's there:
* Balance card (GET /balance)
* Stripe-success polling banner — visible only when ?session_id=
is in the URL (Stripe substitutes it into checkout_success_url
on its way back). React Query refetchInterval polls every 2s
until the topup status reaches credited / failed / canceled,
then a 'Clear from URL' button calls navigation.replace(pathname).
* Buy section: server-authoritative price tier buttons (GET
/price-tiers) → POST /checkout-sessions → window.location to the
Stripe URL. We do NOT hard-code amounts on the frontend; tier
config lives in cloud's billing.price_tiers.
* Stripe Billing Portal button (POST /portal-sessions). Opens in a
new tab so the originating page stays put for easy verification.
Documented behaviour: 400 is expected for users with no Stripe
customer record yet.
* Three lists: transactions / batches / topups.
Plumbing:
* packages/core/types/billing.ts — interfaces mirroring the cloud
response shapes. Status / source / tx_type fields are typed
'string' rather than enum unions to match the schemas' z.string()
parsing (same convention as CloudRuntimeNode); the canonical
enum values are exported as separate type aliases for callers
that want to switch on them.
* packages/core/api/schemas.ts — 9 zod schemas + 7 EMPTY_ fallbacks,
all .loose() so a non-breaking cloud-side field addition doesn't
crash the parser.
* packages/core/api/client.ts — 8 methods using parseWithFallback,
matching the existing cloud-runtime shape.
* packages/core/billing/{queries,mutations,index}.ts — React Query
queryOptions + mutations. Notable choices: balance / lists are
NOT keyed on workspace (account-level data), and the
checkout-session polling stops automatically when status is
terminal so we don't poll forever after a user closes the tab.
* packages/core/package.json + packages/views/package.json — exports
map updated for @multica/core/billing and @multica/views/billing.
Verification:
* pnpm --filter @multica/core typecheck clean
* pnpm --filter @multica/views typecheck — only pre-existing
hast-util-to-html error in editor code (exists on main)
* pnpm --filter @multica/core test — 412 passing
* pnpm --filter @multica/views test — 877 passing, 1 failure
(editor/readonly-content) is also pre-existing on main, not
caused by this change
Out of scope: real production-quality billing UI; sidebar entry; i18n
strings; mobile app. This is a single test page; it gets replaced
when the real UI ships.
* fix(billing): refetch balance/lists when checkout polling reaches terminal
Closes the second-half of the Stripe-return race the previous commit
left dangling.
Symptom:
After Stripe redirects back with ?session_id=..., the banner polls
/checkout-sessions/{id} every 2s and the rest of the page (balance,
transactions, batches, topups) is fetched once on mount. The
webhook race means those four queries usually see pre-credit state
— but the banner is the only thing that keeps polling, so once it
reads 'credited' nothing else on the page knows. The user would
see 'Final status: credited' next to a stale balance card until
they manually refresh.
Fix:
Add useInvalidateBillingDataAfterCredit() in @multica/core/billing —
a hook returning a callback that flushes balance / transactions /
batches / topups (NOT the checkout-session itself; its
refetchInterval already terminated, refetching would just confirm
the same value). The Stripe-success banner runs this callback in
a useEffect keyed on terminal-status transition, so it fires
exactly once when the polling lands.
Strict scope is documented in the hook's JSDoc:
- balance/transactions/batches: only change at the 'credited'
transition (cloud writes ledger + batch + wallet in one DB tx)
- topups: changes on every terminal transition
- For 'failed' / 'canceled' we technically over-fetch the first
three; three cheap round-trips, simplifies the call site, fine
on a test page.
Effect dep is . terminal flips false→true at most once
per session id (the polling stops when terminal is true so the
data won't change again). If the user lands here with a session
that is already terminal (re-opened tab on a credited URL), the
effect still fires on first data load and we still re-fetch —
correct, the cached snapshot is just as stale in that case.
go build / pnpm typecheck / pnpm test clean (core 412 passing; only
pre-existing hast-util-to-html error in unrelated editor code on
views, same as on main).
|
||
|
|
90ddfb04e2 |
feat(self-host): DISABLE_WORKSPACE_CREATION env var (MUL-2777) (#3441)
* feat(self-host): DISABLE_WORKSPACE_CREATION env var (MUL-2777, #3433) When self-hosters set DISABLE_WORKSPACE_CREATION=true, POST /api/workspaces returns 403 for every caller and the UI hides every "Create workspace" affordance (sidebar, modal, /workspaces/new page, onboarding Step 2). This closes the gap where ALLOW_SIGNUP=false still let any signed-in user open an isolated workspace the platform admin couldn't see. - server: new Config.DisableWorkspaceCreation, gate in CreateWorkspace, workspace_creation_disabled in /api/config, Go tests. - frontend: new workspaceCreationDisabled in configStore, hide sidebar entry, swap NewWorkspacePage / CreateWorkspaceModal / onboarding StepWorkspace to a "creation disabled, ask for invite" state when the flag is on, EN + zh-Hans locale strings. - ops: .env.example, docker-compose.selfhost, helm values + configmap, SELF_HOSTING.md, SELF_HOSTING_ADVANCED.md, environment-variables docs (EN + zh). Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): drive create path off workspaceCreationAllowed (#3433) PR #3441 review: when DISABLE_WORKSPACE_CREATION=true and the user already has a workspace, StepWorkspace still walked the resume copy (`headline_resume` / `lede_resume` mentioning "or start another") and `creatingActive` ignored the flag, leaving a stale clickable create CTA possible if /api/config arrived late. Refactor StepWorkspace to derive a single `workspaceCreationAllowed` boolean from the config store. It now drives: - Initial `mode` state (defaults to "existing" when disabled + reusing so the CTA is pre-armed for the only valid action). - `creatingActive` so the footer CTA cannot fall back into the create branch even mid-render. - Eyebrow / headline / lede strings — adds `creation_disabled_{eyebrow,headline,lede}_resume` (EN + zh-Hans) for the disabled + reusing variant. Tests: cover the three reachable shapes — flag off + no existing, flag on + no existing, flag on + existing. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
09f9c7e2ce |
MUL-2764 feat(agent): wire mcp_config through ACP runtimes (Hermes / Kimi / Kiro) (#3439)
* MUL-2764 feat(agent): wire mcp_config through ACP runtimes (Hermes / Kimi / Kiro) The MCP config Tab (#3419) already lets admins save mcp_config on an agent, and the daemon plumbs it through to `agent.ExecOptions.McpConfig` for every runtime. Claude and Codex consume it; the three ACP runtimes (Hermes / Kimi / Kiro) ignored the field and hardcoded an empty `mcpServers: []` in their `session/new` requests. Add `buildACPMcpServers` to translate the Claude-style `{"mcpServers": {"<name>": {...}}}` object-of-objects into the array shape ACP requires (`[{name, command, args, env: [{name,value}, ...]}, ...]` for stdio; `[{type, name, url, headers: [...]}, ...]` for http/sse), then pass the translated array on `session/new` (all three) and `session/load` (kiro resume). Malformed JSON fails the launch closed — same contract Codex's `renderCodexMcpServersBlock` uses — so users see a real error instead of silently running with no MCP servers. Individual unclassifiable entries (no command, no url) are skipped with a warning so one bad row can't take MCP down for the rest of the agent. Co-authored-by: multica-agent <github@multica.ai> * MUL-2764 fix(agent): wire mcp_config through ACP resume + gate http/sse on capability Addresses the two blockers Elon raised on #3439: 1. session/resume now carries mcpServers for Hermes and Kimi (Kiro's session/load already did). Per the ACP Session Setup spec the resume path re-attaches MCP servers, and without this a resumed task lost access to MCP tools that a fresh task on the same agent would have had. Pinned with new TestHermesResumeIncludesMcpServers and TestKimiResumeIncludesMcpServers integration tests that inspect the recorded wire request. 2. Added extractACPMcpCapabilities + filterACPMcpServersByCapability so http/sse MCP entries get dropped (with a daemon-log warning naming the entry) when the runtime's initialize response doesn't advertise mcpCapabilities.http / .sse. Sending those entries to a stdio-only runtime is a spec violation and reliably tanks session/new; now they get filtered and the rest of the session still starts. Stdio entries pass through unconditionally. Both backends wire the filter in right after initialize so session/new and session/resume see the same filtered list. Also added TestKiroLoadIncludesMcpServersFromConfig — Elon flagged that no test pinned "non-empty mcp_config actually reaches the wire" for Kimi/Kiro, so the wire assertions go in for all three runtimes. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
03f70209c4 |
fix(daemon): preserve user CLAUDE.md / AGENTS.md / GEMINI.md in local_directory runs (#3438)
* fix(daemon): preserve user CLAUDE.md / AGENTS.md / GEMINI.md in local_directory runs (MUL-2753) InjectRuntimeConfig previously called os.WriteFile unconditionally, which truncated whatever file lived at the same path. For the local_directory project_resource flow the workdir is the user's own repo, so the agent silently destroyed any repo-level CLAUDE.md / AGENTS.md / GEMINI.md the first time it ran in that directory, and the daemon's local-directory cleanup explicitly skips the user's path so the file was never restored. Write the brief inside a marker block instead: <!-- BEGIN MULTICA-RUNTIME (auto-managed; do not edit) --> ...brief... <!-- END MULTICA-RUNTIME --> writeRuntimeConfigFile handles three states: - file missing -> create with just the marker block, - file present, no marker block -> append the marker block at the end (preserves user-authored content above), and - file present, marker block already there -> replace the block body in place so repeated runs don't grow the file unboundedly. This is the short-term fix called out on MUL-2753. The sidecar question (.agent_context/, .claude/skills/, .multica/project/resources.json) is left for a follow-up — those files don't overwrite user content, just litter the workdir. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): cleanup runtime config marker block after local_directory tasks (MUL-2753) Address Elon's review on PR #3438: 1. Add `CleanupRuntimeConfig` and wire it into the daemon's task path so `local_directory` runs excise the marker block on the way out. Without it, a user's subsequent manual `claude` / `codex` / `gemini` run in the same directory picks up the previous task's stale brief (issue id, trigger comment id, reply rules) and acts on the wrong context. Cloud workspace runs skip the cleanup — their scratch workdir is wiped by the GC loop anyway. 2. If excising the block would leave the file empty / whitespace-only, the file is removed so we don't leave behind a stub the user has to delete by hand. Surviving user content is preserved byte-for-byte. 3. Harden the marker parser: search for the end marker strictly after the begin marker. The previous `strings.Index` pair mishandled two malformed cases — - a stray end marker before any begin (e.g. user pasted a documentation snippet showing the wire format) would cause every run to stack another block, growing the file unboundedly; - a half-block left by a previous crashed run would cause every subsequent run to append a fresh block beneath the half-block. The `locateMarkerBlock` helper now anchors the end search past the begin offset, and treats "begin found, no end after" as "block runs to EOF" so the next write replaces it cleanly. Centralised the provider→filename mapping in `runtimeConfigPath` so Inject and Cleanup can't drift past each other when a new provider is added. Tests cover: parser hardening (stray-end-before-begin idempotency, half-block recovery), Cleanup happy path / file removal / no-op cases / malformed half-block / per-provider mapping, and an end-to-end inject→cleanup round trip that locks in byte-identical restoration of the user's pre-injection file. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): byte-exact inject/cleanup round trip for runtime config (MUL-2753) Address Elon's second-round review on PR #3438. The previous cleanup relied on `TrimRight + "\n"` for trailing newlines and `TrimSpace == ""` for file removal — both compensated for the inject path's "normalise trailing newlines so there's always exactly `\n\n` before the block" step, but they did so by mutating the user's bytes. The result was a real diff on three boundary cases: - file ended without a newline (`rules`) → cleanup added one; - file ended with two or more newlines (`rules\n\n`) → cleanup collapsed to a single newline; - file pre-existed but was empty / whitespace-only → cleanup deleted it. Reshape the contract so the bytes inject adds are the exact bytes cleanup removes, with no user-byte mutation in between: - Define `runtimeManagedSeparator = "\n\n"` as a fixed managed separator that inject always inserts (unconditionally — including for files that already end in two or more newlines) between pre-existing user content and the marker block. - Inject's missing-file branch still writes the block alone (no separator); that absence is the marker Cleanup uses to identify "we created this file from scratch" and is the only condition under which Cleanup is allowed to `os.Remove` the file. - Cleanup detects `HasSuffix(pre, runtimeManagedSeparator)` and strips exactly those bytes; whatever remains is written back verbatim with no `TrimRight` / `TrimSpace`, so the pre-injection bytes survive exactly. The replace-in-place branch is untouched — the managed separator established by the first inject lives in pre and survives across subsequent runs, so byte-exactness is preserved through arbitrary inject→inject→cleanup chains. Tests: - `TestInjectThenCleanupRoundTripByteExactBoundaries` parameterises 9 seed shapes (missing file, empty, whitespace-only, no trailing newline, one trailing newline, two trailing newlines, many trailing newlines, CRLF line endings, no final newline with embedded blank lines) and asserts byte-identical round trip across two full cycles. - `TestInjectReplaceThenCleanupRestoresByteExact` covers the replace-in-place branch for the same boundary seeds. - `TestWriteRuntimeConfigFileAlwaysInsertsFixedManagedSeparator` pins the new invariant at the source: regardless of seed shape, inject emits `<seed><\n\n><marker block>` with no normalisation. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
72e0cb70aa |
fix(transcript): truncate workdir chip + click-to-copy (#3440)
Real workdir paths are routinely long enough to push every other chip off the transcript-dialog metadata row, leaving the row scrolling or wrapping awkwardly. Turn the chip into a fixed-width button: - max-w-[16rem] + truncate so the path tail gets a clean ellipsis no matter the depth - title attribute carries the full relative_work_dir for a hover peek - click copies relative_work_dir to clipboard, icon flips to a green Check for 2s as feedback - swap FolderTree icon for the simpler Folder mark The pre-existing privacy invariant is preserved unchanged: only the server-cleaned relative_work_dir reaches the DOM / title / clipboard; the absolute task.work_dir still never leaves the server response. |
||
|
|
3943358e67 | feat(billing): proxy /api/cloud-billing/* + Stripe webhook to multica-cloud (#3434) | ||
|
|
5e78e5100a |
feat(comments): since-delta new-comment hint + default-on comment session resume (#3432)
* feat(db): add unresolved comment count + list filter queries Add CountUnresolvedComments (excludes the agent's own comments) and ListUnresolvedCommentsForIssue. Both are additive — existing callers stay on the unfiltered queries — so old clients are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(handler): support unresolved-only comment listing Wire an additive `unresolved` query param into ListComments. Defaults off so an old CLI that never sends it gets unchanged behavior; only true/1 enable it. Rejects combining unresolved with thread/recent (whole-issue filter vs navigation models). Includes filter + count query tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(handler): plumb unresolved count + thread root into claim, gate comment resume Populate trigger_parent_id (thread root of the trigger comment) and unresolved_count (excludes the agent's own comments) on comment-triggered claim responses. Both fields are omitempty so old daemons ignore them. Gate comment-triggered session resume behind MULTICA_RESUME_COMMENT_SESSION (default off): resumed comment turns can inherit the prior turn's "Done." final message, so this stays an explicit rollout switch. The runtime-match and poisoned-session guards still apply regardless of the flag. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(daemon): inject unresolved-comments hint + resolve step into agent brief Add a shared BuildUnresolvedCommentsHint helper rendered on both the per-turn prompt and the CLAUDE.md workflow (kept in sync per PR #2816). It ships only the count and the relevant CLI call — never comment bodies — so the server stays cheap. Thread case points at --thread <root>; issue case points at --unresolved. Suppressed when the count is 0. Also add a workflow step telling the agent to `multica comment resolve <thread-root>` once a thread is fully handled, so the unresolved set converges. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): add comment list --unresolved and comment resolve command Add an --unresolved filter to `issue comment list` (wired to the server's unresolved param, rejected when combined with --thread/--recent) and a top-level `comment resolve <id>` command that POSTs to the existing /api/comments/{id}/resolve endpoint, letting an agent close threads it has fully handled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(comments): since-delta new-comment hint + default-on comment resume Simplifies the comment-triggered agent flow down to what's actually needed: - New-comment awareness is now a pure time delta: the claim response carries new_comment_count + new_comments_since (anchored on the prior run's started_at, never completed_at so a long run can't miss comments). The per-turn prompt and CLAUDE.md workflow render one line — "N new comment(s) since your last run, --since <ts>" — via a shared BuildNewCommentsHint so the two surfaces can't drift. Cold start (no prior run) falls back to a plain read. - Comment-triggered tasks resume the prior session by default (same runtime), dropping the MULTICA_RESUME_COMMENT_SESSION rollout gate. The "Focus on THIS comment" prompt guard defends against inheriting the prior turn's "Done." marker; GetLastTaskSession still excludes poisoned sessions. - Drops the resolved-based machinery from the first draft: CountUnresolvedComments / ListUnresolvedCommentsForIssue queries, the `comment list --unresolved` flag, the `multica comment resolve` command, and the resolve workflow step. - Removes the verbose cursor-pagination paragraph from the comment prompt; the --thread/--recent/--since flags stay in the CLI/API, just no longer explained inline every turn. Compatibility: new claim fields are omitempty (old daemons ignore them). Comment resume is default-on and affects even old daemons, which already consume prior_session_id. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f26bdffaf2 |
fix(provider-logo): handle Next.js vs vite PNG import shape divergence
Next.js types PNG imports as StaticImageData ({ src, width, height });
vite/electron-vite types them as plain string. Component is consumed by
both apps/web (Next.js) and apps/desktop (electron-vite), so a single
type can't satisfy both — last CI failed apps/web typecheck.
Normalise via unknown at the import site so neither side's narrower
type causes the other side's branch to collapse to never. assets.d.ts
declares the union; the component derives a plain-string src once at
module load.
|
||
|
|
1195255e43 |
MUL-2771: feat(transcript): server-derived relative work_dir chip (#3428)
* MUL-2771: feat(transcript): server-derived relative work_dir chip Adds a privacy-safe `relative_work_dir` field to the agent task wire shape so the transcript dialog can show where a task ran without leaking the user's home directory. Standard tasks strip the daemon's workspaces root to `<wsUUID>/<taskShort>/workdir`; local_directory tasks fall back to the trailing two path segments (`repos/foo`), which keeps enough context for the user to recognise the directory without exposing $HOME or the username. The derivation lives in `taskToResponse` so every endpoint that serves a task — list, snapshot, claim, rerun, cancel, complete, fail — fills the field consistently. taskToResponse now also populates `workspace_id`, which the prior shape declared but never set. shortTaskID mirrors execenv.shortID; a colocated test pins the two helpers together so future daemon-side layout changes don't silently degrade the chip into the local_directory fallback. Replaces the front-end stripping attempt in PR #3379, which passed issue_id where workspace_id was required and therefore rendered the full absolute path on every standard task. Co-authored-by: multica-agent <github@multica.ai> * MUL-2771: harden privacy guards on transcript work_dir chip Address second-round review feedback from PR #3428: 1. Drop the `title={task.work_dir}` tooltip in the transcript dialog. The visible chip was safe but native browser tooltips re-rendered the absolute `/Users/<name>/...` on hover, leaking into screen shares, screenshots, and recordings — defeating the stated goal of the chip. The absolute path now never reaches the DOM (no title, aria, or data attribute). 2. Replace the "tail two segments" fallback for local_directory paths with explicit home-prefix stripping plus a basename-only final fallback. The old behaviour leaked the username on shallow paths like `/Users/alice/foo`, `/home/alice/project`, and `C:\Users\alice\foo`. The new behaviour recognises common per-user home layouts on macOS, Linux, and Windows (case-insensitive), strips them down to the remainder, and falls back to the basename for any path under an unrecognised root — a single segment can never carry the home prefix. 3. Align the Go and TypeScript field comments with the real fallback policy so future readers see "strip home / basename" instead of the outdated "tail two segments" description. Tests: expanded `TestRelativeWorkDir` to cover shallow `/Users/...`, `/home/...`, and `C:\Users\...` paths, the exact-home edge cases, case-insensitive matching, and the non-home basename-only fallback. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
bae8a84abd |
MUL-2767 feat(agent): add Antigravity runtime backend (#3427)
* feat(agent): add Antigravity runtime backend Adds Google's Antigravity CLI (`agy`) as the 12th supported coding-tool runtime, alongside Claude / Codex / Cursor / Copilot / Gemini / Hermes / Kimi / Kiro / OpenCode / OpenClaw / Pi. The CLI emits plain assistant text on stdout (no structured event stream), so the backend streams stdout line-by-line as `MessageText` events and accumulates the same text as the final `Result.Output`. Session resumption uses `--conversation <id>`; because the conversation UUID is not echoed on stdout, the daemon routes `--log-file` to a temp file and recovers the id from the glog-formatted log lines. MUL-2767 Co-authored-by: multica-agent <github@multica.ai> * fix(agent): correct Antigravity capability contract from Elon review - ModelSelectionSupported now returns false for antigravity. `agy` has no --model flag and antigravityBackend deliberately drops opts.Model, so the UI must render a disabled "Managed by runtime" picker instead of an empty dropdown plus a silently-ignored manual-entry field. Also stop seeding AgentEntry.Model from MULTICA_ANTIGRAVITY_MODEL — the backend would silently ignore it. - Antigravity skills now write to {workDir}/.agents/skills/, the CLI's native workspace path (inherits Gemini CLI's layout per https://antigravity.google/docs/gcli-migration). Previously they went to the .agent_context/skills/ fallback that the CLI doesn't scan. Runtime brief moves antigravity into the native-discovery branch and local_skills.go points the user-level skill root at ~/.gemini/antigravity-cli/skills for Runtime → local skill import. - Doc + UI comment sync: providers matrix / install-agent-runtime / cloud-quickstart / agents-create / tasks (session-resume support) / skills / README all now list Antigravity in the right buckets, and the model-picker / model-dropdown comments cite antigravity (not the stale hermes reference) as the supported=false example. New tests: TestAntigravityModelSelectionUnsupported, TestInjectRuntimeConfigAntigravity (native discovery wording), TestWriteContextFilesAntigravityNativeSkills (.agents/skills/ landing, .agent_context/skills/ NOT written). Co-authored-by: multica-agent <github@multica.ai> * feat(provider-logo): swap inline placeholder for real Antigravity PNG Replaces the hand-drawn planet+arc placeholder with the official asset shipped from Downloads. Stored next to the component; bundlers (Next.js / electron-vite) resolve the PNG import to a URL string at build time. Added a small assets.d.ts so packages/views' tsc accepts PNG / SVG module imports — there was no prior asset usage in this package to register the declaration. --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
d39da9f7f0 |
MUL-2764: feat(agents): add MCP config tab to agent detail page (#3419)
* MUL-2764: feat(agents): add MCP config tab to agent detail page
Backend already stores `mcp_config` and the daemon forwards it to the
runtime CLI via `--mcp-config`; this only adds the UI entry point.
The new tab presents a JSON editor that pretty-prints the existing
config, validates the buffer on every keystroke, and saves through the
existing `PUT /api/agents/{id}` path. Clearing the editor sends
`mcp_config: null`, which the handler reads as "wipe the column" and
the daemon falls back to the CLI's own default.
When the caller can't see secrets (agent actor, or a non-owner
non-admin member), the server already returns `mcp_config: null` with
`mcp_config_redacted: true`; the tab renders a read-only "configured
but hidden" state in that case so a non-privileged member cannot
silently overwrite an admin-owned config by saving an empty editor.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): MCP tab — preserve in-flight edits + warn non-Claude runtimes
- Fix stale-editor sync: compare the local draft against the *previous*
original via a ref, so a background agent refetch updates an untouched
editor instead of being silently ignored. Without this, a draft equal to
the OLD original was treated as user-edited after the prop changed, and
the next Save would write the old config back over a concurrent admin
edit.
- Surface a notice inside the tab when the agent's runtime provider is not
Claude — today's daemon only forwards mcp_config via Claude's
--mcp-config, so saving on e.g. a Codex agent was silent but ineffective.
- Tests for both: rerender resyncs an untouched editor, rerender preserves
an in-flight edit, warning renders on non-Claude / hides on Claude.
MUL-2764
Co-authored-by: multica-agent <github@multica.ai>
* MUL-2764: feat(agents): codex MCP support + hide MCP tab on unsupported runtimes
- Backend: codex.go now translates agent.mcp_config (Claude-style
`{"mcpServers": {...}}`) into `-c mcp_servers.<name>=<inline-toml>`
flags for `codex app-server`, so MCP servers configured in the UI
reach Codex's per-task config layer. Bad mcp_config JSON downgrades
to a warn-and-skip so it can't break the agent launch.
- Frontend: AgentOverviewPane hides the MCP tab when the agent's
runtime provider doesn't read mcp_config — only `claude` and `codex`
are supported today, every other provider sees no MCP tab. The
previous in-tab warning is removed (no longer reachable).
- New shared helper `providerSupportsMcpConfig` lives in
`@multica/core/agents` so views and any future caller share one list
of MCP-aware providers.
- Tests: new go-side coverage for stdio + url + multi-server inputs,
TOML string escaping, malformed-input fallback, and arg ordering vs
custom_args; new views-side coverage for which providers surface the
MCP tab. En + zh-Hans copy and parity test refreshed.
Co-authored-by: multica-agent <github@multica.ai>
* MUL-2764: fix(agents): keep codex mcp_config secrets out of argv/logs
Move the agent's mcp_config from a `-c mcp_servers.<id>=<inline-toml>`
argv flag into a daemon-managed `[mcp_servers.*]` block inside the
per-task `$CODEX_HOME/config.toml`. mcp_servers.<id>.env is a documented
Codex config field and the UI already treats mcp_config as redacted for
non-admins; argv would have leaked those values into `ps aux` and the
`agent command` log line. The file is forced to 0600 to keep secrets in
the daemon owner's lane regardless of the seed file's mode.
Also drop user-supplied `-c/--config mcp_servers.*` entries from
custom_args. Codex `-c` is last-wins (verified against codex-cli 0.132.0),
so without filtering, a custom_args entry could silently shadow whatever
the MCP Tab saved.
Strip inherited `[mcp_servers.*]` tables from the per-task config.toml
when the agent has its own mcp_config, mirroring Claude's
`--strict-mcp-config`: avoids TOML "table already exists" errors on
name collisions and matches admin expectations that the MCP Tab is the
authoritative source for that task.
Co-authored-by: multica-agent <github@multica.ai>
* MUL-2764: fix(agents): codex mcp_config three-state semantics + custom_args compat
Address the third review pass:
1. Distinguish nil vs present-but-empty mcp_config. `{}` and
`{"mcpServers":{}}` now count as "admin saved an explicit (empty)
managed set" — strip inherited user `[mcp_servers.*]` and pin an
empty managed marker block. Only SQL NULL / JSON `null` map to
"absent" and fall back to the user's global `~/.codex/config.toml`.
This aligns Codex with the API's three-state contract (omit / null
/ object) and with Claude's `--strict-mcp-config` semantics.
2. Fail closed on `ensureCodexMcpConfig` errors and on managed
mcp_config without CODEX_HOME. Previous warn-and-launch would
silently inherit the user's global MCP servers and look identical
to a successful apply — exactly the surprise the MCP Tab is meant
to remove.
3. Only filter `-c mcp_servers.*` from `custom_args`/`extra_args`
when the agent has a managed mcp_config. Pre-MUL-2764 agents that
configured MCP via custom_args keep working; once an admin opts
in via the MCP Tab the daemon owns the `mcp_servers` namespace
and overrides are dropped (last-wins safety).
4. Update mcp_config locale intro to mention $CODEX_HOME/config.toml
instead of the now-removed `-c mcp_servers.*` argv path.
Tests:
- Split `TestEnsureCodexMcpConfigEmptyInputsAreNoop` into
`TestEnsureCodexMcpConfigAbsentLeavesUserTablesAlone` (nil/null)
and `TestEnsureCodexMcpConfigEmptyManagedSetStripsUserMcp` (`{}`,
`{"mcpServers":{}}`).
- Add `TestEnsureCodexMcpConfigEmptyManagedSetIdempotent` to pin
byte-identical reruns on the empty managed marker block.
- Add `TestHasManagedCodexMcpConfig` covering the eight relevant
inputs.
- Add `TestBuildCodexArgsPreservesCustomMcpOverridesWhenUnmanaged`
and `TestBuildCodexArgsDropsCustomMcpOverridesWhenManaged` to
pin the new gating.
- Add `TestCodexExecuteFailsClosedWhenMcpConfigInvalid` and
`TestCodexExecuteFailsClosedWhenManagedMcpButNoCodexHome` for the
Execute paths.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
202200bc62 |
feat: publish helm chart to ghcr (#3415)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
56bddc5e06 |
fix(issues): place new issues at top of column in manual sort mode
Fixes PER-145. |
||
|
|
fe2c990296 |
docs(self-host): document Microsoft Exchange / SMTP relay modes and failure diagnostics (#3426)
GH#3405 / MUL-2768. Self-host docs already point at the SMTP path, but on-prem operators ran into two gaps: - The Option B env block in auth-setup and self-host-quickstart only showed a 587 authenticated example, with no copy-pasteable block for the most common Exchange "anonymous internal relay on port 25" pattern, and no explicit mapping between port / auth / TLS / supported-or-not. - troubleshooting "Emails not received" only covered Resend; SMTP failures (smtp dial / starttls / auth / MAIL FROM / RCPT TO / DATA) surface as wrapped errors in the backend logs, but operators had no doc telling them which Exchange-side fix maps to each. Adds: - A relay-mode table (anonymous 25 / authenticated 587 / 465 still unsupported) and two copy-pasteable env blocks in both auth-setup.mdx and self-host-quickstart.mdx (EN + ZH). - Explicit note on the EmailService startup log line so operators can confirm SMTP is the active provider after restart, without leaking credentials. - An SMTP failure-mode table in troubleshooting.mdx (EN + ZH) keyed on the exact wrapped error string, with the Exchange-side fix for each. No code changes; env variable surface unchanged (still SMTP_HOST / SMTP_PORT / SMTP_USERNAME / SMTP_PASSWORD / SMTP_TLS_INSECURE). Port 465 stays "not supported" pending #3340. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5732b0dae8 |
fix(issues): clear deleted ids from recent issues store (#3420)
cleanupDeletedIssueCaches now also calls a new useRecentIssuesStore.forgetIssue(wsId, issueId) action so the persisted Recent Issues bucket no longer keeps deleted ids around. Both the delete mutation and the WS delete event flow through the same cleanup, so this covers self-delete and cross-client delete. Without this, Cmd+K fires a detail query for every recent id on open and returns a steady stream of 404s for issues the user has deleted (#3413). MUL-2765 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1947830d4b |
chore(mobile): re-ignore .DS_Store under apps/mobile/data/
The `!data/**` negation that rescues apps/mobile/data/ from the root .gitignore's `data/` rule was inadvertently pulling .DS_Store back in too — Finder metadata kept showing up in git status. Restate .DS_Store after the negation so last-match-wins re-ignores it. |
||
|
|
2bda4065d0 |
MUL-2708: fix(agent): preserve multi-line Pi prompt on Windows by bypassing the .cmd shim (#3417)
Pi is installed on Windows via npm, which lays down `pi.cmd` → `pi.ps1`
→ `node_modules/@mariozechner/pi-coding-agent/dist/cli.js`. The daemon
spawns Pi with `exec.Command("pi", ...)`; PATHEXT resolves that to
`pi.cmd`, and cmd.exe expands `%*` in the shim by re-tokenising the
original command line, which truncates any argv containing newlines.
buildPiArgs passes the full prompt as the last positional argv, so the
multi-line system+user prompt is silently cut at the first newline
before it reaches the JS entrypoint. The session JSONL then records
only the first line ("You are running as a chat assistant for a Multica
workspace.") and Pi replies as if the user message were missing
(GitHub multica-ai/multica#3306).
Mirror the existing cursor-agent fix: when LookPath resolves Pi to a
.cmd/.bat launcher and a sibling pi.ps1 exists, invoke PowerShell with
`-File <ps1>` directly and forward each arg as a discrete token. This
keeps us on the official launch path while skipping the cmd.exe %*
re-expansion. Falls back to the original launcher when pi.ps1 or
PowerShell can't be located.
The Windows test asserts the rewrite produces the expected argv and
that the multi-line positional prompt survives unchanged.
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|