mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-14 13:49:18 +02:00
v0.3.40
1245 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) | ||
|
|
33bd8aeaa9 |
MUL-4134: fix(lark): preserve same-agent bindings when reconnecting a revoked Feishu bot (#4997)
* fix(lark): allow rebinding a revoked Feishu bot to a different agent When a Feishu/Lark Bot is disconnected from agent A (status → revoked), the row is preserved for audit but still holds the (channel_type, config->>app_id) unique index slot. Binding the same Bot to agent B would fail with: duplicate key value violates unique constraint "idx_channel_installation_type_appid" (SQLSTATE 23505) because UpsertChannelInstallation conflicts on (workspace_id, agent_id, channel_type) — a different agent_id means no conflict match, so it tries INSERT and hits the app_id unique index. Fix: before the upsert, inside the same transaction, hard-delete any revoked installation with the same app_id in the same workspace. The delete is fenced to status=revoked so an active installation can never be silently removed. If no revoked row exists the delete is a no-op (deletes zero rows, returns nil error) and the upsert proceeds normally. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(lark): preserve same-agent bindings when reconnecting a revoked Feishu bot The cleanup added in the previous commit hard-deletes every revoked channel_installation sharing the app_id in the workspace before the upsert — including the row belonging to the agent currently being (re)installed. That regresses the common "disconnect then reconnect the same bot to the same agent" flow: disconnect only flips status to 'revoked' (bindings are preserved), and UpsertChannelInstallation conflicts on (workspace_id, agent_id, channel_type), so before this the same agent's row was reactivated in place — installation_id and every channel_user_binding / channel_chat_session_binding kept. Deleting it first forces an INSERT with a fresh installation_id, orphaning every member's account link (they must re-link) and all chat-session continuity; only the installer is re-bound. Fence the delete with `agent_id <> $agent_id` so it only clears a DIFFERENT agent's revoked row (the genuine app_id-slot blocker). The same agent's revoked row is left for the upsert to reactivate losslessly. Since idx_channel_installation_type_appid is globally unique on (channel_type, app_id), at most one row ever holds a given app_id, so the excluded row is exactly the one the upsert will reuse. Adds DB-backed regression tests: same-agent revoked row preserved, different-agent revoked row deleted, active row never deleted, other workspace fenced, plus end-to-end reactivation semantics (same agent keeps installation_id + bindings; different agent gets a fresh id). Co-authored-by: multica-agent <github@multica.ai> * fix(lark): clean dependent rows when hard-deleting a rebound Feishu installation Addresses review on #4997 (MUL-4134). channel_* has no FK/cascade (MUL-3515 §4), so hard-deleting a different-agent revoked installation left application-owned rows dangling at a removed installation_id: - channel_chat_session_binding: the outbound patcher would resolve a binding, then fail loading the deleted installation — turning a clean no-op into error logs. - channel_binding_token: a still-unexpired bind link (15 min TTL) could be redeemed into the deleted installation, reporting "bound" against a bot that no longer reaches the user. - channel_inbound_audit: dangling installation_id, where migration 124 models the old ON DELETE SET NULL as an app-layer NULL. - channel_user_binding: dead member links (a different agent is a distinct connection; links do not follow and can never be reused). Rework RemoveRevokedInstallationByAppID to resolve the single row holding the app_id and act only when it is revoked, in this workspace, and owned by another agent; then, on the caller's transaction, clear chat-session bindings, pending binding tokens and member links, NULL the audit references, and finally delete the row via the fenced query (defense in depth). Same-agent reconnect and active/other-workspace rows are no-ops. Adds DeleteChannelUserBindingsByInstallation, DeleteChannelBindingTokensByInstallation, and NullChannelInboundAuditInstallationID queries, plus a DB-backed test (TestChannelStore_RebindCleansDependentRows) asserting every dependent is cleaned and the audit row survives detached. Verified the test fails when the cleanup is skipped. Co-authored-by: multica-agent <github@multica.ai> * fix(lark): make the rebind cleanup race-safe with a guarded delete gate Addresses the concurrency must-fix on #4997 (MUL-4134). The prior shape read the candidate installation, checked revoked/workspace/agent in Go, cleaned the dependent rows, then ran the fenced delete. That read-then- clean-then-delete order has a TOCTOU: while B is rebinding the bot to a different agent, A can reconnect to the SAME agent and reactivate the row to 'active' in between. B still wipes A's user/chat/token bindings and NULLs its audit based on the stale "it was revoked" read, then the fenced delete no-ops (status is no longer revoked) — so A's installation survives active but its bindings are gone. Concurrent same-agent data loss, reintroduced. Make the guarded DELETE the atomic gate. DeleteChannelInstallationByAppID becomes DeleteRevokedChannelInstallationByAppID `:one ... RETURNING id`, and RemoveRevokedInstallationByAppID keys all dependent cleanup off the id the delete actually claimed. No separate read. Under READ COMMITTED a concurrent reactivation makes the DELETE re-check status='revoked' against the live row (EvalPlanQual): it claims nothing, returns pgx.ErrNoRows, and no dependents are touched. With no FK the cleanup can follow the claiming delete in the same transaction; any failure rolls the whole thing back. Adds TestChannelStore_RebindGuardedDeleteRaceWithReactivation: two real transactions race on one revoked installation — one reactivates and holds the row lock, the other runs the rebind cleanup and blocks on the guarded delete — asserting the installation and every binding stay intact. Verified this test fails on the old read-then-clean-then-delete shape and passes (also under -race) on the gated version. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: jiangliangyou <jiangliangyou@xiaomi.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: J <j@multica.ai> |
||
|
|
b6adf23f91 |
feat(api): emit Content-Length header on JSON responses (#5021)
The core writeJSON helpers streamed the body via json.NewEncoder(w).Encode after WriteHeader, which forces net/http into chunked transfer encoding and omits Content-Length. Buffer the marshaled body first, set an accurate Content-Length, then write — so API (and health) JSON responses advertise their exact size. writeMeasuredJSON gets the same header. Adds a test asserting the header matches the on-wire body length. Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
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> |
||
|
|
566d51f1c0 |
perf(chat): fix pending-task index mismatch + cut aggregate request storm (MUL-4159) (#5018)
* perf(chat): fix pending-task index mismatch + cut aggregate request storm (MUL-4159) ListPendingChatTasksByCreator was a top DB hotspot. Root cause: the partial index idx_agent_task_queue_chat_pending (migration 040) only covers status IN (queued, dispatched, running), but migration 109 added a fourth in-flight status (waiting_local_directory) that both pending chat queries now filter on. Postgres can only use a partial index when the query predicate is a subset of the index predicate, so the 4-status query stopped using it and degraded to a Seq Scan over the whole agent_task_queue. Implements the reviewed P0-P3 plan in one PR: P0 index fix (split single-statement CONCURRENTLY migrations per repo convention) - 143: CREATE INDEX CONCURRENTLY idx_agent_task_queue_chat_pending_v2 covering all four in-flight statuses (same column list, so GetPendingChatTask still benefits). - 144: DROP the superseded 3-status index, in its own migration. P1 SQL + handler hot path - ListPendingChatTasksByCreator now returns cs.agent_id and states chat_session_id IS NOT NULL so the planner can prove the partial-index subset. - ListPendingChatTasks filters private-agent access against the already-loaded accessible-agent set using the returned agent_id, dropping the extra ListAllChatSessionsByCreator scan on the hot path. - Regenerated sqlc. P2 frontend request amplification - FAB uses the new boolean has-any query gated on enabled:!isOpen, so the minimised button never holds the full aggregate. - use-realtime-sync maintains the pending aggregate (list + has-any) in place from task lifecycle events (queued/dispatch/running/waiting_local_directory -> upsert; completed/failed/cancelled -> remove) instead of invalidating on every chat:message/chat:done, with a debounced fallback invalidate for reconnect / unknown payloads. P3 boolean endpoint - GET /api/chat/pending-tasks/has-any backed by HasPendingChatTasksByCreator (EXISTS). Permission filtering is baked in via agent_id = ANY($3); an empty accessible-agent set short-circuits to false. The detailed list stays for the ChatWindow history / stop-task flows. Tests: new handler tests cover the private-agent gate on both endpoints (hidden from a creator who lost access, visible to the agent owner) plus the boolean status/terminal semantics. EXPLAIN (ANALYZE, BUFFERS) on a 300k-row reproduction: - before (3-status index): Parallel Seq Scan, ~300k rows filtered, shared hit=3012, 12.1 ms. - after (v2 index): Index Scan on idx_agent_task_queue_chat_pending_v2, shared hit=131, 0.07 ms. Co-authored-by: multica-agent <github@multica.ai> * fix(chat): stop optimistic cross-session pending aggregate writes from workspace-fanout task events (MUL-4159) Review on PR #5018 flagged a real privilege-escalation bug in the P2 change: use-realtime-sync optimistically upserted the cross-session pending aggregate (pendingTasks / pendingTasksHasAny) from chat task:* events. Those events are a workspace fanout delivered to every member (server still BroadcastToWorkspace, see cmd/server/listeners.go), and the payload carries no creator / agent visibility. So member B starting a chat task could flip member A's FAB to has_pending=true, bypassing the server-side permission filter on /api/chat/pending-tasks[/has-any]. Fix (option 1 from the review — the self-contained one): never optimistically write the aggregate from task:* events. On every task lifecycle transition, debounced-invalidate the aggregate so it is refetched through the permission-filtering endpoint, which only returns the caller's own creator-owned, accessible-agent tasks. The per-session pendingTask cache is still written directly — it is keyed by chat_session_id and only rendered for sessions the user can open (server-gated), so it is not a cross-user leak. chat:message is still excluded from aggregate refresh, so the MUL-4159 request storm stays fixed; task transitions are per-task and coalesced by the debounce. - Removed upsertPendingAggregate / removePendingAggregate. - Added exported refetchPendingChatAggregate(qc, wsId) — an invalidate, never a setQueryData — used by the debounced handler. - Regression tests: refetchPendingChatAggregate leaves the cached has_pending/list untouched (no optimistic write) and only invalidates for an authoritative server-filtered refetch; no-ops without a workspace id. Verified: @multica/core + @multica/views typecheck; full core vitest suite (752 tests) green including the 2 new guard tests. Co-authored-by: multica-agent <github@multica.ai> * chore(chat): address review nits on pending-tasks endpoints (MUL-4159) - Restore the GetPendingChatTask godoc first line that was clipped when the has-any handler was inserted (nit#1). - ListPendingChatTasks short-circuits to an empty list when the caller has no accessible agents, mirroring HasPendingChatTasks — skips the DB round-trip (nit#2). - Add a cross-creator negative test: user A's in-flight task on a workspace-visible agent returns has_pending=false / empty list for user B, locking the cs.creator_id tenant gate that the agent-visibility filter does not cover (nit#3). Verified: go build ./... and go test ./internal/handler -run PendingChatTasks (7 tests) green against live Postgres. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
2747416380 |
MUL-4117: feat(cli): add workspace member invite command (#5017)
Closes #4967 |
||
|
|
30d3aca600 |
feat(attachments): support HTTP Range resume on proxy download (MUL-3962)
Add HTTP Range support to the attachment proxy-download path so an interrupted download can resume from where it left off instead of restarting at byte 0. - Seekable backends (local disk) delegate to http.ServeContent for full Range / If-Range / 206 / Content-Range / 416 handling. - Forward-only backends (S3/MinIO streaming) get a single-range fallback that advertises Accept-Ranges and serves 206 + Content-Range, with a rangeParseOutcome that returns 416 only for genuinely unsatisfiable byte ranges and otherwise ignores unsupported/empty-object ranges (full 200), matching the seekable path. Closes #4831. MUL-3962. |
||
|
|
e36c0cd404 |
fix: preflight Claude root/sudo launches with an actionable error (#4944)
Detect the root/sudo + bypassPermissions launch condition before starting Claude Code and fail fast with an actionable error (run as non-root, or set IS_SANDBOX=1 in a genuine container/sandbox). Closes #3278 MUL-4095 |
||
|
|
1de0c7d14c |
MUL-4158: allow deleting orphaned profile runtimes
Fixes MUL-4158 |
||
|
|
3cb5dc3ad6 |
chore(analytics): retire redundant PostHog tracking (MUL-4127) (#4996)
* chore(analytics): retire redundant PostHog tracking (MUL-4127) PostHog had become a chaotic, largely-unused second copy of data we already query from the DB and Grafana. Remove the redundant instrumentation. Server: every product event (signup, workspace_created, issue_created, issue_executed, chat_message_sent, team_invite_*, onboarding_*, agent_created, cloud_waitlist_joined, feedback_submitted, contact_sales_submitted, squad_created, autopilot_created) is now in metricsOnlyEvents, so metrics.RecordEvent still increments the Prometheus/Grafana counter but no longer ships to PostHog. DB rows remain the source of truth. Runtime/autopilot/ agent_task lifecycle were already Prometheus-only. Frontend: delete the PostHog-only funnel instrumentation — $pageview (+ web and desktop trackers), download_intent_expressed/page_viewed/initiated, the onboarding_started mirror, onboarding_runtime_path_selected/detected, feedback_opened, and source_backfill_*. The source-backfill modal itself stays (it PATCHes the questionnaire to the DB). Kept on PostHog (frontend only): $exception autocapture and the client_crash / client_unresponsive stability telemetry (no DB equivalent), plus $identify/$set. captureSignupSource (attribution cookie) stays — it still feeds the signup_source Prometheus label. Verified: pnpm typecheck, pnpm lint (0 errors), vitest (core/views/web/desktop), go test ./internal/analytics/... ./internal/metrics/... Co-authored-by: multica-agent <github@multica.ai> * docs(analytics): fix stale PostHog references after MUL-4127 (review follow-up) Addresses review of #4996 — three spots still described server events as active PostHog signals after they became metrics-only: - docs/analytics.md: issue_executed is no longer a PostHog success signal; it is Prometheus-only (multica_issue_executed_total) + issue.first_executed_at, in both the event contract and the Reconciliation section. - docs/analytics.md: the signup $set_once person properties (email, signup_source) are no longer emitted — signup is Prometheus-only; only the bucketed signup_source survives as the multica_signup_total label. - server/internal/metrics/business_events.go: RecordEvent doc comment no longer claims it ships product events to PostHog / "PostHog is reserved for user/product-behaviour events" — every server event is now metrics-only. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> 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> |
||
|
|
39ccb7d342 |
fix(server): do not cancel issue tasks on assignee change (MUL-4113, #4963) (#4975)
* fix(server): don't cancel issue tasks on assignee change (#4963) Changing an issue's assignee previously called CancelTasksForIssue, which cancels every active task on the issue by issue_id alone — regardless of which agent owns the task or how it was triggered. In a multi-agent workspace this silently dropped unrelated in-flight work (a mention-triggered run for another agent, a squad task) with no requeue, and it self-cancelled a run that reassigned the issue from inside its own turn (the daemon then interrupted the live run before its post-handoff cleanup could finish). Reassignment now cancels nothing: ownership handoff no longer implies interruption. The new assignee's run, if any, is still enqueued by WillEnqueueRun and runs alongside whatever was already in flight. Explicit terminal actions — issue -> cancelled and delete issue — still cancel active tasks, unchanged. Applies to both UpdateIssue and BatchUpdateIssues. Adds handler tests that fail against the old behavior (both the previous assignee's own run and an unrelated agent's run got cancelled) and pass now. MUL-4113 Co-authored-by: multica-agent <github@multica.ai> * test(server): cover agent→agent reassign; fix stale WillEnqueueRun comment Addresses review nits on #4975 (MUL-4113, #4963): - Rewrite the outdated WillEnqueueRun doc comment. The assign source no longer cancels existing tasks, so the old "assign cancels existing tasks before enqueuing, pending task moot" premise is wrong. Describe the real invariant instead: the write is guarded by the (issue_id, agent_id) partial unique index, only the status source needs the pending-task dedup, and the assign source safely skips it. - Add a handler test for the core agent→agent handoff path. The existing no-cancel tests only reassigned to a member; this one reassigns from one agent to another and asserts both effects independently: the previous agent's running task survives (no collateral cancel) and the new assignee still gets exactly one run enqueued. MUL-4113 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
75e8bd5b64 |
fix: make release index migrations concurrent (#4995)
Co-authored-by: Eve <eve@multica-ai.local> 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> |
||
|
|
5dfb0bec06 |
Fix Codex MCP allowlist config rendering (#4949)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a69d969fb1 |
fix(sweeper): gate running-task wall clock on runtime liveness (MUL-4107) (#4978)
The server-side running-task sweeper failed rows purely on `started_at >
now() - runningTimeoutSeconds` (2h30m). By its own comment the wall clock
is "mainly for runs whose daemon died without reporting" and "only needs
to sit generously above any realistic single run" — but the predicate
does not actually distinguish a healthy long-running task from an
orphaned one. On self-hosted deployments this kills multi-hour research
/ training runs mid-flight even though the daemon is still heartbeating
and the run is actively producing output.
The daemon side is intentionally unbounded (only inactivity watchdogs:
idle 30m, tool 2h); the server backstop was silently the only wall
clock. `FailStaleTasks` is now AND-gated on runtime liveness:
* dispatched — unchanged; already excludes rows with a live
`prepare_lease_expires_at` (renewed every 15s by the daemon between
claim and StartTask).
* running — new: excluded when the task's `agent_runtime` row is
`online` AND `last_seen_at` is within the runtime stale window
(staleThresholdSeconds = 150s, the same signal
sweepStaleRuntimes already uses).
Healthy long-running tasks on live daemons are no longer killed by the
wall clock. The daemon-dead case remains primarily handled by
sweepStaleRuntimes in the same tick (Redis LivenessStore + DB stale +
FailTasksForOfflineRuntimes); the wall-clock branch is now a defensive
backstop for the pathological case where a runtime row lingers online
with a stale DB heartbeat for longer than the wall clock. `runtime_id
IS NULL` is treated as "not proving liveness" so the wall clock still
fires on that (rare / historical) shape.
The 2h30m default is unchanged — this is a gate, not a threshold
change. Tests updated: 4 existing running-task tests now age out the
runtime so they still exercise the wall clock; 2 new tests cover both
new invariants (healthy runtime → skipped; stale runtime → still
killed).
Fixes #4958
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
083e045ec6 |
MUL-4103: harden Windows browser MCP config (#4976)
* fix: harden Windows browser MCP config Co-authored-by: multica-agent <github@multica.ai> * fix: address browser mcp 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> |
||
|
|
f67f0bc9d8 |
fix: block claude settings flag for antigravity (#4974)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e3de28ecd7 |
fix(agent): pi agent final output excludes intermediate steps (#4894) (MUL-4030)
* fix(agent): pi agent final output excludes intermediate steps
Updated PI agent to only retain the final result in JSON output.
Previously, `text_delta` included both intermediate steps and final
content.
Now, output is reset on each `text_start` to concatenate only the
final text.
* fix(agent) Replace `message_update.text_start` with `turn_start` event. add test
`turn_start` begins a new turn, Reset output on it to exclude
intermediate texts.
|
||
|
|
346f818206 |
fix(runtime): add traecli to custom runtime profile whitelist (#4972)
Trae (traecli) already has a New() backend, launch header (traecli acp serve) and provider branding, but was missing from every protocol_family whitelist, so custom runtime profiles based on Trae were rejected and it never appeared in the family picker. Add traecli to SupportedTypes (Go), RUNTIME_PROFILE_PROTOCOL_FAMILIES (TS), the lockstep test's want map, and a new migration 136 widening the runtime_profile_protocol_family_check constraint. MUL-4094, #4945 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3f17c2717b |
WS-1465 fix autopilot duplicate issue dispatch (#4936)
Co-authored-by: JC的AI分身 <tangyuanjc@JCdeAIfenshendeMac-mini.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
359ef61dc3 |
fix(search): pg_trgm index fallback + statement_timeout guard (MUL-4059) (#4925)
* fix(search): add pg_trgm index fallback + statement_timeout guard (MUL-4059) Root cause of the "search freezes with no response" symptom reported in MUL-4059: the search handler runs LOWER(col) LIKE '%pattern%' queries that expect a pg_bigm GIN index (migrations 032, 033, 036), but every migration wraps the CREATE EXTENSION + CREATE INDEX in a DO/EXCEPTION handler that silently skips when pg_bigm is unavailable. The bundled self-host / dev / CI Postgres image (pgvector/pgvector:pg17) does not ship pg_bigm, so on every self-hosted deployment the migrations no-op and no GIN indexes get built. Every /api/issues/search + /api/projects/search request then falls back to a Seq Scan on `issue` + correlated Seq Scans on `comment` — verified with EXPLAIN on the local dev DB, which has zero title/description/comment search indexes before this change. Two independent guardrails are added, either of which alone would have prevented the reported hang: 1) Migration 134 installs pg_trgm (ships in all standard Postgres + pgvector images) and builds GIN indexes with gin_trgm_ops on `LOWER(title)`, `LOWER(COALESCE(description, ''))`, and `LOWER(content)`. The expression signatures match the search handler's WHERE clauses exactly, so the planner picks the index without further changes. The pg_bigm indexes from 036 are left intact — deployments on AWS RDS with pg_bigm 1.2 keep the CJK-friendly bigram path; deployments without it get the trigram fallback. Verified against a local 25k-row fixture: the description LIKE hits `Bitmap Index Scan on idx_issue_description_trgm` in 0.5 ms. 2) runSearchQuery wraps both search handlers in a short-lived read-only transaction with SET LOCAL statement_timeout = 3 s. In the pathological case where indexes are still missing or the query plan is bad, callers see a fast 503 with a descriptive error instead of a stalled request. Verified against a live Postgres: a deliberate pg_sleep(2) with the test override at 200 ms is cut off in 230 ms with SQLSTATE 57014, as asserted by TestRunSearchQuery_StatementTimeoutFires. Non-goals: this change does not remove the pg_bigm code path, does not change the SQL the handler builds, and does not change the API response shape. It is the minimum diff to unblock production while preserving the CJK-search advantage that pg_bigm provides where it is available. Co-authored-by: multica-agent <github@multica.ai> * fix(search): scope comment subqueries to workspace to unblock prd hang (MUL-4059) Follow-up correction after PRD investigation: pg_bigm IS installed on prd `multica-prod` and all five bigm indexes exist in the correct `LOWER(...) gin_bigm_ops` form. The initial "missing index" hypothesis was wrong; migration 134 (pg_trgm fallback) still helps self-host but does not touch the production hang path. Actual prd EXPLAIN (workspace with 60k issues, keyword "search"): Index Scan using idx_issue_workspace on issue i Rows Removed by Filter: 59123 SubPlan 2 Bitmap Heap Scan on comment c Rows Removed by Index Recheck: 1928275 Heap Blocks: exact=48297 lossy=164696 Bitmap Index Scan on idx_comment_content_bigm rows=536761 Execution Time: 32345.002 ms Root cause: the correlated `EXISTS` over `comment` gets rewritten by the planner into a *hashed* subplan. Without a workspace_id filter in the subquery, that hashed set covers every comment in every workspace matching the LIKE — 536k rows for "search" — which spills work_mem into a lossy bitmap and rechecks 1.9M rows. Two-part fix: 1. Query rewrite. buildSearchQuery now emits `c.workspace_id = $wsParam` inside every comment subquery (WHERE phrase match, WHERE multi-term match, tier 7 rank, tier 8 rank, and the matched_comment_content COALESCE). The same $4 parameter is reused so Postgres treats it as a compile-time constant and pushes it into the hashed subplan's key, collapsing the set to this workspace's comments. 2. Supporting index (migration 135). New `idx_comment_workspace ON comment (workspace_id)`. Without it, the pushed-down filter still triggers a Seq Scan on `comment` because comment has no btree on workspace_id (only the FK constraint and composite (issue_id, ...) indexes). Locally verified against a repro that mirrors prd (5k issues in the target workspace, 100k comments in a sibling workspace all containing "search"): the plan drops from 60 ms (hashed global scan, no support index) to 3 ms (subplan uses idx_comment_workspace). Prd extrapolation from the same shape: 32.3 s → tens of milliseconds. Regression test TestBuildSearchQuery_CommentSubqueryWorkspaceScope asserts every `FROM comment c` in the generated SQL is followed by a `c.workspace_id = $4` filter, so a future refactor can't silently regress the plan back to the global-hash pathology. The statement_timeout guard from the earlier commit in this branch is kept — it still bounds the worst case if any future query shape regresses. Co-authored-by: multica-agent <github@multica.ai> * fix(search): address PR review — unwrap 135 + add project trigram indexes (MUL-4059) Both must-fix items from GPT-Boy's review: 1. Migration 135 unwrapped. The previous version buried `CREATE INDEX idx_comment_workspace` inside `DO $$ ... EXCEPTION WHEN OTHERS $$` — exactly the anti-pattern that caused MUL-4059 in the first place. `idx_comment_workspace` is not a CJK-bonus fallback; it is the critical support that makes the query rewrite land on an Index Scan instead of a Seq Scan. A silent failure (lock timeout, disk full, permission denied, schema drift) MUST abort the migration and fail deployment, not slip through as green. The unwrapped `CREATE INDEX IF NOT EXISTS` now propagates real errors to the migration runner, which aborts and does NOT record the version as applied. IF NOT EXISTS keeps idempotency for the operator-precreated case (`CREATE INDEX CONCURRENTLY ...` before running migrations on large prd tables). 2. Migration 134 now covers project search too. SearchProjects reads `LOWER(project.title)` and `LOWER(COALESCE(project.description, ''))`, and the pg_bigm equivalents in migration 039 silently no-op on pg_bigm-less images just like 032/033/036. Without the trigram fallback, project searches on self-host would still Seq Scan and hit the 3 s statement_timeout guard as a 503 — technically bounded but not actually fixed. Added `idx_project_title_trgm` and `idx_project_description_trgm`; the down migration drops them too. Also: fixed the search.go comment that said callers get a "standard 500" — they get a 503 with SQLSTATE-57014 mapping; the comment now matches reality. Verified: build clean, vet clean, existing search / timeout tests still green. Migration 135 dry-run (dropping the index, re-applying the unwrapped SQL under `ON_ERROR_STOP=1`) creates the index cleanly; a deliberate `CREATE INDEX` on a non-existent column now aborts psql with exit 3, confirming the migration runner would fail loudly on any real error. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
7116691c07 |
fix(github): hide reference-only PR links from the issue PR list (#4611)
* fix(github): hide reference-only PR links from the issue PR list A PR that merely mentions an issue key in passing in its description (e.g. "Related to MUL-3739") was auto-linked and shown in that issue's right-side PR list as if it were a working PR for the issue. Add a reference_only flag to issue_pull_request. The webhook keeps linking generously (so close_intent stays trackable across edits) but flags a link as reference_only unless the key is a genuine target: a title prefix, a branch reference, or a body closing keyword (Closes/Fixes/Resolves). ListPullRequestsByIssue filters reference_only rows, so passing body mentions are hidden from the CLI and the UI PR list while real targets remain. reference_only follows the same terminal preserve gate as close_intent; the auto-advance gate is unchanged. Closes MUL-3739 Co-authored-by: multica-agent <github@multica.ai> * fix(github): exclude reference_only links from the close aggregate A reference_only link is hidden from the issue PR list, but GetIssuePullRequestCloseAggregate still counted it toward open_count. An open body-only mention ("Related to MUL-X") could therefore block the issue from auto-advancing to `done` after a real closing PR merged, while being invisible in the right-side PR list. Filter `AND NOT reference_only` in the aggregate too (reference_only rows never carry close_intent, so merged_with_close_intent_count is unchanged). Add TestWebhook_HiddenBodyMentionDoesNotBlockAutoAdvance. Addresses code review on PR #4611. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5901997bf6 |
fix(squad): wake private-leader squad parent leader on child-done (MUL-4063) (#4934)
* fix(squad): wake private-leader squad parent leader on child-done The child-done parent wake routed squad leaders through canEnqueueSquadLeader/canInvokeAgent, while the agent-parent path (triggerChildDoneAgent) has never gated. Agents default to private visibility, so a default squad leader is private; when a child is closed by an agent/system actor (the normal process-squad pipeline) there is no resolvable human originator, the gate fails closed, and the leader is never woken -- stranding every multi-stage squad pipeline after its first stage. Assigning the parent directly to the leader agent worked only because that path is ungated. Remove the child-done leader-invocation gate so agent and squad child-done follow one path. The parent was already permission-checked at squad-assign time (validateAssigneePair); waking its own leader to advance the next stage is a coordination handoff, not a fresh invocation, and grants no new privilege -- the actor can only wake the leader on the specific parent that leader already owns. If invocation permission is ever reintroduced it must be added to both paths together. Also drops the now-dead actor plumbing threaded solely for the gate, flips the plain-member child-done test to assert the leader is woken, adds an agent-actor regression, and updates the squad / mentioning skill docs. MUL-4063 Co-authored-by: multica-agent <github@multica.ai> * docs(squad): refresh Private Leader Access source map to canInvokeAgent The squad + mentioning source maps still described the old canAccessPrivateAgent model (visibility!=private, agent short-circuit, system->agent remap). The trigger gate is canInvokeAgent (MUL-3963); update both to match and note the child-done wake is now ungated (MUL-4063). Review nit follow-up, docs only. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
cc1f5cda8a |
fix(issues): don't call an intermediate stage final in child-done comment (MUL-4062) (#4932)
The staged child-done system comment derived its "final stage vs next stage" wording from stageProgressSummary over the sub-issues that currently exist. The server has no declarative workflow model — stages are agent-driven and often created lazily (stage N+1's sub-issues are written only after stage N produces the inputs they depend on), so an intermediate stage reaches nextStage==0 exactly like a true final stage. The old else branch then asserted "This was the final stage. Wrap up the parent", pushing leaders/humans to wrap up mid-workflow (GH #4927). Extract the trailing instruction into stageAdvanceInstruction and, when no later stage exists among the created sub-issues, stop asserting finality: name both possibilities (create the next stage, or wrap up) and hand the decision back to the leader. Add a unit test locking in that the nextStage==0 message never claims a definitive final stage. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
129efb7688 |
feat(runtime): allow qoder as a custom runtime profile base (#4883) (#4912)
Qoder CN (`qoderclicn`) users could only reach a working custom runtime by misrouting through the Kiro backend, which launches `<cmd> acp --trust-all-tools`. That is incompatible with Qoder's global `--acp` / `--yolo` argv, so the task failed immediately with `kiro initialize failed` and no run messages. Expose `qoder` in the custom-profile protocol_family whitelist across every lockstep layer: - server/pkg/agent SupportedTypes (+ whitelist pin test) - migration 134 runtime_profile protocol_family CHECK (NOT VALID, mirroring 126) - packages/core RUNTIME_PROFILE_PROTOCOL_FAMILIES The existing qoderBackend already honors an ExecutablePath override and launches `<cmd> --yolo --acp`, so a profile with protocol_family=qoder and command_name=qoderclicn now launches with the correct argv instead of the Kiro shape. Provider branding/logo for qoder already exists. MUL-4018 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1ff99e5afc |
fix: honor completed codex turns during process eof races (#4899)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
d90ee9fa35 |
fix(agents): thread permission_mode/invocation_targets through the template create path (MUL-4010) (#4897)
CreateAgentFromTemplate accepted only the legacy visibility field and dropped
it on the floor: neither permission_mode nor invocation_targets flowed into
the INSERT, so the SQL default (COALESCE(sqlc.narg('permission_mode'),
'private')) pinned every template-created agent as private in the new
invocation-permission model (MUL-3963). Since canInvokeAgent reads
permission_mode — not the legacy visibility column — a request that asked
for a workspace-shared agent (old Web/CLI/Desktop sending
visibility="workspace", or new Web sending permission_mode/public_to +
invocation_targets) silently landed as owner-only. The public_to+targets
inputs from the new Web front-end were also being ignored.
Fix (mirrors handler/agent.go:CreateAgent so the two entry points can't
drift):
- CreateAgentFromTemplateRequest gains PermissionMode *string and
InvocationTargets []AgentInvocationTargetDTO.
- Decode via decodeJSONBodyWithRawFields to distinguish an absent
invocation_targets from an empty one (same rawFields lookup CreateAgent
uses).
- Call parsePermissionInput(wsUUID, req.PermissionMode,
req.InvocationTargets, req.PermissionMode != nil, hasTargets,
&legacyVis) so the legacy 'workspace' mapping ('workspace' -> public_to +
workspace target) is applied uniformly.
- Pass perm.legacyVisibility() into Visibility and perm.mode into
PermissionMode on CreateAgentParams so the visibility mirror column stays
aligned and the permission_mode column reflects the caller's intent
rather than the SQL default.
- Persist the invocation allow-list inside the same tx as the agent row via
a new tx-friendly helper replaceInvocationTargetsWithQueries — an agent
is never observable in a state where the row exists but its targets are
missing. handler-level replaceInvocationTargets delegates to it with
h.Queries, keeping the CreateAgent/UpdateAgent call sites unchanged.
- Enrich the response with invocation targets after commit so a client that
just asked for visibility='workspace' sees the derived legacy visibility
round-trip correctly (previously the response echoed empty
invocation_targets and legacy 'private' regardless of intent).
Regression coverage in agent_template_permission_test.go:
- TestCreateAgentFromTemplate_LegacyVisibilityMapsToPermission: both
legacy visibility values are exercised. workspace -> permission_mode
public_to + a workspace invocation-target row (row-level SELECTs assert
the persistence, not just the response echo); private -> permission_mode
private + zero target rows.
- TestCreateAgentFromTemplate_PublicToWithMemberTarget: new-shape request
(permission_mode='public_to' + a member invocation-target) is honoured
verbatim, derived legacy visibility collapses to 'private' (member-only
public_to), and the DB row for the member target exists.
Uses commit-message as the fixture template (zero external skills), so the
tests don't need to reach any network fetcher.
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
910bbe9309 |
MUL-4024 tighten squad leader self-trigger guard (#4896)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4d968ba875 |
fix(agents): gate create-agent access picker on composio_mcp_apps flag (MUL-4010) (#4888)
Reuse the existing `composio_mcp_apps` feature flag instead of the separate `agent_access_picker` flag introduced in #4879. The MUL-3963 permission_mode + invocation_targets model exists to gate Composio sharing, so the create-flow access picker ships on the same switch as the rest of the Composio rollout — environments that already enable Composio (`FF_COMPOSIO_MCP_APPS=true`) now also see the aligned Private / Public-to picker in Create / Duplicate. - Drop `AGENT_ACCESS_PICKER_FLAG` (frontend keys.ts + index re-export). - Drop `AgentAccessPicker` (server featureflags list). - `CreateAgentDialog` reads `COMPOSIO_MCP_APPS_FLAG` instead. - Tests updated to set the composio flag. All 10 create-agent-dialog tests + Composio-related tabs tests pass. Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
098b1f6362 |
MUL-4015: stamp source_task_id on HTTP-authored agent comments (#4886)
The HTTP CreateComment handler read X-Task-ID for parent-id validation and the no_action gate, but never stamped source_task_id on the comment row. That silently broke the originator inheritance chain used by every task the comment triggers downstream (resolveOriginatorFromTriggerComment climbs comment.source_task_id → parent task's originator_user_id). Consequence: on a squad-assigned issue where the leader agent is private (the default permission_mode for agents), the leader → worker mention hop would enqueue the worker's task with originator_user_id = NULL. When the worker later posts its result comment, invokeOriginatorFromRequest reads that NULL back out, opts.OriginatorUserID becomes "", and routeAssignedSquadLeaderFallback → canInvokeAgent denies the leader wake (private agents admit only the owner, and effectiveUser is empty). The leader → worker → leader coordination loop stayed broken until the leader was triggered by something else. Public-to-workspace leaders papered over the issue via the workspaceBroad admittance path in canInvokeAgent. Fix: capture the same X-Task-ID the handler already reads and pass it as SourceTaskID on the CreateComment call. Only stamp when the task belongs to this issue — a same-agent, different-issue comment must not attribute itself to an unrelated task's originator. All existing gates (parent_id-vs-trigger_comment mismatch, no_action) still fire before the stamp is applied. Regression coverage in squad_worker_comment_wakes_leader_test.go: - worker-agent completion comment wakes a public_to squad leader (baseline: was passing before the fix, kept as guardrail) - worker-agent completion coalesces when a leader task is already queued - worker-agent completion wakes a PRIVATE squad leader through the leader → worker mention hop (the failing case, red before → green after) Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
77ba0fdddb |
MUL-4009: hide not-configured Composio toolkits at Service layer (#4880)
* MUL-4009: hide not-configured Composio toolkits at Service layer Filter out toolkits with no enabled auth config in Service.ListToolkits so the Settings UI only shows connectable apps. A dead 'Not configured' card is noise for end users, so drop the entry entirely instead of showing a greyed label (which existed only to avoid a dead Connect button, MUL-3720). - service.go: only append connectable toolkits; drop the connectable-first sort (all entries are connectable now); a resolver error now returns an error (502) instead of masking to an empty catalog, so the UI shows its honest load-failed state rather than a misleading 'no apps configured'. - Keep the wire 'connectable' field (always true) for backward compat with older desktop clients that branch on it. - composio-tab.tsx: remove the 'Not configured' branch; keep the toolkit.connectable guard as a client-side backstop. - i18n: drop composio.not_connectable / not_connectable_hint from en/ja/ko/zh-Hans. - Update service + handler tests to assert filtering and the resolver-error path. Co-authored-by: multica-agent <github@multica.ai> * MUL-4009: address review — empty-state copy, 502 handler test, stale comments Review follow-up on #4880: - i18n: rewrite composio.page_description / empty_title / empty_description in all 4 locales. The empty state now means 'no toolkit with an enabled auth config in the project', not 'Composio returned no catalog' — the old copy misled users after filtering landed. - Add TestComposio_ListToolkits_ResolverErrorIs502: fakes an auth-config resolver error and asserts ListComposioToolkits returns 502, pinning the no-silent-empty-catalog behavior (composioFakeSDK gains listAuthErr). - Refresh stale 'full catalog / false connectable' docs in packages/core/types/composio.ts, composio/queries.ts, api/client.ts to the connectable-only model. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b24b63c513 |
feat(agents): align create-agent visibility with MUL-3963 access model (MUL-4010) (#4879)
Adopt the private / public_to invocation-permission model in the Create Agent (and Duplicate) dialog to match the AccessPicker on the agent detail page. When the new `agent_access_picker` feature flag is ON, the visibility section is replaced with an inline access editor that: - Toggles between Private (owner-only) and Public - Under Public, offers "Everyone in workspace" plus a member allow-list (multi-select, current user excluded, mirroring AccessPicker semantics) - Preserves team targets on the source agent when duplicating - Collapses an empty public_to (no workspace, no members) back to private on submit — same normalisation the AccessPicker emits The dialog submits `permission_mode` + `invocation_targets` in that mode, matching the authoritative gate; when the flag is OFF (default) it keeps sending the legacy `visibility` field so production is unaffected until the rollout is greenlit. Backend: register the flag in the frontend-public list so it flows to the web config bootstrap. Tests: legacy toggle still submits `visibility`; flag-on submits `permission_mode`/`invocation_targets`, collapses empty public_to to private, and preserves ticked member grants. Co-authored-by: Eve <eve@multica-ai.local> 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> |
||
|
|
8797667fac |
fix(slack): prefer attachment body over fallback (#4866)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e3e3e7e23f |
fix(realtime): bounded replay window for ShardedStreamRelay on restart (#4875)
ShardedStreamRelay shard readers previously started with lastID=, which meant events published while a pod was down were silently lost. Replace the cursor with a bounded time-window start ID derived from (now - ReplayGrace), defaulting to 5 minutes. The timestamp is clamped to 0 to handle misconfigured clocks gracefully. Key changes: - Add ReplayGrace field to ShardedStreamRelayConfig (default 5m) - Add replayStartID() helper with non-negative clamp - Extract readShardOnce() from readShard() for testability - Add REALTIME_RELAY_REPLAY_GRACE env var for runtime tuning - Add regression tests for bounded cursor and replay behavior Closes #4797 |
||
|
|
1e85eb0aac |
Fix Kiro ACP usage accounting (#4867)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
942255d283 |
fix(autopilot): keep create_issue runs visible when runtime offline (#4848)
Co-authored-by: JC的AI分身 <tangyuanjc@JCdeAIfenshendeMac-mini.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4ed8f7478f |
fix(server): key reviewer-loop dedup on reviewed commit SHA (MUL-4003) (#4873)
The agent-task run-dedup keyed only on (issue_id, agent_id), so a completed/pending verdict for commit A was silently reused to satisfy a review request for a NEWER commit B pushed after A's run began — giving B zero review coverage (nearly shipped an unreviewed commit; sibling of the daemon disposition-loss bug in #4337). Fix (no migration — reuses the existing context JSONB column): - CreateAgentTask stamps the reviewed head_sha into the task's context. - HasPendingTaskForIssueAndAgent(+ExcludingTriggerComment) now key dedup on that head_sha: a pending task only dedups a request carrying the SAME head. If HEAD advanced (or the pending task predates the stamp), dedup MISSES and a fresh review enqueues. Empty head_sha (no linked PR) falls back to the previous (issue_id, agent_id) key, so non-PR issues keep coalescing unchanged. - head_sha resolves from the issue's linked PR via GetIssueReviewHeadSha (prefers open/draft, newest by pr_updated_at); ResolveIssueReviewSHA fails soft to '' so a github-table hiccup can never over-dedup a review out of existence. - Threaded through all six dedup trigger sites (comment @mention + edit preview, issue-status, squad-leader assign, child-done agent + squad). Issue-linked tasks never reach quick-create context parsing, so the key rides harmlessly alongside. Adds DB-backed regression tests pinning: advanced-head misses dedup, repush invalidates dedup, same-SHA still dedups, and no-linked-PR legacy fallback (verified non-vacuous against the pre-fix query). Co-authored-by: Multica Ops <multica-ops@tenanture.com> |
||
|
|
c7e10ffa85 |
fix(agent): offer no Claude effort levels when the CLI predates --effort
claudeEffortSuperset treated a --help with no --effort line as 'help format drifted' and fell back to the full level superset. On a host whose claude binary predates the flag (e.g. 2.1.2), that let ValidateThinkingLevel approve the agent's persisted thinking_level, so the daemon injected --effort and the CLI exited 1 with "error: unknown option '--effort'" — hard-failing every task instead of degrading to a plain run. Field case: a stale root-owned /usr/local/bin/claude shadowing a current install for daemons whose PATH orders /usr/local/bin first (Multica desktop app), while a shell- launched daemon on the same machine resolved a current claude — the same agent then failed or succeeded depending on which daemon won the task claim race. Distinguish the two cases: --effort present but value list unparsed → keep the full-superset drift fallback; --effort absent entirely → return no levels, so the daemon's existing per-model guard drops the level with a warning and the task runs without the flag. Co-authored-by: Fable Chief Strategist <noreply@anthropic.com> |
||
|
|
65269ef922 |
fix(daemon): copy Codex model catalog into task home
Fixes #4825 Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6b70146570 |
test(rollup): serialise shared-singleton rollup tests across packages (MUL-3980) (#4854)
`go test ./...` compiles internal/handler and internal/scheduler into
separate binaries and runs them in parallel against the same DATABASE_URL.
Both mutate the global task_usage_hourly_rollup_state singleton (id=1) and
contend for the rollup function's advisory lock 4246, so under `-race` on CI
they interleave and fail flakily:
- TestRollupTaskUsageHourlyCapsWindowAtOneDay reads the scheduler test's
forced-back watermark (0.063 days ≈ the scheduler's now-90min) instead of
"now".
- TestPgCronConcurrentNoDoubleWrite sees a handler rollup tick advance the
watermark past its window, yielding winners=0.
Add a dedicated session-level advisory lock (42463980, distinct from the
function's own 4246) that every test touching the singleton acquires for its
duration, serialising them across test processes. Reproduced the exact CI
failures on a concurrent stress loop (5/5 rounds) and confirmed the guard
eliminates them (8/8 rounds green).
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
e4994cd431 |
fix(github): allow one installation to bind multiple workspaces (#4855)
Connecting the same GitHub App installation in a second workspace silently overwrote the first workspace's binding: github_installation was UNIQUE(installation_id) and CreateGitHubInstallation's upsert overwrote workspace_id on conflict (#4823). Widen the uniqueness key to (workspace_id, installation_id) so each workspace keeps its own binding row, and teach the webhook/lifecycle paths to handle N bindings per installation_id: - CreateGitHubInstallation upserts per (workspace_id, installation_id). - Webhook lookup lists all bindings; PR/check_suite routing uses the oldest binding as the deterministic fallback and still routes per-repo via the existing workspace.repos registry. - installation.deleted/suspend drops every workspace binding and broadcasts to each affected workspace. - installation.created/unsuspend refreshes account metadata across all bindings. - Add a standalone index on installation_id (the dropped unique constraint was the only index behind the webhook lookup). MUL-3950 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c328fdbcd0 |
fix(issues): wake parent squad leader on same-squad/shared-leader child-done (MUL-3969) (#4843)
* fix(issues): wake parent squad leader on same-squad/shared-leader child-done (MUL-3969) The child-done stage-barrier wrote the 'Stage N complete / wrap up the parent' system comment on the parent but suppressed the parent squad leader's wake whenever the finished child was owned by the same squad (childAssigneeIsSquad) or a squad sharing the leader (effectiveChildAgentOwner). That stranded the common 'a squad decomposes its parent into sub-issues it works itself' pattern: the parent silently stalled in in_progress because the leader was never woken to advance the next stage or wrap up. The prior guards assumed the leader had already observed the work via its own coordination cycle on the child, but that wake lands on the CHILD and never carries the parent-level stage-barrier instruction. Remove both self-trigger guards so the squad path mirrors the agent path (MUL-2808): always dispatch, bounded only by HasPendingTaskForIssueAndAgent. The private-leader access gate is unchanged; member/unassigned parents still never wake. Drop the now-dead effectiveChildAgentOwner / childAssigneeIsSquad helpers and the unused child param. Fixes #4838 Co-authored-by: multica-agent <github@multica.ai> * test(issues): fix stale child-done squad-guard comment (MUL-3969) The TestChildDoneTriggersParentAgentWhenChildSquadSharesLeader comment still claimed the both-sides-squads-sharing-a-leader case was guarded on the squad path and referenced the pre-rename test. That guard was removed in MUL-3969; point at the renamed test and state the current behavior. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> 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 |
||
|
|
da624a8835 |
feat: add configurable S3 path-style addressing (#4739)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
078f2aa65c |
fix(handler): keep runtime pending redis keys in one slot
Closes #4767 |