mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-08 06:45:55 +02:00
main
362 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
77a05fb731 |
Revert "feat(daemon): worktree_pool mode for local_directory (MUL-3483) (#4986)" (#5037)
This reverts commit
|
||
|
|
e002ee5a6b | fix(daemon): isolate agent temp dirs (#5005) | ||
|
|
7bb8076ed0 |
feat(daemon): worktree_pool mode for local_directory (MUL-3483) (#4986)
* feat(daemon): add worktree_pool mode for local_directory (MUL-3483) ## What changed Squad workflows bound to the same `local_directory` resource used to serialise on a single path mutex — a documented pain point from GitHub issue #4377. This introduces an opt-in `worktree_pool` mode on the `local_directory` project resource. When enabled, each task gets its own `git worktree add` under a daemon-managed pool root, so sibling tasks on the same base repo now run truly in parallel while `git worktree add/remove/prune` stays serialised behind a per-repo mutex. ## Shape - `local_directory.resource_ref` gains three optional fields: `mode` ("in_place" default / "worktree_pool"), `pool_root` (defaults to `<parent>/.multica-worktrees/<base>`), `max_parallel` (defaults to 4). Legacy rows are byte-identical after round-trip: the server validator strips the pool fields on the default in_place path so older clients keep behaving exactly as before. - New `WorktreePoolManager` (`server/internal/daemon/worktree_pool.go`) owns pool allocation, per-repo git-metadata mutex, and cleanup. - `acquireLocalDirectoryLockIfNeeded` now branches on the ref's mode. in_place stays on `LocalPathLocker` and the shared tree; worktree_pool routes through the pool manager, publishes a lease keyed by task ID, and pins the agent to the freshly allocated worktree in `execenv.PrepareParams.LocalWorkDir`. - Pool saturation is a structured wait_reason (`worktree_pool saturated (N/M) on <path> (holders: ...)`), retrying on the existing cancel-poll interval — same UX as the historical path-mutex wait. ## Safety guardrails (also known footguns from prior art) - Repos with initialised submodules are refused up front. Multi-checkout of a superproject is explicitly unsupported by `git worktree(1)` BUGS and the per-worktree `modules/` directories bloat disk by pool size ×. - Dirty worktrees are NEVER `--force` removed on release. If the agent left uncommitted changes behind we keep the directory (and free the slot) so users can inspect. This is the failure mode claude-code#55724 documented and the pool must not regress into. - The per-repo mutex covers every `git worktree add/remove/prune` and `submodule status` invocation for a given base, matching the in-process-queue fix Anthropic settled on for claude-code#34645 (`.git/config.lock` races on concurrent add). - Task UUID is the source of truth for both branch (`multica/<uuid>`) and worktree path (`<pool_root>/<uuid>`) so a single agent running multiple worker tasks in parallel can never collide. - Non-empty leftover directories at the target path abort the allocation instead of silently starting the agent in an unknown state. ## Explicit MVP non-goals (deferred, tracked as follow-up work) - Windows worktree-remove retry (permission-denied on locked handles). - Detached-HEAD fast path for read-only exploration tasks. - `post-checkout` hook opt-out / serialisation. - Automatic `git lfs install`. - UI surfacing of the pool state / dirty worktree list. ## Tests - `worktree_pool_test.go` (new): full acquire→release lifecycle, parallel allocation, saturation with holder list, slot re-use after release, dirty-worktree preservation, concurrent-acquire serialisation (the config.lock guard), submodule refusal, missing base rejection, pool root auto-mkdir, non-empty leftover refusal, ctx cancel. - Handler validator gains three rejection cases (unknown mode, relative pool_root, negative max_parallel) and a round-trip test that pins the normalised JSON shape for both modes. - Daemon `localDirectoryRef` helpers get a defaults test and the pool root path derivation is pinned. ## Wire-compat and rollout - Default off. Existing rows keep the historical shape (no `mode`, `pool_root`, or `max_parallel` in the JSON) and behave exactly as before. - Opt-in via `--ref '{"local_path":"...","daemon_id":"...","mode":"worktree_pool"}'` today. CLI flag shortcuts (`--mode`, `--pool-root`, `--max-parallel`) can follow in a small tail PR — not blocking. - No DB migration. No UI change required. Co-authored-by: multica-agent <github@multica.ai> * feat(daemon): address worktree_pool review nits (MUL-3483) Follow-up to #4986. Three non-blocking review points from GPT-Boy: 1. **Daemon integration test for lease → runTask plumbing.** `TestAcquireLocalDirectory_WorktreePoolPublishesLease` (and its in_place counterpart) pin the exact contract runTask relies on when it reads `d.localLeases.Load(task.ID)` and feeds `lease.WorkDir` into `execenv.PrepareParams.LocalWorkDir`. A future refactor that drops the Store, mistypes the key, or swaps back to `assignment.AbsPath` on the pool branch will now fail here rather than silently defeat the whole point of worktree_pool mode. 2. **Untracked-only dirty case now classifies as dirty.** `worktreeIsDirty` used `--untracked-files=no`, which meant a worktree with only untracked files was reported "clean" and hit the `git worktree remove` branch — git itself would then refuse the removal because the file exists (so no data was lost), but the log path lied about what happened on disk. Switching to `--untracked-files=normal` routes agents' fresh drafts directly through the "leaving on disk for user inspection" branch, and `TestWorktreePool_UntrackedOnlyIsKept` pins the guarantee so nobody quietly reverts the flag later. 3. **Skill doc note on default `pool_root` location.** `multica-projects-and-resources/SKILL.md` now spells out the three new ref fields (`mode`, `pool_root`, `max_parallel`), the default `<parent>/.multica-worktrees/<repo>` location (next to the repo, not inside it), the write-permission requirement on the parent directory, and the submodule restriction — so agents advising self-host users hit the right doc line rather than reading source. Existing test suite still green: - `go vet ./...` clean - `go test ./internal/daemon/... ./internal/handler/... ./internal/service/...` all pass Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4c510dfef6 |
fix(daemon): harden background-task-safety brief against background-and-yield (MUL-4140) (#4998)
A Multica-managed run goes terminal the moment the top-level turn exits; there is no "background work finishes later and wakes you up" step. When an agent starts background work (a run_in_background shell, a Monitor, an async subagent) and ends its turn to "wait for a completion notification", the work is orphaned and the result comment it meant to post is never sent (MUL-4091 / PR #4970). The existing claude-only protocol guard forces run_in_background tool inputs to foreground and fails loud on async_launched tool results, but it cannot catch the actual MUL-4091 mechanism: a turn that ends cleanly with a "Standing by, I'll report when CI finishes" message. That shape is only addressable behaviorally, and it is harness-agnostic. Harden the Background Task Safety brief (both the legacy/verbose production path and the slim staging path) with explicit hard pins: - never background-and-yield / expect a future wakeup that does not exist here; - do every wait synchronously in a single foreground call (e.g. gh run watch); - the standalone-harness "running in the background, keep working" hint does not apply in Multica-managed runs; - never end a turn with a "standing by" / "I'll report back" sign-off. Add verbose- and slim-path test coverage for the new pins so a future brief trim cannot silently drop them. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b2db309618 |
Skip local directory lock for squad leaders (#4951)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
cb68669c73 |
feat(composio): gate MCP apps behind feature flag (#4876)
* feat(composio): server-side connect flow + connections REST (Notion MVP) (MUL-3720) (#4608)
* feat(composio): server-side connect flow + connections REST (Notion MVP) (MUL-3720)
Compose the merged server/pkg/composio SDK into a user-facing connection
manager: signed-state connect handshake, local user_composio_connection
mirror, idempotent disconnect, and a per-user MCP session helper (not yet
wired into task dispatch).
- migration 127_user_composio_connection (no FK/cascade, per DB rules)
- sqlc queries: upsert (idempotent on user_id+connected_account_id), list
active, owner-scoped get, mark revoked
- internal/integrations/composio: signed HMAC-SHA256 state, BeginConnect,
CompleteCallback (idempotent upsert), ListConnections, Disconnect
(upstream 404 = idempotent success), CreateMCPSession (no-op when empty,
pins connected_accounts per toolkit), CallbackRedirect
- REST handlers under /api/integrations/composio (user-scoped, 503 when
COMPOSIO_API_KEY unset): connect/init, callback (302), connections list,
delete
- router wiring gated by COMPOSIO_API_KEY; COMPOSIO_AUTH_CONFIGS_JSON maps
toolkit->auth_config (MVP: notion); state secret from COMPOSIO_STATE_SECRET
or derived from JWT_SECRET; callback base from COMPOSIO_CALLBACK_BASE_URL
or MULTICA_PUBLIC_URL
- tests: state (expire/tamper/wrong-secret), service (mapping, callback
idempotency, non-success, disconnect owner/404 idempotency, MCP pin),
handlers (httptest), redact regression for Bearer mcp_ tokens
MVP scope: Notion only; no task-dispatch overlay, sharing, or webhook
event handling (later stages).
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): bind callback account to user + idempotent revoked disconnect (MUL-3720)
Address PR 4608 review (CHANGES_REQUESTED):
- callback: verify connected_account_id with Composio before mirroring it.
The signed state only proved user/toolkit/exp, so a valid state paired with
a tampered connected_account_id would be written verbatim. CompleteCallback
now calls ListConnectedAccounts and fails closed (ErrAccountVerification)
unless the account belongs to the state's user (composio_user_id == multica
user id) and was created under the toolkit's auth config. No row is written
on mismatch / unknown account / upstream error.
- disconnect: short-circuit to a no-op when the local row is already revoked,
before touching upstream. Previously a second DELETE re-hit Composio and a
non-404 upstream error surfaced as a 502, breaking the 204-idempotent
contract.
- CreateMCPSession: document the v1 single-active-connection-per-(user,toolkit)
constraint and make duplicate selection deterministic (newest-wins, rows are
connected_at DESC) instead of order-dependent map overwrite. Stage 3 owns the
real single-account-enforcement vs multi-account-shape decision.
Tests: tampered/wrong-auth-config/unknown-account callback rejection, revoked-row
disconnect no-op (asserts upstream not re-hit). composio pkg 85% coverage; all
green.
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): list all toolkits + dynamic auth-config resolution (MUL-3720)
Yushen's follow-up to the Notion MVP: surface the full Composio toolkit
catalog, render it in Settings, and drop the static env mapping in favor of
dynamic auth-config discovery.
Config correctness (per Composio docs):
- Remove COMPOSIO_AUTH_CONFIGS_JSON entirely. The toolkit→auth_config mapping
is now resolved at request time from the project's /auth_configs (cached,
5-min TTL), so enabling a toolkit is a dashboard action, not a redeploy.
- Do NOT add COMPOSIO_PROJECT_ID. The project API key (x-api-key) authenticates
to exactly one project; the project is resolved from the key. Only org-level
endpoints use x-org-api-key, which this integration never calls.
Backend:
- SDK: server/pkg/composio/auth_configs.go — ListAuthConfigs (toolkit_slug,
is_composio_managed, show_disabled, limit, cursor).
- service: dynamic resolver (authConfigMap cache; betterAuthConfig prefers a
custom/white-label config over Composio-managed, newest wins); BeginConnect
and CompleteCallback resolve via it; ListToolkits fetches the full catalog
(paginated, capped) annotated with connectable = has an enabled auth config,
connectable-first ordering.
- handler + route: GET /api/integrations/composio/toolkits (user-scoped, 503
when COMPOSIO_API_KEY unset) returning slug/name/logo/category/connectable.
Frontend:
- core: ComposioToolkit/ComposioConnection types, api client methods, and
composio query options (@multica/core/composio).
- views: Settings → Integrations now has a Composio section rendering every
toolkit as a card with search. Connect is gated on `connectable`;
non-connectable toolkits show a muted "not configured" hint instead of a
dead button. Connected toolkits show a badge + Disconnect (with confirm).
- i18n: composio block added to en/zh-Hans/ja/ko settings.
Tests: SDK + service (dynamic resolution, custom-over-managed preference,
connectable flag, resolver-error soft-degrade) and handler toolkits endpoint;
composio pkg 85.7% coverage. go build/vet/gofmt clean; core+views typecheck,
core+views lint, and core tests (691) all green.
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): close cross-toolkit callback fail-open by signing auth_config_id into state (MUL-3720)
Re-review blocker: CompleteCallback resolved the toolkit's auth config at
callback time and ignored a resolve error/empty result, while
verifyAccountOwnership skipped the auth-config comparison when the expected
value was empty. A user could then pass another toolkit's connected_account_id
into this toolkit's callback — the owner check passed and it was written under
the wrong toolkit_slug/account binding.
Fix: the auth_config_id is already resolved in BeginConnect (before the state
is signed), so sign it into the state and compare it exactly at callback. No
re-resolve, no fail-open. verifyAccountOwnership now fails closed when the
expected auth config is empty (rejects instead of skipping) and requires an
exact match — closing the cross-toolkit binding gap.
Tests: state round-trips auth_config_id; BeginConnect signs it; callback
rejects wrong/cross-toolkit auth config and an empty (no-mapping) auth config
fails closed. composio pkg 85.2% coverage, all green.
Frontend (non-blocking): the Composio settings tab now surfaces an error when
the connections query fails instead of silently rendering everything as
unconnected.
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): hide Settings section entirely when integration unconfigured (MUL-3720)
Decision (option 2, hide-then-merge): don't show a card that leaks the internal
COMPOSIO_API_KEY env-var name to every end user. IntegrationsTab now gates the
whole Composio section (heading + body) on the toolkits query — a 503 means the
key is unset, so the section is withheld instead of rendering the not-configured
card. Admin-only setup guidance is a later, role-gated affordance.
Removed the notConfigured card (and now-unused ApiError import) from
ComposioTab; it only mounts when configured. views typecheck + lint clean.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): Stage 2 frontend polish — callback toast, last_used & expired UI, e2e (MUL-3718) (#4688)
* feat(composio): callback toast + refresh, last_used & expired UI, e2e (MUL-3718)
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): real callback redirect route + StrictMode-safe toast dedup (MUL-3718 review)
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): callback endpoint should not require Multica auth (MUL-3843) (#4709)
* fix(composio): move OAuth callback out of the Auth group (MUL-3843)
Composio 302-redirects the browser to /api/integrations/composio/callback
at the end of the OAuth flow, but PR #4608 mounted it inside the cookie-auth
middleware group. When the session cookie is absent (expired session,
SameSite=Strict / Safari ITP, private window, self-hosted callback subdomain)
the Auth middleware returned a hard 401 and a JSON blob instead of the
settings redirect, breaking the flow.
Identity never came from the cookie anyway: it is carried by the HMAC-signed
state param that CompleteCallback verifies (signature, expiry, replay) and
cross-checked by verifyAccountOwnership; h.Composio == nil still 503s. So the
callback is registered alongside the other public OAuth/webhook routes; the
other four composio endpoints stay session-gated.
Refs MUL-3843, MUL-3715.
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): correct stale callback routing comments (MUL-3843)
The package header and ComposioCallback doc comments still described the
callback as sitting under the Auth middleware group. After the route was
moved out (this PR), update both to state it is a public route whose identity
comes from the signed state — addressing review nit from 张大彪.
Refs MUL-3843.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): inject MCP overlay into agent runtime at task dispatch (MUL-3721) (#4704)
Stage 3 of the Composio epic. Wires the per-user Composio MCP session into
every agent task so the agent process sees the initiator's connected tools
without any prompt-time plumbing.
Server side
- Migration 128 adds agent_task_queue.runtime_mcp_overlay JSONB plus a
BEFORE-UPDATE trigger that wipes the column on any transition into a
terminal status (completed / failed / cancelled). A trigger is the single
source of truth — future queries that flip status cannot bypass it.
- composio.Service.BuildTaskOverlay(userID) reuses CreateMCPSession and
emits the Claude-style { mcpServers: { composio: { type: http, url,
headers } } } shape the daemon's existing sidecar generators consume.
Returns (nil, nil) on zero active connections so we never burn a
Composio session for a user with nothing to call.
- TaskService grows a Composio ComposioOverlayBuilder seam, wired in
router.go after composiointeg.NewService succeeds. Five enqueue paths
(issue / mention / quick-create / chat / auto-retry) attach the overlay
after CreateAgentTask returns and before the daemon is notified — so
every claim reads a settled row, with no second daemon hop. Best-effort:
a builder failure logs and proceeds with no overlay.
- resolveInitiatorFromTriggerComment derives the initiator user from the
trigger comment when it was authored by a member. Agent-authored
triggers are not treated as initiators (their connected-apps view is
empty by construction).
Daemon side
- handler/daemon.go claim path merges task.runtime_mcp_overlay onto
agent.mcp_config via mergeMCPOverlay before populating
TaskAgentData.McpConfig. Overlay wins on server-name collisions
because it carries the live user-scoped session URL. Errors fall back
to the agent config unchanged — a bad overlay must not surprise-disable
saved MCP tools. The existing execenv sidecar generators (cursor /
codex / openclaw / opencode / hermes-kiro) need no changes: they keep
consuming the merged result through TaskAgentData.McpConfig.
Tests
- 9 merge cases (mcp_overlay_test): both-nil short-circuit, agent-only
pass-through, overlay-only canonicalization, two-side merge, name
collision (overlay wins), top-level key preservation, malformed agent
fallback, malformed overlay fallback, non-object server rejection.
- 4 dispatch cases (composio): zero-connections returns nil without
CreateSession, happy-path emits the right shape with the right user
id, empty-URL defensive branch, SDK error surfacing.
- 4 TaskService helper cases: nil Composio is a no-op (Queries-safe),
invalid initiator does not call the builder, nil overlay skips the
UPDATE, builder error swallowed without panic.
- Migration 128 verified to roll up + down + up cleanly against the test
database.
Out of scope (deferred): assignment-triggered enqueue paths with no
trigger comment get no overlay attached today (no initiator UUID flows
through enqueueIssueTask in that case). Retry paths recompute the overlay
fresh from the parent's initiator_user_id instead of inheriting the bearer
from the parent row, so a stale token can never resurface on a retry.
Co-authored-by: Eve <eve@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): per-agent allowlist + originator-scoped MCP overlay (MUL-3869) (#4736)
* feat(composio): per-agent allowlist + originator-scoped MCP overlay (MUL-3869)
Stage 3.1 of the Composio epic (MUL-3721 parent). PR #4704 wired in the
runtime_mcp_overlay column and a per-task dispatch hook; this change
inverts the default from "all-on" to opt-in and locks the overlay to the
agent owner's own connected apps:
- Agents carry composio_toolkit_allowlist TEXT[]. NULL or [] => no MCP.
Owner-only read/write; non-owner GET/PUT silently redacts/drops the
field (same shape as mcp_config).
- agent_task_queue carries originator_user_id UUID. Set from the
top-of-chain HUMAN at every enqueue path:
* issue/mention comment by member -> author_id
* issue/mention comment by agent -> inherit via comment.source_task_id
-> parent task originator_user_id
* quick-create -> requester_id
* chat -> initiator_user_id
* retry -> SQL-inherited from parent row
* autopilot -> NULL (system-driven)
- BuildTaskOverlay (composio dispatch) now takes (ctx, originatorUserID,
agent) and short-circuits on five gates: invalid originator,
originator != agent.owner_id, empty allowlist, empty intersection of
allowlist ∩ active connections, defensive empty session URL. Composio
CreateSession is called with BOTH `toolkits.slugs` (the intersection)
AND `connected_accounts` (the pinned account ids), narrowing the
tool-router twice.
- The originator-vs-owner gate closes the agent-fanout privacy hole: any
workspace member who can @-mention a public agent used to project the
owner's connected apps into their run. Now the overlay only mounts
when the human at the top of the chain IS the agent owner.
Tests:
- dispatch_test.go covers all 5 gates plus uppercase/whitespace slug
normalisation.
- task_runtime_mcp_overlay_test.go covers the no-op gates of the new
applyRuntimeMCPOverlay signature.
- agent_composio_allowlist_test.go (handler): owner roundtrip
(list/empty/null), workspace-admin silent-drop, owner-only GET
visibility, pure normaliseComposioToolkitAllowlist.
- resolve_originator_test.go (service, DB-backed): member-authored,
agent-authored inherits via comment.source_task_id, invalid id.
Migration 129 up/down/up verified against docker postgres.
Co-authored-by: multica-agent <github@multica.ai>
* chore(composio): gofmt + regenerate sqlc with v1.31.1 (MUL-3869 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>
* fix(composio): accept nested connected account auth config
* feat(views): creator-only MCP tab for per-agent Composio allowlist (MUL-3870) (#4743)
Stage 3.2 frontend on top of the Stage 3.1 backend (MUL-3869,
|
||
|
|
d4f57aff7a |
MUL-3944: Fix daemon agent discovery around hook wrappers (#4817)
* Fix agent path discovery around hook wrappers Co-authored-by: multica-agent <github@multica.ai> * Fix login shell hook wrapper discovery Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
65269ef922 |
fix(daemon): copy Codex model catalog into task home
Fixes #4825 Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
0c4c3ff038 |
fix(cli): prevent daemon-managed CLI from silently using user tokens (MUL-3922)
Treat MULTICA_DAEMON_PORT and a workdir daemon-task marker as daemon-managed signals so a task subprocess that loses MULTICA_TOKEN / MULTICA_AGENT_ID / MULTICA_TASK_ID fails closed instead of silently falling back to the user config-file PAT (which made agent writes land as the workspace owner). Adds an actionable error naming a leftover marker for local_directory recovery. Fixes #4204. |
||
|
|
101cc29e20 |
fix(daemon): time out repo cache git commands
Closes #4795 |
||
|
|
7d310a9f6a |
fix: expose local skills for ACP providers (#4800)
Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4b9ea4aa68 |
feat(agent): add ByteDance TRAE CLI (traecli) as an ACP backend (#4724)
Adds the official ByteDance TRAE CLI (the `traecli` binary documented at https://docs.trae.cn/cli — the product paired with the Trae IDE, not the open-source bytedance/trae-agent) as a built-in agent backend. traecli is ACP-native, so it is driven over the standard ACP JSON-RPC transport via `traecli acp serve --yolo`, reusing the shared hermesClient exactly like the Kiro and Qoder backends. Validated end-to-end against the real traecli v0.120.42 with a logged-in account: initialize advertises loadSession:true + mcpCapabilities{http,sse}; session/new returns result.sessionId + models.availableModels (18 models discovered); session/prompt streams session/update notifications with sessionUpdate=agent_message_chunk (hermesClient already normalizes this Zed-ACP wire shape); a real board task ran 14 tool calls and completed in ~47s. Implementation: - server/pkg/agent/traecli.go: ACP backend; session/load resume (loadSession:true), session/set_model, MCP via ACP mcpServers, --yolo bypass-permissions for headless runs, blocked-arg filtering (acp, serve, --yolo, --print, --output-format, --permission-mode) - agent.go: New() + launch header "traecli acp serve" - models.go: discoverTraecliModels via the shared discoverACPModels - daemon/config.go: auto-detect the `traecli` binary (MULTICA_TRAECLI_PATH / MULTICA_TRAECLI_MODEL) - daemon.go: inline the runtime brief (traecli reads .trae/rules/, not AGENTS.md) and surface the runtime as "Trae" (providerDisplayName) - execenv: AGENTS.md + .traecli/skills wiring; ~/.traecli/skills local root - packages/core mcp-support: traecli consumes mcp_config - frontend: official Trae provider logo - docs: providers.mdx matrix + section, CLI_AND_DAEMON.md, README Tests: fake-ACP unit tests matching the real wire format (streaming, blocked-arg filtering, session/set_model failure, session/load resume) plus a gated real-binary smoke test (TestTraecliRealACPSmoke) that skips when traecli is absent or not logged in. Built-in provider only (mirrors qoder): not in SupportedTypes / RUNTIME_PROFILE_PROTOCOL_FAMILIES, so no migration is needed. Resolves #4376. |
||
|
|
444c2f29a5 |
fix(slack): stop the chat agent narrating its history reads (MUL-3871) (#4776)
Every Slack reply was prefixed with process narration like '我先读取 Slack 频道概览, 再打开相关线程…' before the actual answer — the model announcing the history reads the channel-awareness prompt tells it to do. That narration is internal context-gathering, not part of the answer. Add an instruction to the channel-awareness block: do the reads silently and reply with the answer only, no preamble about what it is about to read or just read. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3a6d3522c8 |
feat(slack): two-command channel reads — chat history (overview) + chat thread [id] (MUL-3871) (#4762)
Replaces the single scoped `multica chat history --scope` read with two clean noun-commands so the agent can navigate a channel with many threads (e.g. read the specific thread a user referred to): - `multica chat history` — the channel OVERVIEW: recent top-level messages, each thread tagged with thread_id + reply_count + latest_reply (it does NOT expand thread contents). Backed by GET /api/chat/history + slack.History.ChannelOverview (conversations.history). - `multica chat thread [id]` — read one thread: no id = the thread you're in, an id = a specific thread IN THE SAME channel. Backed by GET /api/chat/thread + slack.History.Thread (conversations.replies; DM falls back to history). The channel stays server-pinned to the session; a thread id is only a within-channel locator, so the security boundary (no cross-channel reads) is unchanged. `--scope` is removed. The prompt now teaches both commands and, via a new chat_in_thread signal (derived from the binding: last_thread_id != last_message_id), tells the agent which to start with — `chat history` for a top-level @mention, `chat thread` for an in-thread one. Tests: slack ChannelOverview/Thread (current/by-id/DM-fallback/no-binding/clamp), handler both endpoints + auth, prompt top-level vs in-thread guidance. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a961d63611 |
feat(slack): make the chat agent explicitly channel-aware (MUL-3871) (#4755)
Before this, the chat prompt only carried a generic, always-on hint ('if this
came from a chat channel...'), and the task carried no channel signal — so the
agent never definitively knew it was inside Slack. For an ambiguous ask like
'what did you just talk about', it could read Multica instead of the Slack
conversation.
- Thread a chat_channel_type ('slack') signal: the server sets it on the chat
task response when the session has a Slack binding
(GetChannelChatSessionBindingBySession); the daemon Task carries it.
- buildChatPrompt now emits an EXPLICIT block only when channel-backed: 'You are
operating inside a Slack conversation … this conversation and its history live
in Slack, NOT in Multica … read it with multica chat history, do NOT look in
Multica.' Web-only chat sessions get no such block (their history is the
Multica chat_session the agent already resumes).
Tests: slack-backed prompt asserts the explicit Slack/“NOT in Multica”/command
copy; web-only prompt asserts the block is absent.
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
50a48cef1e |
feat(slack): unified multica chat history pull for channel backfill (MUL-3871) (#4747)
* feat(slack): add unified `multica chat history` pull for channel backfill (MUL-3871) Agents @mentioned in a Slack thread/channel only saw the triggering message, never the prior conversation (GitHub #4717). Instead of force-assembling a recent-context block on every inbound (the Feishu approach), expose a single channel-agnostic pull command the agent runs on demand. - channel: normalized HistoryMessage/HistoryPage/HistoryOptions vocab so the agent sees one shape regardless of platform. - slack.History: resolves session -> binding -> installation -> bot token and reads conversations.replies (real thread) or conversations.history (DM / top-level channel, capturing sibling messages). thread_ts is recorded on the binding config at session creation to pick the right call. - handler GET /api/chat/history: authorized purely by the task-scoped token (stamped X-Task-ID -> the task's own chat session), so an agent can only read the conversation it is currently running for. - multica chat history CLI command (no args; same for every channel). - buildChatPrompt nudge so the agent discovers the command. Feishu is intentionally untouched. Adding a platform = implement the reader. Co-authored-by: multica-agent <github@multica.ai> * fix(slack): require task-token actor source on chat history endpoint Niko's review caught a privilege-boundary hole: the endpoint trusted X-Task-ID, but it is mounted under the general Auth group where a normal JWT / mul_ PAT request does NOT strip a client-forged X-Task-ID — only the mat_ task-token branch stamps it. A workspace member who knew a chat task id could forge the header and read that task's Slack channel/DM/thread history. Gate on the server-set X-Actor-Source == "task_token" (the Auth middleware deletes any client-supplied value and re-stamps it only on the mat_ branch), then trust X-Task-ID. Adds a regression test: a forged X-Task-ID without the task-token actor source is rejected with 403 and never reaches the reader. Co-authored-by: multica-agent <github@multica.ai> * fix(slack): thread-first history for follow-ups, channel for first turn (MUL-3871) A Slack conversation has two nested histories: the surrounding channel and the agent's own thread (the bot's first reply opens a thread on the @mention). The first version picked replies-vs-history from a thread_ts fixed at session creation, so a session started by a top-level @mention always read CHANNEL history — even on follow-ups inside the bot's thread, which should read THREAD history first. - Add a HistoryScope (auto|thread|channel). The handler resolves auto: first turn (no prior bot reply) -> channel; follow-up -> thread. The agent can override with --scope channel|thread, and the response reports the scope read. - The thread root is derived from the binding (last_thread_id / composite-key suffix), available for every engaged group session, instead of the creation-time thread_ts (now removed from the binding config). - A DM degrades a thread request to channel history (DMs have no threads). - Prompt guidance + CLI help updated to explain the policy. Tests: scope selection (thread/channel/DM-fallback/no-root), root derivation, and handler auto-resolution (first->channel, follow-up->thread, explicit override). Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
93028d303b |
fix(daemon): reconcile in-flight task and workspace state on WS reconnect (#4718)
After a WebSocket disconnect, the daemon's view of running tasks and workspace state can lag the server for up to 5s (per-task cancellation poll) or 30s (workspace sync) because both loops park on coarse tickers that do not observe the WS wakeup channel. This change adds a small fan-out broadcaster (`reconcileBroadcaster`) that the WS connect path fires once per (re)connect. `watchTaskCancellation` and `workspaceSyncLoop` subscribe and re-check immediately on broadcast, without disturbing the ticker cadence. The broadcaster is edge-triggered with a one-slot replay so a broadcast that lands before a subscriber is ready is not lost (closes the daemon-startup race), and back-to-back broadcasts inside 1s are debounced so a flapping connection cannot fan out into a request stampede. Existing behaviour is preserved: shouldInterruptAgent still decides whether to interrupt, the 5s/30s ticker still bounds the worst case, and the WS heartbeat / HTTP heartbeat coordination is untouched. Closes #4665 |
||
|
|
f59cb2f494 |
MUL-3834: harden daemon websocket reconnect (#4699)
* MUL-3834 harden daemon websocket reconnect Co-authored-by: multica-agent <github@multica.ai> * MUL-3834 stabilize daemon websocket liveness tests Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
7d0c73d11f |
MUL-3417: tolerate OpenClaw config file CLI mismatch
Closes MUL-3417 Fixes #4299 |
||
|
|
dfa384ffa2 |
fix(daemon): resolve skill bundles per-skill with size-scaled timeout (#4505) (#4530)
* fix(daemon): resolve skill bundles per-skill with size-scaled timeout (MUL-3650, #4505) Cold-start skill resolution downloaded the agent's entire bundle in one atomic request bounded by the shared 30s control-plane http.Client timeout. On a slow/jittery link a large bundle (15+ skills) could not finish the body read in 30s, and because the cache was only written after the whole batch succeeded, nothing was persisted on failure — so every dispatch re-downloaded the full bundle and timed out again, never converging. Resolve each missing bundle in its own request and cache it the moment it arrives: - daemon: per-skill resolve with a deadline scaled to the bundle's declared size (floor 30s, cap 5m, ~50KB/s floor throughput) instead of the fixed control-plane timeout; each success is persisted independently, so a dispatch that fails on one skill still caches the rest and the next dispatch only re-fetches what is missing. - client: dedicated bundleClient with no fixed Timeout (deadline comes from ctx), a singular ResolveSkillBundle, and a short transient-retry schedule. Tests cover the size-scaled timeout and the cross-dispatch incremental caching / convergence (a failed skill does not discard its siblings, and cached skills are not re-fetched). Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): accept server-side skill updates in per-skill resolve (MUL-3650) Address review on #4530: resolveSkillBundle validated the returned bundle against the claim-time ref, which pinned it to the requested hash. The resolve endpoint intentionally serves the agent's current bundle and hash when the requested hash is stale (the skill can be edited between claim and prepare), so a legitimate updated bundle was rejected as invalid and the task failed. Confirm only that the server returned the requested skill (source/id), then validate self-consistency against a ref derived from the returned bundle and cache it under its own hash — matching the documented endpoint contract. Adds a regression test covering a stale-hash request answered with an updated bundle. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
34bd115808 |
test(execenv): fix stale test name reference in comment (#3028)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3adfaf4285 |
fix(execenv): support OpenClaw 2026.6.x agents schema (#3028) (#4319)
Adapts OpenClaw execenv prep to the 2026.6.x agents schema (agents.list config path removed; agents live in a sqlite registry). Case-insensitive key-missing guard + registry fallback on read, version-aware emission on write so per-task workspace pinning keeps working. Closes #3028 MUL-3643 |
||
|
|
9db80a0940 |
fix(daemon): forbid mid-run progress comments in runtime brief (#4516)
A run could post running progress/plan narration as issue comments, and a review run surfaced its in-progress narration as the result instead of a conclusion (MUL-3605). Add one rule to the Output section's issue-task branch, in both the legacy and slim briefs: post exactly one comment per run — the final result, before the turn exits — and keep plans/progress in the agent's own reasoning. The pre-existing "Final results MUST be delivered … a task that finishes without a result comment is invisible" line already makes the comment mandatory, and "state the outcome, not the process" already rules out progress dumps, so no second rule is added. Chat / quick-create / autopilot keep their own delivery channels. Adds a regression test across both brief paths. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
20eecfb093 | fix(projects): honor repo resource checkout refs (MUL-3593) (#4470) | ||
|
|
1ac3a03e5d |
MUL-3618: dispatch daemon feature flag snapshots (#4509)
* MUL-3618: dispatch daemon feature flag snapshots Co-authored-by: multica-agent <github@multica.ai> * MUL-3618: narrow daemon flag snapshots to process scope Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
76c58a4ee8 |
MUL-3617: remove Gemini CLI runtime (#4503)
* fix: remove gemini cli runtime Co-authored-by: multica-agent <github@multica.ai> * fix: skip unsupported custom runtime profiles Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
8ad673fdb7 |
MUL-3560: gate slim runtime brief behind runtime_brief_slim feature flag (#4449)
The MUL-3560 slim runtime brief — kind-driven dispatcher, per-section
gating, prose compression for ~7k chars saved on the typical
comment-triggered task — now ships behind the `runtime_brief_slim`
feature flag wired via the framework-level service from MUL-3615.
Default: OFF in every environment (production stays on the legacy
brief that has shipped for ~2 years). Staging opts in via the YAML
rule set; ops can override per-process with `FF_RUNTIME_BRIEF_SLIM=true`.
Production is held back until staging has burned in long enough that
we are confident the slim brief does not regress agent behaviour.
Architecture (one toggle point, two code paths, both fully tested):
buildMetaSkillContent (runtime_config.go)
│
└─ useSlimBrief() → false (default)
│ → fall through to the legacy verbose body that ships on
│ main today — byte-for-byte unchanged, no migration risk
│
└─ useSlimBrief() → true
→ buildMetaSkillContentSlim (runtime_config_sections.go)
→ classifyTask → 5-way kind switch → per-section writers
BuildCommentReplyInstructions takes the same gate, so the per-turn
comment prompt and the runtime brief stay in sync on which template
they emit.
What's in this PR:
- runtime_config_flag.go (new): package-scope `runtimeFlags` atomic
pointer + `SetFeatureFlags` setter + `useSlimBrief` toggle point.
Nil-safe: a daemon that forgets to wire the service falls back to
legacy, no panic.
- runtime_config_kind.go (new): `taskKind` enum + `classifyTask` +
`hasIssueContext` predicate. Used only by the slim path.
- runtime_config_sections.go (new): the slim brief itself —
`buildMetaSkillContentSlim` + per-section `writeXxx` helpers
+ `writeAvailableCommandsQuickCreate` minimal variant +
`writeBackgroundTaskSafetySlim` compressed safety section. The
Section × Kind matrix is documented inline on
`buildMetaSkillContentSlim` and the test below checks the
dispatcher does not diverge from the spec.
- reply_instructions.go: `BuildCommentReplyInstructions` gains a
short slim-or-legacy prelude; new `buildCommentReplyInstructionsSlim`
is the compressed cookbook (defers the shell-hazard rationale to
`## Comment Formatting`).
- runtime_config.go: `buildMetaSkillContent` gains a 2-line
dispatcher at the top; the legacy body is otherwise untouched.
- runtime_config_kind_test.go (new): canaries for both paths.
- TestClassifyTask: 5 kinds + 3 tiebreak cases.
- TestTaskKindHasIssueContext: predicate semantics.
- TestSlimFlagOffUsesLegacy: nil flag service → legacy path
(renders "Get full issue details.", a legacy-only substring).
- TestSlimFlagOnUsesSlim: flag on → slim path (renders "full
issue.", a slim-only one-liner) AND must NOT render legacy
"Get full issue details.".
- TestBuildMetaSkillContentSlimKindMatrix: locks the per-kind
section set; heading match is line-anchored so inline references
don't trip absence assertions.
- TestSlimQuickCreateAvailableCommands: locks the minimal-variant
content for quick-create (issue create present, every other
Core command absent).
- TestSlimBriefIsSubstantiallyShorter: ≥ 30% reduction guard so
a future change can't accidentally re-bloat the slim path back
to legacy levels.
- cmd/server/main.go: now calls `execenv.SetFeatureFlags(flags)`
immediately after constructing the feature flag service.
Measured impact (slim vs legacy, claude provider, realistic fixture
with 2 repos + 2 skills + member initiator):
legacy = 19567 chars
slim = 11868 chars Δ = -7699 (-39.3%)
Verification:
- go vet ./internal/daemon/... ./cmd/server/... ok
- go test ./internal/daemon/... ok
- go test ./pkg/featureflag/... ok
- TestSlimBriefIsSubstantiallyShorter logs the 39.3% ratio
- TestSlimFlagOffUsesLegacy + TestSlimFlagOnUsesSlim pass both
directions, so the dispatcher is locked in code.
The pre-existing `internal/handler` test failures
(TestLeaveWorkspace_RevokesOwnRuntimes,
TestDeleteMember_CancelsTasksFromAgentReassignment,
TestDeleteMember_NoRuntimes_DeletesMember) reproduce on plain
`origin/main` with the same `relation "channel_user_binding" does
not exist` SQL error — they are a missing-migration bug from the
recent channels foundation PR (
|
||
|
|
b79777caec |
feat(comments): resolve-aware fold for agent comment reads (MUL-3555) (#4463)
* feat(comments): resolve-aware fold for agent comment reads (MUL-3555) Agents reading a long issue paid tokens for settled discussion. The human timeline already folds resolved threads, but the agent read path (`comment list`) ignored resolved_at entirely — humans saw the conclusion, agents got the full raw discussion. Add an opt-in `fold=true` projection to ListComments that collapses each resolved thread to root + conclusion (reply-resolved) or root only (root-resolved), reusing the human timeline's deriveThreadResolution semantics. The resolved thread's root carries `thread_resolved` + `folded_count`; `--full` brings the dropped comments back. Fold is rejected on partial-thread reads (since/tail) and roots_only, where a resolution comment could be unfetched and silently dropped. CLI `comment list` folds by default on the complete-thread reads (default, --recent, untailed --thread) with a `--full` escape hatch; the agent prompts and runtime brief document the fold + escape. No new endpoint, no human UI change, no SQL/migration change — in-memory projection, same precedent as summary/roots_only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * refactor(daemon): dedupe fold prompt restatements per review (MUL-3555) Howard's PR review flagged DRY redundancy: the resolve-fold rule was restated in full in the task prompt (prompt.go:41/:182) and the brief workflow steps (runtime_config.go:673/:692, reply_instructions cold hint) even though the canonical command catalog (runtime_config.go:477) — always present in the brief — already documents it in full, and the task prompt explicitly defers to it ("follow the rule in your runtime workflow file"). Keep the catalog entry full (the canonical reference); shrink the five inline restatements to a short "resolved threads come back folded — `--full` to expand" pointer. No loss of signal (the agent always has the full catalog in context), ~80-120 tokens/run saved on the worst-case assignment / cold paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5038c983c0 |
MUL-3281: Add daemon skill bundle refs (#4445)
* feat: add daemon skill bundle refs Co-authored-by: multica-agent <github@multica.ai> * fix: tighten skill bundle resolve safeguards Co-authored-by: multica-agent <github@multica.ai> * feat: add task prepare lease Co-authored-by: multica-agent <github@multica.ai> * fix: isolate prepare lease concurrent index migration Co-authored-by: multica-agent <github@multica.ai> * fix: keep prepare lease active through start Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
12ea1f6a8c |
MUL-3495: support custom runtime args and registration errors (#4408)
* feat: support custom runtime args Co-authored-by: multica-agent <github@multica.ai> * fix: address custom runtime 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> |
||
|
|
4ab335b8a5 |
MUL-3416: Issue pre-trigger preview + Handoff Note (#4383)
* feat(issues): unify run-enqueue decision behind WillEnqueueRun + preview endpoint Collapse the issue update/batch enqueue copies into one service predicate service.IssueService.WillEnqueueRun, shared verbatim with a new dry-run endpoint POST /api/issues/preview-trigger so the four entry points stop drifting (squad/self-loop/batch omissions, MUL-3375). The private-agent gate stays at the HTTP boundary: write paths inject allow-all, preview injects the real gate so it never leaks a private agent's readiness. Add suppress_run to issue update/batch: the change applies but no run starts. Remove the now-dead handler mirrors shouldEnqueueSquadLeaderOnAssign / isSquadLeaderReady. service.Create and the comment trigger chain are untouched. Tests: preview behavior, preview<->write-path match, batch aggregation, member no-trigger, suppress_run skip, malformed-body 400. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(issues): inject handoff note into assigned runs via first-class task field Add an optional handoff_note carried by issue assign/promote into the run's opening prompt and issue_context.md, via a dedicated agent_task_queue column (migration 122) and a daemon assignment-handoff render branch — never a fabricated comment, never trigger_comment_id (MUL-3375 §6.1). Thread the note through enqueueIssueTask/enqueueMentionTask + WithHandoff public variants and dispatchIssueRun; suppress_run or a parked write drops it (no run = nothing to inject). Soft version gate: MinHandoffCLIVersion + HandoffSupported, surfaced per-trigger as handoff_supported in the preview so the UI can gray the note box on old daemons; the assignment never hard-fails. Tests: daemon prompt + issue_context render via the assignment branch (not quick-create/comment), version helper matrix, note persists on the task, suppressed assign enqueues nothing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(issues): leave a display-only handoff record on the timeline When an assign/promote with a handoff note starts a run, write one type='handoff' timeline record via TaskService.RecordHandoff — a direct Queries.CreateComment + timeline event that bypasses Handler.CreateComment, so it never reaches triggerTasksForComment and cannot start a second run (MUL-3375 §6.2, the must-not-retrigger invariant). Author is the actor who handed off; body is the note. Migration 123 admits the 'handoff' comment type. Recorded only on a real run start: suppress_run or a parked write writes nothing. enqueueSquadLeaderTask now reports whether it enqueued so the trace is gated on an actual dispatch. Test: exactly one handoff record on assign-with-note, exactly one task (no re-trigger), and no record when suppressed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(issues): frontend plumbing for issue-trigger preview + handoff (core) Add api.previewIssueTrigger + IssueTriggerPreviewSchema (zod parseWithFallback), the use-issue-trigger-preview hook, issueKeys.issueTriggerPreview(+All) with WS queue-state invalidation, suppress_run/handoff_note on UpdateIssueRequest, the 'handoff' CommentType, and stripping of the control fields from optimistic update/batch cache patches (MUL-3375 §9). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): exclude handoff records from new-comment counting type='handoff' is a display-only timeline record, not conversation. Exclude it from CountNewCommentsSince so a handoff note never inflates the count of "new comments to catch up on" fed to a claiming agent (MUL-3375 §12). Analytics already excludes it (RecordHandoff is a direct write that emits no analytics event), and the comment-trigger path is already bypassed. Test: a handoff record does not bump the new-comment count; a real comment does. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(issues): pre-trigger preview UI, handoff note, timeline card (web/desktop) Wire the §9 frontend onto the preview endpoint + handoff fields: - Delete the backlog blocking dialog (backlog-agent-hint*) and its modal type; the over-eager nag is gone. Backlog awareness is now a passive label. - RunConfirmModal: single assign + batch assign/status route here. Shows the backend predicate's verdict ("将启动 @X" / "将启动 N 个" / parked), an optional handoff note (assign only, soft-gated by handoff_supported), and 暂不启动 — then applies via update/batch. No frontend guessing. - create modal: passive CreateRunHint ("将启动 @X" / backlog parked). - single status change stays a direct apply (unchanged). - timeline: render type='handoff' as a distinct, non-interactive handoff card. - i18n run_confirm + handoff_card across en/ja/ko/zh-Hans; drop backlog action keys; locale parity green. Tests: use-issue-actions (assign → run-confirm modal, member → direct), create-issue + comment-card suites updated/green; views typecheck + lint clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * test(issues): use a valid anchor in the handoff count-exclusion test CountNewCommentsSince filters id <> @anchor_id; SQL id <> NULL is NULL and excludes every row, so an empty anchor made the control assertion read 0. The production caller always passes a real anchor — mirror that with a non-matching sentinel uuid. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * test(issues): RunConfirmModal apply logic (start/suppress/note-gate/batch) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * test(core): preview schema malformed/missing/null fallback coverage Cover IssueTriggerPreviewSchema via parseWithFallback (MUL-3375): well-formed parse, top-level + item default fills (empty/older backend), and fallback to { triggers: [], total_count: 0 } for malformed shapes, a dropped required issue_id, a wrong-typed total_count, and null/non-object bodies — so the four entry points degrade to "nothing will start" instead of throwing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * refactor(issues): remove display-only handoff timeline record (留痕) The handoff "留痕" timeline record (type='handoff' comment written on run start) was judged superfluous and dropped per product call. This removes only the display-only trace; the handoff NOTE injection into the run's opening prompt + issue_context.md is untouched. - backend: drop RecordHandoff + its call in dispatchIssueRun - db: drop the `type <> 'handoff'` exclusion in CountNewCommentsSince and migration 123 (comment_type_check reverts to the 4-type set from 001); no production data exists for this unreleased feature - frontend: drop the "handoff" CommentType, HandoffCard, and handoff_card i18n (all locales) - tests: drop handoff_count_test.go and the record-write assertions in issue_trigger_preview_test.go (note-injection tests retained) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(issues): dismissable run-confirm modal + team-handoff copy Two fixes to the pre-trigger confirm modal (MUL-3375). 1. Dismissable: switch RunConfirmModal from AlertDialog to the standard shadcn Dialog so it has the close (X) button + Esc + click-outside. Previously the only choices were "start" / "don't start now" with no way to abort the action entirely; dismissing now cancels with no write. 2. Copy: rework the action-surface wording away from the backend term "run" toward team-handoff voice — 指派 / 开始 / 交接 (run stays only on record surfaces). Unifies the note's three names to "交接说明", and parallels the rewrite across en/ja/ko. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * chore(agent): bump handoff note min CLI version to 0.3.28 The daemon release that renders handoff notes ships in 0.3.28 (0.3.27 was the prior tag), so move the soft-gate threshold up. Below this the note is silently dropped and the frontend grays the note box — assignment is never blocked. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(issues): skip run-confirm when batch-moving issues to backlog A move into backlog never starts a run (service/issue_trigger.go), so the pre-trigger confirm modal degenerated to an empty "won't start" box with a single Apply button — pure friction. Apply directly instead, matching the single-issue status path. Other target statuses still route through the modal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(issues): refine pre-trigger preview hint and copy - Move the create-issue run hint to a reveal band (grid 0fr→1fr) above the property toolbar. It was sharing the footer button row and, lacking a width constraint, reflowed the submit buttons whenever it appeared. Restyle to a borderless, comment-style avatar+caption that is purely a caption (non-interactive avatar). - Distinguish squad from agent in the pre-trigger copy: a squad's leader evaluates and delegates rather than "starting work" itself. Add will_start_named_squad / will_start_squad / create_will_start_squad across en/zh/ja/ko (reusing the squad_leader_* evaluate→arrange vocabulary) and branch run-confirm + the create hint on squad assignees. - Bold the assignee name in the run-confirm headline via a language-safe sentinel split (no per-language prefix/suffix keys). - Align zh "开始处理" → "开始工作" on the single-assign copy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(issues): stub ActorAvatar in create-issue suite CreateRunHint now renders an ActorAvatar for agent/squad assignees, which pulls in getActorInitials/getActorAvatarUrl + the workspace/presence/navigation hook tree. This form-focused suite only stubbed getActorName, so the squad-forwarding test crashed with "getActorInitials is not a function". Stub the avatar inert — its own behavior is covered elsewhere. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Walt <walt@multica.ai> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
48b8dbf439 |
feat(daemon): surface sub-issue stages in the always-on runtime brief (#4426)
Agents creating sub-issues only saw the runtime brief's Sub-issue Creation section, which taught the manual todo/backlog serial chain and never mentioned stages — the `--stage` flow was documented only in the multica-working-on-issues skill, which an agent reads only if it opens it. So agents defaulted to hand-managed backlog chains and rarely reached for stages. - Add an "Ordering with stages" paragraph to the brief's Sub-issue Creation section nudging agents to group ordered/waiting sub-issues with --stage instead of hand-promoting a backlog chain. - List --stage on the brief's issue create / update command lines and add multica issue children to the Core command list for discoverability. - Extend the brief test with the new stage assertions. The Sub-issue Creation section stays gated to issue-bound runs (skipped for chat/quick-create/autopilot), unconditional on parent_issue_id, and free of parent-notification guidance — all existing canaries still pass. MUL-3508 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
da72e2fa22 |
feat(daemon): inject project description into the agent brief (MUL-3465) (#4395)
* feat(daemon): inject project description into the agent brief Issues bound to a project only surfaced the project title in the runtime brief; the project description (durable, project-wide context the owner sets) was loaded but dropped. Carry it end-to-end: - claim handler reads proj.Description onto the response (issue-bound and quick-create paths) - new ProjectDescription field on AgentTaskResponse, daemon Task, and TaskContextForEnv - rendered in the brief's `## Project Context` section and written to .multica/project/resources.json as project_description Empty descriptions render nothing (no extra heading). Updated the projects-and-resources built-in skill docs in the same change. MUL-3465 Co-authored-by: multica-agent <github@multica.ai> * feat(projects): clarify project description is injected as agent context The project description is now durable context injected into every task's brief, but the UI still presented it as a plain "Description" field, so existing descriptions could silently become agent input. Add a hint under the description editor on the project detail page and in the create-project modal, in all four locales, stating it is shared with agents as context for every task in the project. No data-semantics change. Addresses review feedback on PR #4395. MUL-3465 Co-authored-by: multica-agent <github@multica.ai> * test(handler): assert project description flows through task claim The execenv tests cover brief rendering, but nothing pinned the claim handler boundary where proj.Description is read onto the response. Add two tests — issue-bound and quick-create paths — so a regression in that assignment fails loudly instead of silently dropping the description. Addresses review feedback on PR #4395. MUL-3465 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
78342a39ce |
MUL-3305: feat(agent): add qoder CLI as a choice of agent provider. (#2461)
* feat(agent): Qoder ACP runtime, chat reconnect recovery, and task linkage - Add Qoder CLI backend (ACP transport, model discovery, blocked-args policy) - Wire daemon/runtime config, docs, and UI provider assets - Retry terminal task reports; add backoff unit tests - Chat: SQL attach user message to task; handler + optimistic cache reconcile - Invalidate chat/task-messages caches on WS reconnect; extract helper + tests Co-authored-by: Orca <help@stably.ai> Co-authored-by: Cursor <cursoragent@cursor.com> * chore: drop non-Qoder changes (chat reconnect, task link, terminal report retries) Keep only Qoder runtime, docs, daemon config/execenv, and UI provider assets. Co-authored-by: Orca <help@stably.ai> Co-authored-by: Cursor <cursoragent@cursor.com> * fix(agent): harden Qoder ACP drain and wire project skills path - Stop streaming to msgCh after reader wait so grace timeout cannot race close - Resolve injected skills to .qoder/skills per Qoder CLI discovery - Update AGENTS.md skill copy and add execenv tests Co-authored-by: Orca <help@stably.ai> Co-authored-by: Cursor <cursoragent@cursor.com> * feat(qoder): add provider logo and wire MCP config into ACP sessions - Add inline SVG QoderLogo component to provider-logo.tsx, replacing the generic Monitor icon placeholder - Add convertMcpConfigForACP helper to convert Claude-style MCP server config (object map) into ACP array format for session/new and session/resume - Add unit tests for convertMcpConfigForACP covering stdio, SSE, empty/nil, and multi-server cases Co-authored-by: Orca <help@stably.ai> * fix(test): capture both return values from InjectRuntimeConfig in Qoder test Co-authored-by: Orca <help@stably.ai> * fix(qoder): preserve remote MCP headers and promote provider errors Addresses review feedback on #2461 (Bohan-J): two runtime-correctness issues in the Qoder ACP backend. 1. Remote MCP headers were dropped. The bespoke convertMcpConfigForACP only forwarded url/type, so an authenticated remote MCP server looked configured in Multica but failed inside the Qoder session. Replace it with the shared buildACPMcpServers helper (same path Hermes/Kimi/Kiro use), which preserves headers as [{name, value}], sorts for deterministic output, and handles remote transport aliases. Fail closed on malformed mcp_config instead of silently dropping servers. 2. Provider failures could report as completed tasks. stderr was wired via io.MultiWriter and the result was only promoted to failed when output was empty, so a terminal upstream error (HTTP 429 / expired token) racing a stopReason=end_turn with text still became "completed". Switch to StderrPipe + an explicit copier, drain it (bounded by the existing grace window, since qodercli can leave a child holding the inherited fds) before the decision, and run the shared promoteACPResultOnProviderError. Tests: replace the convertMcpConfigForACP unit tests with two end-to-end Qoder tests — one asserts the Authorization header reaches the session/new payload as {name, value}, the other asserts a terminal stderr error with non-empty output reports failed. Co-authored-by: Orca <help@stably.ai> * fix(qoder): align ACP session handling Co-authored-by: Orca <help@stably.ai> * fix(agent): guard qoder late output after drain Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4fe8b54e9b |
MUL-3446: keep chat output in chat (#4387)
* MUL-3446: keep chat output in chat Co-authored-by: multica-agent <github@multica.ai> * MUL-3446: simplify chat output guidance Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5fd3d01d13 |
MUL-3502: OST-1161: Bound assignment comment catch-up
Squashed PR #4392. Updates assignment/comment catch-up guidance to use recent 10 and aligns related examples. |
||
|
|
b4c9e4423c |
test: enable -race detector in Go test pipeline (WOR-61) (#4274)
* test: enable -race detector in Go test pipeline (WOR-61) Add the -race flag to all three Go test invocation sites so the existing concurrency regression harness (workdir_race_test.go for #3999, runtime_gone_test.go, runtime_profile_drift_test.go) actually exercises the race detector. The daemon package alone has 28+ goroutine launch points with no automated race coverage before this change. Sites updated: - Makefile:299 (make test, local) - .github/workflows/ci.yml:101 (CI backend job) - .github/workflows/release.yml:55 (release verify job) go test already runs a vet subset by default, so no separate -vet flag is added. No production code touched. Co-authored-by: multica-agent <github@multica.ai> * test(execenv): serialize runtimeGOOS-mutating test (WOR-61) TestInjectRuntimeConfigIssueMetadataCodexFormattingUnchanged called t.Parallel() while mutating the package-level runtimeGOOS to drive the windows/linux branches, racing with the other parallel tests that read runtimeGOOS in buildMetaSkillContent. The -race flag enabled in the prior commit surfaced it as 3 WARNING: DATA RACE reports and 11 "race detected" failures in CI (only the execenv package failed). Drop t.Parallel() and add the "// Not parallel: mutates the package-level runtimeGOOS." comment already used by the six sibling writer tests across execenv_test.go and reply_instructions_test.go. This is test-isolation only; no production code, no mutex/atomic, no signature change. Verified locally: go test -race -count=1 ./internal/daemon/execenv/ -> ok 2.276s go test -race -count=1 ./internal/daemon/... -> all 3 pkgs ok Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: hzz <331380069@qq.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
0d0edac32f |
feat(daemon): discover local skills from ~/.agents/skills (MUL-3333) (#4244)
* feat(daemon): discover local skills from ~/.agents/skills (MUL-3333)
Upgrade local skill discovery and import from a single provider root to an
ordered multi-root scan: the runtime's own skill directory (e.g.
~/.claude/skills) first, then the cross-tool universal root ~/.agents/skills.
- Rename localSkillRootForProvider -> localSkillRootsForProvider, returning
ordered roots [provider, universal] with a kind classifier.
- listRuntimeLocalSkills iterates the roots, gives each root its OWN visited
set (so a cross-root symlink alias is not collapsed), dedupes strictly by
Key with the provider root winning, and sorts once after the merge.
- loadRuntimeLocalSkillBundle walks the same priority order and only falls
through to the next root on os.IsNotExist; any other stat error is returned
so import never silently resolves a different same-key skill.
- Add a Root ("provider" | "universal") field to the local skill summary
(daemon + handler structs and the TS RuntimeLocalSkillSummary type) so the
UI can label a skill's origin without a future schema break.
Backward compatible: every skill visible today keeps its Key, SourcePath and
FileCount; the universal root only surfaces additional, non-conflicting skills.
Out of scope (follow-up issues): execution-time injection of ~/.agents/skills
into runtimes (e.g. Codex seedUserCodexSkills) and workspace-relative
.agents/skills discovery.
Tests cover universal-root discovery + import, provider-wins conflict
priority, both-roots merge, missing/both-missing roots, nested layouts,
IsNotExist fallback, the no-fallthrough-on-read-error guarantee, and the
per-root visited cross-root symlink alias. Docs updated in en/zh/ja/ko.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): fall through to next root when a same-key dir has no SKILL.md
loadRuntimeLocalSkillBundle previously only fell through to the next root on
os.IsNotExist for the skill DIRECTORY. A provider-root directory that shares a
skill's key but contains no SKILL.md (so listRuntimeLocalSkills descends past
it and surfaces the universal-root skill instead) made load stop on the
invalid provider dir and error — list and load disagreed, and the import the
user picked from the list could not be fetched.
Make the validity predicate match list: a root "has" the skill at a key only
when it is a directory containing a SKILL.md. A missing entry, a non-directory,
or a directory without a SKILL.md all mean "this root doesn't have it" and we
continue to the next root. Only a genuine non-IsNotExist stat error or an
unreadable existing SKILL.md (permission/IO) is returned, so we still never
silently substitute a different-content same-key skill from a lower-priority
root (Eve review #1, preserved by the existing read-error guard test).
Adds regression tests for the provider-dir-without-SKILL.md and provider-non-dir
fall-through cases.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
e3a829f05e |
feat(daemon): disk-usage cross-root aggregation + migration FK/readiness fixes (MUL-3404) (#4290)
Bundles the MUL-3404 disk-usage feature with the two Preflight BLOCK fixes. - feat(daemon): `disk-usage --all-profiles` aggregates across every workspace root (default + each ~/.multica/profiles/* root, incl. the Desktop app's), with a per-root breakdown and combined grand total; the cross-root hint now also fires when the current root is non-empty. - fix(db): drop DB-level foreign keys/cascades from the new autopilot_subscriber and comment.source_task_id migrations (resolved in the app layer — autopilot delete now removes subscribers in a transaction); the autopilot_subscriber down-migration relabels reason='autopilot' to 'manual' instead of deleting. - fix(server): readiness verifies every required migration is applied, not just the lexically-last one, so an out-of-order migration can't be masked. MUL-3404. |
||
|
|
e7daf876bd |
fix(daemon): reclaim autopilot_run workdir on terminal status (MUL-3403) (#4287)
* fix(daemon): reclaim autopilot_run workdir on terminal status (MUL-3403) Autopilot run workdirs are never reused — there is no PriorWorkDir path that hands a later run the same directory, so every run gets a fresh one. Yet GC waited the full GCTTL (default 24h) before reclaiming a terminal run's dir. Combined with one fresh dir per run, high-frequency autopilots piled up hundreds of stale dirs (508 dirs / 22GB in the field report). Drop the TTL gate so a terminal run (completed/failed/skipped/ issue_created) is reclaimed immediately, mirroring gcDecisionQuickCreate. Existing safety constraints are untouched: active-env-root short-circuit, 404 -> orphanByMTime, non-404 error -> skip, and the local_directory override all still apply. Co-authored-by: multica-agent <github@multica.ai> * docs(daemon): fix GetAutopilotRunGCCheck comment — completed_at is not a TTL anchor The endpoint comment still claimed the daemon uses completed_at as the TTL anchor for terminal runs. GC now decides purely on terminal status (the workdir is never reused, so a terminal run is reclaimed on sight); completed_at is returned for the API contract / diagnostics only. Addresses the review nit on #4287. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1279f22d1c |
MUL-3325: add background task safety brief (#4257)
* fix(daemon): add background task safety brief Co-authored-by: multica-agent <github@multica.ai> * fix(agent): force Claude background tools foreground Co-authored-by: multica-agent <github@multica.ai> * fix(agent): narrow Claude async launch detection Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
77ac17ef49 |
Make custom runtimes appear immediately (#4234)
* Make custom runtimes appear immediately * Scope daemon profile refresh by authorized runtimes * Relay runtime profile refresh hints * Localize runtime profile close label |
||
|
|
6bb8cac9ea |
MUL-3332: daemon picks up new custom runtime profiles without restart (#4225)
* MUL-3332: daemon picks up new custom runtime profiles without restart The workspaceSyncLoop's already-tracked branch refreshed only settings and repos via refreshWorkspaceRepos and never re-fetched runtime profiles, so a custom runtime profile created via the web UI / CLI did not become a registered runtime row until the daemon restarted (or a runtimeGone recovery happened to fire). Detect server-side profile drift each sync tick by hashing the workspace's profile list with profileSetSignature(), caching the digest on workspaceState.profileSetSig, and triggering reregisterWorkspaceAfterRuntimeGone when the live signature differs from the cached one. Steady-state syncs cost exactly one extra GetRuntimeProfiles round trip; only real drift fans out to a Register call. The fetch is best-effort: a 404 / network blip preserves the cached signature so a transient failure cannot loop the daemon into spurious re-registrations. Tests in runtime_profile_drift_test.go cover digest stability under reorder, field-by-field drift detection (add / enable-flip / command_name / protocol_family / fixed_args / visibility), the no-drift hot path (no re-register), the new-profile drift path (single re-register + index update + sig converges), and best-effort fetch error handling. Co-authored-by: multica-agent <github@multica.ai> * MUL-3332: split orphan recovery from profile drift; converge to zero Addresses two blocking review concerns on #4225 (raised by GPT-Boy): 1. Profile drift must not kill running tasks on existing runtimes. The first cut reused reregisterWorkspaceAfterRuntimeGone, which after re-register calls /recover-orphans for every returned runtime ID. The server's RecoverOrphanedTasksForRuntime hard-fails every dispatched/running/waiting_local_directory row on that runtime — the correct response when a runtime row was actually deleted server-side, but a catastrophic false positive on profile drift: a built-in runtime still actively executing the user's tasks would have its work killed just because the user added an unrelated sibling custom profile. Fix: extract applyRegisterResponseInPlace as the shared in-place state converger between the two paths, and stop calling /recover-orphans from the drift path. reregisterWorkspaceAfterRuntimeGone keeps the /recover-orphans call because in that path the rows really were gone. 2. Disabling the only profile on a custom-only daemon must converge. The first cut hit registerRuntimesForWorkspace's len(runtimes)==0 guard and bailed out, so the disabled profile's runtime stayed alive in local tracking and on the server (still polling, still heartbeating, still online for the full 150 s stale-heartbeat window). Fix: introduce ErrNoRuntimesToRegister as a sentinel, have registerRuntimesForWorkspace return profileSig even on the empty case (so the drift path can cache the converged-empty signature), and have the drift refresh's error handler take a convergeWorkspaceRuntimesToZero branch that clears local runtimeIDs / runtimeIndex entries and Deregisters the orphaned IDs so the server marks them offline immediately. The same Deregister step also runs on partial drift (a built-in survives, the disabled profile's runtime drops) so the user sees the dropped runtime go offline within the next sync tick instead of after the 150 s sweep. Tests: - TestRefreshWorkspaceRuntimeProfiles_DriftWithRunningRuntimeSkipsOrphanRecovery (mixed built-in + custom, add another profile, asserts zero /recover-orphans calls). - TestRefreshWorkspaceRuntimeProfiles_DisableConvergesCustomOnlyDaemon (custom-only daemon, disable only profile, asserts local state cleared, signature converges to empty digest, Deregister called with the orphaned ID, no recover-orphans, follow-up tick is no-op). - TestRefreshWorkspaceRuntimeProfiles_DisableOneOfManyDeregistersDroppedID (partial drift: only the dropped ID is Deregistered, surviving built-in is left alone and not orphan-recovered). - TestRefreshWorkspaceRuntimeProfiles_NewProfileTriggersReregister extended to also assert no /recover-orphans calls. - TestRegisterRuntimes_SkipsProfileNotOnPath strengthened to assert the ErrNoRuntimesToRegister sentinel and that profileSig is still returned on the empty path. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1f5cb51d4e |
MUL-3284: Web UI + CLI (custom runtime PR3) (#4177)
* MUL-3284 PR3 (CLI): multica runtime profile subcommands + local path override
- cmd_runtime_profile.go: `multica runtime profile` group — list / create /
update / delete against /api/workspaces/{id}/runtime-profiles, plus set-path
/ unset-path for a per-machine command override. protocol-family validated
client-side via agent.IsSupportedType / agent.SupportedTypes; visibility
validated; update only sends changed flags (protocol_family immutable);
delete surfaces the server 409 body when agents are still bound.
- internal/cli/config.go: ProfileCommandOverrides map[string]string on
CLIConfig (omitempty), through the existing marshal/unmarshal so set/unset
round-trips without dropping other fields.
- internal/daemon: Config.ProfileCommandOverrides, loaded from CLIConfig;
appendProfileRuntimes now prefers an override path when set AND executable,
else falls back to exec.LookPath(command_name), else skips+logs as before.
- Tests: cmd_runtime_profile_test.go (registration, create/update/delete incl.
bad-family + missing-flag + 409 surfacing, set/unset path round-trip,
relative-path rejection, config preservation); cli/config round-trip;
daemon prefers-override / falls-back-when-not-executable.
Verified: go build ./..., go vet, go test ./cmd/multica/... ./internal/daemon/...
./internal/cli/... all pass.
Co-authored-by: multica-agent <github@multica.ai>
* MUL-3284 PR3 (Web): custom runtime profiles in the Runtime page
Single-list integration — no new page, no tabs/grouping. Built-in protocol
families and custom profiles render mixed in one catalog, each row badged
built-in vs custom (progressive disclosure).
- packages/core: RUNTIME_PROFILE_PROTOCOL_FAMILIES (single-source 13-family
whitelist, matches server agent.SupportedTypes + migration 120 CHECK) and
RuntimeProtocolFamily / RuntimeProfile types; api client
list/get/create/update/deleteRuntimeProfile against
/api/workspaces/{id}/runtime-profiles; runtimes/profiles.ts query +
mutation hooks and a 409 "agents still bound" conflict parser.
- packages/views/runtimes: runtime-profile-catalog (mixed built-in+custom
rows), runtime-profiles-dialog (header "+ Add runtime" → step 1 pick
protocol family → step 2 display_name/command_name/description; edit form
for custom; admin-gated), delete-runtime-profile-dialog (confirm + graceful
409), runtimes-page / runtime-list integration.
- i18n: new strings added to all four locales (en, zh-Hans, ja, ko).
- a11y: dialogs are focus-trapped, Esc-closable, labelled; full
create/edit/delete flow is keyboard + screen-reader operable.
Iron rule honored: no generic per-agent args UI here (those stay on Agent
config). fixed_args is not surfaced as a general args field.
Verified: turbo typecheck + lint + test pass for @multica/core, @multica/views,
@multica/web; the @multica/web production build succeeds.
Co-authored-by: multica-agent <github@multica.ai>
* MUL-3284 PR3: hide fixed_args from Web + CLI (not yet wired to launch)
Review fix. fixed_args was surfaced as a working feature, but the daemon does
not splice it into the agent launch command — exposing it promised admins a
no-op. Per the call, remove it from every user-facing surface while keeping the
underlying column/struct "carried but not exposed".
- Web (runtime-profiles-dialog.tsx + runtime-profile-catalog.ts): drop the
detail row, the create body field, the update patch field, and the form
textarea; remove the parseFixedArgs/fixedArgsToText helpers and the
fixedArgs form value. Left a NOTE pointing at the daemon TODO.
- i18n: removed the fixed_args strings from all four locales (en/zh-Hans/ja/ko).
- CLI (cmd_runtime_profile.go): removed the `--fixed-arg` flag from create and
update and stopped sending `fixed_args`; updated the "no fields" message.
Test now asserts the CLI never sends fixed_args.
Untouched (the carried-but-not-exposed layer): the runtime_profile.fixed_args
column, the server handler's accept/return, and the daemon's RuntimeProfile
field — all keep the existing TODO(MUL-3284) to wire it into the launch path
(with a test proving args reach the backend) before any UI/CLI re-exposes it.
Verified: turbo typecheck+lint+test pass for @multica/core and @multica/views;
go build/vet/test pass for ./cmd/multica/.
Co-authored-by: multica-agent <github@multica.ai>
* MUL-3284 PR3: stop exposing profile visibility=private (server forces workspace)
Double-review (Eve) caught a fixed_args-shaped hole: visibility=private was a
user-facing toggle (Web form + detail + CLI), but the three server read paths
(ListRuntimeProfiles, daemon ListEnabledRuntimeProfilesForWorkspace,
DaemonRegister) never enforce it — so a "private" profile's name/command would
leak to other members and could be registered by other machines' daemons
(lateral data leak). Same "don't paint a pie" fix as fixed_args: hide the
control everywhere and force the stored value.
- Server (runtime_profile.go): drop `visibility` from the create + update
request structs; CreateRuntimeProfile always stores 'workspace'
(runtimeProfileDefaultVisibility); UpdateRuntimeProfile no longer accepts it;
removed validRuntimeProfileVisibility. The column + response field stay
(always 'workspace') as the carried-but-not-exposed layer.
- Web (runtime-profiles-dialog.tsx): removed the visibility form fieldset,
the VisibilityOption component, the detail row, the visibility state, and the
create/update submit fields.
- i18n: removed the profile visibility strings from all four locales
(profiles.detail.visibility, profiles.visibility.*, profiles.form.visibility_*).
Top-level runtime/agent visibility strings are untouched.
- CLI (cmd_runtime_profile.go): removed `--visibility` from create/update and
the VISIBILITY list column; removed validateVisibility; stopped sending the
field.
- Tests: new TestCreateRuntimeProfile_ForcesWorkspaceVisibility (POST
visibility:"private" -> response and DB row are 'workspace'); CLI create test
now asserts visibility is never sent.
Follow-up MUL-3308 tracks implementing real creator-visibility (and wiring
fixed_args to the launch path); TODOs left in server/Web/CLI point to it.
Verified: turbo typecheck+lint+test pass (@multica/core, @multica/views);
go build/vet pass; go test ./cmd/multica/... and the full ./internal/handler/
suite pass against a migrated Postgres 17.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
52e76e7b23 |
MUL-3284: server API + daemon (custom runtime PR2) (#4149)
* MUL-3284: add runtime_profile schema (custom runtime PR1) Schema-only foundation for custom runtimes. Additive migration 120: - New workspace-level `runtime_profile` table: the shared, team-visible definition of a custom runtime (e.g. an in-house Codex wrapper). protocol_family is CHECK-constrained to the exact backend list in agent.New() (server/pkg/agent/agent.go). The only args column is `fixed_args` (args every agent on the runtime must inherit); there is deliberately no generic per-agent args field — those stay on agent.custom_args. - `agent_runtime.profile_id` (nullable, FK -> runtime_profile ON DELETE CASCADE): NULL = built-in runtime, non-NULL = a registered instance of a custom profile. - Partial unique index agent_runtime_workspace_daemon_profile_key on (workspace_id, daemon_id, profile_id) WHERE profile_id IS NOT NULL. The legacy UNIQUE (workspace_id, daemon_id, provider) constraint is left INTACT so the existing registration upsert (ON CONFLICT (workspace_id, daemon_id, provider) in runtime.sql) keeps resolving its arbiter and the server stays green. Converting that key to a partial (WHERE profile_id IS NULL) index and making the upsert profile-aware is PR2's registration work, not this migration. Verified up + down against Postgres 17: full `migrate up` applies 120; schema shows the table, column, partial index and intact legacy constraint; functional checks pass (partial index blocks dup (ws,daemon,profile), allows same profile on another daemon; CHECK and display_name uniqueness reject bad input; legacy ON CONFLICT still resolves; profile delete cascades to instances); down/up round-trip is clean. Co-authored-by: multica-agent <github@multica.ai> * MUL-3284: drop DB FKs/cascade from runtime_profile migration (review fix) Per review (house rule: no new database foreign keys / cascades; relational integrity lives in the application layer): - runtime_profile.workspace_id: drop REFERENCES workspace ON DELETE CASCADE -> plain UUID NOT NULL. - runtime_profile.created_by: drop REFERENCES "user" ON DELETE SET NULL -> plain UUID. - agent_runtime.profile_id: drop REFERENCES runtime_profile ON DELETE CASCADE -> plain UUID. CHECK constraints, UNIQUE (workspace_id, display_name), the workspace index, and the partial unique index agent_runtime_workspace_daemon_profile_key are unchanged. The legacy UNIQUE (workspace_id, daemon_id, provider) constraint remains untouched. Behavioral consequence: the database no longer auto-removes a profile's agent_runtime instance rows on profile delete. That cleanup moves into PR2's profile-delete path. Up-migration comments document this; down-migration comment no longer references FKs/cascade. Re-verified on Postgres 17: migrate up applies 120; no FK constraints exist on the new columns; partial index still blocks dup (ws,daemon,profile_id); CHECK and display_name uniqueness still reject bad input; deleting a profile now leaves the runtime row orphaned (proving cascade is gone); down/up round-trip clean with the legacy constraint intact. Co-authored-by: multica-agent <github@multica.ai> * MUL-3284 PR2 (server): runtime_profile CRUD + profile-aware registration Server/DB half of the custom-runtime feature. - Migration 121: convert the legacy UNIQUE (workspace_id, daemon_id, provider) constraint on agent_runtime into a partial unique index scoped to built-in rows (WHERE profile_id IS NULL). With 120's partial index on profile_id this lets one daemon host the built-in provider AND custom profiles of the same protocol family without collision. - Queries: runtime_profile CRUD; ListEnabledRuntimeProfilesForWorkspace (daemon-facing); CountAgentsByProfile + DeleteAgentRuntimesByProfile for the app-layer cascade; profile-aware UpsertAgentRuntimeWithProfile; the built-in UpsertAgentRuntime ON CONFLICT now spells out WHERE profile_id IS NULL so it targets the right partial index. sqlc regenerated. - agent.SupportedTypes / IsSupportedType: single-source protocol_family whitelist, in lockstep with agent.New and the migration 120 CHECK. - Handlers + routes: runtime_profile CRUD (member-read, admin-write) with protocol_family whitelist validation, display_name uniqueness (409), and fixed_args validation (no generic per-agent args — iron rule); a daemon-token endpoint GET /api/daemon/workspaces/{id}/runtime-profiles; DeleteRuntimeProfile does the app-layer cascade (delete instance rows then profile, in one tx) and refuses (409) while active agents are bound. - DaemonRegister accepts an optional per-runtime profile_id: validates the profile belongs to the workspace and is enabled, registers via the profile-aware upsert, and skips legacy hostname merge for custom rows. AgentRuntimeResponse now carries profile_id. Verified on Postgres 17: migrate up through 121; built-in + custom codex coexist on one daemon; both upsert arbiters are idempotent; delete-by-profile cascade removes only the custom instance; migrate down reverses 121 then 120 and replays clean. go build ./... and go vet pass; handler test package compiles. Daemon-side wiring (fetch profiles, PATH-resolve command_name, register with profile_id, exec uses command_name) lands in a follow-up commit on this branch. Co-authored-by: multica-agent <github@multica.ai> * MUL-3284 PR2 (daemon): pull profiles, PATH-resolve, register, exec command Daemon-side half of custom runtime profiles, against the server contract on this branch. - client.go: GetRuntimeProfiles(workspaceID) -> GET /api/daemon/workspaces/{id}/runtime-profiles (mirrors GetWorkspaceRepos); RuntimeProfile / RuntimeProfilesResponse types. - types.go: Runtime gains profile_id (parsed from the register response so runtimeIndex carries it). - daemon.go: * appendProfileRuntimes — called inside registerRuntimesForWorkspace before the empty-runtimes guard. Best-effort fetch (older server 404s are logged and swallowed; never fails registration). Per enabled profile: resolve command_name via PATH (exec.LookPath, behind a `lookPath` test hook), skip+log when absent, best-effort version probe, record the resolved absolute path keyed by profile_id, and append a registration entry {name, type=protocol_family, version, status:online, profile_id}. A custom-only host (no built-in agents) still registers. * profileCommandPaths map (guarded by d.mu) + recordProfileCommandPath / customCommandPathForRuntime helpers. * runTask: looks up the claimed task's RuntimeID -> profile command path and overrides the executable path, synthesizing an AgentEntry so a custom runtime runs even when the host has no built-in agent of the same provider. provider (=protocol_family) is unchanged so agent.New still selects the right backend. - Tests: GetRuntimeProfiles request shape; profile runtime appended + path recorded (custom-only host); profile skipped when command not on PATH; profiles-fetch-404 is best-effort; customCommandPathForRuntime bookkeeping. - agent: lockstep test pinning SupportedTypes to agent.New and the migration 120 protocol_family CHECK. Iron rule honored: profile carries no generic per-agent args. fixed_args are parsed and carried but intentionally NOT wired into the launch command yet (optional/best-effort; explicit TODO(MUL-3284) in appendProfileRuntimes). Verified: go build ./... clean; go vet ./internal/daemon/... clean; go test ./internal/daemon/... pass (existing + 5 new); full go test ./internal/handler/ suite passes against a migrated Postgres 17; agent lockstep test passes. Co-authored-by: multica-agent <github@multica.ai> * MUL-3284 PR2: profile delete runs full archived-agent cascade (fix 500) Review fix. DeleteRuntimeProfile previously guarded only on ACTIVE agents, but agent.runtime_id is ON DELETE RESTRICT — a profile whose runtimes had only ARCHIVED agents passed the guard, then DeleteAgentRuntimesByProfile hit the FK and the handler 500'd. Now it mirrors the mature runtime-delete cascade (DeleteAgentRuntime): in one transaction it enumerates the profile's runtime rows, refuses (409) any with active agents or active squads led by archived agents, then for each runtime pauses autopilots pinned to its archived agents, drops archived squads led by them, and hard-deletes the archived agents before removing the runtime rows and the profile. No code path can now fall through to a raw FK error. - queries: ListAgentRuntimeIDsByProfile (sqlc regen). Reuses the existing per-runtime teardown queries (CountActiveSquadsWithArchivedLeadersByRuntime, ListArchivedAgentIDsByRuntime, PauseAutopilotsByAgentAssignees, DeleteSquadsByArchivedAgentsOnRuntime, DeleteArchivedAgentsByRuntime). - tests: TestDeleteRuntimeProfile_ArchivedAgentCascade (archived-only profile deletes cleanly: 204, runtime + archived agent + profile gone) and TestDeleteRuntimeProfile_ActiveAgentBlocks (active agent → 409, survives). Verified against Postgres 17: both new tests pass; full handler suite, daemon tests, and agent lockstep test pass; go vet clean. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
18a58e80c0 |
MUL-3316: fix(execenv): switch agent prompt to --content-file to prevent heredoc flag swallowing (#4182) (#4191)
* fix(execenv): switch agent prompt to --content-file to prevent heredoc flag swallowing (#4182) The Linux/macOS reply template recommended --content-stdin with a quoted HEREDOC. That pattern is safe for the trivial single-flag comment-add case that BuildCommentReplyInstructions emits, but as soon as a model wraps extra flags around the heredoc on multica issue create / update — assignee, project — the bash heredoc/flag boundary is fragile in two ways the model cannot see: - A 'BODY \\' terminator with a trailing token is not recognised as the heredoc end, so flag lines after it are swallowed into the description (OXY-78: residual flag text leaked into the description, command exit 0). - A clean terminator turns the trailing '--assignee ...' line into a separate failing shell statement, while the create itself already exited 0 with no assignee (OXY-76: assignee silently dropped, no residual text). In both cases the CLI never receives the swallowed flags, the API request omits the fields, and the daemon has no visibility. The created issue lands with assignee_id: null / project_id: null. This commit: * Switches the Linux/macOS branch of BuildCommentReplyInstructions to --content-file with a 3-step recipe (write file, post, rm) so the body never reaches the shell and all flags live on one shell-token line. There is no heredoc boundary for flags to leak across. * Adds a parallel cleanup step (Remove-Item) to the Windows branch so the cross-platform template is one shape. * Rewrites the runtime_config.go ## Comment Formatting non-Windows section to mandate --content-file and explicitly ban --content-stdin HEREDOC for agent-authored comments, citing #4182. * Reorders the Available Commands menu lines for issue create / update / comment add to put --content-file / --description-file ahead of the stdin variant and add a per-line note pointing at #4182. * Updates and renames the affected tests (TestBuildCommentReplyInstructionsCodexLinux, TestBuildCommentReplyInstructionsNonCodexLinux, TestInjectRuntimeConfigLinuxCommentFormattingEmphasizesFile, TestInjectRuntimeConfigIssueMetadataCodexFormattingUnchanged) so the new file-first contract is pinned and the old HEREDOC mandate is in the banned-strings lists. This converges Linux/macOS with the long-standing Windows file-only path, so the cross-platform guidance is now one shape. It also strictly improves on the previous MUL-2904 guardrail by eliminating shell exposure of the body entirely (no body ever reaches the shell, so backtick / $() / $VAR substitution cannot corrupt it). Closes GitHub multica-ai/multica#4182. No CLI or backend changes — --content-file / --description-file already exist. Co-authored-by: multica-agent <github@multica.ai> * docs(prompt): correct stale BuildPrompt comment to file-first (#4182) --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: CC-Girl <cc-girl@multica.ai> |
||
|
|
f9c193e06b |
fix: fail closed on agent task auth tokens (#4142)
Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
34d4cd3a28 |
feat(openclaw): support connecting to existing OpenClaw gateway (#3260) [MUL-3158] (#3664)
* feat(openclaw): support connecting to existing OpenClaw gateway (#3260) When the daemon host is a lightweight dev machine or CI coordinator, the heavy agent work (LLM inference, code execution, tool use) often belongs on a more powerful remote server already running an OpenClaw gateway. Multica historically hard-coded `openclaw agent --local`, forcing every turn to execute in-process on the daemon host. This change adds an opt-in gateway routing mode controlled per-agent via `runtime_config`: { "mode": "gateway", "gateway": { "host": "...", "port": 18789, "token": "...", "tls": false } } - Backend: ExecOptions gains OpenclawMode + OpenclawGateway; buildOpenclawArgs drops `--local` when mode == "gateway". Per-task openclaw-config.json wrapper pins gateway.{host,port,auth.{mode,token},tls} so users do not need to edit the daemon host's `~/.openclaw/openclaw.json` to point at a different endpoint. - Daemon: AgentData carries the raw runtime_config; decoding is fail-soft (malformed JSON falls back to local mode rather than blocking dispatch). - API: gateway.token is masked to "***" on every GET; PATCH replays the sentinel back, and the update handler restores the persisted token so the round-trip never destroys the secret. Defense-in-depth masking on WS broadcasts, plus String/MarshalJSON masking on the in-memory struct to block stray `%+v` / json.Marshal leaks. - UI: openclaw-only "Routing" tab on the agent detail page with mode selector + structured endpoint form. Token uses a "saved — submit a new value to rotate" UX and matching backend preserve hook. Empty `runtime_config` keeps the historical embedded behaviour, so existing agents are unaffected. * fix(openclaw): address #3664 review — drop dead gateway field, gate pin on mode Per Bohan-J's review: - Remove the dead ExecOptions.OpenclawGateway field (+ its String/MarshalJSON and the daemon.go construction block). It carried the plaintext bearer token but was never read — buildOpenclawArgs only consumes OpenclawMode and the live gateway path runs through execenv.OpenclawGatewayPin — so this narrows the secret's footprint. - Gate the gateway pin on mode=="gateway" in decodeOpenclawRuntimeConfig: a {"mode":"local","gateway":{...,"token"}} payload no longer writes the token into the 0o600 per-task wrapper that --local makes openclaw ignore. - Warn on an unrecognized non-empty mode (e.g. "gatway") instead of silently falling back to local. - Run preserveMaskedGatewayToken in CreateAgent too, so a literal "***" at create time can't persist as a real bearer token. - Document the gateway host:port trust boundary (SSRF note for shared daemon hosts). Adds regression tests for the local-mode pin drop and the unknown-mode warning. |
||
|
|
f415099c4a |
MUL-3263: support managed MCP config for Cursor (#4081)
* feat: support managed MCP config for Cursor Co-authored-by: multica-agent <github@multica.ai> * fix: address Cursor MCP review feedback Co-authored-by: multica-agent <github@multica.ai> * docs: include Cursor in skills MCP support Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c8ab73d38d |
MUL-3244: Bind quick-create attachments to created issues (#4062)
* fix: bind quick-create attachments to created issues Co-authored-by: multica-agent <github@multica.ai> * test: use real image markdown in quick-create attachment test Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |