Commit Graph

4376 Commits

Author SHA1 Message Date
Bohan Jiang
286ed61bb0 fix(agent): reap claude process group on cancellation (#5918) [MUL-5288] (#5957)
* fix(agent): reap claude process group on cancellation (#5918)

The Claude backend spawned its child with a bare exec.CommandContext, so
cancellation SIGKILLed only the leader. On a resumed stream-json session
(no wall-clock timeout) the MCP servers and tool subprocesses it spawned
were orphaned and kept running — 64+ min in #5918 — while, under
--max-concurrent-tasks 1, holding the only slot and starving the queue.

Put claude in its own process group and drive a group-wide
SIGTERM->grace->SIGKILL on cancel/timeout before closing stdout, mirroring
the fix already made for codex (#4520) and opencode (#4533). Add
claude_cancel_unix_test.go covering the graceful and SIGKILL-escalation
paths.

MUL-5288

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

* fix(agent): gate claude SIGKILL escalation on whole process group

Review found the grace-window escalation keyed off procDone (leader exit),
not the process group. A SIGTERM-ignoring descendant that does not hold
claude's stdout lets the leader exit, closes procDone, and skips the group
SIGKILL — leaking exactly the orphan #5918 targets.

Escalate to a group SIGKILL unless waitProcessGroupGone confirms the whole
group has exited within the grace window (matching codex). It returns as
soon as the group empties, so the graceful path adds no latency. Add a
mixed-signal regression: TERM-respecting leader + TERM-ignoring,
stdio-detached descendant, which fails against the leader-keyed escalation.

MUL-5288

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

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-27 02:01:41 +08:00
Bohan Jiang
4931ee6f7a docs(chat): correct the chat sandbox description (MUL-5287) (#5956)
The Chat docs claimed agents in a chat are fully sandboxed and cannot
see or change issues (`multica issue list` returns empty, issue API
calls blocked by permission checks). That is not what the server
enforces: a chat run is dispatched with the same owner-scoped,
workspace-bound task token as an issue run, and /api/issues is guarded
only by workspace membership — no chat-origin check. So a chat agent can
run `multica issue create`/`list` and operate on the workspace when
explicitly asked (creator recorded as the agent), matching the report in
GH #5917.

Reframe the section around the real boundary: chat isolation is about
context and privacy, not permissions. The genuinely-true guarantees are
preserved (private transcript, no issue context in the prompt, no
auto-promotion of chat content into issues); the false 'cannot see/change
issues / fully sandboxed' claims are corrected. Updated en/zh/ja/ko.

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-27 01:24:00 +08:00
YYClaw
af1ff00e14 test(cli): use example domains in setup fixtures (#5944) 2026-07-27 01:10:46 +08:00
Multica Eve
b0bae3f95e docs(changelog): add v0.4.11 release entry (2026-07-25) (MUL-5284) (#5916)
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
v0.4.11
2026-07-25 03:38:12 +08:00
Jiayuan Zhang
e30776dd9b feat(agent): add Claude Opus 5 to the Claude runtime catalog (MUL-5282) (#5910)
Opus 5 is the current flagship in Claude Code's bundled catalog (verified
against claude-code 2.1.219: id `claude-opus-5`, display name "Opus 5",
pricing tier_5_25, capabilities include xhigh_effort/max_effort). Without a
catalog entry the model picker never offered it, and — more damaging —
ModelKnownIncompatibleWithProvider treats any unlisted `claude-*` id as a
known mismatch, so an agent manually pinned to `claude-opus-5` had the value
erased on save.

- Add `claude-opus-5` to claudeStaticModels(). Sonnet 4.6 stays the sole
  badged default; Opus remains a deliberate opt-in.
- Allow the full low/medium/high/xhigh/max effort range in
  claudeModelEffortAllow, matching the rest of the Opus family.
- Price it on the standard 5/25 Opus tier in both the server table and the
  frontend estimator, so Opus 5 usage lands in cost totals instead of the
  unmapped-model diagnostic. The existing `[1m]` and `<provider>/` tolerances
  cover the other spellings runtimes report.

Verified: go test ./pkg/agent/... ./internal/metrics/..., vitest
runtimes/utils.test.ts, tsc --noEmit on packages/views.

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-25 02:48:03 +08:00
Jiayuan Zhang
28a4203bc2 MUL-5261: revert domain-specific prompt additions (#5913)
Revert the built-in focused-testing skill (#5877) and the always-on
Repository Setup Preflight brief section (#5886). Both delivered
software-engineering domain content through platform-level prompt
surfaces that every agent receives regardless of workspace type.

- multica-focused-testing was the only built-in skill that did not
  describe a Multica platform contract, and the only one without
  `user-invocable: false` / `allowed-tools: Bash(multica *)`. Built-in
  skills are meta/system skills; a workspace with no repository bound
  still carried it in its skill index and slash-command menu.
- Repository Setup Preflight was emitted for every non-quick-create task
  without consulting `ctx.Repos`, so non-code workspaces received
  build/dependency instructions in the always-on brief. writeRepositories
  already elides itself when no repo is bound; this section did not.

Pure revert. No replacement behavior is introduced here.

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-25 02:47:06 +08:00
Bohan Jiang
ecce589867 MUL-5265: GitHub API-snapshot PR cards — CI status + mergeability (#5889)
* feat(github): API-snapshot PR cards — CI status + mergeability (MUL-5265)

Fetch each linked PR's CI checks and mergeability from the GitHub GraphQL
API as the single source of truth (Plan C). Webhooks, page visits and a
bounded TTL sweep are refresh triggers only; nothing is inferred from
webhook payloads anymore.

Backend (server/internal/integrations/ghsnapshot):
- installation-token cache + GraphQL client (private key / tokens never logged)
- one paginated pullRequest query -> normalized per-check snapshot
- outbound queue: (installation,repo,PR) dedup + single in-flight per PR,
  bounded worker pool, Retry-After / rate-limit backoff, jitter
- head-SHA-guarded atomic batch replace (a slow response for an old head
  can never overwrite a newer head's snapshot)
- bounded chase window (30s->5m, stops on terminal/closed) + page-visit +
  TTL refresh; clean degradation when no App private key is configured

Removes the old suite-level webhook aggregation display path (query +
handlers + tests). check_suite / check_run / status are now pure triggers.

Frontend: PR card shows two independent tri-state elements (CI status +
mergeability). "Ready to merge" only when merge state is clean; no-checks
and unknown-mergeable never assert a positive verdict; progress strip
removed; four locales; stale marker.

Docs: github-integration + environment-variables (four languages) — now
required App private key, read-only Checks/Commit-statuses permissions,
new event subscriptions, capability boundaries and troubleshooting.

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

* fix(github): address PR snapshot review blockers

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

* fix(github): bound snapshot refresh scheduling

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

* fix(github): concurrent check-run index migration + singleflight token mint

Address Elon's third-round review on the MUL-5265 PR snapshot pipeline.

Must-fix — migration built a non-concurrent index. The
github_pull_request_check_run table declared PRIMARY KEY (pr_id, ordinal)
inside CREATE TABLE, which builds a unique index synchronously and violates
the repo rule that every migration-created index (including on a new table)
use CREATE UNIQUE INDEX CONCURRENTLY in its own single-statement file. Split:
222 now creates the table without a primary key; new 223 adds the
(pr_id, ordinal) unique index CONCURRENTLY. The atomic delete-all/insert
write path already guarantees ordinal uniqueness, so a plain unique index is
sufficient; the index also serves the pr_id-prefix list aggregation and the
workspace/PR cleanup deletes.

Nit — token mint now singleflights per installation. installationToken
released the lock before minting, so the N workers of one installation could
mint N tokens on a cold cache or a simultaneous renew. Concurrent callers for
the same installation are now collapsed via singleflight into one HTTP mint;
added a -race concurrent-mint test asserting a single mint under 16 callers.

Verified: fresh DB migrates through 223 (table has no PK, concurrent unique
index present); ghsnapshot suite + new test pass under -race; migration lint
and handler github/workspace-delete tests pass; sqlc produced no diff;
go build / vet / gofmt / git diff --check clean.

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

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-24 18:30:20 +08:00
Bohan Jiang
3c6bebaff0 MUL-5274: allow explicit persistent service handoffs (#5895)
* fix(runtime): allow explicit persistent service handoffs

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

* fix(runtime): resolve review ambiguities in persistent-service handoff wording

Address MUL-5274 review findings on #5895:

- Drop the "The rules above apply only to work owned by the current run"
  scoping sentence: with the persistent-service exception inserted above
  it, it would have swept in work that is precisely no longer run-owned
  after handoff. The external-systems bullet carries the boundary on its
  own, and both pin tests now reject any "The rules above" reintroduction.
- Replace "detach it" (skill-level mechanism) with the lifecycle
  contract: hand off only once the service no longer depends on this run.
- End the negative-boundary bullet with "the CI-specific rules below
  still apply" instead of "must be collected before exit", which
  misread as license to start CI polling and collect it.

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

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-24 18:21:28 +08:00
Multica Eve
afb3def1b7 docs(changelog): add v0.4.10 release entry (2026-07-24) (MUL-5273) (#5894)
* docs(changelog): add v0.5.0 release entry (2026-07-24) (MUL-5273)

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

* docs(changelog): include #5893 board/list load-more fix in v0.5.0 (MUL-5273)

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

* docs(changelog): use version 0.4.10 (patch bump) per review (MUL-5273)

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

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
v0.4.10
2026-07-24 17:45:56 +08:00
Naiyuan Qing
f599fe3b85 fix(views): board/list load-more no longer flashes the skeleton + friendlier footer (#5893)
* fix(views): board/list load-more stops flashing the full skeleton

Tail (load-more) page fetches were folded into the surface-level isLoading through the per-branch aggregate, so scrolling to load more in List and status-grouped Board replaced the already-rendered rows with the full-surface skeleton. Track head-page pending separately and gate the surface isLoading/isRefreshing on heads only; per-branch pagination state that drives the load-more sentinel is unchanged.

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

* feat(views): friendlier infinite-scroll footer for board/list/swimlane

Collapse four copies of the error/has-more/reached-end ternary into a shared ListLoadMoreFooter: the loading spinner now carries a "Loading…" label so a slow fetch no longer reads as stuck, and a column that actually paginated shows a muted "No more" marker at the end instead of stopping silently. InfiniteScrollSentinel gains an optional label; adds the table.no_more string in en/zh-Hans/ja/ko.

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-07-24 17:14:11 +08:00
Rusty Raven
485f0f3124 fix(search): stop Home/End propagation in command palette inputs (#5870)
* fix(search): stop Home/End propagation in command palette inputs

cmdk's root CommandPrimitive captures Home/End at the container level
for list navigation (jumping to first/last item). When the search
input is focused those keys never reach the native text-input handler,
so the text caret does not move to the beginning/end of the query.

Fix: add an onKeyDown handler on CommandPrimitive.Input that calls
stopPropagation() for Home and End before the event reaches cmdk's
root listener. The existing caller-supplied onKeyDown is still called
so callers can layer their own handlers.

Applied in two places:
- packages/ui/components/ui/command.tsx (CommandInput wrapper — generic
  command palette used across the app)
- packages/views/search/search-command.tsx (SearchCommand — Ctrl+K global
  search palette; uses CommandPrimitive.Input directly)

Fixes #5655

* test(search): cover Home/End caret handling in command palette inputs

Add regression coverage for the Home/End fix (MUL-5260, PR #5870):

- search-command.test.tsx: Home/End move the query caret and do NOT
  hijack cmdk result selection, while ArrowUp/ArrowDown still navigate.
- command-input.test.tsx: the shared CommandInput stops Home/End from
  bubbling to cmdk's root handler while still forwarding every key to a
  caller-provided onKeyDown; other keys keep bubbling. Lives in
  @multica/views because @multica/ui has no test runner.

Verified as a genuine guard: reverting the fix hunks fails exactly these
new assertions.

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

---------

Co-authored-by: NevilleQingNY <nevilleqing@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-24 17:03:36 +08:00
Bohan Jiang
df74e513d7 docs: remove duplicate cloud links (#5892)
Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-24 16:59:22 +08:00
Bohan Jiang
c0c4066965 docs: add documentation link to readme (#5891)
Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-24 16:55:33 +08:00
Naiyuan Qing
00e658401b feat(views): revamp execution log — virtualized, readable, two-tier header (#5890)
Combines the transcript work (previously split across #5860 virtual
scroll and #5871 reading hierarchy) into one branch on current main,
folding in the runtime-alias display from #5881.

- Virtualize the event list (react-virtuoso) so a multi-thousand-event
  run mounts a bounded number of DOM rows (#5733), with firstItemIndex
  anchoring for newest-first live prepends.
- Reading hierarchy via a pure trace-event-presenter: agent text and
  errors render expanded in place through RichContent (compact,
  log-scale markdown); thinking/tool rows fold to one line; tool detail
  expands into a quiet surface with "show all".
- Persisted 3-way expand density (smart/expanded/collapsed) with per-row
  overrides; legacy defaultExpanded boolean migrated. Filters always
  persist (dropped the preserve-filters toggle).
- Two-tier header: identity row (status, agent, trigger source,
  triggered-by) + list toolbar (created/duration/events facts left,
  shared Button controls right); runtime/provider/workdir/timestamps
  move to an ⓘ popover, runtime shown via runtimeDisplayName (#5881).
- Copy-all exports full event bodies (redacted) with RFC 3339 timestamps
  (#5873), not the truncated summary.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 16:44:43 +08:00
Bohan Jiang
73b0015475 feat(vcs): make self-hosted Git providers self-host-only (MUL-3772, MUL-5138) (#5888)
* feat(vcs): gate self-hosted Git providers to self-host deployments only (MUL-3772)

The Forgejo/Gitea/GitLab integration is intended for self-hosted Multica, where
Multica can reach a Git instance on the operator's own network. On the managed
multi-tenant cloud it adds an SSRF surface (connect validates a user-supplied
instance URL from the server) and would store third-party Git tokens for all
tenants under one key, while only serving the small subset of users whose
instance is publicly reachable. Product decision: offer it on self-host only.

- Add an explicit deployment switch MULTICA_VCS_INTEGRATION_ENABLED (default
  off). Connect, rotate, and webhook now require BOTH the switch on AND a valid
  MULTICA_VCS_SECRET_KEY — the switch is the product boundary, not key presence
  alone. When off, connect/rotate return 404 and the webhook returns a bare 404
  (no config leak), independent of the frontend.
- /api/config exposes vcs_integration_available (mirrors the switch, omitted
  when false) so the Settings UI hides the whole "Git providers" section on
  cloud instead of surfacing an operator-only "missing key" hint.
- docker-compose.selfhost.yml defaults the switch on; .env.example documents it.
- Docs (en/zh) lead with a callout: available on self-hosted Multica only, not
  Multica Cloud, and clarify "self-hosted" means Multica itself, not just Git.

#5006 / #5883 stay in place — the schema and backend capability are retained;
this only gates availability. No cloud VCS connection can exist (connect always
required the key, which the cloud never set), so nothing needs migrating.

Verified: go build/vet + VCS/config handler tests on a fresh migrated DB
(incl. a new disabled-deployment 404 test); pnpm typecheck (core + views) and
the integrations-tab + core schema/config vitest suites pass.

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

* fix(vcs): complete self-host integration gating (MUL-5138)

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

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-24 16:39:22 +08:00
YikaJ
2691110867 fix(agent): scope Hermes provider-error sniffer to real error boundaries (#5864)
Fixes #5862.

The Hermes ACP provider-error sniffer over-matched conversation/tool JSON that
Hermes echoes to stderr as `[INFO] root:` records, flipping already-completed
runs to failed. Error matching is now scoped to real provider-error boundaries:

- Skip INFO/DEBUG root-logger records (single- and multi-line JSON) via a
  structural state machine, so echoed payloads never participate in matching.
- Keep genuine bare provider errors (⚠️//📝 Error:) and non-root [ERROR]
  records, including after a possibly-truncated INFO record.
- Length only bounds the persisted error summary, never whether a line is
  classified as an error (fixes the earlier #1952-style regression).

Reported and initial fix by @YikaJ; remaining boundary fixes added directly by
the maintainers to land this quickly as a production bug.
2026-07-24 16:31:00 +08:00
Rusty Raven
999f73e61f fix(projects): show owner/repo label for GitHub resource URLs (MUL-5221) (#5795)
* fix(projects): show owner/repo label for GitHub resource URLs instead of truncated full URL

Long GitHub URLs in the project-resources sidebar are end-truncated by CSS
`truncate`, making similar repos like org/very-long-repo-a and
org/very-long-repo-b indistinguishable from the visible prefix alone.

Extract just the `owner/repo` segment from the URL for display; keep the full
URL in the tooltip so it remains accessible on hover. Applied to both the
resource row and the repository picker popover. Custom labels are unaffected.

Fixes #5771

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

* fix(projects): strip .git suffix and handle scp SSH URLs in githubShortLabel; add middle-truncation for long repo names

Addresses #5795 review feedback:
- Strip .git suffix (example-org/very-long-repo-name.git → without .git)
- Handle git@github.com:owner/repo.git (scp shorthand) which new URL() rejects
- Add midTruncate() so repos sharing a long common prefix are distinguishable
  by their tail (customer-alpha vs customer-beta) rather than being end-truncated
  identically by CSS

Fixes #5771.

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

* refactor(projects): share githubShortLabel across project resource UIs

Move githubShortLabel/midTruncate out of project-resources-section into
packages/views/common/github-url.ts so the create-project repository picker
reuses the same owner/repo shortening (it was still rendering full URLs) and
recognize the www.github.com host. Add github-url.test.ts covering the
https / ssh / scp / enterprise / malformed cases plus middle-truncation.

Addresses #5795 review feedback: shared helper, create-project coverage, tests.

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

---------

Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Bohan Jiang <52446949+Bohan-J@users.noreply.github.com>
2026-07-24 15:55:57 +08:00
Naiyuan Qing
7cc763bbfb fix(runtimes): show runtime aliases across user-facing surfaces (MUL-5248) (#5881)
Several UIs rendered the raw daemon name (runtime.name) and dropped the
user's custom_name alias: the Skills "Copy from runtime" selector, the
Skills list/detail source, the agent creation runtime chip, the agent
runtime filter, the agent overview, the skills/MCP runtime hints, the
runtime delete confirmations, the desktop runtime window title, the
onboarding runtime cards/hints, and the task transcript chip.

Route every user-visible runtime label through the shared display contract:
- runtimeDisplayLabel (alias + provider) for standalone labels
- runtimeDisplayName (alias only) where a provider icon/text sits beside it
- machine.title + runtimeRowLabel(runtime, machine.title) inside machine groups

The Skills "Copy from runtime" selector is now machine-grouped via the shared
buildRuntimeMachines/runtimeRowLabel helpers instead of a flat raw-name list;
runtime.name stays the source of identity for hostname parsing, grouping,
search, and protocol payloads only.

Add a conventions rule (en + zh) forbidding raw runtime.name in user-visible
JSX/i18n/Select labels/document titles.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-24 15:54:48 +08:00
Multica Eve
d84e5654df fix(runtime): add repository setup preflight (#5886)
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-24 15:52:48 +08:00
Multica Eve
a96532e4d4 MUL-5261: add stack-aware focused testing skill (#5877)
* docs: standardize focused Vitest command

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

* fix(daemon): guard focused test commands system-wide

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

* fix(agent): load focused test guidance by stack

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

* chore(agent): remove unused skill metadata

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

* fix(agent): verify focused test guidance

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

* docs(agent): trim focused testing skill

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

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-24 15:48:48 +08:00
Bohan Jiang
07a0a4272d fix(lark): scope group recent_context to the Lark topic (MUL-5224) (#5882)
Feishu 话题 sessions are isolated per topic (#5061), but the group
recent_context prefetch read the whole chat: a @-mention inside topic B
could pull topic A's messages into B's prompt and persist them into B's
turn (#5835). Fix the leak at the fetch:

- ListChatMessages sends container_id_type=thread&container_id=<thread_id>
  when a topic id is present; the thread container rejects end_time, so the
  window is anchored to the trigger time client-side instead.
- Parse thread_id off the REST item and fail-close on it: a topic fetch
  keeps only exact thread_id matches, dropping any missing/mismatched item
  so a sibling topic can never leak even if the API returns one.
- A topic fetch failure degrades to the readable note and NEVER falls back
  to a chat-wide fetch (that would re-open the leak); a very busy topic may
  get empty context, which is the safe trade.
- Exclude the Bot's own interactive-card replies from the window (they
  flatten to a zero-signal [interactive card] placeholder).
- Non-topic group @-mentions keep the chat-level, end_time-anchored path
  unchanged.

Closes #5835

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-24 15:38:13 +08:00
Bohan Jiang
3f2e1c68d7 fix(migrations): resolve VCS migration prefix collision (213-215) and unblock deploy (MUL-3772) (#5883)
Renumber the six VCS migrations off the 213/214/215 prefix collisions to a unique contiguous tail 216-221 (tables before indexes), and make 214_chat_session_project idempotent (ADD COLUMN IF NOT EXISTS) so the #5868 rename-induced re-run no-ops instead of crashing. Fixes the red migration lint on main and unblocks the dev deploy.
2026-07-24 15:28:43 +08:00
Bohan Jiang
2aafa41a5f MUL-5258: feat(issues): show agent-working indicator in Table view (#5875)
* feat(issues): show agent-working indicator in Table view

Reuse the self-contained IssueAgentActivityIndicator (already in List and
Board views) in the Table view's Issue column, rendered right after the
identifier and before the title — matching the List view placement so the
'agent working' cue sits in the same spot across all three views. The
indicator returns null when no agent is active, so inactive rows are
unchanged.

MUL-5258

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

* test(issues): cover Table agent-working badge insertion

The previous stub returned null with no assertion, so deleting the badge
insertion in table-view.tsx would still pass every test here. Render a
marker carrying the issue id and assert the Table cell mounts it for the
row's issue, positioned between the identifier and the title (identifier →
activity → title, matching List/Board). Verified the new test fails when the
insertion is removed.

MUL-5258

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

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-24 15:02:33 +08:00
dixonl90
581d9527ba feat(vcs): self-hosted Git providers (Forgejo, Gitea, GitLab) alongside GitHub (MUL-3772) (#5006)
Adds self-hosted Git provider support (Forgejo, Gitea, GitLab) alongside GitHub:
per-workspace token connection, a provider-dispatched webhook, PR/MR and CI
mirroring, and the shared issue auto-link / auto-close machinery. Off until
MULTICA_VCS_SECRET_KEY is set, so existing deployments are unaffected.

Co-authored-by: Bohan <bohan@devv.ai>
2026-07-24 15:01:27 +08:00
Multica Eve
a767c5b361 feat(transcript): include timestamps in copied events (#5873)
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-24 13:56:25 +08:00
Naiyuan Qing
7d2f20f2ee feat(chat): warn when agent daemon is too old for project context (#5867)
Chat project context (MUL-5150, #5765) is rendered into the run brief by the
daemon, so daemons older than the release that ships it silently drop the
project description while still honoring the server-extracted repos. Mirror
the handoff-note soft gate: resolve the active agent's runtime cli_version
from the warm runtime cache and, when it is a stale release build, surface a
warning next to the composer chip and inside the project submenu. Selection
is never blocked — dev-describe builds and unknown runtimes stay silent.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 13:32:44 +08:00
Bohan Jiang
be7c3521aa fix(migrations): renumber chat_session_project off colliding prefix 213 (#5868)
PR #5765 (MUL-5150) branched before #5841 merged, so both landed a 213
migration and Backend CI's TestMigrationNumericPrefixesStayUniqueAfterLegacySet
failed on main. Bump #5765's pair to the next free prefixes, preserving the
column-before-index order:
  213_chat_session_project       -> 214_chat_session_project
  214_chat_session_project_index -> 215_chat_session_project_index

MUL-5251

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-24 13:03:25 +08:00
ohyun_227
ffc66c68ff MUL-5245: docs: add Windows install instructions and split OS-specific steps into collapsible sections (#5859)
* docs: add windows self-hosting

* docs: add windows self-hosting
2026-07-24 12:55:36 +08:00
Jiayuan Zhang
a3fe6d91dd MUL-5150: add project context to Chat (#5765)
* feat(chat): add project context

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

* fix(chat): resolve MUL-5150 review blockers

- Renumber project-context migrations to unique prefixes after current main:
  206_chat_session_project -> 212 (column), 207_chat_session_project_index ->
  213 (concurrent index). 206/207 collided with 206_agent_disabled_runtime_skills
  and main's 207-211 client_usage_daily set.
- Add the 4 missing chat input.project_context keys to ja/ko locales so the
  locale parity test passes (en/zh-Hans already had them).
- Lock the project-context control while a send is in flight (isSubmitting),
  not just while the agent is running. A brand-new chat creates its session
  lazily during send bound to the project at click time; switching project
  mid-send would create the session against the stale project and clear the
  editor as if the send landed on the new selection. Add a regression test.

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

* fix(chat): complete project context handling

* fix(chat): pin fresh chat to open session's agent on project switch

Switching an existing session to a different project opens a fresh chat but
only cleared the active session, dropping selection back to the stored
`selectedAgentId`. When that preference was stale (open session belongs to
agent B while the persisted pick is still agent A), the lazily-created session
and its first send bound to the wrong agent (agent A).

Extract the project-switch decision into a shared `planProjectContextChange`
pure helper in use-chat-controller.ts and route both chat surfaces (the chat
tab controller and the floating ChatWindow) through it, so the fresh chat is
pinned to the open session's agent and the rule cannot drift between the two
copies. Add a dual-entry regression test (pure-fn guard + controller
integration) covering the stale selectedAgentId case.

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

* chore(ci): re-trigger required checks on latest head

The prior push updated the branch ref but GitHub did not emit a pull_request
synchronize for it (PR head-sync lag), so CI/Mobile Verify never ran on the
commit carrying the stale-agent project-switch fix. Empty commit to force a
fresh synchronize on a head that includes it.

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

* fix(chat): renumber project migrations to 213/214 after main added 212

Current main added 212_agent_service_tier; the PR's 212/213 chat migrations
collided with it on the merge ref, failing TestMigrationNumericPrefixesStay
UniqueAfterLegacySet. Merge current main and move the chat column migration to
213 and the concurrent index migration to 214 (column before index preserved).

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

* fix(chat): lock ProjectPicker clear control during send (keyboard path)

The send-pending lock only put pointer-events-none on the wrapper, which
blocks the mouse but leaves ProjectPicker's inline clear button in the tab
order — a keyboard user could Tab to "Remove from project" and press Enter
mid-send, detaching the project after the lazily-created session already went
out with the old one (reopens the mid-send retarget path via keyboard).

Add an explicit `disabled` capability to the shared ProjectPicker that locks
the trigger, the menu (forced closed), and the inline clear button (disabled +
out of the tab order). Defaults to false, so issue/create/autopilot callers
keep their hover/keyboard clear. ChatInput passes disabled while the project
selection is locked.

Tests: real-ProjectPicker regression (keyboard activation of the clear control
is inert when disabled; still works when enabled) + ChatInput wiring assertion
that the picker is disabled mid-send.

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

---------

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Walt <walt@multica.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com>
Co-authored-by: NevilleQingNY <nevilleqing@gmail.com>
2026-07-24 11:30:27 +08:00
苗大
a90aa92d0c fix(web): resolve API and docs upstreams at runtime (#4840)
* fix(web): resolve upstream URLs at runtime

* fix(web): keep unconfigured upstreams same-origin

* fix(web): restore dev-only localhost fallbacks for API and docs upstreams

`next dev` on a developer machine now falls back to the conventional
http://localhost:8080 backend (honoring BACKEND_PORT) and
http://localhost:4000 docs origin when nothing is configured, so a bare
`pnpm dev:web` keeps proxying out of the box. Builds and the runtime
proxy keep the strict resolvers, so prebuilt images still leave unset
upstreams unproxied.

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

---------

Co-authored-by: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 11:02:46 +08:00
Bohan Jiang
8d18d3a9ec Revert "MUL-5180: fix(github): surface CI status on PR cards (#5811)" (#5855)
This reverts commit 139cc89200.

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-24 02:52:22 +08:00
Bohan Jiang
bce0c05856 refactor(agent): drop dead Cursor cost fields (CLI reports no cost) (#5852)
MUL-5240

cursor-agent's stream-json never populates total_cost_usd or a per-step
cost — the result event's usage object carries token counts only. Both
fields were speculatively copied from Claude Code's schema in the original
Cursor runtime PR (#1057) and were never read. Verified against the real
CLI (2026.07.20, stream-json and json) plus Cursor's CLI docs.

Remove the two dead fields and document that Cursor spend stays estimated
from the static rate table (no authoritative per-turn cost to carry, unlike
Grok). No behavior change.

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-24 01:55:07 +08:00
Bohan Jiang
a92bd5387e MUL-5231 fix(agent): parse Cursor top-level thinking and tool_call events (#5846)
Cursor stream-json emits reasoning and tool calls as top-level `thinking` /
`tool_call` events; the parser only looked for them inside assistant messages,
so transcripts showed a single step. Match the top-level events, taking the
tool name from the nested `<name>ToolCall` key and normalizing the packed
call_id. Subtypes are matched explicitly — only `started` opens a tool, only
`completed` closes it, only `delta` carries reasoning — so an unknown/missing
subtype is ignored rather than synthesizing a fake result (which would decrement
the daemon in-flight tool count early and misfire the watchdog) or polluting
reasoning. Covered by a recorded-stream fixture test, an unknown-subtype
regression test, and an opt-in real-CLI smoke test.
2026-07-24 01:52:15 +08:00
Bohan Jiang
e5a48eb59d fix(agent): accept ACP configOptions model catalog (kimi-code 0.29) (#5851)
kimi-code 0.29 dropped the top-level `models.availableModels` /
`currentModelId` block from its ACP `session/new` response and moved the
same catalog into a `configOptions` entry with `id`/`category` of
"model". parseACPSessionNewModels only understood the old shape, so
discovery silently returned an empty catalog and the model picker showed
"no available models" for an online, correctly-detected kimi runtime.

Parse `configOptions` as a fallback: the `models` block still wins when
present, so no existing ACP provider changes behaviour. `options[].value`
becomes the model id, `options[].name` the label, and `currentValue`
marks the default. Non-model options (thinking level) are deliberately
skipped — they are a separate product surface and would offer values
`session/set_model` cannot honour.

Also log a debug line with the top-level response keys (keys only, never
values) when session/new succeeds but advertises no catalog, so the next
round of upstream schema drift is visible in daemon.log instead of
looking like a PATH or install failure.

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-24 01:45:24 +08:00
Bohan Jiang
ffa8e16369 MUL-5228 fix(usage): bill Grok at xAI's reported cost, fix $0 resumed sessions (#5841)
* fix(agent): attribute Grok usage from the turn's own model id

A resumed Grok session with no configured model recorded its entire spend
under the model id "unknown", which matches no pricing row — so the task
reported $0 cost instead of its real spend.

grok.go only learned the model from the session handshake, and ACP's
`session/load` carries no model id (only `session/new` does). When neither
the agent nor MULTICA_GROK_MODEL pins a model, `daemon.go` legitimately
passes an empty model, leaving nothing to attribute the usage to.

Every Grok turn stamps `result._meta.modelId` with what it actually billed
against. Parse it in the shared ACP result parser and use it as the fallback
in grok.go. Other ACP backends are untouched — they keep whatever the
handshake gave them.

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

* fix(metrics): price the Grok catalog in server-side cost metrics

server/internal/metrics/pricing.go carried no Grok rows at all, so
RecordLLMUsage took the unpriced branch for every Grok turn: llm_cost_usd
reported zero Grok spend while the tokens accumulated in
llm_unpriced_tokens. Internal cost monitoring simply could not see Grok.

Add the six SKUs xAI publishes rates for, mirroring the frontend table in
packages/views/runtimes/utils.ts. Aliases are anchored exact matches like
the gpt-5.6 rows, so `grok-composer-*` (in the catalog, absent from the
price sheet) stays unmapped instead of inheriting a guessed rate.

Short-context tier on purpose: xAI bills a request at 2x once its prompt
reaches 200K tokens, but a usage record aggregates every model call in a
turn and cannot say which tier an individual request hit.

A regression test re-derives the cost of a real grok 0.2.106 turn from the
table and checks it against the costUsdTicks xAI returned for that turn.

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

* docs(changelog): scope the Grok cost claim to what was actually fixed

The v0.4.9 entry promised "accurate cost" in all four languages, but the
fix corrected catalog pricing and cached-input double-counting — it did not
implement xAI's 2x long-context tier, so a turn whose requests reach 200K
prompt tokens still under-reports by up to 50%. Say what was fixed instead.

Also correct two stale claims in the pricing comment: the daemon tags usage
rows with the runtime provider `grok`, not `xai` (the bare `grok-*` keys are
what make them resolve), and record why thresholding the long-context tier
on an aggregated row would be worse than not pricing it at all.

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

* feat(usage): carry the provider's own cost through to the usage record

Cost has always been derived client-side as tokens x a static rate, which
cannot express request-level pricing rules. xAI bills a Grok request at 2x
once its prompt reaches 200K tokens, and a task_usage row aggregates every
model call in a turn — so the stored token counts genuinely cannot say which
tier any individual request hit. Thresholding on the aggregate would be worse
than the status quo: it turns a bounded 50% under-estimate into an unbounded
over-estimate for turns made of many short requests.

Grok already reports what it charged, per turn, in `_meta.usage.costUsdTicks`.
Parse it, carry it through agent -> daemon -> API, and store it on task_usage
as a nullable BIGINT of 1e-10 USD ticks (integer, so sub-cent turns stay exact
end to end). NULL means the provider reported no cost — every pre-existing row
and every provider that doesn't return one. No backfill: there is no
authoritative figure to recover for those, and inventing one is the guess this
removes.

A single hourly bucket can mix rows that carry a cost with rows that don't, so
task_usage_hourly gains both halves: `cost_usd_ticks` sums the authoritative
side, and `uncosted_*_tokens` carry exactly the tokens that still need a
rate-table estimate. Consumers report authoritative + estimate(uncosted),
which degrades to today's behaviour when nothing in the bucket is
authoritative. The existing token columns keep covering every row, so token
displays are untouched. The new columns are additive with defaults, so the
unique key, the dirty-queue shape, and migration 102's triggers are unaffected.

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

* feat(usage): prefer the provider's own cost over the rate table

With the authoritative figure now stored, both cost consumers use it: the
usage dashboard (estimateCost / estimateCostBreakdown) and the server-side
llm_cost_usd metric. Each reports `authoritative + estimate(uncosted tokens)`,
so a row or bucket that mixes priced and unpriced sources stays whole.

The static rate tables remain, but for Grok they are now a fallback — they
still price usage recorded by a daemon too old to report cost, and every
provider that reports none. Custom pricing overrides likewise apply only to
the estimated half: they are a user's guess at a rate, and the authoritative
half is not a guess. A model with no rate-table row but a provider-reported
cost now also drops out of the "unmapped models" banner, since asking the user
to supply a rate for it would invite overriding a real bill.

llm_cost_usd is labelled by token_type and the provider reports one number per
turn, so the charge is distributed across the buckets in the rate table's own
proportions. Only the total is authoritative; the split stays an estimate,
which is why this scales the existing buckets rather than inventing a label.
estimateCostBreakdown does the same, keeping the stacked chart summing to the
headline figure instead of silently under-drawing every Grok row.

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

* docs(changelog): say Grok cost now follows xAI's actual charge

The earlier wording scoped the claim down to catalog pricing and cached input
because the long-context tier was still unhandled. It is handled now — the
cost comes from what xAI charged for the turn — so the entry can say so.

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

* fix(usage): keep the provider's cost when the model has no rate row

Both cost consumers bailed out before reading the authoritative figure when
the rate table had no row for the model. A `grok-composer-*` turn — in the
Grok Build catalog, absent from xAI's price sheet — was therefore reported as
$0 spend even though xAI told us exactly what it charged.

Worse on the client: estimateCost returned the real cost while
estimateCostBreakdown returned zeros, so the headline and the stacked chart
disagreed on precisely the rows whose cost is exact — and the unmapped-models
banner was (correctly) hidden, so nothing explained the discrepancy.

Handle the charge before the rate lookup in both places. Without rates there
is nothing to split a total by, so it lands whole in the `input` bucket, the
same fallback distributeAuthoritativeCost already uses when it has no shape to
scale. Tokens with no rate keep going to llm_unpriced_tokens: "unpriced"
describes the rate table, not the money.

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

* perf(usage): drop the historical rewrite from the cost-split migration

Migration 213 rewrote every existing task_usage_hourly row to seed the
uncosted counters. That is a full-table UPDATE inside a schema migration —
lock time, WAL and bloat all scaling with table size — for rows this issue
explicitly does not care about.

Deleting the UPDATE alone would have zeroed historical cost: with
`NOT NULL DEFAULT 0`, an untouched row asserts "nothing here needs
estimating", so every pre-split bucket would report $0 until the rollup
happened to touch it. Make the uncosted columns nullable with no default
instead. NULL means "never recomputed since the split existed", readers
COALESCE it to the row's own token total ("estimate all of it"), and the
pre-split behaviour is preserved exactly — with nothing to seed, so no
rewrite. A bare ADD COLUMN is metadata-only, so this is now fast DDL.

Rows heal into the split naturally as the rollup recomputes their buckets.

Verified on a fresh database: a legacy-shaped row reads back as its full
tokens to estimate, and a group mixing legacy and post-split buckets sums to
the authoritative cost plus both rows' estimable tokens.

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

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: J <agent@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-24 01:42:08 +08:00
Bohan Jiang
139cc89200 MUL-5180: fix(github): surface CI status on PR cards (#5811)
* fix(github): surface CI status on PR cards (MUL-5180)

The CI mirroring pipeline (MUL-2228, MUL-2392) has never received a single
event in production. The GitHub App setup docs only ever asked operators to
grant `pull_requests: read` and subscribe to `pull_request`, so GitHub never
delivered `check_suite` — `handleCheckSuiteEvent` sat dead behind a
subscription nobody was told to enable. Every linked PR reports
checks_passed/failed/pending = 0 and the sidebar row falls through to
"Checks haven't reported yet" forever.

Docs (the root cause), all four locales:
- add `Checks: Read-only` permission + `Check suite` event to the App setup
  table
- drop the stale "CI check states are not modeled" claim, which predates
  MUL-2228 and is what let the setup table stay incomplete
- add a "PR rows show no CI status" troubleshooting entry with the public
  `/apps/<slug>` probe to confirm what an App is actually subscribed to, and
  a warning that existing installations must accept the new permission
  before any `check_suite` is delivered

UI:
- give the actionable status kinds (checks failed/pending/passed, conflicts,
  ready) their own icon + color. CI outcome previously rendered as plain
  muted 11px text, visually identical to the diff stats beside it — a failing
  build read the same as "+437 −6 · 6 files". Terminal and unknown kinds stay
  muted; the row's state icon already carries that meaning.

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

* fix(github): unbreak docs build, stop overclaiming CI completeness (MUL-5180)

Both must-fixes from review.

1. docs production build failed. `<your App>` in prose was parsed as a JSX
   tag, so `pnpm --filter @multica/docs build` died with `Expected a closing
   tag for <your>`. Dropped the angle brackets. Repo CI never caught this
   because no workflow runs the docs production build — only Vercel does,
   which is why the PR's GitHub checks were green while the deployment
   errored.

2. `Checks: Read-only` cannot support the pending status the docs promised.
   GitHub's webhook contract delivers `check_suite.requested` /
   `.rerequested` only to Apps holding Checks *write*; read-level access
   receives `completed` only. Verified against GitHub's published docs.

   Direction chosen: keep read-only, degrade honestly to final-results-only.
   Checks *write* is a repo-write capability (create/update check runs), not
   a wider read — escalating every installation to it just to render an
   in-flight spinner is not a trade to make on the operator's behalf, and it
   contradicts the integration's read-only posture.

   The concrete bug this leaves is premature green: with two reporting apps,
   the first to complete makes total=1/passed=1 and the row claimed "All
   checks passed" while the second was still running and might fail. Copy is
   now "Checks passed" in all four locales — it reports what reported and
   never asserts completeness. `derivePullRequestStatusKind` documents why.

   Docs gain a "what CI status can and cannot tell you" section (all four
   locales) with the read-vs-write delivery table, both consequences stated
   plainly, and the opt-in path for teams that do want in-flight status: set
   Checks to Read and write on their own App and the existing pending code
   lights up with no code change. The pending promise is removed from the
   read-only setup path.

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

* fix(github): ignore non-completed check_suite actions (MUL-5180)

Review was right: the `Read and write` opt-in the previous commit documented
does not produce reliable pending, and following it would break the card.

`check_suite.requested` / `.rerequested` are not observations that some CI
provider started. GitHub sends them only to Apps holding Checks write, and
per the CI-checks App docs they mean "GitHub has created a check suite for
YOUR app on this commit; now add your check runs to it".

Multica observes other apps' results and never creates check runs. Recording
such a suite parks a `queued` row nothing can ever complete, and since
`checks_pending` outranks `checks_passed` in derivePullRequestStatusKind, one
stuck row freezes every PR on that installation at "checks running" and hides
the real pass/fail result. Any self-hoster who already grants Checks write
hits this on every push, so the gate is on the action, not the permission.

- handleCheckSuiteEvent drops every action except `completed`, with the
  reasoning and the "don't resurrect requested as a running signal" warning
  recorded at the gate.
- TestWebhook_CheckSuite_QueuedCountsAsPending encoded the wrong delivery
  semantics (two external apps sending `requested`, which GitHub never does).
  Replaced by TestWebhook_CheckSuite_NonCompletedActionsIgnored, which pins
  the drop and checks a later `completed` suite still lands.
- The two out-of-order stash tests used `requested` payloads to exercise
  paths that are really about completed suites; both now use `completed` and
  assert the same guarantees.
- Docs (four locales): the write opt-in is gone. In-flight CI is documented
  as unsupported at any permission level, with the actual reason and the note
  that real running status needs polling or a check_run model instead.

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

* fix(github): make legacy non-completed check suites inert (MUL-5180)

Review was right again: the previous commit gated the webhook entry point but
left the pre-upgrade state — and the people it was meant to protect (self-
hosters who already granted Checks write) are exactly the ones holding it.

Two leftovers, both now closed:

1. Rows already in github_pull_request_check_suite. The old handler stored
   GitHub's `requested` suites as `queued`; nothing will ever complete them.
   ListPullRequestsByIssue still counted them, so `checks_pending` kept
   outranking `checks_passed` and the PR stayed pinned to "checks running"
   for as long as its head SHA stood. The aggregation now selects only
   `completed` suites.

   Filtering beats deleting here: recovery is automatic on deploy, needs no
   migration over a table that can be large, and holds for any writer that
   misses a gate — not just for today's legacy rows. DISTINCT ON runs after
   the filter, so an app whose newest suite is a stuck `queued` still reports
   its most recent completed verdict instead of disappearing.

2. Rows already in github_pending_check_suite. replayPendingCheckSuitesForPR
   is a second write path into the live table that never passes through
   handleCheckSuiteEvent, so the next `pull_request` event would re-inject a
   permanently-queued suite after the fix shipped. It now skips non-completed
   rows; the drain is DELETE ... RETURNING, so skipping discards them.

Both are covered by regression tests that seed the legacy row directly — the
fixed handler can no longer produce one — and both were confirmed to fail
with their respective fix reverted. The stash test additionally asserts its
fixture landed under the repo address the drain keys on; the first draft used
the wrong owner and passed vacuously.

Also corrects the aggregateChecksConclusion doc comment, which still
described "pending" as a not-yet-completed suite. It is now reachable only
for a completed suite carrying a null conclusion, and is explicitly not a
"CI is running" signal.

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

* test(github): assert the legacy stash row is consumed by the drain (MUL-5180)

Review nits.

The stash test proved its fixture existed before the webhook but never that
the drain consumed it, so a future change to firePullRequestWebhookWithHead's
repo address would make the assertions pass for the wrong reason again — the
same way the first draft of this test did. Asserting the stash is empty
afterwards closes that gap from the other side.

Also fixes two comment typos: `an "CI is running"` -> `a`, and drops the
"merged-but-open PR" state, which cannot exist.

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

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-23 21:23:25 +08:00
Bohan Jiang
e2ce3a2da8 MUL-5223 fix(runtime): forbid blocking on external CI in the runtime brief (#5840)
* fix(runtime): forbid blocking on external CI in the brief (MUL-5223)

The external-work boundary added in #5803 did not stop agents from
waiting on GitHub Actions. Two holes: the section's only concrete
"how to wait" example was a blocking foreground call, which is exactly
the shape of `gh pr checks --watch`; and the "unless acceptance
criteria require it" escape was satisfied by the repo's own merge
requirement that CI be green.

Name the banned tool shapes, allow a single non-blocking status
snapshot, deny branch protection as an acceptance criterion, and give
the replacement hand-off phrasing (local test result + PR link).

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

* fix(runtime): scope the CI-wait ban so the explicit exception stays executable (MUL-5223)

Review feedback on #5840:

- The ban read as absolute ("Blocking on external CI is never part of
  your deliverable") while the next bullet allowed waiting when the
  task explicitly asks for the CI result, leaving no way to satisfy
  both. The ban is now scoped to "unless the explicit exception below
  applies", and the exception names the one executable shape: a single
  foreground blocking watch inside the same turn.
- `gh pr merge --auto` enables auto-merge and returns; it is not a
  wait. Only waiting for it to land is banned.

Both hard-pin tests now also pin the exception so it cannot be dropped
or re-absolutised.

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

* polish(runtime): group Background Task Safety into run-owned and external-CI clusters (MUL-5223)

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

* polish(runtime): cut redundant phrasing from the external-CI cluster (MUL-5223)

The cluster said "report and finish" three different ways and carried
two rhetorical tails. Fold the delivery template into the post-push
playbook bullet, tighten the merge-gate denial, and drop filler.
5 bullets -> 4, -36 words, every behavioral fact and test pin intact.

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

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-23 18:38:51 +08:00
YYClaw
e17e04b312 MUL-5215: fix(agents): keep creation errors visible (#5746)
* fix(agents): keep creation errors visible

* fix(agents): localize duplicate-name errors
2026-07-23 18:29:28 +08:00
Bohan Jiang
ecbdbda09e fix(agent): stop double-counting Grok cached input tokens (#5838)
Grok Build reports cachedReadTokens inside inputTokens (totalTokens ==
input + output on a real 0.2.106 turn, and that turn's costUsdTicks
matches xAI's rates only when the cached prefix is billed once). The
shared ACP parser persisted both counters raw, so the usage dashboard
charged the cached prefix at the full input rate *and* the cache-read
rate — ~4x the real spend on a cache-heavy turn.

Re-bucket cached reads out of input when totalTokens proves the overlap,
the same normalization codex.go already applies. Backends that report
mutually-exclusive buckets or omit totalTokens are untouched.

Co-authored-by: J <agent@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
v0.4.9
2026-07-23 17:55:33 +08:00
Multica Eve
98072e2e56 fix(issues): filter working agents by active task issues (#5839)
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-23 17:54:52 +08:00
Multica Eve
f51e019788 docs(changelog): add v0.4.9 release entry (2026-07-23) (MUL-5219) (#5832)
* docs(changelog): add v0.4.9 release entry (2026-07-23)

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

* chore(web): bump version to 0.4.9

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

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-23 17:40:38 +08:00
Bohan Jiang
71fb9c71df docs: document the Windows Defender false positive on the bundled CLI (#5829)
Windows Security flags multica.exe inside app.asar.unpacked as
Trojan:Script/Wacatac.B!ml and quarantines it. The !ml suffix is a
machine-learning heuristic verdict, not a signature match: our Windows
builds are not Authenticode-signed (release.yml sets
CSC_IDENTITY_AUTO_DISCOVERY=false and no certificate is configured), and
an unsigned, low-prevalence Go binary that spawns background processes
and opens sockets is the classic false-positive profile.

Add a Desktop docs section (en/zh/ja/ko) covering how to verify the
binary against checksums.txt, restore it from quarantine, exclude both
the install directory and %APPDATA%\\Multica (Desktop re-downloads the
CLI there, so excluding only the install dir loops), and report the
false positive to Microsoft.

MUL-5216

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-23 17:15:15 +08:00
Bohan Jiang
fb9e1a8c72 MUL-5191 fix(usage): price the Grok catalog + keep custom pricing editable (#5827)
* feat(usage): price the published xAI Grok catalog

Grok usage resolved to no pricing row, so every Grok token contributed
$0.00 to cost totals and the models sat permanently in the unmapped
diagnostic. Add the six SKUs xAI publishes rates for.

Short-context rates on purpose: xAI bills a request at 2x once its
prompt reaches 200K tokens, but aggregated usage rows carry no
per-request prompt sizes — the same trade-off the Anthropic `[1m]`
context tag already takes. `grok-composer-*` ships in the Grok Build
catalog but is absent from the price sheet, so it stays unmapped rather
than inheriting a guessed rate.

Source: https://docs.x.ai/developers/pricing
Co-authored-by: multica-agent <github@multica.ai>

* fix(usage): keep custom pricing editable after it is saved

The custom-pricing dialog had exactly one entry point: the button inside
the unmapped-models banner. That banner only rendered while something in
the window was unpriced, and `resolvePricing` falls back to the custom
store — so saving a rate for the last unmapped model made it resolve,
the banner disappeared, and the saved rate could no longer be edited or
removed. Clearing localStorage was the only way out, which also wiped
every other override.

Keep the bar mounted whenever there is something to manage: warning
state while models are unpriced, a quiet row with an edit button once
they all resolve but overrides exist. Hidden entirely otherwise.

The existing store mock hard-coded `getCustomPricing` to undefined, so
no test could ever observe a saved override; make it stateful and add
the regression case.

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

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-23 16:56:48 +08:00
Multica Eve
423a5c59cb MUL-5200: unify working-agent filters across issue views (#5819)
* fix(issues): query workspace working agents independently

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

* feat(agents): filter working agents by source type

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

* feat(issues): scope working agents to My Issues

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

* test(agents): cover My Issues squad relations

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

* fix(issues): unify working-agent filters across views

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

* fix(issues): preserve empty working-agent filters

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

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-23 16:54:41 +08:00
Bohan Jiang
1fef98c24f fix(desktop): open in-app links in a tab instead of the browser (MUL-5208) (#5826)
* fix(desktop): open in-app links in a tab instead of the browser (MUL-5208)

A link written as an absolute URL on this deployment's own origin
(`https://<app-host>/acme/issues/1` — what "copy link" produces, and what
agents paste into chat) fell through openLink's external branch to
window.open, which Electron routes to shell.openExternal. Clicking an issue
link in Desktop chat therefore opened a browser window instead of a tab.

openLink now resolves such a URL back to its in-app path and takes the same
route a relative path does. Backend-served prefixes (/api/, /_next/) stay
external so attachment downloads keep working.

Two supporting fixes the change depends on:

- The `multica:navigate` event had no listener on web, so in-app paths in
  content were dead links there; normalizing app URLs would have extended
  that to every pasted app URL. The web platform layer now answers the event
  with a router push.
- The desktop handler opened every path inside the active workspace's tab
  group. A cross-workspace link now goes through switchWorkspace, matching
  what the navigation adapter already does for pushes.

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

* fix(desktop): scope in-app link conversion to workspace pages, answer it in issue windows

Review follow-ups on MUL-5208.

1. The non-page exclusion list only covered /api/ and /_next/, so a
   same-origin /uploads/* link — local-storage attachments, served by the
   backend and proxied by web — was routed as an app page, opening a dead tab
   instead of the file. Replaced the deny-list with the app's own routing
   model: an absolute URL converts to an in-app path only when its first
   segment is a slug a workspace could own, which the existing reserved-slug
   list (shared with the backend) already answers. /api, /uploads, /_next,
   /favicon.ico and the pre-workspace routes all stay external without a
   second list to keep in sync.

2. A dedicated issue window derives the same app origin from its adapter but
   had no multica:navigate listener, so a same-origin link there became a
   silent no-op (it used to reach the browser). The window now answers the
   event: another issue opens in place — matching what its adapter push and
   mention chips already do — and any other app page, which this single-route
   window cannot host, goes to the browser.

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

---------

Co-authored-by: J <agent@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-23 16:50:23 +08:00
Naiyuan Qing
3d9d3c0aa5 Revert "Unify resize interaction feedback (#5779)" (#5831)
This reverts commit 7d6a1dfab1.
2026-07-23 16:39:06 +08:00
YYClaw
e3ebf317ae fix(i18n): capitalize on-behalf-of attribution label (MUL-5026) (#5670) 2026-07-23 16:35:22 +08:00
Naiyuan Qing
4bf17f5786 Revert "fix(resize): keep drag cursor consistent for table & chat resize (MUL…" (#5830)
This reverts commit 975cd49c3f.
2026-07-23 16:30:37 +08:00
Naiyuan Qing
975cd49c3f fix(resize): keep drag cursor consistent for table & chat resize (MUL-5166) (#5824)
* fix(resize): keep the drag cursor consistent for table and chat resize

Table column resize and the chat floating-window resize locked the active
cursor via document.body.style.cursor, which loses the CSS cascade to any
descendant that declares its own cursor (clickable rows, inputs, buttons).
So the resize cursor showed on hover but flipped back to pointer/text/default
once the drag pointer swept over those elements — inconsistent with the
sidebar rail and react-resizable-panels, which force it globally.

Switch both to the same robust contract used by the sidebar/panels: set a
data attribute on <html> during the drag and let a global
`html[...], html[...] * { cursor: ...; user-select: none } !important` rule in
base.css win over every descendant. Chat keys the attribute by direction
(left/top/corner) so col/row/nw-resize stay correct.

MUL-5166

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(chat): clean up chat resize cursor lock on every drag-end path

The chat window resize started the drag with setPointerCapture but only
removed data-chat-resizing on pointerup. A pointer-capture drag can end via
pointercancel, lostpointercapture, or window blur without ever firing
pointerup; since the attribute now drives a global
`html[...] * { cursor; user-select } !important` rule, a missed cleanup would
strand it and freeze the whole page in the resize cursor with text selection
disabled.

Mirror the sidebar rail: one idempotent finishDrag() wired to pointerup,
pointercancel, lostpointercapture and blur, releasing pointer capture and
removing every listener plus the attribute from a single path. Add a
regression test covering each termination path.

MUL-5166

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

* fix(ui): release table resize cursor lock on window blur too

Table column resize wires stopResize to pointerup/pointercancel but not to
window blur. With the resize cursor now held by a global
`html[data-table-resizing] * { cursor; user-select } !important` rule, an
alt-tab mid-drag (button released while the window is blurred, so no pointerup
arrives) would strand the attribute and freeze the page. stopResize is
idempotent, so also wire it to blur — matching the sidebar rail and chat.

MUL-5166

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-07-23 16:20:24 +08:00
Multica Eve
40f9ecdd56 MUL-5202: unify Issue Query across List, Board, and Swimlane (#5820)
* MUL-5202: migrate status issue surfaces to table query

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

* MUL-5202: unify grouped issue surfaces

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

* MUL-5202: cover move safety boundaries

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

* MUL-5202: preserve server swimlane semantics

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

* MUL-5202: keep grouped surface facets exact

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

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-23 15:58:25 +08:00