Commit Graph

2885 Commits

Author SHA1 Message Date
Lambda
e47977212c fix(handler): make on_comment and @mention paths mutually exclusive
Without (issue, agent) coalescing, a single member comment that @mentions
the assignee was enqueueing two tasks with identical trigger_comment_id —
once via shouldEnqueueOnComment → EnqueueTaskForIssue and once via
enqueueMentionedAgentTasks → EnqueueTaskForMention. The same double-fire
hit plain replies that inherit the assignee mention from the thread root.

Add commentMentionsAssignee, which uses the shared
shouldInheritParentMentions logic to compute the same effective mention
set the @mention path will see. Extend the on_comment gate to skip when
that helper says the assignee will be triggered through the @mention
path. Net result: exactly one task per (comment, assignee), with the
trigger comment preserved.

TestOnCommentTriggerDecision updated to reflect the new contract; assignee
mention cases that used to assert the on_comment branch fires now assert
it skips (mention path covers them). New integration test
TestAssigneeMentionDoesNotDoubleEnqueue pins the end-to-end behavior.

Also softens the "ClaimAgentTask guarantees serial execution" wording in
the code and the RFC: it is a coordination-side property under the
current single-poller-per-runtime model, not a DB-level lock on
(issue, agent). FOR UPDATE SKIP LOCKED locks the row being claimed, not
the key.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-09 14:42:25 +08:00
Lambda
b0b667b907 refactor(handler): drop @mention coalescing dedup, enqueue per trigger
Each @mention or assignee comment now creates its own queued task instead
of folding into a single pending task per (issue, agent). The previous
HasPendingTaskForIssueAndAgent short-circuit in enqueueMentionedAgentTasks
and shouldEnqueueOnComment is gone; ClaimAgentTask already enforces
per-(issue, agent) serial execution at claim time, so multiple queued rows
drain one-by-one without overlap.

Why: see docs/rfcs/0001-mention-dedup-policy.md. Coalescing dropped the
trigger comment for every mention after the first, gave no UI feedback,
and collapsed distinct intents (different threads, different requests).

Drops the unused HasPendingTaskForIssue and HasPendingTaskForIssueAndAgent
queries and regenerates sqlc.

Adds TestRepeatedMentionsEnqueueSeparateTasks pinning the new contract:
two back-to-back @mentions queue two tasks with distinct trigger_comment_ids.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-09 14:29:55 +08:00
Valentin Mihov
560e081d8f Pass agent instructions inline to Hermes (#2283) 2026-05-09 14:23:41 +08:00
Bohan Jiang
73b401d47a i18n(views): translate workspace slug error messages (#2312)
The slug_reserved error introduced in #2228 was hardcoded English, and
the older inline format/conflict errors in step-workspace.tsx had the
same problem. Move all of them to the workspace + onboarding locale
namespaces (en + zh-Hans) and drop the now-unused string constants
from slug.ts.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-09 14:19:32 +08:00
Mark Gaze
c926dfe44b fix(views): validate workspace slug against reserved ones when creating (#2228) 2026-05-09 14:11:56 +08:00
Multica Eve
46eed3b298 Add task dispatched analytics event (#2310)
Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-09 14:11:20 +08:00
Bohan Jiang
0eb23df234 fix(agent): scope pi colon-to-slash normalization to legacy format (#2309)
PR #2281 added table-format support to parsePiModels but kept the
unconditional `strings.Replace(":", "/", 1)`, which would silently
rewrite a `:` inside a model name read from column 1 of the table
output (e.g. `claude-sonnet-4-6:exp` would become
`claude-sonnet-4-6/exp`). Move the replace into the legacy
`provider:model` branch so only the colon-as-separator case is
normalized, and restore a short doc comment describing the dual-
format contract. Test extended with a colon-bearing table row.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-09 13:56:49 +08:00
Jiayuan Zhang
c3832302b9 fix(transcript): expand long single-line Agent messages (multica#2282) (#2308)
Agent text rows in the run-records dialog only got a chevron when the
message had a newline; a long single-line reply was rendered with
truncate and the trailing content was unreachable. Other event types
(tool_use, tool_result, thinking, error) are expandable on any
non-empty content — bring text in line.

Also lead the collapsed summary with the first non-empty line instead
of the last, so multi-paragraph replies preview the lede rather than
the closing remark and the row stays stable while messages stream.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-09 07:53:45 +02:00
Leonardo Diego
8d5a6138fe fix: parse pi --list-models table format for model discovery (#2281)
The pi CLI changed its --list-models output from a single-field
'provider:model' format to a multi-column table with separate
'provider' and 'model' columns. The existing parser only looked
at the first whitespace-delimited field (the provider name) and
skipped lines without ':' or '/' — discarding every model entry.

Update parsePiModels to handle both formats:
- New table format: combine fields[0] (provider) + fields[1] (model)
- Legacy format: single field with ':' or '/' separator

Add regression test for the table format using real pi output.
2026-05-09 13:51:32 +08:00
Jiayuan Zhang
0cd50e14eb feat(agent-live-card): show queued tasks in issue live banner (MUL-1897) (#2307)
The issue-detail "agent live" banner only showed dispatched/running tasks.
A task that was queued — runtime offline, busy on a prior task, or held
behind a coalesced sibling — left the issue silent until claim, which
reads as "the trigger never landed".

Include 'queued' in `ListActiveTasksByIssue`, then branch the renderer:
queued banners use a non-spinning Clock, "{name} 排队中 / is queued"
copy, "queued for Ns" elapsed anchored on `created_at`, and hide the
transcript button (no execution log yet). Cancel still works because
`CancelAgentTask` already accepts queued.

Client-side re-sort by lifecycle (running → dispatched → queued) so the
sticky slot stays on the most-active task even when a queued sibling
was created more recently.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-09 07:33:12 +02:00
Multica Eve
ce00e05169 Add canonical PostHog core metrics events (#2302)
* Add canonical PostHog core metrics events

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

* Address analytics review feedback

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

* Tighten analytics review follow-ups

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

---------

Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-09 13:12:00 +08:00
Jiayuan Zhang
bb3d2b70ea fix(ui): let DropdownMenu popup size to content (#2306)
DropdownMenuContent had `w-(--anchor-width)` which locks the popup
width to the trigger. With icon-sm kebab triggers (~32px) the popup
was clamped by `min-w-32` to 128px, and longer items like
"Unresolve thread" / "标记为已解决" wrapped onto two lines.

Anchor-width matching is the right behavior for Select / Combobox
(both keep that class), but a generic kebab menu should size to its
own content. Drop the `w-(--anchor-width)` and keep `min-w-32` as the
floor.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-09 07:07:58 +02:00
hvejsel
bf186504b0 fix(timeline): sync around state on falsy prop transitions (#1968 follow-up) (#2230)
When the inbox split-pane is open and the user clicks a comment-notification
for issue X, then a non-comment notification for the SAME issue (status,
assignment, sub-issue), <IssueDetail> stays mounted (keyed on issueId in
inbox-page.tsx so composer drafts and scroll position survive). The hook's
internal `around` state has to react to the prop transitioning back to falsy
— otherwise the around-mode cache is re-served on every subsequent click and
entries outside the original window appear "lost" until a hard refresh.

The truthy guard on the effect skipped the falsy branch:

  useEffect(() => {
    if (options.around) setAround(options.around);  // ← skipped on null
  }, [options.around]);

Replace it with an unconditional sync. useState's initialiser already covers
the mount-time read; the effect now covers all subsequent prop transitions
including → null.

Adds a regression test that asserts the hook re-keys useInfiniteQuery on the
truthy → undefined transition.

Co-authored-by: Sara <sara@sara.local>
2026-05-09 12:58:06 +08:00
Bohan Jiang
b17f975a17 docs(cli): clarify issue rerun semantics (current assignee, fresh session) (#2304)
* docs(cli): clarify `issue rerun` semantics

The CLI table described `multica issue rerun <id>` as "Rerun the most
recent agent task", which led users to expect it would re-run whichever
agent ran last. The actual behavior is to enqueue a fresh task for the
issue's **current** agent assignee, regardless of who ran most
recently — see `TaskService.RerunIssue` in
`server/internal/service/task.go`.

Also fix a stale claim in `tasks.mdx`: the "Manual rerun" section
described session inheritance as "Yes", but commit b1345685 made manual
rerun pass `force_fresh_session=true` precisely to avoid replaying a
poisoned session. Only **automatic retry** still inherits the session.

Updates EN + ZH mirrors of `cli.mdx` and `tasks.mdx`.

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

* docs(tasks): tighten rerun trigger surface; clean stale Go comments

Apply review feedback on PR #2304:

- `tasks.mdx` / `tasks.zh.mdx`: rerun is triggered via CLI or the
  `/api/issues/{id}/rerun` endpoint, not "UI or CLI" — there's no rerun
  affordance in web/desktop today.
- `tasks.mdx` / `tasks.zh.mdx`: comparison table — manual rerun applies
  to "Issues with an agent assignee", not "All sources". The handler
  rejects with `issue is not assigned to an agent` for anything else,
  and there's no rerun path for chat or autopilot tasks.
- `task_lifecycle.go`: `RerunIssue` doc comment claimed the new task
  "carries the most recent session_id/work_dir so the agent can resume".
  That has been false since b1345685 — rewrite to reflect the actual
  `force_fresh_session=true` contract.
- `agent.sql` (regenerated `agent.sql.go`): `GetLastTaskSession` doc
  said it serves "auto-retry / manual rerun"; manual rerun is now
  routed around it via `force_fresh_session=true`. Note both the
  auto-retry path it does serve and the rerun escape hatch.

No logic change.

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-09 12:46:37 +08:00
Bohan Jiang
190ef87475 docs(cli): clarify <id> accepts both issue key and UUID (#2305)
The CLI now accepts routable short IDs across issue/autopilot/project/label/task
commands (shipped 2026-05-08), but the docs still only show <id> placeholders,
so new users wonder whether `multica issue list` -> `multica issue get MUL-123`
is supposed to work. Add a callout to the cheat sheet pages and a concrete
`MUL-123` example to the reference page so the supported flow is discoverable
without reading --help for every command.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-09 12:37:21 +08:00
Bohan Jiang
590ac7953e docs(cli): drop stale multica runtime ping command from CLI reference (#2303)
The `runtime ping` command was removed in #1554 along with the Test
Connection feature; runtime reachability is now detected via daemon
heartbeat. The English and Chinese CLI reference pages still listed the
removed command, which sent users to a non-existent subcommand.

Closes multica-ai/multica#2276

Co-authored-by: multica-agent <github@multica.ai>
2026-05-09 12:23:17 +08:00
Jiayuan Zhang
3b3be9d7bd feat(comments): resolve threads with collapsible bar (MUL-1895) (#2300)
* feat(comments): resolve threads with collapsible bar (MUL-1895)

Adds a Linear-style resolve action on comment thread roots. Resolved
threads collapse to a single "N resolved comments from X" bar in the
activity feed; clicking expands the thread inline (per-session, not
persisted). Replying inside a resolved thread auto-unresolves it.

Backend
- migration 069: resolved_at, resolved_by_type, resolved_by_id on comment
- sqlc ResolveComment / UnresolveComment queries (idempotent via COALESCE)
- POST/DELETE /api/comments/{id}/resolve handlers, root-only validation
- CreateComment auto-clears resolved_at when a reply lands in a resolved
  thread, publishing comment:unresolved
- comment:resolved / comment:unresolved events; CommentResponse and
  TimelineEntry both surface the new fields

Frontend
- Comment + TimelineEntry types extended; payloads typed; WS sync wired
- useResolveComment optimistic mutation with rollback
- ResolvedThreadBar component for the collapsed view
- Resolve / Unresolve menu items on root comments; Collapse strip on the
  expanded resolved card
- en + zh-Hans locale strings

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

* fix(comments): cover agent reply path, expand-state hygiene, nested counts (MUL-1895)

Addresses three review issues from Emacs on PR #2300:

1. TaskService.createAgentComment bypasses Handler.CreateComment, so the
   auto-unresolve wired into the handler did not fire when an agent replied
   in a resolved thread (task / mention / on_comment paths). Extracted the
   logic to TaskService.AutoUnresolveThreadOnReply so both reply paths share
   it; rewired Handler.CreateComment to call the new method.

2. Resolving an already-expanded thread no longer collapses it back to the
   bar because expandedResolved still contained the id. Added
   clearResolvedExpand + handleResolveToggle wrapper so resolve / unresolve
   always wipe the session expand entry.

3. ResolvedThreadBar received only direct children, while CommentCard's
   expanded view recurses through descendants. Extracted the recursive
   walk into thread-utils.collectThreadReplies and called from both —
   counts and author lists now match.

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

* test(comments): mock useResolveComment + add zh-Hans plural key

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-09 05:49:33 +02:00
Jiayuan Zhang
bf0665a1a8 fix(desktop): copy issue link reflects connected env, not localhost (#2298)
* fix(desktop): derive appUrl from apiUrl in dev so copy-link follows the connected env

Local desktop dev was hardcoding appUrl to http://localhost:3000, so the
"Copy issue link" output pointed at localhost even when the renderer was
connected to a remote (e.g. test) backend — the resulting URL only worked
on the developer's machine.

- runtime-config dev path now mirrors the production loader: when
  VITE_APP_URL is unset, derive appUrl from apiUrl (host-only). The
  localhost api host is special-cased to keep the local web port (3000),
  while a remote api host (api.test.x) yields a remote appUrl.
- Web navigation adapter now implements getShareableUrl directly with
  window.location.origin instead of leaving it undefined.
- NavigationAdapter.getShareableUrl is now required; copyLink callers
  drop the window.location fallback branch and call it unconditionally.
- Add the missing getShareableUrl mock in issue-detail.test.tsx.

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

* fix(desktop): strip leading api. label when deriving appUrl

Address Emacs' code review on PR #2298. The previous derivation kept the
api hostname unchanged, so VITE_API_URL=https://api.test.multica.ai
produced appUrl=https://api.test.multica.ai — not the env's actual web
URL. Multica's convention exposes the api at api.<web-host>; strip that
leading label (when the host has at least 3 labels, to avoid mangling
short hosts like api.local) so a single api configuration produces the
correct shareable web origin.

- api.multica.ai      → multica.ai
- api.test.multica.ai → test.multica.ai
- api-staging.x.com   → unchanged (no leading "api." label)
- congvc-x99.ts.net   → unchanged

Update both the dev and production tests; also fix the existing
runtime-config-loader test that asserted the unstripped value.

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-09 05:13:55 +02:00
Bohan Jiang
bda475cbba refactor(reserved-slugs): single JSON source for backend + frontend (#2148)
Reserved workspace slugs lived in two parallel files (`workspace_reserved_slugs.go`
and `packages/core/paths/reserved-slugs.ts`) with no parity check. Adding or
renaming a global route on one side without the other would slip through CI
and surface only when a real user hit the collision.

Collapse the two lists into one source: `server/internal/handler/reserved_slugs.json`.
Go embeds the JSON via `//go:embed` and parses it at package init; the TS file
is regenerated by `scripts/generate-reserved-slugs.mjs` (run via
`pnpm generate:reserved-slugs`). CI re-runs the generator and `git diff
--exit-code`s the TS output, so a stale TS file cannot land. The slug set is
unchanged (87 entries, byte-equivalent slug literals).

Update CLAUDE.md to describe the new "edit JSON, run generator" workflow.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-08 19:14:12 +08:00
Bohan Jiang
d1a6881707 docs(changelog): add v0.2.28 entry for 2026-05-08 release (#2271)
Daemon disk-usage CLI, Skill picker search, Timeline polish and
task_usage daily rollup. Single-line bullets matching prior entries.

Co-authored-by: multica-agent <github@multica.ai>
v0.2.28
2026-05-08 17:46:00 +08:00
Bohan Jiang
97df9b90f5 refactor(daemon): rename repoCache interface, relax /health test timeout (#2270)
Two follow-up nits from PR #2211 review:

- Rename the package-local `repoCache` interface to `repoCacheBackend`
  so the field declaration `repoCache repoCacheBackend` no longer shadows
  its own type name.
- Bump the `/health`-must-respond timeout in
  `TestHealthHandlerRespondsWhileTaskRepoLookupWaits` from 200ms to 1s.
  The regression case blocks indefinitely on the old code, so a 1s
  upper bound still fail-fast detects it while leaving headroom for
  loaded CI runners.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-08 17:38:06 +08:00
Bohan Jiang
61ce8a8090 feat(daemon): add disk-usage CLI to surface per-task / per-workspace footprint (#2267)
* feat(daemon): add disk-usage CLI to surface per-task / per-workspace footprint

Adds `multica daemon disk-usage [--by-workspace] [--by-task] [--top N]
[--output json]`, walking the workspaces root to report task and workspace
disk consumption without requiring a running daemon. Sizing reuses the GC
artifact patternSet (basename-only) so the reported "artifact" footprint
matches what `cleanTaskArtifacts` would actually reclaim, and the walk
honors the same safety contract: never enters .git, never follows symlinks,
counts only regular files.

Refactors WorkspacesRoot resolution into an exported `ResolveWorkspacesRoot`
so the read-only CLI picks the same root the running daemon would have.

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

* fix(daemon): distinguish displayed totals from scan totals; add workspace artifact ratio

- Track scan-wide TotalTaskCount / TotalWorkspaceCount on the report so
  `--top N` no longer leaves the table footer claiming the truncated row
  count is the full count. The CLI now prints a "Showing top N of M …
  Displayed: X. Scan total: Y" line whenever truncation happens, and keeps
  the bare "Total: …" footer for the un-truncated case.
- Add ArtifactRatio (0..1) on WorkspaceDiskUsage and TotalArtifactRatio on
  the report. The workspace table renders an `ARTIFACT %` column. ratio()
  guards size=0 so empty workspaces report 0% instead of NaN%.

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-08 17:14:52 +08:00
Bohan Jiang
fe8326fa0c feat(agents): add search box to skill picker dialog (#2269)
Filters available skills by name + description (case-insensitive) as the
user types. Auto-focuses on open and clears the query on close. Shows a
distinct "no match" empty state vs. the existing "all assigned" one.

Closes #2266

Co-authored-by: multica-agent <github@multica.ai>
2026-05-08 17:12:11 +08:00
Qiang Zhang
f1dc3dc986 fix: keep daemon health responsive during repo lookup (#2211) 2026-05-08 16:51:36 +08:00
Thanh Minh
0b64f09c12 fix(runtimes): exclude archived agents from counts (#2166)
* fix(runtimes): exclude archived agents from counts

* test(runtimes): align workload fixture with shared types
2026-05-08 16:33:31 +08:00
Bohan Jiang
823f124d67 feat(daemon): extend GC to chat / autopilot / quick-create tasks (#2260)
* feat(daemon): extend GC to chat / autopilot / quick-create tasks

Before this change the daemon's GC was strictly issue-centric: only tasks
with a non-empty issue_id ever wrote .gc_meta.json, and shouldCleanTaskDir
called only the issue gc-check endpoint. Chat / autopilot run / quick-create
tasks fell through to the GCOrphanTTL mtime path, which mis-killed active
chat sessions while leaving deleted ones around far longer than necessary.

Schema:
- GCMeta gains a Kind discriminator and per-kind ID fields
  (ChatSessionID / AutopilotRunID / TaskID). WriteGCMeta now takes a
  GCMeta struct so the call site classifies the task explicitly.
- ReadGCMeta defaults empty Kind to GCKindIssue, so legacy on-disk meta
  files keep flowing through the issue path with no migration required.

Server endpoints (siblings of /api/daemon/issues/{id}/gc-check, all behind
requireDaemonWorkspaceAccess for the same anti-enumeration shape):
- GET /api/daemon/chat-sessions/{id}/gc-check  -> {status, updated_at}
- GET /api/daemon/autopilot-runs/{id}/gc-check -> {status, completed_at}
- GET /api/daemon/tasks/{id}/gc-check          -> {status, completed_at}

shouldCleanTaskDir dispatches on Kind:
- chat: active is hard-skipped (no mtime fallback) so idle sessions are
  never reclaimed; archived + GCTTL cleans; 404 falls back to mtime to
  stay safe for cross-workspace tokens.
- autopilot_run: terminal (completed/failed/skipped/issue_created) +
  GCTTL cleans; running/pending skips. Uses run.completed_at as the TTL
  anchor since autopilot_run has no updated_at column.
- quick_create: terminal task status cleans immediately (workdir is not
  reused by the linked issue task, which has its own envRoot); running
  skips.

Also drops the "skipping .gc_meta.json: issue_id is empty" warn — with
the new kind dispatch, chat/autopilot/quick-create tasks now write a
proper meta file instead of triggering this log.

Refs: GC follow-up to PR #2077 (symptom fix) and #2115 (chat hard delete).
Co-authored-by: multica-agent <github@multica.ai>

* fix(daemon): chat gc-check 404 cleans immediately, no mtime gate

PR review caught that the chat 404 path was routing through
orphanByMTime, which deferred reclamation to GCOrphanTTL (72h) when
acceptance #3 calls for cleanup within one GC cycle (≤ 1h) after the
user hard-deletes a session.

Every chat_session_id we ever ask about was written by this same daemon
under its current token, so the cross-workspace probe defense the issue
path needs doesn't apply here. Drop the gate and clean on 404 directly.

Test updates:
- TestShouldCleanTaskDir_KindDispatch/chat_404 flips the locked
  expectation from gcActionSkip to gcActionClean.
- Adds TestShouldCleanTaskDir_ChatHardDeletedFreshMtime: GCOrphanTTL
  set to a year so any mtime-based path is unmistakably out, and the
  fresh-mtime workdir still cleans on the chat-404 fast path.

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-08 16:12:48 +08:00
Bohan Jiang
b1d874ef50 fix(timeline): rescue orphaned replies + bump page size to 50 (#2263)
Two related changes for the same UX problem (#1857 follow-up).

1. Orphan-reply rescue. The grouping in issue-detail.tsx put replies under
   their parent's CommentCard, looking them up via repliesByParent.get(parentId).
   When a reply's parent wasn't in the loaded timeline — pagination boundary,
   merge truncation, future backend bug — the entire reply subtree dropped
   off the screen, since the orphan replies sat in the map with no
   CommentCard around to render them. MUL-1847 hit this on the OLD backend:
   1 root + 29 replies, the root was the oldest entry and the merge dropped
   it, so all 29 replies vanished from the UI even though the API returned
   them.

   The fix: a reply whose parent_id points to a comment NOT in the loaded
   timeline is promoted to top-level. It still loses its visual indentation
   under the missing parent, but it stops disappearing.

2. Page size 50. With activities now decoupled from the comment budget
   (#2253) and the off-by-one fixed (#2259), 50 fits the typical issue
   without any "Show older" interaction. Cost is bounded — SQL fetches
   limit+1 = 51 comments + 50 activities through the keyset index from
   migration 068; response body grows ~70% over 30 but stays well under
   the legacy compat path's 200-row cap. UI renders 100 entries
   comfortably; CommentCards memoize.

   Frontend default in `client.ts` (`limit = 50`) matches the new backend
   default (`timelineDefaultLimit = 50`) so pages walk consistently.

Test: render-level case in `issue-detail.test.tsx` mocks a timeline page
containing only an orphaned reply (parent_id refers to a missing id) and
asserts the reply text appears.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-08 16:08:56 +08:00
Multica Eve
eb067ff077 fix(server): aggregate task_usage into daily rollup table to cut DB load (#2256)
* fix(server): aggregate task_usage into daily rollup table to cut DB load

ListRuntimeUsage previously did a SUM(...) GROUP BY DATE(created_at), provider,
model over the raw task_usage stream once per runtime row on the runtimes
list and once per detail page load, scaling O(events) per call. This is the
hot read path responsible for sustained load on Postgres.

Switch the read path to a materialized daily rollup table maintained by a
pg_cron job:

- 072_task_usage_daily_rollup: schema for task_usage_daily +
  task_usage_rollup_state, plus rollup_task_usage_daily_window(p_from, p_to)
  (window primitive used by both cron and offline backfill, idempotent via
  ON CONFLICT DO UPDATE adding deltas) and rollup_task_usage_daily() (cron
  entry point — pg_try_advisory_lock(4242) for serialization, watermark
  advancement, 5-minute safety lag for late-visible inserts). Also adds
  idx_task_usage_created_at to help the two lazy endpoints
  (ListRuntimeUsageByAgent / GetRuntimeUsageByHour) that still hit the
  raw table.

- 073_task_usage_daily_pgcron: CREATE EXTENSION IF NOT EXISTS pg_cron in a
  DO/EXCEPTION block (mirrors the migration 032 pg_bigm pattern so envs
  without shared_preload_libraries=pg_cron skip gracefully) and schedules
  rollup_task_usage_daily() every 5 minutes when the extension is present.

- queries/runtime_usage.sql ListRuntimeUsage rewritten to read from
  task_usage_daily; sqlc regenerated. Other usage queries unchanged.

- cmd/backfill_task_usage_daily: one-shot Go command that walks
  task_usage in monthly slices through rollup_task_usage_daily_window,
  then stamps the watermark to now()-5m so the cron resumes cleanly.
  Run once after migrations have applied, before relying on the rollup.

- runtime_test.go: TestGetRuntimeUsage_BucketsByUsageTime now invokes
  rollup_task_usage_daily_window after fixture inserts so the handler
  sees the rolled-up rows. Synthetic daily rows cleaned up after each
  test.

- runtime_rollup_test.go: new tests covering aggregation correctness,
  idempotency contract of ON CONFLICT DO UPDATE, and the watermark
  advancing exactly to now()-5m via the cron entry point.

Deployment order: apply migrations → run backfill_task_usage_daily once
→ pg_cron picks up subsequent windows automatically. Today bucket may be
up to ~10 minutes stale (5 min cron + 5 min lag) by design.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(server): make task_usage_daily rollup safe to overlap, replay, and correct

Addresses 4 review blockers on the original PR:

1. Cron/backfill double-count race: the rollup function is now idempotent.
   Window calls find DIRTY KEYS via task_usage.updated_at, then RECOMPUTE
   each bucket from ground truth and REPLACE the daily row (no more
   additive ON CONFLICT). Cron and backfill can now overlap safely.

2. Silent pg_cron absence: the read path is gated behind a new
   USAGE_DAILY_ROLLUP_ENABLED feature flag (default off). The raw
   task_usage scan is preserved as the fallback. Operators flip the
   flag per-environment after backfill + cron are confirmed healthy
   (task_usage_rollup_lag_seconds() helper added for monitoring).

3. UpsertTaskUsage corrections invisible to rollup: added
   task_usage.updated_at column (default now(), backfilled from
   created_at), and bumped it on conflict. Corrections now mark the
   bucket dirty and the next window call recomputes it correctly.

4. CREATE INDEX blocking writes on hot table: split into separate
   single-statement migrations using CREATE INDEX CONCURRENTLY
   (074, 075), matching the 035/067 pattern.

Also: cron.schedule() removed from migrations entirely. Migration 076
only enables the extension (gracefully on unsupported envs); the actual
schedule is a documented operator runbook step that runs AFTER backfill.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(server): trigger-driven invalidation + online-safe migration for task_usage_daily

Round-2 review feedback on PR #2256:

1. Add explicit dirty-bucket queue (task_usage_daily_dirty) populated by
   triggers on agent_task_queue (UPDATE OF runtime_id, DELETE) and
   task_usage (DELETE). The rollup window function drains both this queue
   and the updated_at-based discovery, so runtime reassignment and
   issue-cascade deletes no longer leave the rollup divergent from the
   raw query.

   Triggers join via agent (not issue) to look up workspace_id, because
   when the cascade comes from issue, the issue row is already gone by
   the time atq's BEFORE DELETE fires; agent stays alive.

2. Make migration 072 online-safe: only ADD COLUMN updated_at TIMESTAMPTZ
   (nullable, no default → metadata-only ALTER, no row rewrite) and a
   separate ALTER for SET DEFAULT now() (also metadata-only). No bulk
   UPDATE on the hot task_usage table. The rollup window function's
   dirty_keys CTE handles legacy NULL rows via an OR branch, supported
   by partial index idx_task_usage_created_at_legacy.

3. Refresh stale documentation in cmd/backfill_task_usage_daily/main.go
   header to describe the current recompute/replace semantics, idempotent
   re-runnability, and the actual migration numbering (072..077).

Tests:
- TestRollupTaskUsageDaily_InvalidationOnReassign: verifies usage moves
  between runtime buckets after ReassignTasksToRuntime-style update.
- TestRollupTaskUsageDaily_InvalidationOnIssueDelete: verifies daily
  bucket is cleared after issue delete cascades through atq → task_usage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(server): close dirty-queue race + move legacy partial index to its own concurrent migration

Round-3 review feedback on PR #2256:

1. Blocker: dirty-queue invalidations could be silently lost under
   concurrency. ON CONFLICT DO NOTHING let a late trigger see the row
   already enqueued, no-op, and then the rollup drain (WHERE
   enqueued_at < p_to) would delete the original row — losing the
   late invalidation. Switched all three trigger enqueue paths to
   ON CONFLICT DO UPDATE SET enqueued_at = GREATEST(existing,
   EXCLUDED.enqueued_at), so any invalidation arriving during a
   rollup tick keeps enqueued_at > p_to (p_to = now() - 5min) and
   survives the post-tick drain.

2. High: idx_task_usage_created_at_legacy (partial index on hot
   task_usage table) was being created in the regular 077 migration
   without CONCURRENTLY. Moved to new migration 078 with
   CREATE INDEX CONCURRENTLY, matching the pattern of 074/075.
   077's down migration leaves the index alone (it is owned by 078).

3. Minor: gofmt -w on runtime_rollup_test.go and
   backfill_task_usage_daily/main.go (tabs were lost in the original
   heredoc append). PR description rewritten to describe the current
   recompute/replace + dirty queue + feature flag design and the
   072..078 migration ordering.

Tests still green: TestRollupTaskUsageDaily_* (including both new
invalidation regressions), TestGetRuntimeUsage_*, TestWorkspaceUsage_*.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(server): unify workspace_id source via agent in rollup window function

Round-4 review feedback (J) on PR #2256:

M1 (must-fix): The dirty queue triggers resolved workspace_id via
`agent.workspace_id`, but the window function's `dirty_from_updates`
discovery and `recomputed` recompute join used `issue.workspace_id`.
There is no schema-level FK guaranteeing
`agent.workspace_id == issue.workspace_id`. Any divergence (future
cross-workspace task scenarios, data repairs, migration bugs) would
cause:

  - dirty queue rows with workspace_id from agent
  - recompute join filtering by workspace_id from issue
  - 0 matches in recompute → bucket erroneously hits the
    deleted_empty branch and the daily row is silently dropped
  - dirty_from_updates path attributing usage to the wrong workspace

Replaced both CTEs to JOIN agent (not issue) so trigger / discovery /
recompute share one workspace_id source. Comment in 077 explains the
constraint.

N1: Refreshed two stale references in
cmd/backfill_task_usage_daily/main.go (header now says "072..078";
stampWatermark warning now mentions migration 073, where the rollup
state table is actually introduced).

Test: New TestRollupTaskUsageDaily_WorkspaceMismatch constructs an
atq with agent.workspace_id != issue.workspace_id, asserts the bucket
lands under agent's workspace (not issue's), and re-asserts after a
runtime reassign in the foreign workspace. Acts as a canary if the
schema invariant changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica.ai>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
2026-05-08 15:35:21 +08:00
Bohan Jiang
6400868412 fix(timeline): off-by-one — exact-limit comments no longer triggers Show older (#2259)
Pre-fix the gate was `len(comments) >= limit`, which fired even when the
issue had EXACTLY <limit> comments. The "Show older" affordance appeared,
the user clicked, the next page fetched zero rows. User flagged it on
MUL-1857 — "this issue happens to have 30 comments; the button shouldn't
appear in that case."

The fix is the standard over-fetch probe: ask the SQL for limit+1 rows; if
it returned more than limit, drop the extra and report hasMore=true.
Otherwise hasMore=false.

- New helper `commentOverflow(rows, limit) -> ([]db.Comment, bool)` replaces
  the count-based `hasMoreCommentsBeyond`. Works for both DESC (latest /
  before) and ASC (after / around-newer) since both want "keep first
  <limit>".
- All four mode handlers (latest, before, after, around) now ask for
  limit+1 comments and route through the helper.
- Activities still cap at <limit> with no overflow probe — they don't gate
  pagination (#1857), so the boundary doesn't matter for them.

Tests:
- TestCommentOverflow pins the truth table with the boundary case
  ("exactly limit comments" → hasMore=false).
- TestListTimeline_ExactlyLimitCommentsHidesShowOlder is the DB-backed
  regression: 30 comments, limit=30, asserts has_more_before=false and
  next_cursor=nil.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-08 15:24:39 +08:00
Bohan Jiang
bbbbcf9b6e fix(timeline): make Show older / Show newer affordances clearly clickable (MUL-1858) (#2257)
The pre-fix top "Show older" was a bare <button> sandwiched between two
horizontal divider lines, styled `text-xs text-muted-foreground`. Visually
it read as a divider, not an action — users on issues with hidden older
entries thought the comments had vanished and didn't notice the affordance.

Convert all three timeline pagination affordances to shadcn Button:

- Top: outline button with ChevronUp icon, "Show older"
- Bottom (in around-mode pages): outline button with ChevronDown icon,
  "Show newer"; default-variant button with ArrowDownToLine icon,
  "Jump to latest" (or "Jump to latest · N new")

No behavior change — same fetchOlder / fetchNewer / jumpToLatest hooks,
same i18n keys. Just the visual treatment.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-08 14:59:01 +08:00
Bohan Jiang
161194b86f fix(timeline): exclude activities from comment page budget (#2253)
* fix(timeline): exclude activities from comment page budget

The /timeline endpoint paginated comments + activities through one shared
50-row budget, so an issue with a chatty agent (status flips, task_completed
markers, assignee toggles per run) could trigger "show older" with as few as
10-20 actual comments — users opened the page and thought their discussion
had vanished.

- Comment limit drops from 50 to 30 (the visible page size users wanted).
- has_more_before / has_more_after gate on comments alone via the new
  hasMoreCommentsBeyond helper. Activity rows still ride along at the same
  per-call SQL cap but no longer push real comments off-page.
- Merge functions stop truncating at the page limit; both pools are
  individually bounded by SQL, so dropping rows here only re-introduced the
  bug. The legacy (pre-cursor) path applies its 200-row cap inline.
- Test rewrite: TestHasMoreBeyond → TestHasMoreCommentsBeyond, replaced the
  #2192 merge-truncation regression with a #1857 "dense activity does not
  hide comments" test that pins the new contract directly.

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

* fix(timeline): per-pool keyset cursor for comments and activities

Pre-fix, next_cursor / prev_cursor anchored on the merged page boundary
(oldest / newest entry overall). When activity rows were older than every
fetched comment — common on issues created with a status change before the
first comment — the latest page emitted a cursor pointing at that activity,
and the next "show older" call sent that timestamp into ListCommentsBefore,
skipping every unreturned comment in between. GPT-Boy flagged this on
PR #2253 with the 80-comment / 30-activity scenario where 50 comments
became permanently unreachable.

The fix splits the cursor into independent comment and activity positions:

- timelineCursor carries (CommentT, CommentID, ActivityT, ActivityID).
  encode/decode signatures changed accordingly.
- New cursorPos type and four bounds helpers (commentBoundsDesc / Asc,
  activityBoundsDesc / Asc) extract per-pool oldest/newest from fetched
  rows, with a carry fallback so empty pools advance past the input cursor
  instead of resetting.
- All four mode handlers (latest, before, after, around) now derive cursors
  from each pool's own bounds. Removed the entryTimestamp / entryID helpers
  that re-parsed the merged entry slice.

Tests:
- TestTimelineCursor_RoundTrip pins the encode/decode contract for the new
  dual-pool format (and rejects garbage input).
- TestListTimeline_PerPoolCursorWalksAllComments reproduces GPT-Boy's exact
  scenario (30 activities older than 80 comments, limit=30) and asserts
  every comment is reachable through repeated `before=<cursor>` walks.

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-08 14:58:54 +08:00
Multica Eve
9a3a99cef8 fix: make CLI short IDs routable
Make CLI table IDs routable across issue, autopilot, project, label, and task-run workflows. Adds scoped UUID-prefix resolution, --full-id table options, issue KEY display, safer actor/name output, and updated CLI docs/runtime prompt.
2026-05-08 14:32:03 +08:00
ASDFGHoney
14ab487c95 feat(issues): show identifier in detail page breadcrumb (#2244)
Parent and child issues already render their identifier on the issue
detail page; only the issue you're viewing is missing one. Add it to
the breadcrumb between the parent identifier (when present) and the
title, matching the existing parent identifier styling.

Refs multica-ai/multica#2243

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 14:30:46 +08:00
Matt Van Horn
6b7294aa5b fix(daemon): use brew prefix symlink for self-restart so Linux Cellar deletion does not orphan runtimes (#2076)
* fix(daemon): use brew prefix symlink for self-restart so Linux Cellar deletion does not orphan runtimes

After brew upgrade on Linux, os.Executable() resolves /proc/self/exe to
the Cellar path (e.g. .../Cellar/multica/0.2.9/bin/multica), which
brew cleanup deletes. The previous IsBrewInstall() short-circuit skipped
EvalSymlinks to 'preserve' the symlink, but on Linux there was nothing
to preserve - the path was already resolved.

Use cli.GetBrewPrefix() to resolve the stable symlink path
<brewPrefix>/bin/multica for brew installs. Fall back to
EvalSymlinks(os.Executable()) with a warning log when GetBrewPrefix()
returns empty (brew binary missing from PATH).

Introduce package-level function vars (isBrewInstall, getBrewPrefix) so
the daemon test can override them without modifying the cli package.

Closes #1624

* fix(daemon): harden brew-prefix fallback and document the WHY

When `brew --prefix` is unavailable but the binary is under a known Cellar
root, recover the prefix from cli.MatchKnownBrewPrefix and target
<prefix>/bin/multica instead of falling back to the resolved Cellar path
(which brew cleanup just deleted).

- Extract knownBrewPrefixes + MatchKnownBrewPrefix in cli/update.go and
  reuse from IsBrewInstall to keep one source of truth for the install-root
  list.
- Add a WHY comment above the brew branch in triggerRestart explaining the
  /proc/self/exe -> Cellar -> deleted-by-brew-cleanup chain.
- Cover both fallback paths (matched / unmatched) in daemon_test.go.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-05-08 12:08:56 +08:00
Bohan Jiang
d964d37f97 Revert "fix(cli): add --content-file / --description-file for non-ASCII on Wi…" (#2252)
This reverts commit 9650788709.
2026-05-08 12:04:03 +08:00
Bohan Jiang
9650788709 fix(cli): add --content-file / --description-file for non-ASCII on Windows (#2247)
* fix(cli): add --content-file / --description-file for non-ASCII on Windows

Windows PowerShell 5.1 (the Win11 default) and cmd.exe re-encode HEREDOC
content through the active console codepage before piping it to a child
process. Characters the codepage cannot represent are silently replaced
with `?`, so agents on Chinese Win11 hosts emitting `--content-stdin` /
`--description-stdin` HEREDOCs land all of their Chinese as `?` in the
issue body and comments. The daemon log shows the original Chinese
correctly because slog writes to a file directly, so the regression
hides until the user opens the issue page.

Add a `--content-file <path>` / `--description-file <path>` source to
`resolveTextFlag`: the CLI reads the file straight off disk, preserves
UTF-8 bytes verbatim, and skips the shell entirely. The runtime config
injected into AGENTS.md / CLAUDE.md now surfaces this as the canonical
Windows fallback when the daemon host runs on Windows; non-Windows hosts
keep the existing stdin/HEREDOC guidance untouched.

Closes #2198, #2236.

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

* fix(execenv): route every Windows-host stdin directive at --content-file

GPT-Boy on PR #2247 caught that the previous patch only inserted a Windows
fallback into the Available Commands section. Two later prompt surfaces
still hard-coded `--content-stdin` and overrode it for the agent:

- The Codex-specific paragraph in `buildMetaSkillContent`, which always
  said "always use `--content-stdin` with a HEREDOC".
- `BuildCommentReplyInstructions`, which is re-emitted on every turn for
  comment-triggered tasks (both via the AGENTS.md/CLAUDE.md workflow and
  the daemon's per-turn prompt) and mandated the same HEREDOC pipe.

On Windows hosts we now branch both surfaces to a file-based template:
the agent writes the body to a UTF-8 file with its file-write tool and
posts via `--content-file <path>`. Non-Windows hosts keep the existing
stdin/HEREDOC guidance untouched.

Tests:

- `TestBuildCommentReplyInstructionsWindowsUsesContentFile` pins the
  Windows / non-Windows reply-instruction text directly.
- `TestInjectRuntimeConfigWindowsCommentTriggerHasNoStdin` asserts that
  the end-to-end CLAUDE.md / AGENTS.md surface for a comment-triggered
  Windows task has no remaining `--content-stdin` directive that could
  override the Windows fallback (covers Claude + Codex providers).

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

* fix(execenv): make Windows comment block file-first, pin tests by GOOS

GPT-Boy's second review on PR #2247 flagged two follow-up blockers:

1. The Windows comment/description block in `buildMetaSkillContent` was
   "stdin first, file caveat appended" — agents on Windows still saw
   "Agent-authored comments should always pipe content via stdin" /
   "MUST pipe via stdin" / `--description-stdin` directives before
   reaching the Windows fallback, so the contradicting instruction was
   live in the same prompt. Rewrite the entire Available Commands
   bullet for Windows hosts as file-first: the headline line names
   `--content-file`, the bulleted rules name `--content-file` /
   `--description-file`, and stdin only appears in anti-prescriptive
   "do NOT pipe via …" prose.

2. The existing non-Windows tests (TestBuildCommentReplyInstructions
   IncludesTriggerID, TestInjectRuntimeConfigDirectsMultiLineWritesToStdin,
   TestInjectRuntimeConfigCodexEmphasizesStdinForFormattedComments,
   TestInjectRuntimeConfigCommentTriggerUsesHelper) all depended on
   `runtimeGOOS` defaulting to non-Windows; they would silently fail on
   a Windows test runner. Pin them to `runtimeGOOS = "linux"` via
   save+restore and drop t.Parallel so they don't race with the
   GOOS-mutating Windows tests.

Test additions:

- TestInjectRuntimeConfigWindowsRecommendsContentFile now asserts the
  Windows AGENTS.md does NOT contain prescriptive stdin phrasings
  (`MUST pipe via stdin`, `use --description-stdin and pipe a HEREDOC`,
  `<<'COMMENT'`, `Agent-authored comments should always pipe content via
  stdin`, `always use --content-stdin`) on top of the file-first
  positive assertions. The ban list pins prescriptive substrings, not
  bare flag names, so anti-prescriptive prose like "do NOT pipe via
  --content-stdin" doesn't trip the ban.
- TestInjectRuntimeConfigWindowsCommentTriggerHasNoStdin gets the same
  expanded ban list across the Available Commands, Codex paragraph,
  and per-turn reply template surfaces.
- The non-Windows side of TestInjectRuntimeConfigWindowsRecommendsContentFile
  pins that the Linux stdin/HEREDOC contract is still in place, so a
  future refactor can't accidentally move every host to file-first.

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-08 12:01:19 +08:00
Bohan Jiang
00ba0aa4e6 fix(desktop): replace Electron placeholder icons with Multica asterisk for Windows + Linux (#2248)
Both `apps/desktop/build/icon.ico` (Windows installer + Multica.exe) and
`apps/desktop/build/icon.png` (Linux deb/rpm/AppImage) were the default
electron-vite scaffold "atom" placeholder. They were never updated when
the macOS `icon.icns` was switched to the Multica asterisk in #1074, and
have shipped as-is in every v0.2.x release including v0.2.26 — closes
GitHub #2195.

Source: 1024×1024 PNG extracted from the existing build/icon.icns
(icon_512x512@2x), so all three platforms now share the same artwork.

- icon.ico: BMP frames at 16/24/32/48/64/128 + PNG-compressed 256×256.
  Matches electron-builder's "≥256×256" requirement and the BMP-then-PNG
  format mix Windows Explorer / NSIS render best across Win10/11.
- icon.png: 1024×1024 RGBA, replacing the previous 512×512 placeholder.

No electron-builder.yml change needed — buildResources: build picks
both files up automatically.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-08 11:42:14 +08:00
LinYushen
de356561bc docs(changelog): add v0.2.27 entry
* docs(changelog): add v0.2.27 entry

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

* docs(changelog): simplify v0.2.27 wording

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-07 18:10:50 +08:00
Naiyuan Qing
47aa32a04d refactor(chat): unify session list into single dropdown with grouped active/archived (#2220)
The chat window used to fire two parallel session queries (active subset
+ full list) and surfaced them through two UI entry points (the title
dropdown + a History icon panel). The two caches drifted during the
WS-invalidate window — visible as "completed → reload → ghost row"
flickers — and the History toggle was a redundant entry into the same
underlying data.

Collapse to one cache (full list, ?status=all) and one entry point
(dropdown). The dropdown groups locally into Active / Archived; the
archived group is collapsed by default with a count, and per-row
delete moves into the dropdown via hover-revealed trash + confirm
dialog. Backend stays untouched: old desktop builds still hit
GET /chat-sessions without ?status and continue receiving the active
subset, so installed clients are unaffected.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.2.27
2026-05-07 17:34:07 +08:00
LinYushen
a6e8ae964e fix(skills): handle GitHub API 403 / rate limit during skill import (#2215)
Importing a skill from a github.com URL probes the commits API to
disambiguate slash-bearing refs. On self-hosted servers the IP is often
already over GitHub's 60-req/hour unauthenticated limit, so the very
first probe returns 403 and the previous code aborted the entire
import ("validating ref \"main/skills/pptx\": github API returned
status 403").

Two changes make this resilient:

* Forward GITHUB_TOKEN as a bearer token on every api.github.com request
  via a new doGitHubAPIGet / addGitHubAuthHeader helper. With a token,
  the limit becomes 5000 req/hour and the issue disappears entirely.
* When the API still returns 401/403/429 (no token, or limit exhausted
  on the higher tier) treat the probe as indeterminate via
  errGitHubAPIBlocked, keep trying remaining candidates, and finally
  fall back to parseGitHubURL's optimistic single-segment split. This
  covers the common case (single-word refs like "main") even when the
  API is fully blocked. A warn log points operators at GITHUB_TOKEN.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-07 16:28:34 +08:00
LinYushen
cc527c34be perf(heartbeat): batch runtime last_seen_at writes (#2213)
Batches runtime heartbeat last_seen_at updates while preserving the 60s flush / 150s sweeper stale-window invariant. Also drains pending heartbeat writes during graceful shutdown.
2026-05-07 15:50:27 +08:00
LinYushen
250ada1fb3 chore(db): drop unused agent_task_queue.last_heartbeat_at (#2212)
Drops the unused agent_task_queue.last_heartbeat_at column and removes the hot-path task heartbeat write.
2026-05-07 15:45:29 +08:00
Multica Eve
d82a2d8a04 feat(skills): support importing skills from github.com URLs (#2209) 2026-05-07 15:22:34 +08:00
Naiyuan Qing
48e3131bf9 feat: harden desktop frontend against API response drift (MUL-1828) (#2208)
* docs(claude): add API Response Compatibility section

Narrows the existing "no backwards compat" rule to internal code only,
and adds a new section that codifies the defensive boundary at API
edges: parse-don't-cast, never pin UI to a single field, enum drift
must downgrade not crash.

Driven by #2143/#2147/#2192 — all three were the desktop client white-
screening on backend response shape changes the client wasn't built
against.

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

* feat(core): add zod-based API response validation layer

Introduces a defensive boundary so a malformed backend response
degrades into a safe fallback (empty page, [], etc.) instead of
throwing inside React render.

- Adds zod to the pnpm catalog and as a @multica/core dependency.
- New parseWithFallback helper in core/api/schema.ts that runs
  safeParse, logs a warn with the endpoint + zod issues on failure,
  and returns the caller-supplied fallback. Never throws.
- Schemas in core/api/schemas.ts are deliberately lenient (string
  enums kept as z.string() so unknown values still parse, optional
  fields default, nested records use .loose() for unknown keys).
- Wires setSchemaLogger from CoreProvider so warnings flow through
  the same logger as the rest of the API client.

This is the primitive — see the next commit for the call-site wiring.

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

* feat(api): guard top 5 high-risk endpoints with parseWithFallback

Wraps the response of the five endpoints whose UIs white-screened in
past incidents (#2143/#2147/#2192) so a contract drift returns a safe
fallback instead of crashing the consumer:

- listIssues          → ListIssuesResponseSchema, fallback { issues: [], total: 0 }
- listTimeline        → TimelinePageSchema,        fallback empty page
- listComments        → CommentsListSchema,        fallback []
- listIssueSubscribers → SubscribersListSchema,    fallback []
- listChildIssues     → ChildIssuesResponseSchema, fallback { issues: [] }

getIssue is intentionally NOT wrapped: there is no sensible "empty
issue" — the entire detail page depends on real fields. The page-level
ErrorBoundary (separate commit) catches that case.

Adds schema.test.ts with 9 cases covering the five failure modes
listed in MUL-1828: missing fields, wrong types, enum drift, null
body, and null arrays.

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

* feat(ui): add ErrorBoundary and wrap high-risk pages

Section-level error boundary (no third-party dep — class component +
default fallback in @multica/ui). Supports a fallback render prop and
resetKeys for auto-recovery on resource navigation.

Wraps the surfaces that white-screened in past incidents:

- IssueDetail (web + desktop + inbox split-pane) — keyed on issueId
  so navigating to a different issue clears the boundary automatically.
- IssuesPage (web + desktop).

Boundaries are placed at consumer call sites rather than inside
IssueDetail itself so we don't have to refactor the 1100-line
component, and so a crash inside one inbox split-pane doesn't take
down the inbox list next to it.

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

* fix(core): make all API schemas .loose() to preserve unknown fields

zod 4 z.object() defaults to STRIP, which silently drops fields the
schema didn't list. That makes the schema layer a sync point: a future
PR adding a TS field but forgetting the schema would have the field
disappear at runtime while TS still claims it exists — the exact bug-
class this PR is meant to prevent, just inverted.

Apply .loose() to every object schema (TimelineEntry, TimelinePage,
Comment, Issue, ListIssuesResponse, Subscriber, ChildIssuesResponse)
so unknown server-side fields pass through unchanged. Add a regression
test that feeds a payload with extra fields at both entry and page
level, and a direct unit test for parseWithFallback decoupled from any
endpoint. Update the listIssues fallback test to use a wrong-type
payload — under .loose() the previous "{ unexpected: true }" payload
parses successfully (every declared field has a default) instead of
triggering the fallback path it was meant to exercise.

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

* docs(claude): strip field-specific examples from API Compatibility section

The original wording embedded current schema field names (entries,
has_more_before, has_more_after, cursor, status, type) directly in the
rules. CLAUDE.md should state the rule, not the implementation — once a
field is renamed the doc drifts out of sync with the code, and the
specific names don't add anything the abstract rule doesn't.

Keep the rule, drop the field-level archaeology.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-07 15:09:55 +08:00
Naiyuan Qing
dce51e3a27 fix(views): guard IME composition on Enter-to-submit handlers (#2207)
* fix(views): guard IME composition on Enter-to-submit handlers

Chinese/Japanese/Korean IMEs use Enter to commit a multi-key
composition. When that Enter also triggers a submit/create handler,
the form fires before the user has finished typing.

Add a shared `isImeComposing` predicate in @multica/core/utils that
checks both `nativeEvent.isComposing` and `keyCode === 229` (Safari
clears isComposing on the commit keydown but keyCode stays 229).
Apply the guard to every Enter→action handler in packages/views where
the input can hold IME text: workspace name, agent name/description,
skill name, label name/edit, mention suggestion picker, property
picker search, delete-workspace typed confirmation.

Tiptap submit-shortcut already guards via `view.composing`; left as is.
Skipped numeric/email/URL/file-path inputs where IME does not apply.

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

* style(agents): align Escape handling with early return in inspector

Three onKeyDown handlers in agent-detail-inspector.tsx now follow the same
shape as labels-panel: handle Escape with an explicit return, then the IME
guard, then Enter submit.

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

---------

Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:17:35 +08:00
Naiyuan Qing
099dda0603 fix(timeline): include merge-truncation case in has_more_before (#2192) (#2204)
* fix(timeline): include merge-truncation case in has_more_before (#2192)

Older comments became unreachable on issues where activity-log entries
crowded them out of the latest 50-entry page. The 'show earlier' button
was hidden and no cursor was emitted because the has_more_before formula
only caught the per-table SQL cap case and missed the in-memory merge
truncation case.

Reproduces with 48 comments + 49 activities, default limit 50: neither
table individually returns >= limit rows, but their sum (97) exceeds the
merged page size, so the merge silently drops 47 older comments. The old
formula reported has_more_before=false; the client never asked for page 2.

Fix: extract hasMoreBeyond(c, a, e, limit) with the missing third
disjunct - comments + activities > entries - applied uniformly to
listTimelineLatest / Before / After / Around.

Backwards compatible: API contract unchanged. Pre-cursor clients
(<=v0.2.25) still hit listTimelineLegacy and never read these fields.
Newer clients see has_more_before flip from 'wrongly false' to correctly
true/false - no field renames, no shape changes.

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

* fix(issues): show count badge when activities are coalesced (#2192)

The timeline coalesces consecutive same-actor + same-action activities
within a 2-minute window so 48 status_changed entries don't take 48 rows.
The count badge was only rendered for task_completed / task_failed; for
status_changed (and every other action) the coalesced batch silently
collapsed to a single line with no hint that N entries were merged.

Add a coalesced_badge translation and render '×N' next to the activity
text whenever coalesced_count > 1, suppressing it on task_completed /
task_failed which already include the count in their translation copy.

This pairs with the backend fix for #2192: once the older-comments page
becomes reachable again, the activity rows above it should make the
density of the merged batch visible rather than misleading the user
into thinking only one event happened.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 13:22:16 +08:00
Jiayuan Zhang
fe956fc670 feat(issues): add Copy local workdir path to issue menu (#2196)
* feat(issues): add Copy local workdir path to issue menu

Surface the daemon-pinned task work_dir on the AgentTaskResponse and add a
"Copy local workdir path" action to the issue dropdown / context menu. The
action picks the most recent task with a recorded work_dir and writes it
to the clipboard so users can jump straight to the local execution
directory to inspect results.

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

* fix(issues): preserve user activation in Copy local workdir path

Move the task list subscription out of useIssueActions and into
IssueActionsMenuItems, where Base UI lazily mounts the menu content
only after the user opens the menu. The click handler now reads
straight from the cached query result and writes to the clipboard
synchronously, so the awaited fetch no longer drops the browser's
transient user activation when the cache is cold (e.g. opening the
context menu on an issue list row that hasn't pre-populated the
ExecutionLogSection cache).

Per Emacs PR review.

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-07 06:05:14 +02:00
Mark Gaze
f9cdd487e0 fix(projects): pre-fill the status and project to match the parent issue when creating sub-issue (#2177) 2026-05-07 08:10:25 +08:00
Jiayuan Zhang
5d51a0c9df feat(cli): add multica workspace update (#2191)
* feat(cli): add `multica workspace update` to edit workspace metadata

Closes the CLI-side gap for #2178: the `PATCH /api/workspaces/{id}`
endpoint and TS client method already exist, only the CLI subcommand
was missing. Supports partial updates of name, description, context,
and issue_prefix; long fields accept stdin via `--description-stdin` /
`--context-stdin`. `slug` stays immutable, `settings`/`repos` are out
of scope (deferred). Empty PATCH is rejected locally so we don't fire
a no-op `EventWorkspaceUpdated` broadcast. Permission gate is
unchanged (server-side admin/owner middleware).

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

* fix(cli): address review on workspace update command

- Reject `--issue-prefix ""` (and whitespace-only) explicitly. The
  server handler silently skips empty prefixes, so the previous
  behavior was a 200 OK with no actual change — exactly the kind of
  invisible no-op Emacs flagged in review.
- Restore the `## Issues` H2 in the zh CLI reference. The earlier
  edit dropped it, leaving issue commands nested under the Workspaces
  section.

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

* docs(cli): list `workspace update` in the en + zh top-level reference

Mirrors the existing zh-only entry under apps/docs/content/docs/cli/
into the English overview so the new command is discoverable from
both locales.

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-07 00:49:36 +02:00
Jiayuan Zhang
d07c7c2a15 feat(inbox): auto-select next item after archiving the selected one (#2190)
Archiving the currently selected inbox item used to clear the selection
and leave the detail panel empty, forcing the user to click the next
item to keep going. Pick the next (older) item from the deduplicated
list, falling back to the previous (newer) one when archiving at the
bottom, and only clear when nothing is left.

Route the detail panel's onDone path through the same handleArchive so
the auto-select behavior is shared.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-07 06:19:46 +08:00