Commit Graph

3715 Commits

Author SHA1 Message Date
J
80ffe259cf docs(channel): fix migration 124 rollout order and document self-host cutover
The previous ROLLOUT note shipped the new (channel_*) build before
running migration 124, so the channel_*-backed HTTP paths (installation
list/install/revoke, chat-session delete, member revoke) would 500 in
the window between new-pod boot and the deferred migration. Restate the
runbook around two explicit invariants — channel_* must exist before the
new build serves those paths, and the old/new hubs must never overlap —
and order the steps so channel_* is created first (park old hub -> snapshot
-> deploy parked new build -> unpark). Document that default self-host
(entrypoint migrate + single-replica Recreate) satisfies both invariants
automatically and needs no manual steps; only prd / multi-replica rolling
self-host needs the switch procedure. Clarify in main.go that the
hub-park switch is generation-agnostic (parks whichever hub the build
carries), which is what enables the preparatory release.

Refs MUL-3515

Co-authored-by: multica-agent <github@multica.ai>
2026-06-23 22:00:20 +08:00
J
ea1238e9d2 docs(channel): point migration 124 ROLLOUT note at the hub-disable switch
Refine the rollout note to use MULTICA_LARK_HUB_DISABLED for a bot-only cutover (new pods serve API with the hub parked while old pods drain; flip the switch off after the migration), instead of the earlier whole-API recreate. Comment-only.

MUL-3515

Co-authored-by: multica-agent <github@multica.ai>
2026-06-23 17:41:35 +08:00
J
aafe3a31fd feat(lark): add MULTICA_LARK_HUB_DISABLED switch for the channel cutover
The lark_*->channel_* cutover needs a way to make the Feishu bot briefly unavailable WITHOUT taking down the whole multica-api process — the Lark hub is a goroutine inside it, not a separate Deployment. MULTICA_LARK_HUB_DISABLED=true parks the hub at startup: the API serves HTTP normally but never claims a WS lease or opens a Feishu connection.

Rollout (see migration 124 ROLLOUT note): ship the new release with the flag SET so new pods run API-only while old pods (hub on lark_*) drain during the rolling deploy — the two hubs never overlap. After the old pods are gone and migration 124 has run, flip the flag off; the new hub comes up on channel_*. The old backend does NOT need this switch — its hub stops when k8s terminates the old pods, not via a flag. Nil-ing LarkHub reuses the existing not-configured path so both the startup start and the shutdown join skip it.

MUL-3515

Co-authored-by: multica-agent <github@multica.ai>
2026-06-23 17:40:07 +08:00
J
7962a2528a docs(channel): document non-rolling cutover constraint for the lark->channel migration
Elon deploy review: keeping the lark_* tables (deferred drop) stops old v0.3.27 code from crashing, but is not full expand/contract. Migration 124 is a one-time backfill; afterwards new code runs on channel_* (lease + dedup on channel_*) while pre-cutover code runs on lark_* (lease + dedup on lark_*). If both run concurrently during a rolling deploy, each side claims the same Feishu bot's WS lease on its own table and double-processes inbound events.

This release therefore requires a NON-ROLLING cutover (stop the old hub before applying migration 124 + starting new code; rollback is not lossless once new code writes channel_*). Documented where deployers/reviewers see it: migration 124 header gains a ROLLOUT note; the channel_store.go header is corrected (lark_* tables are retained one release for rollback safety, not "gone"; the store still never touches them). Comment-only — no schema/codegen/behavior change.

MUL-3515

Co-authored-by: multica-agent <github@multica.ai>
2026-06-23 17:16:52 +08:00
J
7af0de4e8c fix(channel): skip orphaned installations in hub-boot active scan
Preflight deploy review: channel_installation dropped the workspace/agent FK (MUL-3515 §4), so unlike lark_installation it does not cascade away when its workspace is deleted or its agent is hard-deleted (e.g. runtime teardown). The hub-boot query then keeps opening a WebSocket for a bot whose owner is gone.

JOIN ListActiveChannelInstallations to live workspace + agent so an orphaned installation is never connected, uniformly for every deletion path. The JOIN matches the old ON DELETE CASCADE semantics (row existence, not agent archival), so an archived-but-present agent's installation is still listed; the orphaned row's encrypted secret is thereby never decrypted/used.

Tests: a real-DB handler test asserts a deleted-workspace/agent installation and a non-Feishu one are both excluded; the lark scope test's active-list assertion moved there since the JOIN now needs real workspace/agent fixtures. (Physically deleting dormant orphaned channel rows on workspace/agent deletion is a separate app-layer-cleanup follow-up.)

MUL-3515

Co-authored-by: multica-agent <github@multica.ai>
2026-06-23 16:54:18 +08:00
J
a873081746 refactor(channel): defer lark_* table drop to a follow-up migration
Preflight deploy review: dropping lark_* in the same release that cuts over (old migration 125) is not rollback/rolling-safe — the v0.3.27 release still reads lark_*, so a rolling deploy or a post-deploy code rollback would hit "relation does not exist". Remove the drop and keep the old tables for one release (standard expand/contract): migration 124 already backfilled lark_* -> channel_*, the new code reads/writes only channel_*, and the physical drop moves to a separate cleanup migration once this ships and is observed.

The lark_* tables remain in the schema, so sqlc regenerates the (now unused) db.Lark* models; queries/lark.sql stays deleted (the new code uses channel_*). No code path reads lark_* — only the destructive drop is deferred, keeping the design's no-compat-layer / no-dual-write rule while being deploy-safe.

MUL-3515

Co-authored-by: multica-agent <github@multica.ai>
2026-06-23 16:54:18 +08:00
J
4364a1dfe6 fix(migrations): remove trailing blank line at EOF of 125 down migration
git diff --check flagged a blank line at EOF of 125_drop_lark_tables.down.sql (a pg_dump-generation artifact). Whitespace only; the recreate SQL is unchanged.

MUL-3515

Co-authored-by: multica-agent <github@multica.ai>
2026-06-23 14:18:59 +08:00
J
207d1c61bc refactor(channel): drop lark_* tables and queries (remove old path)
The Go cutover (previous commit) moved the lark package entirely onto channel_* and the domain types, leaving the lark_* tables, queries/lark.sql, and the generated db.Lark* models unused. Remove them per the design (§5: replace, do not keep both): migration 125 drops the seven lark_* tables (data already lives in channel_* since migration 124), and queries/lark.sql is deleted + sqlc regenerated, removing the db.Lark* models and lark query methods.

The 125 down recreates the authoritative pre-drop schema (bot_union_id, region, per-installation dedup PK, thread-reply columns). Verified on PG17: fresh migrate up ends with lark_* gone + channel_* present; isolated 125 down/up round-trips correctly; go build / vet / gofmt clean; go test -race ./internal/integrations/... and the handler suite pass.

MUL-3515

Co-authored-by: multica-agent <github@multica.ai>
2026-06-23 14:01:10 +08:00
J
155eab94ac refactor(channel): replace db.Lark* translation layer with lark domain types
S2 introduced ChannelStore as a translation layer that read/wrote channel_* but kept the retired db.Lark* struct/param shapes so the dispatcher/hub/services and their ~20k lines of tests did not have to change. This collapses that layer: the store now takes and returns the package's flat domain types (Installation, UserBinding, ChatSessionBinding, InboundMessageDedup, BindingTokenRow, OutboundCardMessage) and the *Params types in params.go, with channel-neutral field names (ChannelUserID / ChannelChatID / ...). All call sites, fakes, and tests move to the domain types.

No behavior change: only channel_* is read/written (as before); db.Lark* is now unused, and the lark_* tables + queries/lark.sql are removed in the next commit. Verified on PG17: go build / vet / gofmt clean, go test -race ./internal/integrations/... green (the ~20k-line fake suite), and the lark + handler suites pass.

MUL-3515

Co-authored-by: multica-agent <github@multica.ai>
2026-06-23 13:54:28 +08:00
J
9fdaadf2ea fix(channel): scope Lark/Feishu store reads to channel_type='feishu'
The S2 cutover routed the Feishu integration onto channel_*, but the Lark-facing ChannelStore wrappers read installation / chat-session-binding / outbound-card rows across ALL channel_type values. Once a second IM exists, that would let the Lark hub supervise a non-Feishu installation, the Lark install list show it, /lark/installations/{id} revoke another channel's row, and the outbound patcher / typing indicator act on a non-Feishu chat binding or card.

Add a channel_type predicate to the six read/list channel queries and pass channelTypeFeishu from every wrapper: GetChannelInstallation, GetChannelInstallationInWorkspace, ListChannelInstallationsByWorkspace, ListActiveChannelInstallations, GetChannelChatSessionBindingBySession, GetChannelOutboundCardByTask.

The S3 cleanup deletes (DeleteChannelUserBindingsByWorkspaceMember / DeleteChannelChatSessionBindingBySession) stay all-channel on purpose: a member leaving or a chat_session being deleted should clear every IM's binding. Adds a real-DB test that seeds a Slack installation/binding/card next to the Feishu ones and asserts the Lark wrappers never return them.

MUL-3515

Co-authored-by: multica-agent <github@multica.ai>
2026-06-23 12:34:39 +08:00
J
1b27d3551d feat(channel): prune channel bindings on member removal + chat session delete
MUL-3515 §4 dropped every channel_* foreign key, so the old ON DELETE CASCADE that cleared a user's channel_user_binding when they left a workspace, and a chat's channel_chat_session_binding when its chat_session was deleted, no longer fires. Re-establish that integrity in the application layer, inside the existing transactions: revokeAndRemoveMember -> DeleteChannelUserBindingsByWorkspaceMember, DeleteChatSession -> DeleteChannelChatSessionBindingBySession.

Adds real-DB tests for both paths, including a scoping check that a remaining member's binding survives the prune. Verified on PG17: both new tests plus the existing revocation tests and the full handler package pass.

MUL-3515

Co-authored-by: multica-agent <github@multica.ai>
2026-06-23 12:10:08 +08:00
J
c874bbc57b fix(channel): renumber generalization migration 123 -> 124
main merged 123_issue_stage after this branch forked, so the branch's 123_channel_generalization now collides on the migration number. The runner keys schema_migrations by full version string and would still apply both, but a duplicate number is a merge hazard and convention violation, so move the channel migration to the next free slot (124).

issue_stage (ALTER issue ADD COLUMN stage) and the channel generalization touch disjoint tables; verified on PG17 that 123_issue_stage applies cleanly on a DB already carrying 124_channel_generalization, so the two are order-independent. sqlc regenerated (v1.31.1): only the migration-number comment changed.

MUL-3515

Co-authored-by: multica-agent <github@multica.ai>
2026-06-23 12:10:08 +08:00
J
c1b739fe26 feat(lark): route inbound integration onto channel_* + explicit membership checks
Cutover step 2 (MUL-3515): switch the Feishu Go code from the lark_* queries to
channel_* via a ChannelStore adapter, and replace the removed member foreign key
with explicit application-layer membership checks. No user-visible behavior change.

- channel_store.go: ChannelStore embeds *db.Queries and SHADOWS the ~24 lark
  query methods with channel_*-backed equivalents, keeping the db.Lark*
  signatures so the dispatcher/hub/services and their ~20k lines of tests stay
  untouched; the feishu JSONB config is (de)coded by store.go. Adds
  IsWorkspaceMember and a tx-aware WithTx. Only production wiring swaps
  *db.Queries for *ChannelStore.

- Membership re-check (§4 removed the lark_user_binding -> member FK, so a
  binding row no longer proves current membership):
  * the dispatcher inbound identity step verifies membership after the binding
    lookup; a former member's stale binding is dropped as non_workspace_member
    + audited and never reaches chat_session (§4.3 safety property).
  * RedeemAndBind and BindInstallerTx replace the now-dead FK (23503) branch
    with an explicit IsWorkspaceMember gate, preserving the existing
    ErrBindingNotWorkspaceMember outcome without burning the token.

- router wires the ChannelStore into the patcher, typing indicator, dispatcher,
  hub, and the union_id/region backfills; constructor-based services wrap
  *db.Queries internally so their signatures and nil-check tests are unchanged.

Verified: go build ./... ; go vet ; gofmt ; go test -race ./internal/integrations/...
(full lark suite green unchanged + new membership drop/error tests). Adapter
field mappings (secret base64, union_id RMW, chat-id/open-id remaps, dedup,
token, card) checked end-to-end against a PG17 channel_* schema.

lark_* tables and queries remain (unused at runtime) until the S3 cleanup-hooks
and S4 drop-tables/rename commits.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-23 01:06:18 +08:00
J
0901e2077d feat(lark): channel-backed Feishu store + fix base64 backfill wrapping
Cutover step 1 of switching the lark Go code from lark_* onto the channel_*
tables (MUL-3515). Introduces the JSONB config boundary the rest of the
cutover sits on, and fixes a latent backfill bug surfaced while building it.

- migration 123: strip newlines from the app_secret_encrypted base64 backfill.
  PostgreSQL encode(...,'base64') MIME-wraps at 76 chars, and a secretbox-
  sealed ~72-byte secret exceeds that. Go's encoding/json decodes a JSON
  string into []byte with base64.StdEncoding, which rejects embedded newlines,
  so without the strip every migrated installation would fail to decrypt its
  app secret once reads move to channel_installation.config.

- store.go: flat domain types (Installation / UserBinding / ChatSessionBinding)
  with field parity to the retired db.Lark* rows, plus the feishu config codec.
  Row->domain mappers decode the JSONB config; the secret decoder is
  whitespace-tolerant so legacy MIME-wrapped data still round-trips, while the
  encoder emits unwrapped base64. Binding config encodes an absent union_id as
  "{}" so the upsert's jsonb_strip_nulls merge never clobbers a stored union_id.

- store_test.go: 72-byte secret round-trip, MIME-wrapped tolerance, optional
  null-strip, and flat-column preservation. Verified on PG17.

Field parity keeps the upcoming ~190 db.LarkInstallation call sites a
mechanical rename. No call sites switched yet; behavior unchanged.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-23 00:19:00 +08:00
J
c6ab060977 fix(channel): name app_id query param + multi-IM install key + null-safe binding merge
Addresses review on MUL-3515 (PR #4412):

- GetChannelInstallationByAppID: explicitly name params and cast app_id to
  ::text so sqlc emits AppID string. A bare $2 next to `config ->> 'app_id'`
  was mis-attributed to the JSONB config column, generating Config []byte.

- channel_installation uniqueness -> (workspace_id, agent_id, channel_type),
  with the UpsertChannelInstallation conflict key matched. Lets one agent
  hold one installation per IM (feishu + slack + ...) instead of a later
  install clobbering an earlier one. Behaviorally identical in the current
  feishu-only world; "one agent, at most one IM overall" stays an app-layer
  rule per MUL-3515 §4, not a DB constraint.

- CreateChannelUserBinding merges jsonb_strip_nulls(EXCLUDED.config) so a
  re-bind carrying {"union_id": null} no longer erases an already-captured
  union_id, restoring the old COALESCE(EXCLUDED.union_id, ...) semantics.

Regenerated with sqlc v1.31.1. Verified on PG17: re-install replaces in
place, feishu+slack coexist, null re-bind keeps union_id, real union_id wins.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-22 23:40:51 +08:00
J
37ca88be7e feat(channel): generalize lark_* tables into channel_* (DB layer)
Migration 123 creates channel_installation / channel_user_binding /
channel_chat_session_binding / channel_inbound_message_dedup /
channel_inbound_audit / channel_outbound_card_message /
channel_binding_token. Each carries a channel_type discriminator and a
JSONB config for platform-specific identifiers/credentials; cross-platform
columns stay flat. Existing Feishu rows are backfilled (channel_type=
'feishu', app_secret_encrypted via base64). NO foreign keys / cascades
(MUL-3515 §4) — integrity moves to the app layer in the cutover.

queries/channel.sql ports the lark query surface to channel_*, JSONB-aware,
plus DeleteChannelUserBindingsByWorkspaceMember /
DeleteChannelChatSessionBindingBySession for the app-layer cleanup that
replaces the removed cascades.

lark_* tables/queries are left in place here and removed once the Go
cutover lands, so this commit ships green on its own.

Verified: sqlc generate, go build ./..., full migrate chain (1..123) on
Postgres 17, and a real-data backfill spot-check (base64 round-trip,
NULL-strip, functional unique index on (channel_type, app_id)).

MUL-3515

Co-authored-by: multica-agent <github@multica.ai>
2026-06-22 18:21:06 +08:00
J
5374c21a99 feat(integrations): add platform-agnostic channel foundation
Introduce server/internal/integrations/channel — the contract every
inbound IM integration implements, so the core never learns a platform's
event JSON. Four pieces:

- Channel interface (Type/Connect/Disconnect/Send/Capabilities) + Factory
  + Config (channel_type + opaque JSON blob, maps to channel_installation).
- Normalized InboundMessage/OutboundMessage envelopes + Source/MediaRef/
  ReplyCtx/MsgType/ChatType. Envelope holds only cross-platform-true
  fields; platform specifics live in Raw, read only by the adapter.
- Capability bitmask: declaration only, no degrade logic in core.
- Registry: Type->Factory map, last-writer-wins, concurrency-safe.

Pure package (no DB/network/platform deps). Foundation for MUL-3515; the
lark cutover + lark_*->channel_* generalization land in follow-up PRs.

MUL-3515

Co-authored-by: multica-agent <github@multica.ai>
2026-06-22 17:54:06 +08:00
Multica Eve
b5a180b21e docs: update June 22 changelog (#4406)
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
v0.3.27
2026-06-22 17:30:31 +08:00
YYClaw
329384f052 chore(makefile): expand clean target to remove build caches (#4394)
Extend the clean target beyond server binaries/temp files to also
remove Next.js/source/Expo/Electron build outputs, Turbo caches, and
tsbuildinfo files across apps and packages.
2026-06-22 17:20:42 +08:00
Jiayuan Zhang
916cee5c5d feat(issues): open agent activity chip on hover (#4405)
The header 'agent is working' chip previously required a click to reveal the
activity card. Open it on hover instead so the live signal reads as a
glanceable status surface. The hover config lives on Base UI's Popover.Trigger
(openOnHover + delay/closeDelay), and the trigger stays a real button so
click/keyboard access is retained for touch and a11y.

Add a regression test asserting openOnHover is wired on the trigger so a
click-only implementation can no longer pass.

MUL-3507

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-22 17:06:15 +08:00
Naiyuan Qing
4fe8b54e9b MUL-3446: keep chat output in chat (#4387)
* MUL-3446: keep chat output in chat

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

* MUL-3446: simplify chat output guidance

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-06-22 15:51:03 +08:00
BeliyDym
5fd3d01d13 MUL-3502: OST-1161: Bound assignment comment catch-up
Squashed PR #4392. Updates assignment/comment catch-up guidance to use recent 10 and aligns related examples.
2026-06-22 15:46:47 +08:00
Jiayuan Zhang
cf30991f91 feat(sidebar): add dismissible Join Discord card (#4400)
Add a Join Discord promo card pinned to the bottom of the left sidebar
(above the help launcher). Dismiss state persists per-user in
localStorage so it stays hidden once closed.

Extract the shared DiscordIcon + invite URL into layout/discord.tsx so
the help launcher and the card reuse one source. i18n copy added for
en / zh-Hans / ja / ko.

MUL-3505

Co-authored-by: multica-agent <github@multica.ai>
2026-06-22 09:35:41 +02:00
YOMXXX
9d053c57f9 MUL-3420: fix(runtimes): clarify custom runtime deletion 2026-06-22 15:01:22 +08:00
Bohan Jiang
8a9f15dbc9 feat: add Discord community entry points (#4388)
Add a Discord invite (https://discord.gg/W8gYBn226t) in three places:
- Website footer: social icon + link in the Resources group (en/zh/ja/ko)
- In-app help menu: Discord item in the help launcher (all 4 locales)
- GitHub repo README: badge + link (README.md and README.zh-CN.md)

MUL-3492

Co-authored-by: multica-agent <github@multica.ai>
2026-06-22 14:00:36 +08:00
Bohan Jiang
5556f4570b fix(issue): skip child-done notification when parent is in backlog (#4391)
When a sub-issue transitions to done, notifyParentOfChildDone posts a
system comment on the parent and wakes the parent's assignee. A parent
deliberately parked in backlog should stay inert: waking it lets the
assignee promote sibling backlog sub-issues into todo, which is the
unwanted auto-activation reported in GitHub Discussion #4320 (MUL-3497).

Add a backlog guard alongside the existing done/cancelled guard so the
whole notification (comment + mention + trigger) is skipped until the
user explicitly moves the parent out of backlog.

MUL-3497

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-22 13:56:48 +08:00
Bohan Jiang
b13e1808a4 refactor(codex): make permission approval auto-grant observable (#4390)
The daemon auto-grants Codex item/permissions/requestApproval requests by
echoing back the network / fileSystem profile scoped to the current turn.
Previously a malformed params payload and any permission key outside
network / fileSystem were dropped silently, so a future app-server
protocol that adds a new permission shape would be narrowed away with no
trace in daemon logs.

Log both cases (parse failure and dropped keys) without changing the
granted response. Addresses review nits on #4346 / MUL-3451.

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-22 13:43:42 +08:00
lethean-kun
0aa3b53c25 MUL-3378 feat(lark): reply inside the originating thread (话题) instead of the group (#4262)
* feat(lark): reply inside the originating thread (话题) instead of the group

When a user @-mentions the bot inside a Lark topic/thread, the bot now
replies back into that thread rather than posting a fresh message at the
chat level. Behavior is automatic and scoped: only triggers that were
themselves inside a thread get a threaded reply, so normal group/p2p
chats are unchanged.

The outbound path is event-driven and decoupled from the inbound
message, so the trigger message_id + thread_id are persisted on
lark_chat_session_binding (migration 122) at ingest time. The patcher
then routes the agent reply (text / markdown card / error card) and the
OutcomeReplier notices (/issue confirmation, offline/archived) through
Lark's reply endpoint with reply_in_thread=true when a thread is present,
falling back to a chat-level send if the threaded reply fails.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(lark): classify thread-reply failures before chat-level fallback

Only retry a threaded reply at the chat level when Lark returns an
explicit "this message/topic cannot receive a threaded reply" error
(recalled trigger, topic gone, topics disabled, aggregated message,
etc.). Transport errors, 5xx, timeouts, rate limits, and ambiguous
failures are now logged and returned as failures instead of being
retried, so we never duplicate a reply or leak a thread-only reply
into the main group chat.

The three reply-capable send methods now return a structured *APIError
carrying the Lark business code, and isThreadReplyUnsupported drives
the fallback via an allowlist. sendWithThreadFallback is promoted to a
package-level function so the immediate OutcomeReplier sends (/issue
confirmation, offline/archived notices) share the same classified
fallback path instead of silently swallowing thread-reply failures.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: kun <kuen@micous.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 13:34:40 +08:00
Bohan Jiang
39ab355585 fix(skills): authenticate raw.githubusercontent.com downloads for private repo imports (#4389)
Skill import builds raw.githubusercontent.com URLs by hand and fetches them
via fetchRawFile, which sent no Authorization header. GitHub API calls were
authenticated by #2215 (doGitHubAPIGet/addGitHubAuthHeader) but the raw
content download path was missed, so importing a skill from a private/internal
GitHub repo listed the directory fine and then 404'd on the actual file
download, surfacing as a generic 502.

Attach the existing GITHUB_TOKEN bearer header in fetchRawFile, but only when
the URL host is raw.githubusercontent.com. fetchRawFile is shared with
clawhub.ai / skills.sh downloads, so the token must not leak to those hosts.
The host gate is extracted into newRawFileRequest so it is unit-testable
without a live round-trip.

MUL-3496

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-22 13:34:06 +08:00
Bohan Jiang
81bde585ba MUL-3467: batch load squad roster skills (#4386)
* MUL-3467: batch load squad roster skills

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

* test: isolate redis-backed test databases

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

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-22 13:14:22 +08:00
hal9000botagent
91e6c779d6 feat(squad): surface member skills in leader briefing roster (#4363)
When an issue is assigned to a squad, only the leader is triggered. The
leader briefing's Squad Roster listed each member's name, type, role, and
mention link — but not the member's assigned skills, so the leader had to
infer capability from the free-text role label when deciding who to
delegate to.

renderMemberRow now loads each agent member's assigned skills via
ListAgentSkillSummaries and formatRosterRow renders them as
"skills: a, b" (or "no skills assigned" when the agent has none). Builtin
multica-* skills are excluded (they live outside agent_skill); human
members carry no skills segment; a skill-lookup error degrades to the
prior name+role row rather than asserting a misleading "no skills".
Operating-protocol step 1 now tells the leader to match the task to each
member's listed skills.

Updates the multica-squads builtin skill and its source map to document
the new roster content, and adds
TestBuildSquadLeaderBriefing_MemberSkillsInRoster.

Co-authored-by: hal9000botagent <hal9000botagent@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 12:33:12 +08:00
Stiliyan Monev
9d7060caf1 fix(auth): autofocus OTP input on verification step (#4344)
* fix(auth): autofocus OTP input on verification step

The email-verification step renders the OTP input without focus, so
users must click the field before typing the code. This is friction on
every login, especially when switching accounts.

Add `autoFocus` to the InputOTP so the cursor lands in the field as
soon as the step mounts. Mirrors the existing email-step input and the
mobile OTP component, both of which already autofocus.

* test(web): polyfill document.elementFromPoint for input-otp in jsdom

Autofocusing the OTP input makes input-otp run its focus-time DOM
measurement, which calls document.elementFromPoint. jsdom doesn't
implement it, so the web login test threw an unhandled error.

packages/views/test/setup.ts already stubs this for the same reason;
mirror the stub in the web test setup (which already stubs
ResizeObserver for input-otp).

* test(auth): assert OTP input autofocuses on verification step

Guards the autofocus behavior: the test fails if the autoFocus prop is
removed from the verification-step InputOTP. Lives in packages/views
since it covers shared component behavior, mocking @multica/core.
2026-06-22 10:00:57 +08:00
Naiyuan Qing
6d0e875dbb feat: add opt-in react-grab dev element inspector (web + desktop) (#4381)
* feat(web): add opt-in react-grab dev element inspector

Loads the react-grab overlay (hold ⌘C / Ctrl+C + click to copy an
element's source path + component stack) only when REACT_GRAB is set in
a local, gitignored apps/web/.env.local. Both the NODE_ENV and REACT_GRAB
guards are evaluated server-side in the root layout, so the <Script> tag
is omitted from the HTML for anyone who hasn't opted in — no effect on
other developers or production.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(desktop): add opt-in react-grab dev element inspector

Mirrors the web wiring for the Electron renderer: injects the react-grab
overlay (hold ⌘C / Ctrl+C + click to copy an element's source path +
component stack) only when VITE_REACT_GRAB is set in a local, gitignored
apps/desktop/.env.development.local. Guarded by import.meta.env.DEV so the
branch is tree-shaken out of production builds; never activates for other
developers. No CSP/sandbox blocks the unpkg script (webSecurity is off).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(web): unify react-grab opt-in var to VITE_REACT_GRAB

Use the same env var name as the desktop renderer so one variable name
controls both apps. The desktop renderer is bundled by Vite, which only
exposes VITE_-prefixed vars to client code, so the shared name must carry
the VITE_ prefix; web reads it server-side where the name is unconstrained.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 09:49:24 +08:00
YOMXXX
737c976b0d fix(cli): guide remote setup callbacks (#4360)
## Summary
- expose `--callback-host` on `multica setup` and `multica setup cloud`, reusing the existing login callback override
- print an SSH tunnel hint when browser login runs in an SSH session with a loopback callback
- show local flags in parent-command help so `multica setup --help` documents the callback option

Fixes #4357

## Tests
- `go test ./cmd/multica -run 'TestCallbackHostFlagValueReadsParentSetupFlag|TestSetupHelpShowsCallbackHostFlag|TestSetupCallbackHostFlagWiring|TestBrowserLoginInstructionsSSHRemoteHint'`
- `go run ./cmd/multica setup --help`
- `go run ./cmd/multica setup cloud --help`
- `git diff --check`
- `go test ./...`
2026-06-22 00:06:14 +08:00
beast
31d942d010 MUL-3438: fix(projects): require admin for project deletion (#4327)
* fix(projects): require admin for project deletion

* test(projects): clean up orphaned member rows in delete-permission helper

The schema uses no foreign keys or cascades, so deleting the test user
left its member row behind in the shared test workspace, polluting later
tests in the package. Delete the member row before the user in both the
pre-seed cleanup and t.Cleanup.

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

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-21 23:54:58 +08:00
Hzzzzzx
745832b536 MUL-3433: fix(daemon): restore claim slow-log payload observability without gzip
Squash merge PR #4322 after review approval.\n\nMUL-3433
2026-06-21 23:41:05 +08:00
Multica Eve
140a113c38 docs: add 2026-06-19 changelog entry (#4340)
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-21 23:28:15 +08:00
YOMXXX
4bbaf5363c fix(codex): handle app-server permission requests (#4346) 2026-06-20 13:42:27 +08:00
Jiayuan Zhang
c0c41fa0b4 fix(views): gate right sidebar motion to toggles (#4335) 2026-06-19 16:40:49 +08:00
Jiayuan Zhang
06ae9b2a0c fix sidebar issue pin labels (#4334) 2026-06-19 16:23:10 +08:00
Jiayuan Zhang
49e1c2a8af docs: simplify agent guidance (#4333) 2026-06-19 16:22:16 +08:00
Jiayuan Zhang
27fcbb015f Polish desktop sidebar motion
Polish desktop chrome/sidebar alignment and add motion-based transitions for left and right sidebars.
2026-06-19 06:26:14 +02:00
Jiayuan Zhang
ba7be23f7c fix pinned sidebar active state
Fix sidebar active state so exact pinned routes do not also highlight their parent Workspace nav item.
2026-06-19 06:25:58 +02:00
LinYushen
e1c6754304 Revert "fix: gzip daemon claim responses (#4259)" (#4307)
This reverts commit 2f398c36ad.
v0.3.26
2026-06-18 17:58:43 +08:00
Multica Eve
990e7a6d74 MUL-3421: add 2026-06-18 changelog entry (#4305)
* docs: add 2026-06-18 changelog entry

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

* docs: clarify 0.3.25 changelog title

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

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: J <j@multica.ai>
v0.3.25
2026-06-18 17:27:14 +08:00
Naiyuan Qing
9eaea31892 fix(realtime): guard WS frames without a string type + drop benign ResizeObserver noise (MUL-3418) (#4304)
* fix(realtime): drop WS frames without a string type (MUL-3418)

The onmessage handler dispatched every parsed frame to onAny and the
ws.on subscribers without validating its shape. A frame whose parsed JSON
lacked a string `type` (an out-of-protocol frame injected by a proxy /
extension, or a bare JSON primitive) reached the realtime-sync onAny
dispatcher, where `msg.type.split(":")` threw an uncaught TypeError out of
onmessage. The browser reported it via window.onerror, flooding PostHog
`$exception` (~12k events). Non-fatal (no crash, connection stays up), but
pure noise.

Validate once at this trust boundary: a frame must be an object carrying a
string `type`, otherwise drop it as a no-op. The bad-frame log is
rate-limited to one entry per connection — a misbehaving source can repeat
the frame hundreds of times per session.

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

* feat(analytics): drop benign ResizeObserver exceptions from telemetry

ResizeObserver "loop ..." errors are the dominant `$exception` bucket
(~31k events / ~1.5k users). They are a benign, self-recovering browser
quirk with no actionable signal and otherwise drown real failures and burn
the event budget. Drop them entirely in before_send (ahead of redaction
and the dedupe fuse, which only caps repeats). The match is narrow — only
the benign "loop" phrasing — so a genuine ResizeObserver bug still reports.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-18 17:24:48 +08:00
Multica Eve
8b3b054d17 test(agent): cover Hermes custom model IDs with colons (#4300)
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-18 15:51:13 +08:00
Hzzzzzx
b4c9e4423c test: enable -race detector in Go test pipeline (WOR-61) (#4274)
* test: enable -race detector in Go test pipeline (WOR-61)

Add the -race flag to all three Go test invocation sites so the existing
concurrency regression harness (workdir_race_test.go for #3999,
runtime_gone_test.go, runtime_profile_drift_test.go) actually exercises
the race detector. The daemon package alone has 28+ goroutine launch
points with no automated race coverage before this change.

Sites updated:
- Makefile:299        (make test, local)
- .github/workflows/ci.yml:101      (CI backend job)
- .github/workflows/release.yml:55  (release verify job)

go test already runs a vet subset by default, so no separate -vet flag
is added. No production code touched.

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

* test(execenv): serialize runtimeGOOS-mutating test (WOR-61)

TestInjectRuntimeConfigIssueMetadataCodexFormattingUnchanged called
t.Parallel() while mutating the package-level runtimeGOOS to drive the
windows/linux branches, racing with the other parallel tests that read
runtimeGOOS in buildMetaSkillContent. The -race flag enabled in the
prior commit surfaced it as 3 WARNING: DATA RACE reports and 11
"race detected" failures in CI (only the execenv package failed).

Drop t.Parallel() and add the "// Not parallel: mutates the package-level
runtimeGOOS." comment already used by the six sibling writer tests across
execenv_test.go and reply_instructions_test.go. This is test-isolation
only; no production code, no mutex/atomic, no signature change.

Verified locally:
  go test -race -count=1 ./internal/daemon/execenv/   -> ok 2.276s
  go test -race -count=1 ./internal/daemon/...        -> all 3 pkgs ok

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

---------

Co-authored-by: hzz <331380069@qq.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-18 15:50:24 +08:00
LinYushen
0d0edac32f feat(daemon): discover local skills from ~/.agents/skills (MUL-3333) (#4244)
* feat(daemon): discover local skills from ~/.agents/skills (MUL-3333)

Upgrade local skill discovery and import from a single provider root to an
ordered multi-root scan: the runtime's own skill directory (e.g.
~/.claude/skills) first, then the cross-tool universal root ~/.agents/skills.

- Rename localSkillRootForProvider -> localSkillRootsForProvider, returning
  ordered roots [provider, universal] with a kind classifier.
- listRuntimeLocalSkills iterates the roots, gives each root its OWN visited
  set (so a cross-root symlink alias is not collapsed), dedupes strictly by
  Key with the provider root winning, and sorts once after the merge.
- loadRuntimeLocalSkillBundle walks the same priority order and only falls
  through to the next root on os.IsNotExist; any other stat error is returned
  so import never silently resolves a different same-key skill.
- Add a Root ("provider" | "universal") field to the local skill summary
  (daemon + handler structs and the TS RuntimeLocalSkillSummary type) so the
  UI can label a skill's origin without a future schema break.

Backward compatible: every skill visible today keeps its Key, SourcePath and
FileCount; the universal root only surfaces additional, non-conflicting skills.

Out of scope (follow-up issues): execution-time injection of ~/.agents/skills
into runtimes (e.g. Codex seedUserCodexSkills) and workspace-relative
.agents/skills discovery.

Tests cover universal-root discovery + import, provider-wins conflict
priority, both-roots merge, missing/both-missing roots, nested layouts,
IsNotExist fallback, the no-fallthrough-on-read-error guarantee, and the
per-root visited cross-root symlink alias. Docs updated in en/zh/ja/ko.

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

* fix(daemon): fall through to next root when a same-key dir has no SKILL.md

loadRuntimeLocalSkillBundle previously only fell through to the next root on
os.IsNotExist for the skill DIRECTORY. A provider-root directory that shares a
skill's key but contains no SKILL.md (so listRuntimeLocalSkills descends past
it and surfaces the universal-root skill instead) made load stop on the
invalid provider dir and error — list and load disagreed, and the import the
user picked from the list could not be fetched.

Make the validity predicate match list: a root "has" the skill at a key only
when it is a directory containing a SKILL.md. A missing entry, a non-directory,
or a directory without a SKILL.md all mean "this root doesn't have it" and we
continue to the next root. Only a genuine non-IsNotExist stat error or an
unreadable existing SKILL.md (permission/IO) is returned, so we still never
silently substitute a different-content same-key skill from a lower-priority
root (Eve review #1, preserved by the existing read-error guard test).

Adds regression tests for the provider-dir-without-SKILL.md and provider-non-dir
fall-through cases.

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-06-18 15:24:41 +08:00
Naiyuan Qing
e4b53eb5c0 fix(editor): stop chat @mention groups from overlapping (#4297)
In context mode (chat) a query merges context items (current page / recently
viewed) with search results into one popup. The old contextLayout made only the
"Recent" group scrollable (`min-h-0`) while every other group was `shrink-0`, and
the Recent `<section>` did not clip its own overflow. When the search groups
(Users/Issues) filled the height, the flex algorithm squeezed Recent toward zero
and its un-clipped rows painted on top of the groups below — the overlap users saw.

Collapse the two render branches into a single `overflow-y-auto` flex column so
every group just stacks and the whole popup scrolls; no group can collapse onto
another. The context variant only differs in width / max-height / chrome.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:58:00 +08:00