* 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>
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>
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>
* 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>
* 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>
* 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>
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.
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
* 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>
* 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>
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>
* 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>
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>
* 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>
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.
* 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>
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>
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>
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>
* 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)
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>
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>
* 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>
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>
* 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>
* 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>
* 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>
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>
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>
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>
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>
* 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>
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>