3945 Commits

Author SHA1 Message Date
Multica Eve
9d4283ae7a docs(changelog): add v0.3.40 release entry (2026-07-07) (#5033)
* docs(changelog): add v0.3.40 release entry (2026-07-07)

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

* docs(changelog): drop reverted worktree_pool feature from v0.3.40 (#5037 reverted #4986)

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

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
v0.3.40
2026-07-07 17:56:33 +08:00
LinYushen
77a05fb731 Revert "feat(daemon): worktree_pool mode for local_directory (MUL-3483) (#4986)" (#5037)
This reverts commit 7bb8076ed0.
2026-07-07 17:35:33 +08:00
YOMXXX
e002ee5a6b fix(daemon): isolate agent temp dirs (#5005) 2026-07-07 16:21:47 +08:00
Multica Eve
4f371c5c9c fix: expose mcp config for supported providers (#5024)
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-07 15:32:38 +08:00
Bohan Jiang
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>
2026-07-07 15:07:02 +08:00
Multica Eve
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>
2026-07-07 14:43:19 +08:00
Naiyuan Qing
074efeec57 fix(web): stop login arrival effect from racing fresh form logins (#5009) (#5019)
The /login page's already-authenticated effect fired on any user change,
including the one verifyCode writes mid form-login while handleVerify is
still fetching the workspace list. It then read the cold list cache
(getQueryData ?? []) and raced handleSuccess with a replace to
/workspaces/new — users with existing workspaces could land on the
create-workspace page after login.

Two changes, both in the arrival effect:

- Ownership: latch once auth settles as logged-out on this page; any
  user appearing afterwards came from the login form, whose
  handleSuccess owns post-login navigation. The effect now only serves
  visitors who arrived authenticated. The desktop-handoff branch stays
  above the latch so form logins with platform=desktop still mint the
  deep-link token.
- Correct data: for genuine arrived-authenticated visitors, fetch the
  list via ensureQueryData instead of reading a possibly-cold cache,
  which misrouted workspace owners to /workspaces/new on fresh page
  loads.

Regression tests verified red against the previous implementation.

Fixes #5009

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:28:42 +08:00
Multica Eve
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>
2026-07-07 14:10:07 +08:00
Naiyuan Qing
93aa959239 fix(workspace): await delete before navigating; suppress self-initiated realtime relocate (MUL-4129) (#4983)
* fix(workspace): drop workspace from list cache while delete is pending (MUL-4129)

The delete-workspace flow navigates away before awaiting the DELETE
(required ordering — see navigateAwayFromCurrentWorkspace's
CancelledError notes), but useDeleteWorkspace left the workspace in the
list cache until onSettled. During the pending window any list refetch
re-presented the deleting workspace as a selectable/current option.

Optimistically remove it in onMutate (after cancelling in-flight list
fetches), roll the snapshot back in onError so a failed delete restores
the workspace alongside the existing error toast, and keep the
onSettled invalidate as the server-truth reconcile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(workspace): own storage cleanup on delete success and tombstone pending deletes (MUL-4129)

Address final-review blockers on #4980:

1. The realtime workspace:deleted handler reverse-looks-up the slug from
   the list cache to clear the ${key}:${slug} persisted namespace; the
   optimistic removal empties that row on the initiating client, so the
   lookup misses and cleanup was silently skipped. Capture the slug in
   onMutate before removal and clear storage in onSuccess only — a
   failed DELETE rolls back and must not touch persisted state.

2. cancelQueries only covered fetches already in flight at onMutate.
   Add a pending-delete tombstone (marked onMutate, lifted onSettled
   before the reconcile invalidate) filtered inside workspaceListOptions'
   queryFn, so invalidation/reconnect/fetchQuery refetches that land
   mid-pending cannot write the not-yet-committed row back into cache.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* docs(claude-md): scope optimistic updates to same-screen field patches

Replace the blanket "mutations optimistic by default" state rule with
three scoped rules: optimistic only for predictable same-screen field
patches; navigating/confirming flows (create/delete/leave) await the
server first; chat send uses the pending-message pattern.

Aligned with TanStack Query maintainer guidance and React Router's
pending-UI criteria; the old blanket rule is what steered the original
MUL-4129 fix toward optimistic entity removal.

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

* fix(workspace): await delete before navigating; drop optimistic removal (MUL-4129)

Rework of the previous approach on this branch. The optimistic removal
emptied the workspace list cache at click time, while the settings page
was still mounted on the old slug — useWorkspaceId (URL slug + list
lookup) then threw 'no workspace selected'. Root cause of the original
navigate-first ordering was dual ownership of delete handling between
the initiating flow and the realtime workspace:deleted handler.

- useDeleteWorkspace: no optimistic removal, no rollback; onMutate only
  marks the delete self-initiated and captures the slug; onSuccess owns
  storage cleanup; onSettled invalidates.
- pending-delete.ts: repurposed from tombstone filter to self-initiated
  marker; kept on success (suppresses the WS echo), lifted on failure.
- use-realtime-sync: workspace:deleted no-ops for self-initiated
  deletes; it now only serves deletes initiated elsewhere.
- workspace-tab: confirm dialog stays open in loading state, navigate
  only after the DELETE succeeds; failure leaves the user in place with
  nothing to roll back. Replace throwing useWorkspaceId with
  workspace?.id + enabled gating (independent crash on external
  deletes of the current workspace).

Known debt: useLeaveWorkspace still navigates before awaiting
(member:removed has no self-initiated marker yet).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-07 13:45:49 +08:00
LinYushen
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>
2026-07-07 13:20:30 +08:00
Bohan Jiang
2747416380 MUL-4117: feat(cli): add workspace member invite command (#5017)
Closes #4967
2026-07-07 12:43:33 +08:00
n374
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.
2026-07-07 12:38:07 +08:00
Matt Van Horn
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
2026-07-07 12:19:50 +08:00
Multica Eve
1de0c7d14c MUL-4158: allow deleting orphaned profile runtimes
Fixes MUL-4158
2026-07-07 12:01:14 +08:00
Bohan Jiang
bcad2edc9e feat(issues): add Cmd+F in-page find to issue detail (MUL-4126) (#4989)
* feat(issues): add Cmd+F in-page find to issue detail

Replace the stopgap "find-in-page is virtualized" toast with a real find
bar (MUL-4126). Cmd/Ctrl+F opens a floating bar with keyword input, live
match count, and prev/next navigation that scrolls to and highlights each
match.

- Opening find force-renders the comment timeline flat (reusing the
  existing highlightCommentId escape hatch) so off-screen comments become
  searchable — the root cause of the original complaint.
- Matches are painted with the CSS Custom Highlight API (ranges only, no
  DOM mutation), so highlighting layers cleanly over React-rendered
  markdown and the contenteditable title/description editors.
- Scroll-to-match drives container.scrollTop directly (never native
  scrollIntoView; #3929).

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

* fix(issues): keep in-page find usable without CSS Custom Highlight API

On browsers lacking the CSS Custom Highlight API, `!supported` was folded
into the match-collection path, so Cmd/Ctrl+F opened the bar and swallowed
native find but reported 0 matches and could not navigate — strictly worse
than the native find it replaced (MUL-4126 review).

Feature-guard only the paint calls now: match collection, count, active
index, and scroll-to-match run regardless of support, while
`CSS.highlights.set/delete` / `new Highlight` stay behind the guard. The
MutationObserver re-derives ranges even when unsupported so fallback
counting/navigation track live DOM churn.

Adds a hook test that drives the degraded path (jsdom has no highlight API)
and asserts counting + prev/next still work.

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

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-07 11:54:08 +08:00
Bohan Jiang
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>
2026-07-07 01:43:15 +08:00
Jiayuan Zhang
380a2b8cb8 fix(chat): default chat window to closed on workspace open (#5003)
Opening a workspace no longer pops the chat window for users who have
never toggled it — the FAB keeps chat discoverable. An explicit stored
open/closed preference is still honoured on reload.

Closes MUL-4149

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-06 23:25:27 +08:00
Brook Xi
466fe2ca0b docs(readme): sync runtime list, drop retired Gemini (#4990)
* docs(readme): sync runtime list, drop retired Gemini

Gemini CLI was removed from the runtime whitelist (agent.SupportedTypes)
back in v0.3.29; Google also officially retired Gemini CLI on 2026-06-18
in favor of Antigravity CLI. Both README.md and README.zh-CN.md still
advertised Gemini and had drifted from the canonical list.

Sync every runtime/provider list in both READMEs to the 14 entries in
server/pkg/agent/agent.go: remove Gemini, add the missing CodeBuddy,
Antigravity and Trae CLI, and align ordering.

* docs(readme): use consistent "Trae CLI" naming

Align the intro and "Create an agent" provider lists with the
architecture diagram and runtime table, which already say "Trae CLI".

MUL-4145

---------

Co-authored-by: Bohan Jiang <bhjiang@outlook.com>
v0.3.39
2026-07-06 19:54:32 +08:00
Bohan Jiang
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>
2026-07-06 19:47:00 +08:00
Bohan Jiang
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>
2026-07-06 19:11:47 +08:00
Multica Eve
75e8bd5b64 fix: make release index migrations concurrent (#4995)
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-06 19:06:53 +08:00
YYClaw
12d901638e fix(desktop): resolve electron-vite bin via PATH in dev script (#4992)
Under the hoisted linker (node-linker=hoisted) electron-vite's bin only
lands in the repo-root node_modules/.bin, so the hardcoded
apps/desktop/node_modules/.bin path fails. Use envWithLocalBins to put
both .bin directories on PATH and invoke electron-vite by name.
2026-07-06 19:01:53 +08:00
Bohan Jiang
7183ec9b6d feat: add agent list search (#4988)
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-06 18:31:46 +08:00
Multica Eve
c5f0618caf docs(changelog): add v0.3.39 (2026-07-06) release notes (#4985)
* docs(changelog): add v0.3.39 (2026-07-06) release notes

Adds today's release entry across en / zh / ja / ko locales.

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

* docs(changelog): simplify v0.3.39 copy, drop technical jargon

Rewrites every entry to describe user-visible outcomes instead of implementation details. Also adds the squad-leader lock fix (#4951) which was merged into main after the initial cut.

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

---------

Co-authored-by: Multica Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-06 18:06:14 +08:00
ZIce
b2db309618 Skip local directory lock for squad leaders (#4951)
Co-authored-by: multica-agent <github@multica.ai>
2026-07-06 17:19:56 +08:00
Naiyuan Qing
152edab8d0 Revert "fix(workspace): drop workspace from list cache while delete is pendin…" (#4982)
This reverts commit f9b7e5035a.
2026-07-06 17:02:01 +08:00
Naiyuan Qing
f9b7e5035a fix(workspace): drop workspace from list cache while delete is pending (MUL-4129) (#4980)
The delete-workspace flow navigates away before awaiting the DELETE
(required ordering — see navigateAwayFromCurrentWorkspace's
CancelledError notes), but useDeleteWorkspace left the workspace in the
list cache until onSettled. During the pending window any list refetch
re-presented the deleting workspace as a selectable/current option.

Optimistically remove it in onMutate (after cancelling in-flight list
fetches), roll the snapshot back in onError so a failed delete restores
the workspace alongside the existing error toast, and keep the
onSettled invalidate as the server-truth reconcile.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-06 17:00:57 +08:00
ZIce
5dfb0bec06 Fix Codex MCP allowlist config rendering (#4949)
Co-authored-by: multica-agent <github@multica.ai>
2026-07-06 16:49:29 +08:00
Multica Eve
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>
2026-07-06 16:29:02 +08:00
Multica Eve
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>
2026-07-06 16:14:03 +08:00
Multica Eve
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>
2026-07-06 15:58:38 +08:00
ZIce
24f2923f2b docs: list Qoder in runtime provider copy (#4952)
Update public docs and landing copy to the current 14-runtime list; add Qoder / Trae CLI across localized docs. Follow-up: finish JA tool counts (tasks.ja / skills.ja) and align localized Trae section anchors with the /providers#trae links.

Closes #4945

Co-authored-by: vicksiyi <zeroicework@163.com>
2026-07-06 15:44:36 +08:00
etern
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.

a1b336d73e/packages/coding-agent/docs/rpc.md (events)
2026-07-06 15:34:22 +08:00
Bohan Jiang
a48b6f70ef fix(issues): replace nested "More" action submenu with "Relations" (MUL-3972) (#4847)
The issue action menu (3-dot / right-click) nested a "More" submenu inside the
already-open menu, so opening the menu surfaced yet another "More" — the first
level told you nothing about what was inside.

Rename that submenu to the semantically explicit "Relations" (关系 / 関係 / 관계)
with a Network icon, matching the noun-labelled pattern of the sibling submenus
(Status, Priority, Start date, Due date). Its contents are unchanged — create/add
sub-issue and set/remove parent — and stay grouped so future relation types
(blocks, duplicates, related) have a home.

- Rename i18n key actions.more -> actions.relations across en/zh-Hans/ja/ko
- Swap MoreHorizontal icon for Network
- Update the shared menu test

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-06 15:11:41 +08:00
Bohan Jiang
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>
2026-07-06 15:03:17 +08:00
tangyuanjc
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>
2026-07-06 13:16:54 +08:00
Multica Eve
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>
2026-07-06 12:34:51 +08:00
Naiyuan Qing
c84a939c83 fix(views): enable multi-file selection on all attachment upload buttons (#4962)
The FileUploadButton component already fans out per-file onSelect
callbacks and every editor surface already handles N concurrent
uploads (drag-drop and paste were multi-file all along), but five
call sites never passed the `multiple` prop, so the OS file dialog
capped picks at one file: chat composer, create-issue modal,
quick-create modal, issue description, and feedback modal.

Fixes MUL-4074.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-06 09:43:03 +08:00
Naiyuan Qing
ebd0248be2 MUL-4016: fix mention tokenizer stacktrace backtracking (#4889)
* MUL-4016: fix mention tokenizer stacktrace backtracking

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

* fix(editor): de-ambiguate escaped-label regexes to kill ReDoS (MUL-4016)

The mention/slash/file-card label regexes used `(?:\\.|[^\]])` where both
alternatives can consume a backslash. On an unterminated match, each `\x`
run is enumerated 2^n ways — pasting a Java stacktrace (`\~\[...\]`) or a
crafted ~50-char string freezes the main thread for seconds (GitHub #4881).

Exclude backslash from the char class (`[^\]\\]`) so a backslash can only be
consumed by `\\.`. The alternatives become disjoint and matching is linear;
legal escaped-bracket labels like `David\[TF\]` still parse unchanged.

Fixed in all four sites that shared the pattern:
- mention-extension.ts tokenize()
- slash-command-extension.ts start() + tokenize()
- file-card.tsx FILE_CARD_MARKDOWN_RE
- packages/ui/markdown/file-cards.ts NEW_FILE_CARD_RE (runs on every
  read-only comment/description render, not just the editor)

Adds adversarial regression tests (repeated `\a` + missing closing bracket)
that fail in ~10-40s against the old regexes and pass in <1ms after the fix.
Builds on #4889's marker-first mention start().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(editor): escape backslash in mention/slash labels for round-trip (MUL-4016)

Follow-up to the de-ambiguation fix, addressing Howard's PR review.

The linear tokenizer now treats "\" as an escape lead (\\.), so a label whose
serialized form contains a bare "\" adjacent to the closing "]" no longer parses
back — the "\]" is consumed as an escaped bracket and swallows the boundary. The
old ambiguous regex tolerated this by chance; the de-ambiguation exposes it.

mention/slash renderMarkdown escaped only [ and ], not \. Switch both to the
shared escapeMarkdownLabel() (escapes [ ] \ ( )) and mirror it on parse with
replace(/\\([[\]\\()])/g, "$1"), matching what file-card already does. This also
converges the three tokenizers on one escape contract. file-card was already
correct and is unchanged.

Adds parameterized round-trip tests for labels containing "\" / "\]" / parens
(e.g. "A\\", "ends\\", "a\\]b"); these fail on the old serializer and pass now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 08:43:51 +08:00
Jiayuan Zhang
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>
2026-07-05 13:30:18 +08:00
Bohan Jiang
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>
2026-07-05 02:35:19 +08:00
Bohan Jiang
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>
2026-07-05 01:35:00 +08:00
Bohan Jiang
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>
2026-07-04 23:16:01 +08:00
Multica Eve
1ae0a1f5bf fix(agents): defend AccessPicker against undefined invocation_targets (GH #4915) (#4924)
Opening the agent detail page in v0.3.37 crashed the whole route with
"Cannot read properties of undefined (reading 'some')" whenever the
cached agent record's `invocation_targets` was missing at runtime — even
though the TypeScript type declares it a required `AgentInvocationTarget[]`.

Root cause: `AccessPicker`'s `hasWorkspaceTarget` / `selectedMemberIds` /
`selectedTeamIds` helpers called `.some()` / `.filter()` directly on the
prop, and the same unguarded pattern was mirrored in `AgentMcpTab` and
the `canAssignAgentToIssue` permission gate. The field can legitimately
be undefined at runtime because:

- `packages/core/api/schemas.ts` declares `invocation_targets` as
  `.optional()`, and `MinimalAgentSchema` (used for the create-from-
  template response) also marks it optional.
- `api.listAgents` / `api.getAgent` return raw JSON without running
  through the schema, so an older self-host backend that predates
  MUL-3963 (permission_mode + invocation_targets) — the exact scenario
  in GH #4915 — yields an Agent with the field missing.

Fix (`?? []`) at every site that indexes into the list, matching the
already-established pattern in `create-agent-dialog.tsx`:

- `access-picker.tsx`: coerce inside the three helpers and widen the
  prop type to `AgentInvocationTarget[] | undefined` so the accepted
  runtime shape is documented.
- `agent-mcp-tab.tsx`: `(agent.invocation_targets ?? []).some(...)`.
- `permissions/rules.ts`: `agent.invocation_targets ?? []` before the
  workspace / member membership checks.

Adds three regression tests (AccessPicker owner + read-only paths,
AgentMcpTab shared-warning path, canAssignAgentToIssue) that pass
`undefined` for invocation_targets and assert the UI degrades to the
private / empty-allowlist state instead of throwing.

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
v0.3.38
2026-07-04 18:07:03 +08:00
Multica Eve
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>
v0.3.37
2026-07-03 18:30:24 +08:00
Multica Eve
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>
v0.3.36
2026-07-03 17:56:02 +08:00
Multica Eve
022c8a63d0 docs(changelog): add v0.3.36 entry for the 2026-07-03 release (#4893)
* docs(changelog): add v0.3.36 entry for the 2026-07-03 release

Summarize the changes on main since v0.3.35: the Composio MCP apps
rollout (server-side connect flow, per-agent allowlist, and the new
Private / public-to invocation permission model — all behind the
composio_mcp_apps feature flag), transcript filter/expansion memory,
and Helm support for an externally managed PostgreSQL.

Bug fixes: empty ordered-list caret repair, daemon agent CLI discovery
through login-shell hook wrappers, per-commit-SHA reviewer-loop dedup,
bounded ShardedStreamRelay replay window, Kiro ACP usage accounting,
autopilot create_issue visibility while runtime is offline, Slack
attachment-body preference, Codex model catalog in task home, legacy
/squads and /usage redirects, correct filename in the desktop save
dialog, private squad-leader wake from HTTP-authored worker comments,
Composio Settings hides toolkits with no auth config, and a graceful
fallback when the host Claude CLI predates --effort.

Bullets stay in product language across en / zh / ja / ko. Only the
Issue concept is preserved untranslated in the Chinese entry; agent,
Squad, and sub-issue follow the translated glossary.

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

* docs(changelog): drop Composio bullets — still behind feature flag

Per Yushen: the Composio app connections feature (and the two Composio-
gated polish bullets — Settings toolkit filtering and the create-agent
access picker) is still gated by composio_mcp_apps and is not user-
visible in this release. Removing those bullets from all four locales
(en / zh / ja / ko) and retitling the v0.3.36 entry around the two
remaining user-visible features (transcript view memory, Helm external
PostgreSQL) plus the bug-fix list.

Structure and every other bullet unchanged.

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-07-03 17:46:01 +08:00
Multica Eve
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>
2026-07-03 17:42:06 +08:00
Multica Eve
02a845b31c chore: add trailing newline to README (MUL-4026) (#4890)
No-op whitespace change to exercise the manual-merge test flow
requested in MUL-4026.

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-03 17:19:15 +08:00
Naiyuan Qing
dd9996d0aa MUL-4014: persist transcript filters and expansion (#4884)
* feat(transcript): persist log view preferences

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

* fix(transcript): wrap modal header controls

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-07-03 16:45:53 +08:00