Commit Graph

3549 Commits

Author SHA1 Message Date
J
5e71df77c7 test(transcript): cover coalesce created_at behavior
Lock in that coalescing streaming fragments carries the latest
created_at, and falls back to the previous timestamp when the merged
fragment has none.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-09 19:54:50 +08:00
J
6aa946a7f0 refactor(transcript): reuse taskMessageToPayload in WS broadcast
The ReportTaskMessages WebSocket broadcast hand-built the payload and
duplicated the created_at formatting that taskMessageToPayload already
does. Reuse the helper with the just-inserted row, which carries the
same redacted values and the DB-assigned timestamp.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-09 19:54:49 +08:00
Antoine GIRARD
9f21d0b634 feat(transcript): add timestamps to run transcript entries (MUL-3174) (#3951)
Threads the existing task_message.created_at column through the full stack (Go protocol -> REST/WS handlers -> TS types -> transcript dialog) so agent run transcripts show per-entry timestamps, helping users spot stalled runs. Additive, no migration.
2026-06-09 19:53:30 +08:00
Naiyuan Qing
6d646db577 fix(issues): soften comment sticky mask (#3956) 2026-06-09 18:27:49 +08:00
Multica Eve
d9347f0715 MUL-3175: add June 9 changelog entry (#3954)
* docs(changelog): add June 9 release notes

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

* docs(changelog): polish zh release copy

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

* docs(changelog): clarify zh release 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>
2026-06-09 18:17:10 +08:00
Naiyuan Qing
b542c40936 fix(issues): keep sticky comment highlight consistent (#3955)
* fix(issues): keep sticky comment highlight consistent

* fix(issues): remove reply resolve-thread action
2026-06-09 18:02:11 +08:00
Naiyuan Qing
2bdc8344dd fix(issues): align sticky comment header padding (#3952) 2026-06-09 17:17:14 +08:00
shutcode
a2ef95445b MUL-2794 fix(agent): stop Cursor sessions on terminal result (#3165)
Treats Cursor's stream-json terminal `result` event as the protocol completion boundary so a lingering Cursor worker process can no longer hold the daemon task open after the agent has produced its final result.

- Tighten `cmd.WaitDelay` to 500ms (set before `Start()`)
- Set `resultSeen` and `cancel()` on terminal `result`
- Preserve completed/failed status across the cancellation via two `!resultSeen` guards in the post-loop status decision
- Add unix fake-CLI coverage for success and `is_error` terminal results
2026-06-09 16:49:48 +08:00
Naiyuan Qing
c983905d5c feat(issues): per-comment thread resolution with sticky collapse (#3910)
* feat(issues): per-comment thread resolution with sticky collapse

Allow resolving any comment, not just roots. Resolving a root folds the
whole thread into one bar (existing); resolving a reply marks it as the
thread's resolution ("Resolve thread with comment") and folds the other
replies behind a "N comments" bar, with the resolution kept visible and
badged. Which comment is the resolution is a pure frontend derivation
(root wins, else latest resolved reply), so no write-side bookkeeping is
needed and any resolved_at combination renders one resolution.

- backend: drop the "only root comments can be resolved" guard
- views: deriveThreadResolution + reply-resolution rendering, sticky
  collapse/fold bars (overflow-clip on the card so sticky resolves to the
  timeline scroll parent), scroll the folded thread back into view on
  collapse, ListChevronsDownUp icon, locales (en/ja/ko/zh-Hans)

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

* feat(issues): sticky comment headers for long comments

Pin each comment's header (root + replies) to the timeline's scroll
parent while reading, so a long comment keeps its author + actions
visible instead of scrolling out of reach. Exactly one header is pinned
at a time:

- Reply headers stick within their own CommentRow box (release at the
  reply's end).
- The root header is wrapped in a root-section container so its sticky
  containing block spans only the header + root body — without it the
  containing block is the whole thread and the root header stays stuck
  behind every reply. Replies render outside the wrapper, gated on open.
- Skip the root header sticky whenever a resolution collapse bar already
  owns the top-0 slot (root resolved+expanded, or reply-resolution
  expanded) to avoid two bars stacking at the same offset.

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-09 16:32:22 +08:00
LinYushen
70ccbd9bce Revert "MUL-3132: harden /uploads/* (auth, no listing, nosniff, tight CSP) (#…" (#3944)
This reverts commit 8ff68502fc.
2026-06-09 14:50:56 +08:00
Bohan Jiang
998ebe97e4 fix(autopilot): fail create-issue runs on any terminal task failure (#3943)
Generalize SyncRunFromLinkedIssueTask beyond Codex no-progress: any
terminal create-issue task failure with no retry still in flight now fails
the linked autopilot run, so it can no longer hang in issue_created
(invisible to the failure-rate auto-pause monitor).

- fail the linked run for any terminal task failure, gated by the existing
  HasActiveTaskForIssue wait-for-retry guard
- remove the isNoProgressTaskFailure classifier (subsumed; drops duplicated
  pkg/agent marker literals)
- drop the redundant GetIssue/origin lookup; GetAutopilotRunByIssue leads
  and short-circuits ordinary failures in one query
- tests: keep no-progress regression, add agent_error (non-retryable) and
  retry-pending cases

Follow-up to #3927. VEN-661 / VEN-662 / MUL-3164
2026-06-09 14:48:20 +08:00
Naiyuan Qing
90b639888d fix(issues): confine inbox deep-link scroll to the timeline container (#3929) (#3942)
Opening an inbox comment notification on an issue with a running agent
shoved the whole desktop page — header included — off the top, and no
amount of scrolling brought it back; only toggling the right sidebar
(which reflows the panel group) restored it.

Root cause: the deep-link landing uses native
scrollIntoView({block:"center"}) on the target comment. Native
scrollIntoView is spec'd to scroll EVERY scrollable ancestor. On a cold
mount where the timeline is still streaming (is-working) and the sidebar
panel starts collapsed, the inner timeline scroller can't center the
target on its own, so the scroll propagates up and scrolls the desktop
shell's overflow:hidden wrapper (desktop-layout.tsx). That wrapper has no
scrollbar and doesn't auto-clamp, so the page stays shoved up until a
resize reflows it. Desktop-only: web sits in a document-scroll context
with a real scrollbar that self-corrects.

Fix: drive the timeline container's scrollTop directly and re-center
across rAF frames until async heights settle, instead of leaning on the
ancestor scroll. The scroll never touches an ancestor, so the header can
no longer be pushed off-screen.

Tests assert the user-facing contract (lands on + highlights the target
comment) rather than the scroll mechanism, which jsdom can't lay out.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 14:46:46 +08:00
elrrrrrrr
254ec945f5 fix(agent/codex): shut down gracefully so OTEL telemetry flushes (#3888)
Codex telemetry was never reaching the OTLP collector for tasks run by the
daemon. The per-task config (including the [otel] block) is copied into
CODEX_HOME correctly, but the lifecycle goroutine closed stdin and then
immediately cancelled the run context, which SIGKILLs the app-server. Codex's
OTEL batch exporters only force-flush on a graceful shutdown, so the buffered
spans/metrics/logs were dropped before they could be exported — short tasks
lost everything, long tasks lost the final batch.

Let codex exit on its own after stdin EOF (running its shutdown + flush path)
and only force-cancel after a bounded grace period if it doesn't, so the reader
goroutine still can't block forever. Also set cmd.WaitDelay, matching the other
long-lived backends (claude, copilot, cursor, …).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 14:46:07 +08:00
Multica Eve
13e9485a3b MUL-3130: persist stable /api/attachments/<id>/download URL in comment markdown (#3937)
* MUL-3130: persist a stable attachment download URL in comment markdown

Comment image attachments rendered as broken placeholders ~30 minutes
after upload because the editor was persisting a short-lived
HMAC-signed URL into the comment body. After PR #3903 (MUL-3132)
hardened /uploads/* with auth, `attachmentToResponse` started signing
`attachment.url` as `/uploads/<key>?exp=<unix>&sig=<HMAC>` for
LocalStorage so token-auth clients could keep loading inline images.
The signature has a 30-min TTL by design — but `useFileUpload` was
returning that signed value as `link` and the editor was writing
`![file](signed-url)` straight into the markdown, so the comment
permanently captured a URL that stopped working as soon as the
signature expired.

The fix is to persist a stable per-attachment URL that the server can
re-sign on every request:

* `useFileUpload` now returns `link = /api/attachments/<id>/download`
  (avatar uploads without an id still fall back to `att.url` so the
  pre-attachment-row code paths keep working).
* `DownloadAttachment` self-resolves the workspace from the attachment
  row instead of reading X-Workspace-Slug / X-Workspace-ID headers,
  and the route is registered under the auth-only group so a native
  browser <img>/<video> resource load (which cannot attach those
  headers) succeeds. Membership is checked inside the handler with
  a 404 deny shape so the route does not act as an IDOR oracle.
* A new `GetAttachmentByIDOnly` SQL query supports the workspace-
  derivation step.
* `AttachmentDownloadProvider` now extracts the attachment id from
  the stable URL when matching markdown refs to attachment records,
  with a fallback to the existing url-equality check for legacy
  comments (and S3/CloudFront markdown that points straight at the
  CDN).
* `contentReferencesAttachment` covers both URL shapes for the
  composer / standalone-list dedup paths so an attachment uploaded
  before the fix and one uploaded after both deduplicate cleanly.

Tests:
- New unit tests for the URL helpers (16 tests, packages/core).
- Backend regression test: bare `<img src>`-style request without
  workspace headers now succeeds for a member (200) and 404s for a
  non-member, replacing the previous "400 without workspace context"
  contract.
- Existing TestDownload*, TestServeLocalUpload*, TestAttachmentTo
  Response* and the 1220 frontend views tests all pass.

Refs: MUL-3130, GitHub issue #3891
Co-authored-by: multica-agent <github@multica.ai>

* MUL-3130: address PR review — split markdown link from upload link, swap render src

Two follow-ups from GPT-Boy's review on PR #3937.

(1) Don't reroute every upload consumer through the workspace-gated
    download endpoint.

    The previous change made `useFileUpload`'s `link` field unconditionally
    return `/api/attachments/<id>/download` whenever the upload had an id.
    But `useFileUpload` is also used by avatar / logo pickers
    (account-tab, workspace-tab, agents/avatar-picker, squads/squad-detail-page)
    that persist `result.link` directly into `avatar_url`. Avatars are
    referenced cross-workspace (mention chips, member lists, inbox
    items), so binding their URL to a workspace-membership-gated
    endpoint would silently break cross-workspace avatar visibility.

    The fix splits the URL into two semantically distinct fields:

      - `link`         — same as `att.url` (legacy contract). Avatar /
                          logo callers continue to use this and remain
                          on whatever URL semantics the storage backend
                          dictates.
      - `markdownLink` — the stable per-attachment URL
                          `/api/attachments/<id>/download`. Only the
                          editor's markdown-persisting flow consumes
                          this. Falls back to `link` for the
                          no-workspace upload branch (where there is
                          no attachment-row id to address).

    `editor/extensions/file-upload.ts` switches `image.src` and
    `fileCard.href` to `markdownLink ?? link` so comment markdown gets
    the stable shape while avatar callers stay on `link` unchanged.

(2) Make the render-time img src loadable for token-mode clients.

    Persisting the stable `/api/attachments/<id>/download` URL fixes the
    expiry problem but the path itself sits behind `middleware.Auth`,
    which expects either a `multica_auth` cookie or a Bearer token in
    `Authorization`. Native `<img>`/`<video>` resource loads from
    token-mode clients (Electron's default mode, the mobile app,
    legacy-token web sessions) cannot attach the Authorization header,
    so the bare URL would 401 immediately rather than 30 minutes later.

    `Attachment.normalize` now runs the resolved record through a new
    `pickInlineMediaURL` helper that returns:

      - `record.download_url` when it's an absolute URL with a
         recognised CDN signature query (CloudFront-signed
         `Signature` / `Expires` / `Key-Pair-Id`, or
         `X-Amz-Signature` for raw S3 presigns) — these load as
         native resource src in any client.
      - else `record.url`, which on the LocalStorage backend carries
         a freshly-minted `/uploads/<key>?exp&sig` query whose
         signature IS the auth (token-mode-loadable). On non-CF S3
         backends this is the raw stored URL — same behaviour as
         today.
      - else the original input URL (legacy / unresolved markdown
         keeps its existing path).

    This gives the same effect for both `kind: "record"` and
    `kind: "url"` attachment inputs: once a record is in hand, the
    rendered media src is whichever URL the current backend exposes
    a working signature on.

Tests:

  - New `file-upload.test.ts` regression pinning that `markdownLink`
    is what lands in the markdown body when the upload result returns
    both a short-lived storage URL and a stable download path.
  - Updated `attachment.test.tsx` to reflect the new render-time
    swap (the rendered img src now follows the freshly signed URL,
    not the raw storage URL) and added a record-mode regression
    pinning the LocalStorage default — when `download_url` is the
    bare /api/attachments/<id>/download path, the renderer must fall
    through to the signed `record.url`.
  - Updated `chat-input.test.tsx` makeUpload helper for the new
    `markdownLink` UploadResult field.
  - 1222 frontend views tests + 507 core tests + typecheck across
    @multica/{core,ui,views} all pass.

Refs: MUL-3130, GitHub issue #3891. Builds on a740f7a35.
Co-authored-by: multica-agent <github@multica.ai>

* MUL-3130: chat upload map keys on persisted markdownLink, not the short-lived link

GPT-Boy's second-round review on PR #3937 caught a chat-only blocker
left over from the previous fix.

After the previous commit split `UploadResult.link` into `link`
(legacy avatar/logo URL) and `markdownLink` (stable per-attachment
URL persisted into markdown), the comment editor's image src + file
card href correctly switched to `markdownLink ?? link`. But chat
input still kept the upload-map key on the old `link`:

  uploadMapRef.current.set(result.link, result.id)
  …
  if (content.includes(url)) activeIds.push(id)

In the LocalStorage backend `link` is the short-lived
`/uploads/<key>?exp=&sig=` URL. The editor persists the stable
`/api/attachments/<id>/download` URL into the message body, so
`content.includes(url)` never matches and the send call drops
`attachment_ids`. The attachment ends up bound only to the chat
session, not to the message — agents reading message-level metadata
see no attachments.

Fix: key the upload map on the same value the editor actually wrote
into the markdown body (`markdownLink || link`). The
`content.includes(url)` check then matches and the attachment id is
correctly forwarded on send.

Tests:

- Updated the chat-input mock editor to insert `markdownLink || link`
  into its value, mirroring the real editor's persisted-URL choice
  (uploadAndInsertFile in editor/extensions/file-upload.ts). Without
  this the mock would silently paper over the bug.
- Added a regression test where the upload result returns a
  short-lived `link = /uploads/...?exp&sig` and a stable
  `markdownLink = /api/attachments/<id>/download`. Asserts (a) the
  message body carries the stable URL and never the signed query,
  and (b) the bound `attachment_ids` includes the attachment id.

All 1223 frontend views tests pass (was 1222, +1 new regression).
Typecheck and 507 core tests still green.

Refs: MUL-3130, PR #3937 review by GPT-Boy. Builds on f66a522d0.
Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-09 14:26:36 +08:00
stevenayl
ee6200de25 fix(autopilot): fail no-progress issue runs (#3927)
Fail create-issue autopilot runs that hang in issue_created after a Codex
no-progress / semantic-inactivity task failure, so they surface as failed
and count toward the failure-rate auto-pause monitor.

- route failed create-issue issue tasks (no direct autopilot_run_id) into linked run sync
- fail linked runs only for Codex no-progress / semantic-inactivity failures
- wait when an active retry task still exists for the issue
- add classifier coverage + a DB-backed listener regression

VEN-661 / VEN-662 / MUL-3164
2026-06-09 14:25:04 +08:00
Chenyu-24601
8b94764c47 feat(daemon): configurable OpenClaw binary path / state dir via CLIConfig.Backends (MUL-3157)
Summary:
- Add CLI config schema for OpenClaw backend binary path and state dir overrides.
- Apply those overrides during daemon LoadConfig using the existing env-var based probe/spawn path.
- Cover backward compatibility, precedence, partial overrides, and fail-soft config loading.

Verification:
- go test ./internal/cli ./internal/daemon
- go vet ./internal/cli ./internal/daemon
- GitHub CI passed
2026-06-09 14:05:37 +08:00
Bohan Jiang
42251b42fc fix(cli): honor MULTICA_SERVER_URL in setup self-host (#3912) (#3938)
* fix(cli): honor MULTICA_SERVER_URL in setup self-host

`multica setup self-host` resolved the backend URL only from the
--server-url flag, falling back to http://localhost:8080 when the flag
was absent. It never consulted MULTICA_SERVER_URL, even though that env
var is documented on the root --server-url flag and in `multica --help`,
and is honored by every other command via resolveServerURL. A self-host
user who set the env var instead of the flag still hit localhost and got
"Server at http://localhost:8080 is not reachable".

Route server-url and app-url through cli.FlagOrEnv so the documented env
vars (MULTICA_SERVER_URL / MULTICA_APP_URL) are honored when the matching
flag is not set, with the flag still taking precedence. userProvided now
reflects flag-or-env, so an env-sourced remote URL still triggers the
explicit app_url prompt. Not platform-specific despite the report.

Fixes GitHub #3912.

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

* fix(cli): normalize MULTICA_SERVER_URL in setup self-host

MULTICA_SERVER_URL is documented as a ws:// daemon address
(ws://localhost:8080/ws) and every other command normalizes it via
NormalizeServerBaseURL before use. setup self-host consumed the resolved
value raw and probed <url>/health, so a self-hoster who set the
documented ws:// form would still fail the reachability check.

Run the flag/env value through normalizeAPIBaseURL (ws->http, wss->https,
strip /ws) so the documented form works and the stored server_url stays a
clean http(s) base. Add a normalization test case and a focused test for
the MULTICA_APP_URL env path (review nit).

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

* docs(self-host): note setup self-host honors MULTICA_SERVER_URL / MULTICA_APP_URL

Document that `setup self-host` reads the env vars when the matching flag
is omitted (flag wins), and that MULTICA_SERVER_URL accepts the ws://…/ws
daemon form. Added to en/zh/ja/ko quickstart for parity.

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-09 14:02:06 +08:00
Bohan Jiang
7dc05d28bc fix(projects): validate project status/priority — return 400 instead of 500 (#3925) (#3939)
* fix(projects): return 400 (not 500) for invalid project status/priority

CreateProject/UpdateProject passed an unvalidated status/priority straight to
the INSERT, so an unknown value (e.g. --status active) tripped the table's
CHECK constraint and surfaced as a blanket 500 'failed to create project'
with no server-side log to diagnose it (#3925).

Pre-validate both enums against the column CHECK lists and return a 400 with
the allowed values. Back it with isCheckViolation -> 400 for any other
constrained column, and log the underlying error on genuine 500s so transient
DB failures are diagnosable.

MUL-3153

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

* fix(cli): validate project --status in create/update

project create and project update forwarded --status to the server without
checking it, while project status already validated. Share a single
validateProjectStatus helper across all three so a typo fails fast with the
valid list instead of a server round-trip.

MUL-3153

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-09 13:54:53 +08:00
LinYushen
9ff801f926 docs(cli): error-message conventions + sign-in copy (PR3, MUL-3104) (#3900)
* docs(cli): add Error Messages conventions + refine sign-in copy (PR3)

Final pass of the CLI error-message work (MUL-3104).

- CLI_AND_DAEMON.md: new "Error Messages" section documenting the user-facing
  contract — friendly single-line messages, server validation passthrough,
  English default with automatic Chinese on a zh locale, the tiered exit codes
  (0/1/2/3/4/5), --debug / MULTICA_DEBUG for the full chain, and
  MULTICA_HTTP_TIMEOUT.
- cmd_auth.go: clarify three high-frequency sign-in errors so the message
  states what failed and the next step — local login-callback server start
  (hints at port/firewall), access-token creation, and token verification
  (suggests retrying `multica login` and checking the token is valid/not
  expired). All keep %w so exit-code tiering and --debug detail are preserved.

cmd_id_resolver.go is left as-is — its not-found / ambiguous-prefix messages
already point at `list --full-id` and need no change. The user-facing
FormatError layer is unchanged, so its existing PR1/PR2 test coverage still
applies; no test asserted the old verb strings.

Refs MUL-3104. PR3 of 3 (final).

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

* fix(cli): make login failure guidance visible via typed user-message wrapper

Addresses 张大彪's PR3 review: the refined sign-in copy was wrapped with %w,
so FormatError returned the centralized *HTTPError/*NetworkError copy and the
new guidance only appeared under --debug.

- Add cli.UserMessageError + cli.WithUserMessage: a typed wrapper carrying a
  user-facing message that FormatError surfaces by default, recognized before
  the network/http branches. Unwrap() is preserved, so ExitCodeFor still
  classifies by the underlying typed error and --debug still prints the full
  original chain.
- cmd_auth.go: wrap the OAuth access-token-creation and PAT-verification
  failures with WithUserMessage (OAuth copy no longer mentions a passed token,
  since that flow has none), and move the token-specific 'valid / not expired'
  hint to the real Enter your personal access token:  verification site (was the generic
  'invalid token: %w').
- Focused tests: under a wrapped *HTTPError(401) the default FormatError shows
  the login hint, ExitCodeFor returns ExitAuth, and --debug retains the raw
  chain; a wrapped *NetworkError still classifies as ExitNetwork.
- CLI_AND_DAEMON.md: narrow 'every error' to command errors returned to the
  top-level handler, noting commands like setup's fast /health probe bypass it.

Refs MUL-3104, PR #3900.

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-06-09 13:15:51 +08:00
Multica Eve
8ff68502fc MUL-3132: harden /uploads/* (auth, no listing, nosniff, tight CSP) (#3903)
* MUL-3132: harden /uploads/* (auth, no listing, nosniff, tight CSP)

Closes the open hardening items from the SVG XSS disclosure
(security-findings-2026-06-02). The primary chain (PR #3023 / #3050)
is intact; this PR addresses every remaining recommendation from the
disclosure's hardening list except 'serve uploads from a separate
origin' (a structural change beyond this fix).

Changes:

- /uploads/* now requires authentication. The route is wrapped in
  middleware.Auth so anonymous internet users can no longer fetch
  workspace attachments by guessing the URL. A new ServeLocalUpload
  handler then enforces the second layer:
    - workspaces/{wsID}/* paths require membership in wsID (uses
      MembershipCache for the hot path);
    - users/{userID}/* paths allow any authenticated user (avatars
      are referenced cross-workspace);
    - any other prefix returns 404, so a future feature cannot drop
      content under /uploads/<other-prefix>/ and inherit a relaxed
      policy by accident.
  Non-members see 404 (not 403) so the route does not act as an IDOR
  oracle for workspace IDs.

- Directory listing on /uploads/* is rejected at the storage layer:
  empty keys, trailing-slash keys, and any key that resolves to a
  directory return 404 before http.ServeFile would render an HTML
  index. UUID filenames were obscurity, but enumerating them
  shouldn't be free.

- Every successful /uploads/* response carries
  X-Content-Type-Options: nosniff and a tight per-response CSP
  (default-src 'none'; sandbox; frame-ancestors 'none'), overriding
  the application-wide CSP. This is belt-and-suspenders if a future
  regression weakens the Content-Disposition: attachment path.

- UploadFile rejects HTML-family uploads at the edge (.html, .htm,
  .xhtml, .shtml, .xht, .phtml, plus a content-type denylist for
  text/html and application/xhtml+xml so renamed payloads cannot
  bypass the extension check). SVG and JS remain allowed because
  their existing serve-side defenses neutralize them and source-code
  attachments preview as text/plain via /api/attachments/{id}/content.

Tests:

- storage: TestLocalStorage_ServeFile_RejectsDirectoryListing,
  TestLocalStorage_ServeFile_HardeningHeaders.
- handler: TestIsUploadDenied (pure), TestUploadFile_RejectsHTMLByExtension,
  TestUploadFile_RejectsHTMLByContentType, TestUploadFile_AllowsLegitimateImage,
  and the full ServeLocalUpload matrix (RequiresAuth, MemberCanRead,
  NonMemberDenied, RejectsDirectoryInPath, UnknownPrefixDenied,
  UserPrefixAllowsAnyAuthedUser).
- Full server test suite passes.

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

* MUL-3132: HMAC-signed query auth for /uploads/* (token-auth client compat)

Addresses J's Request Changes review on PR #3903.

Problem: PR #3903 wrapped /uploads/* in middleware.Auth, but native
<img>/<video>/<iframe> resource loads cannot attach Authorization
headers. Token-auth clients (Desktop default, legacy-token Web
sessions, mobile) were breaking on inline attachment rendering even
though the API itself authenticated fine.

Fix: implement HMAC-signed query parameters for /uploads/*, mirroring
S3 + CloudFront presigned URLs.

- storage.SignLocalUploadURL(rawURL, key, secret, expiry) appends
  '?exp=<unix>&sig=<HMAC-SHA256(key|exp)>' query params; signature
  is bound to one specific key, has a TTL matching CloudFront mode
  (defaultAttachmentDownloadURLTTL = 30 min), constant-time compared
  on verify.
- storage.VerifyLocalUploadSignature(key, exp, sig, secret, now)
  rejects expired, tampered, wrong-secret, and key-mismatched
  signatures.
- ServeLocalUpload now has two auth paths: signed-query (no Auth
  middleware needed; signature itself is the authority) and
  Bearer/cookie (membership-gated as before). Partial signed-query
  fails closed.
- The route in router.go dispatches between the two: if both exp+sig
  query params are present, route to inner handler unwrapped; else
  wrap in middleware.Auth.
- attachmentToResponse appends signed query to URL when the storage
  backend is *LocalStorage. CloudFront-signed download URLs and S3
  paths are unchanged.

Tests:
- storage: TestSignAndVerifyLocalUploadURL_RoundTrip,
  TestVerifyLocalUploadSignature_RejectsExpired, _RejectsTamperedSig,
  _BoundToKey, _RejectsWrongSecret,
  TestSignLocalUploadURL_PreservesExistingQuery,
  TestLocalUploadSignatureFromQuery_EmptyOnAbsence (7 pure tests).
- handler: TestServeLocalUpload_{SignedQueryBypassesAuth,
  SignedQueryRejectsExpired, SignedQueryRejectsTampered,
  SignedQueryBoundToOneKey, PartialSignedQueryFailsClosed},
  TestAttachmentToResponse_LocalStorageMintsSignedURL.

Full server test suite passes.

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

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-09 11:59:00 +08:00
Jiayuan Zhang
072404d912 fix(issues): header chip shows 'is queued' when no agent is running (#3923)
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-09 03:11:10 +08:00
Jiayuan Zhang
0c80c33c62 feat(issues): add brand border beam to active agent header chip (#3921)
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-09 02:34:22 +08:00
Bohan Jiang
24b162cdbc feat(daemon): surface the real task initiator to the agent runtime (MUL-2645) (#3899)
* feat(daemon): surface the real task initiator to the agent runtime (MUL-2645)

In a multi-person workspace the agent runtime only ever saw the runtime
OWNER identity: the brief's `## Requesting User` is sourced from
runtime.OwnerID and the task-scoped token is owner-bound, so every
requester (whoever commented, @mentioned, or chatted) appeared to the
agent as the owner. Agents that route by initiator for permission,
privacy, or audit all misjudged.

Resolve the real task initiator at claim time and surface it distinctly
from the owner:
- comment / mention trigger -> triggering comment's author (member or agent)
- chat task -> chat session creator (sessions are creator-only)
- on-assign / autopilot / quick-create -> no attributable initiator (omitted)

Adds initiator_{type,id,name,email} to the claim response, the daemon
Task, and TaskContextForEnv, rendered into the brief as a new
`## Task Initiator` section. The section documents the privacy boundary:
the agent's credentials stay owner-scoped, so this is an attested
identity for the agent's own routing/privacy logic, not act-as. No DB
migration — both paths are derivable from existing rows.

Tests: brief rendering (member/agent/omit/sanitize) + email guard unit
tests, and claim-handler tests for the comment and chat paths.

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

* fix(chat): store real sender as task initiator, not chat_session creator (MUL-2645)

Review fix (Niko, PR #3899). v1 resolved the chat task initiator from
chat_session.creator_id at claim time. That is correct for web chat and
Lark p2p (creator == sender), but WRONG for Lark group chats: the group
session creator is deliberately the installer (stable identity across
member churn), not the message sender. So in a Lark group, every member
who triggered the agent showed up in the brief as the installer/owner —
the exact bug this issue is about, still live at that entry point.

Capture the real sender at enqueue time instead of deriving it from the
session creator at claim time:

- migration 117: agent_task_queue.initiator_user_id (FK user, ON DELETE
  SET NULL); NULL for non-chat and pre-migration rows.
- EnqueueChatTask now takes an explicit initiatorUserID. Web chat passes
  the authenticated request user; the Lark dispatcher threads the inbound
  sender (binding.MulticaUserID) through scheduleRun -> flushChatRun. The
  debouncer keeps the latest scheduled flush per session, so in a multi-
  sender silence window the LATEST sender wins (documented + tested).
- claim handler resolves the initiator from task.initiator_user_id and
  drops the creator_id fallback entirely.

The Lark group session creator stays the installer (unchanged) — only the
task initiator is corrected, keeping the two concepts cleanly separate.

Tests: dispatcher group regression (initiator = sender, not installer),
latest-sender-wins, p2p initiator assertion; the chat claim handler test
now sets creator != initiator and asserts the stored sender wins.

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-08 19:29:57 +08:00
chyax98
26ca943d45 feat(lark): add typing indicator lifecycle for inbound messages (#3860)
When a message is successfully ingested, send a Typing reaction to
the user's message. When the agent replies (EventChatDone) or fails
(EventTaskFailed), clear the reaction before the reply is visible.

- Add AddMessageReaction / DeleteMessageReaction to APIClient
- Implement reaction HTTP calls in httpAPIClient
- Introduce TypingIndicatorManager for per-session state tracking
- Wire into Hub (add on ingest) and Patcher (clear before reply)
- Skip typing for messages older than 2 minutes (WS replay guard)

Co-authored-by: miaolong001 <miaolong@xd.com>
2026-06-08 19:27:08 +08:00
Naiyuan Qing
139cd755e2 Revert "MUL-3127: reuse submit button in reply input (#3901)" (#3906)
This reverts commit d434f038c9.
v0.3.18
2026-06-08 17:54:47 +08:00
Naiyuan Qing
51a214b4c0 MUL-3134: restore header agent popover (#3905)
Co-authored-by: multica-agent <github@multica.ai>
2026-06-08 17:47:56 +08:00
Multica Eve
54dbb57aef MUL-3138: add June 8 changelog entry (#3904)
* docs: add June 8 changelog entry

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

* docs: simplify June 8 changelog titles

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

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-08 17:37:31 +08:00
Naiyuan Qing
f6999a9dcb MUL-3134: simplify issue agent header chip (#3902)
* MUL-3134: simplify issue agent header chip

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

* MUL-3134: remove execution log event count

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-06-08 17:29:34 +08:00
liujianqiang-niu
5be7d1bc17 MUL-3136 fix(openclaw): parse config path from last non-empty line of CLI output
Fix OpenClaw config discovery when `openclaw config file` prints Doctor warning UI before the actual config path. The daemon now uses the last non-empty stdout line as the path while preserving the existing tilde expansion, absolute-path validation, stat checks, and fail-closed behavior.

Tests: go test ./internal/daemon/execenv
2026-06-08 17:22:02 +08:00
Naiyuan Qing
d434f038c9 MUL-3127: reuse submit button in reply input (#3901)
Co-authored-by: multica-agent <github@multica.ai>
2026-06-08 16:40:33 +08:00
LinYushen
b83b41ff44 feat(cli): per-status error copy with actionable hints (PR2, MUL-3104) (#3897)
* feat(cli): refine per-status error copy with actionable hints (PR2)

Builds on PR1's translation layer. Each HTTP-status message now carries an
actionable next step, in both English and Chinese:

- 401: run `multica login`; plus a self-hosted / non-OAuth fallback telling
  the user to ask their administrator for valid credentials
- 403: check the workspace / ask an admin to grant access
- 404: check the ID or run the matching `list` command
- 409: re-fetch the latest state and retry
- 422: check values / run with --help
- 429: wait and retry; reduce call frequency if it persists
- 5xx: retry, contact support, and re-run with --debug for the raw response

Also adds ErrorKind.String() (stable snake_case identifiers) and uses it in
--debug output instead of the raw int, and clears the pre-existing gofmt dirt
Eve flagged in cmd_config.go, cmd_version.go, and help.go.

Tests: TestErrorKindString (all kinds + uniqueness + out-of-range fallback)
and TestFormatErrorActionableHints (locks the per-status hints in EN and ZH).

Refs MUL-3104. PR2 of 3.

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

* test(cli): cover validation (400/422) actionable hint

TestFormatErrorActionableHints omitted KindValidation, so deleting the 400/422
hint would have gone unnoticed. Add 400 and 422 cases (no server message, so
the generic validation copy is used) asserting EN contains --help / expected
format and ZH contains --help / 格式 / 参数.

Refs MUL-3104, PR #3897.

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-06-08 16:02:09 +08:00
LinYushen
28de8b8bde feat(cli): central error translation layer (PR1, MUL-3104) (#3892)
* feat(cli): add central error translation layer (PR1)

Introduce server/internal/cli/errors.go, a single user-facing error
translation layer that collapses raw transport errors, HTTP status
errors, and internal verb-wrapped chains into clear, localized messages.

- ErrorKind classification (network timeout/DNS/refused/TLS/offline,
  401/403/404/409/400+422/429/5xx, unknown)
- NetworkError wraps transport errors and strips the raw URL from the
  user-facing message; classifyNetworkError categorizes via errors.As/Is
  with string fallbacks
- HTTPError.Kind() maps status codes onto ErrorKind
- FormatError: bilingual output (English default, auto-switch to Chinese
  on a zh LC_ALL/LC_MESSAGES/LANG locale), validation errors surface the
  server message; --debug / MULTICA_DEBUG appends the full raw chain
- ExitCodeFor: tiered exit codes (network=2, auth=3, 404=4, validation=5,
  other=1)
- client.go: default HTTP timeout 15s -> 30s, overridable via
  MULTICA_HTTP_TIMEOUT; wrap every transport Do() error as *NetworkError
- main.go: route errors through FormatError + ExitCodeFor, add persistent
  --debug flag

Unit tests cover every ErrorKind, classification, language detection,
exit codes, server-message extraction, and timeout parsing.

Refs MUL-3104. PR1 of 3; PR2/PR3 (status-code copy refinement and
per-command customization) follow separately.

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

* fix(cli): address review — unify command timeouts and classify all helper errors

Must-fix 1: command-level contexts no longer truncate MULTICA_HTTP_TIMEOUT.
Added cli.APITimeout/AtLeastAPITimeout/APIContext (budget = transport timeout
+ small grace, honoring MULTICA_HTTP_TIMEOUT) and replaced the hardcoded 15s
context.WithTimeout in every API command (14 files, 92 sites) with
cli.APIContext. The issue-create/comment path now uses APITimeout() with a
60s floor for attachment uploads.

Must-fix 2: all API helpers now return *HTTPError on status >= 400. Added a
shared newHTTPError(method, path, resp) and routed GetJSON, GetJSONWithHeaders,
PostJSON, PutJSON, PatchJSON, DeleteJSON, DeleteJSONWithBody, UploadFile,
UploadFileWithURL, DownloadFile (and HealthCheck) through it, so issue
update/status/metadata (PUT), comment list (GetJSONWithHeaders), project/label/
comment delete (DELETE) and agent/workspace/autopilot update (PUT/PATCH) all
get HTTPError.Kind() classification, friendly copy, and the tiered exit code
instead of the raw string + exit 1.

Tests: new errors_integration_test.go drives the real helpers against a fake
server and asserts FormatError copy + ExitCodeFor for 401/403/404/422/500
across all 10 helpers, plus a slow-server test proving the command context
does not cancel before the transport timeout. Updated the UploadFileWithURL
assertion to check for *HTTPError.

Refs MUL-3104, PR #3892.

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

* fix(cli): make remaining fixed-timeout API commands honor MULTICA_HTTP_TIMEOUT

Closes out the timeout work: the last API command paths still used a
hardcoded context deadline that capped MULTICA_HTTP_TIMEOUT. Converted them
to cli.AtLeastAPITimeout(<original floor>) so the env override scales them up
while preserving each original lower bound:

- cmd_autopilot.go  autopilot trigger      30s -> AtLeastAPITimeout(30s)
- cmd_attachment.go attachment download    60s -> AtLeastAPITimeout(60s)
- cmd_agent.go      avatar upload           60s -> AtLeastAPITimeout(60s)
- cmd_skill.go      skill import / search    60s -> AtLeastAPITimeout(60s)
- cmd_runtime.go    runtime update         150s -> AtLeastAPITimeout(150s)
- cmd_login.go      workspace-creation poll 10s -> AtLeastAPITimeout(10s)

The login poll keeps a short 10s floor to stay responsive within its 5-minute
loop, but it is NOT a silent exception: AtLeastAPITimeout means it still scales
with MULTICA_HTTP_TIMEOUT. Documented in code and covered by a new subtest in
TestAPITimeoutRespectsEnv.

Refs MUL-3104, PR #3892.

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

* style(cli): gofmt cmd_attachment.go to unblock backend CI

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-06-08 15:34:59 +08:00
Bohan Jiang
1ddf89a8f2 feat(daemon): enable Antigravity (agy) per-agent model selection (MUL-3125) (#3894)
* feat(daemon): wire agy --model and model discovery for Antigravity

agy 1.0.6 added a --model flag and an `agy models` catalog command, which
were the #1 blocker in the earlier agy-backend review (MUL-3125). The
antigravity backend already shipped but deliberately dropped opts.Model
because agy 1.0.1 had no way to select a model.

- buildAntigravityArgs now passes --model <display name> when opts.Model is
  set; the value is the exact `agy models` display string (spaces + parens),
  passed as a single exec arg so no shell quoting is needed.
- Block --model in custom_args so it can't override the managed value.
- ListModels("antigravity") enumerates via `agy models` (no static fallback:
  agy silently no-ops on unrecognised models, so a stale guess would turn a
  typo into a successful empty run).
- ModelSelectionSupported now returns true for every built-in provider; the
  hook stays for any future model-less runtime.
- Daemon probe reads MULTICA_ANTIGRAVITY_MODEL for the daemon-wide default.

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

* docs(providers): mark Antigravity model selection as supported

Antigravity gained --model in agy 1.0.6 (MUL-3125). Update the provider
matrix + prose (en/zh/ja/ko) from "managed internally / no --model" to
dynamic discovery via `agy models`, and refresh the now-stale picker
comments. Flag the display-string (not slug) shape and agy's silent no-op
on unrecognised values.

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

* fix(daemon): reject unknown Antigravity model at spawn (MUL-3125)

agy exits 0 with empty output on an unrecognised --model, so a stale/typo'd
value would surface as a 'completed' but empty task. Validate opts.Model
against the `agy models` catalog in Execute before spawning: a non-empty
model the CLI does not advertise fails fast with an actionable error listing
the real choices. opts.Model is the single funnel for agent.model and the
MULTICA_ANTIGRAVITY_MODEL default, so this one check covers every source
(UI free-text, API, persisted value, env) — addressing Elon's review that a
UI-only guard is bypassable.

Validation is fail-OPEN: if the catalog can't be discovered we pass the
value through and let agy resolve it, so a discovery hiccup never blocks a
run. Pure antigravityModelError() is unit-tested (valid / unknown / near-miss
/ empty-model / empty-catalog); verified live against real agy 1.0.6.

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-08 15:32:53 +08:00
Bohan Jiang
3389e887e0 MUL-2614: randomize self-host Postgres password (#3893)
* fix: randomize self-host postgres password

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

* docs: sync self-host password guidance

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-08 15:26:47 +08:00
0xMomo
ef75f80d9d fix(daemon): clean stale agent branches during repo gc (MUL-2550) (#3039)
* fix(daemon): 清理陈旧 agent 分支

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

* fix(daemon): 串行化 bare repo gc

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

* test(daemon): adapt health repo cache mock

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

* fix(daemon): gate gc maintenance on stale-branch deletion

Address review feedback on the bare-repo GC change:

- Only run `reflog expire` + `git gc --prune=30.days` when we actually
  deleted a stale agent branch this cycle. Previously the heavy step
  ran every GC tick on every cached repo even when there was nothing
  to reclaim, turning a stale-ref cleanup into a periodic full-repo
  maintenance job under the per-repo lock.
- Split git command timeouts: `gc --prune=30.days` now gets a
  10-minute budget instead of sharing the 30s ceiling that was scoped
  for the original `worktree prune` call. Light commands stay at 30s.
- Drop the redundant `gc --auto` — `gc --prune=30.days` already
  performs the maintenance `gc --auto` would have triggered.
- Narrow the agent-namespace ref query from `refs/heads/agent` to
  `refs/heads/agent/` so the pattern can't surface a literal
  `agent` branch outside the daemon namespace.

Tests:
- New TestPruneWorktree_IgnoresLiteralAgentBranch pins the trailing-
  slash narrowing.
- New TestPruneWorktree_SkipsMaintenanceWhenNothingDeleted uses an
  unreachable, backdated loose object as a sentinel to verify that
  `gc --prune` runs only when a stale agent branch was reaped.

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

---------

Co-authored-by: 0xNini Code Dev <agent@multica.local>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: 0xNini <0xnini@iMac-Pro.local>
Co-authored-by: J <j@multica.ai>
2026-06-08 15:25:14 +08:00
Qiang Zhang
1abd0e33a6 fix(transcript): close dialog on desktop navigation (#2903) 2026-06-08 15:15:44 +08:00
Bohan Jiang
3808049361 fix(codex): set semantic thread names (#3887)
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-08 14:53:31 +08:00
Naiyuan Qing
a02b3dfb4a feat(issues): move agent live signal into the issue-detail header (#3879)
* feat(issues): move agent live signal into the issue-detail header

Replace the in-body sticky "agent is working" card (AgentLiveCard) with a
compact chip in the issue-detail header, so the live signal sits in one
fixed place and never competes with sticky banners in the content column.

- New IssueAgentHeaderChip: avatar(s) + live-ticking blue elapsed time;
  click opens a popover listing every active task.
- Popover reuses ExecutionLogSection's ActiveTaskRow (now exported) so the
  popover and the right panel are literally the same row — no duplication.
- PopoverContent gains an optional keepMounted so the row's confirm dialog
  survives the popover closing on Stop.
- Running rows in ExecutionLogSection drop the blue spinner for a
  live-ticking blue elapsed timer (panel + popover share this).
- Source the chip from the workspace agent-task snapshot filtered by issue
  (same source as board/list indicators, zero extra network); delete the
  old AgentLiveCard + its test and its heavy per-issue WS machinery.

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

* feat(issues): live event count on the agent chip + execution-log rows

Show a live "N events (elapsed)" on running agents, consistent across the
header chip, its popover rows, and the right-panel execution log.

- Read the shared per-task message cache (taskMessagesOptions, kept live by
  useRealtimeSync's global task:message handler) instead of a bespoke
  subscription — one source of truth, deduped across chip / popover / panel /
  transcript, no extra WS wiring.
- Extract <RunningStat> (event count in info-blue + elapsed in muted parens)
  so all surfaces render the running stat identically.
- ExecutionLogSection running rows now show the same "N events (elapsed)";
  the transcript opened from them streams live from the shared cache.
- Chip: single running shows events (elapsed); multiple shows "N working".
- i18n: add agent_live.event_count (4 locales).

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-08 14:51:20 +08:00
Bohan Jiang
dfc159e1aa feat: skip agent triggering on /note-prefixed comments (MUL-3115, #3649) (#3885)
* feat(comments): skip agent triggering on /note-prefixed comments

A comment whose first token is the reserved /note prefix (case-insensitive)
is stored like any other comment but never wakes an agent. The guard sits at
the top of triggerTasksForComment, the single chokepoint, so it covers all
three trigger paths — assignee, squad leader, and @mentioned agents. Gating
only shouldEnqueueOnComment (as originally proposed) would still let
"/note @agent ..." through the mention path.

Lets members leave human-only tips/notes on agent-assigned issues without
burning an agent run. MUL-3115, closes #3649.

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

* feat(editor): add /note built-in slash command to comment composer

Enable the `/` menu in the issue comment and reply composers in a new
"command" mode that lists fixed built-in commands instead of the chat
skill picker. Currently one command, /note, which marks a comment as a
human-only note that won't trigger the assigned agent.

Selecting it inserts the plain-text "/note " prefix (not a rich node), so
a menu pick and a hand-typed command are byte-identical and the backend
detects either with a simple prefix match. The command menu renders nothing
on a non-matching `/` (hideOnEmpty) so typing a date like 6/8 isn't noisy.
The chat skill picker is unchanged. MUL-3115.

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

* refactor(editor): match /note by label prefix and localize its description

Address PR review feedback:
- buildBuiltinCommandItems now matches the command label as a prefix only,
  dropping the description substring match copied from the skill picker. With
  one command this keeps the menu predictable (/no surfaces note; /deploy or a
  description word like /agent shows nothing) and avoids Enter selecting note
  unexpectedly.
- The command description is now a localized UI string: added
  slash_command.commands.note to all four editor locales (en/ja/ko/zh-Hans)
  and the menu renders it via the typed translator. The /label itself stays
  literal since it's the typed token the backend matches.

MUL-3115.

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

* fix(editor): shorten /note command description to avoid truncation

The slash menu item is single-line (truncate, w-72), so the longer copy was
cut off. Shorten to "won't trigger any agents" across all four locales — also
more accurate, since /note skips assignee, squad leader, and @mentioned agents,
not just the assigned one.

MUL-3115.

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-08 14:50:52 +08:00
xiaoyue26
10076ae773 MUL-3123 fix(realtime): support X-Forwarded-Host in WebSocket checkOrigin 2026-06-08 14:43:20 +08:00
Bohan Jiang
7b453ff604 fix: show assignee avatar in command search (#3889)
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-08 14:29:45 +08:00
Bohan Jiang
f5db77340f feat(web): native notification banners for the web app (MUL-3116) (#3883)
The in-app inbox (sidebar badge, real-time WS updates, settings, inbox
page) was already shared and worked on web. The only Desktop-only piece
was the native OS banner: handleInboxNew called desktopAPI.showNotification,
which is undefined on web, so no banner fired for new inbox items while the
app was unfocused.

Add the browser equivalent, keeping handleInboxNew as the single decision
point (focus + source-workspace mute gating stays shared with desktop):

- packages/core/platform/system-notification.ts: browser Notification engine
  (showWebNotification) + permission helpers + a click-handler registry. Lives
  in core (the caller does) but injects the click-routing decision so core
  stays headless.
- handleInboxNew: branch desktopAPI (unchanged) → else showWebNotification.
- apps/web WebNotificationBridge: registers click routing to the source
  workspace's inbox (?issue=…), mirroring desktop's DesktopInboxBridge.
- Settings → Notifications: web-only opt-in to grant browser permission
  (hidden on desktop / where the API is unavailable); en/zh-Hans/ja/ko.

Permission is an explicit settings opt-in (no auto-prompt on load, per
browser best practice). Tests cover the engine and the web path in
handleInboxNew.

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-08 14:12:20 +08:00
Bohan Jiang
0da879ec89 fix(runtime): pause autopilots inside the runtime-delete teardown transaction (#3880)
DeleteAgentRuntime paused autopilots for the runtime's archived agents
just outside the teardown transaction, so a pause that succeeded before a
later delete failed (and rolled back) left autopilots paused while the
runtime survived. Move ListArchivedAgentIDsByRuntime +
PauseAutopilotsByAgentAssignees inside the tx via qtx and treat a pause
error as a hard failure, matching ArchiveAgentsAndDeleteRuntime.

Co-authored-by: J <agent-j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-08 14:04:13 +08:00
NanamiKite
2e34016f1f fix(daemon): interrupt local agent on server-side terminal task states (#3878)
shouldInterruptAgent now treats every terminal task status (completed/failed/cancelled, via isAgentTaskTerminal) plus a 404 task-not-found as an interruption signal, so the daemon stops a local agent once the backend has finalized the task — e.g. the runtime offline sweeper flipping running -> failed during a disconnect/reconnect. Previously only `cancelled`/404 interrupted, so the agent ran to completion and its CompleteTask call failed against a non-running row, wasting compute and adding log noise.

Closes #3877
2026-06-08 14:00:30 +08:00
yuhaowin
bcc7cd3688 fix(projects): add backdrop-blur to compact list header (#3783)
The sticky header in the Projects compact list was missing backdrop-blur,
causing underlying content to bleed through the semi-transparent bg-muted/30
background when scrolling. Matches the DataTable header pattern used in the
Agents module.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-08 13:45:59 +08:00
LinYushen
b89b9cb4d6 test(migrate): concurrent migration race test using real Postgres (MUL-2956) (#3712)
* test(migrate): add concurrent migration race test using real Postgres (MUL-2956)

Follow-up to MUL-2923 / #3658, which added a Postgres advisory lock to
serialize the migration loop across concurrent runners (multi-replica
backend startup, scale-up, manual `migrate up` overlap). That PR shipped
without a test because cmd/migrate/ had no harness; this commit adds it.

Refactor: extract runMigrations(ctx, pool, runOptions) from main(), with
the lock key, the bookkeeping table, and the file list now injectable.
main() behavior is unchanged. Identifier interpolation goes through
pgx.Identifier{}.Sanitize so callers can pass "schema.schema_migrations"
safely.

Tests (cmd/migrate/migrate_concurrent_test.go) — every case isolates
itself in a unique throwaway schema and a unique lock key, so they
never touch the real schema_migrations table or block real production
runners that share the database. Skip cleanly when DATABASE_URL is
unreachable, matching the pattern already used in
internal/handler/handler_test.go and internal/metrics/business_sampler_pgsleep_test.go.

  - TestRunMigrationsConcurrentPending: 16 goroutines apply 5
    deliberately non-idempotent migrations (bare CREATE TABLE +
    ALTER TABLE ADD COLUMN). Without the lock, concurrent CREATE TABLE
    races trip "duplicate key value violates unique constraint
    pg_type_typname_nsp_index" — proving the lock is doing its job.
  - TestRunMigrationsConcurrentAlreadyApplied: 16 goroutines hit the
    EXISTS no-op path against a pre-populated bookkeeping table; the
    state must be unchanged.
  - TestRunMigrationsAdvisoryLockSerializes: an external connection
    holds the same advisory lock; we assert that zero of the 16
    runners complete during a 1 s observation window, then release
    the side lock and let them all finish. Catches the original
    MUL-2923 bug where the lock got attached to a random pooled
    connection.
  - TestRunMigrationsConcurrentMixedPoolStress: same pending case but
    with a deliberately small pool (runners/2), forcing pgxpool.Acquire
    contention to overlap with pg_advisory_lock contention.

Verified locally: `go test -race -count=10 ./cmd/migrate/` passes in
~15 s. Mutation test (lock acquire/release replaced with `SELECT 1`)
confirms the pending and lock-serializes tests both fail loudly,
catching the regression they were written to detect.

go.mod tidy promotes golang.org/x/sync to a direct dependency
(now imported by the test for errgroup) and incidentally fixes a
stale `// indirect` annotation on prometheus/client_model, which is
already imported directly by internal/metrics/testutil.go.

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

* test(migrate): gofmt + address review nits (MUL-2956)

- gofmt -w cmd/migrate/migrate_concurrent_test.go: fixture struct field
  alignment.
- quoteQualifiedIdentifier: actually reject identifiers with more than
  one dot (the previous version split on the first dot only and would
  silently sanitize "a.b.c" into "a"."b.c", contradicting the comment).
  Inline the splitter via strings.Split now that we explicitly check the
  component count.
- Soften the test's lock-key comment from "never collide" to the
  accurate probabilistic statement (~1 in 2^62 collision odds with the
  production constant).

go test -race -count=10 ./cmd/migrate/ still passes (~15 s).

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

* test(migrate): direction whitelist + tidy go.mod (MUL-2956)

Address two follow-ups from review:

- runMigrations now whitelist-checks opts.Direction up-front and
  returns an error for anything that is not "up" or "down". The
  previous shape relied on `opts.Direction == "up"` and an else branch,
  so a typo or empty string would silently fall through to the
  rollback path. Add TestRunMigrationsRejectsInvalidDirection covering
  the empty string, "UP"/"DOWN" case mismatches, "rollback", and a
  whitespace-padded value; the check fires before any pool work, so
  the test runs without Postgres.
- go mod tidy: promotes google.golang.org/protobuf to a direct
  dependency (it is imported directly elsewhere in the module and was
  stale-marked indirect).

go test -race -count=10 ./cmd/migrate/ green (~15.7 s, 50/50).

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

---------

Co-authored-by: wei-heshang <wei-heshang@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-08 13:33:16 +08:00
HMYDK
4190de3d64 fix(skills): quote description values in built-in SKILL.md YAML frontmatter (#3852)
Built-in SKILL.md description values contained unquoted ': ' sequences, which strict YAML parsers (e.g. Codex) reject — silently dropping the skill at load.

- Quote all eight built-in skill descriptions.
- ensureSkillFrontmatter() re-synthesizes frontmatter that has a name but fails YAML validation, so malformed imports are repaired instead of dropped.
- Unify frontmatter delimiter parsing into a single frontmatterParts helper.
- Add strict-YAML regression tests over the built-in skills, plus unit tests for the recovery branch and delimiter variants.

Closes #3851.
2026-06-08 13:10:24 +08:00
Thanh Minh
8abdc77961 MUL-2489 fix(runtime): delete archived squads before runtime teardown (#2955)
* fix(runtime): delete squads referencing archived agents before runtime teardown

The DeleteAgentRuntime handler was failing with 500 'failed to clean up
archived agents' because squad.leader_id has an ON DELETE RESTRICT FK on
agent(id). When an archived agent was still referenced as a squad leader
(even on an archived squad), the DELETE FROM agent query was blocked.

Fix: add DeleteSquadsByArchivedAgentsOnRuntime query that removes squads
whose leader_id points to an archived agent on the target runtime, and
call it before DeleteArchivedAgentsByRuntime in the handler.

Closes TMI-85

* test(runtime): cover squad cleanup before archived-agent deletion

Adds four tests around the DeleteSquadsByArchivedAgentsOnRuntime fix:

* TestDeleteSquadsByArchivedAgentsOnRuntime_Query — query-level: deletes
  squads whose leader is an archived agent on the target runtime, leaves
  squads with active leaders or archived leaders on a different runtime
  alone, and is safe to call when nothing matches. Covers the archived-
  squad case that originally hid the FK blocker from `multica squad list`.
* TestDeleteAgentRuntime_RemovesSquadsLedByArchivedAgents — handler
  end-to-end regression for TMI-85. Reverting the handler change makes
  this fail with the exact 500 'failed to clean up archived agents' the
  user reported.
* TestDeleteAgentRuntime_NoSquadsRegression — happy path for runtimes
  whose archived agents were never squad leaders, ensuring the new step
  is a no-op there.
* TestDeleteAgentRuntime_StillBlockedByActiveAgents — preserves the 409
  CountActiveAgentsByRuntime guard so the active-agent contract isn't
  silently regressed by the new cleanup ordering.

Refs TMI-85

* chore: remove internal issue tracker references from test comments

* fix(runtime): keep active squads during runtime teardown

* fix(runtime): block runtime delete on active archived-leader squads

* fix(runtime): make runtime delete 409 path a no-op

---------

Co-authored-by: Kiro <kiro@multica.ai>
2026-06-08 13:08:38 +08:00
Prince Pal
d6e00e0909 fix(daemon): fail loudly when self-restart spawn fails (#2503)
* fix(daemon): fail loudly when self-restart spawn fails

* fix(daemon): surface log reopen failures on restart
2026-06-08 13:02:31 +08:00
Wes
5e7587ad07 Optimize daemon runtime wakeups (#3859) 2026-06-08 12:51:13 +08:00